brep_kernel/meshing/mesh_segment.rs
1//! Mesh face segmentation — stage 2 of the Rust mesh-import pipeline (the
2//! rust-first port of the app's `buildImportSolidsFromGeometry`: deflection-
3//! angle face grouping + analytic-primitive recognition; stage 1 is
4//! `mesh_to_faceted_brep`).
5//!
6//! `segment_mesh_faces` groups the triangles of an indexed mesh (or a raw
7//! STL-style soup when `indices` is empty) into smooth regions by dihedral-
8//! angle region growing, then recognizes each region's analytic carrier by
9//! least-squares fitting in the order plane → cylinder → cone → sphere →
10//! torus. A fit is accepted only when every region vertex sits within
11//! `fit_tolerance · region-scale` of the carrier AND the triangle normals
12//! agree with the carrier normal within `normal_tolerance_deg` (area-
13//! trimmed: the worst slivers up to 0.1% of the region area are exempt);
14//! regions with no accepted carrier stay `Freeform`.
15//!
16//! Tangent-smooth compounds defeat pure dihedral growing (a fillet blend
17//! runs tangentially into its walls, so the walls and the blend merge into
18//! one smooth region). Mirroring the app's planar-extraction semantics, a
19//! refinement pass splits such regions: seed-plane-anchored coplanar groups
20//! are peeled off first (gated by `planar_extraction_angle_deg` and
21//! `planar_min_area_percent`) and the leftover connected components are
22//! re-fitted through the same cascade. A component the cascade still
23//! rejects is then peeled into cylinders — a rounded outline of tangent
24//! arcs with different radii, a fillet running out into a boss — by
25//! seed-and-grow over its planar strips at the mesh's own precision, so the
26//! first strip past a tangent junction is refused where the acceptance
27//! tolerance would have admitted it.
28//!
29//! `segment_mesh_faces` is geometry-only analysis: no `BrepSolid` is built
30//! there. The output — per-triangle region ids plus per-region carrier
31//! records, all serde-serializable — is the input contract for stage 3,
32//! `mesh_regions_to_brep`, which rebuilds a validated `BrepSolid` whose
33//! faces are the segmented regions (exact planes, full revolve walls and
34//! partial cylinder patches in v1) instead of one face per triangle.
35
36use crate::fit::solve_small;
37use crate::topology::{
38 BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord,
39};
40use crate::{
41 interpolate_curve, make_line, make_plane, make_revolution,
42 mesh_to_faceted_brep, solid_signed_volume, NurbsCurve, Vec3, Vec4,
43};
44use rustc_hash::FxHashMap as HashMap;
45use serde::{Deserialize, Serialize};
46
47/// Region id given to triangles that could not be assigned to any region
48/// (degenerate triangles with no non-degenerate neighbor).
49pub const UNASSIGNED_REGION: u32 = u32::MAX;
50
51/// Options for `segment_mesh_faces`. All tolerances are dimensionless or in
52/// degrees; distances derive from the region/mesh scale so the segmentation
53/// is size-invariant.
54#[derive(Clone, Debug, Deserialize, Serialize)]
55#[serde(default)]
56pub struct SegmentOptions {
57 /// Region-growing gate: triangles on either side of an edge join the
58 /// same smooth region when their dihedral (normal-to-normal) angle is
59 /// below this threshold, in degrees.
60 pub deflection_angle_deg: f64,
61 /// Regions with fewer triangles than this are not fitted (they stay
62 /// `Freeform`). The app used 8 for noisy scanned meshes; the default 1
63 /// fits everything.
64 pub min_region_triangles: usize,
65 /// Carrier acceptance: maximum vertex deviation from the fitted carrier
66 /// as a fraction of the region's bounding-box diagonal.
67 pub fit_tolerance: f64,
68 /// Carrier acceptance: maximum angle between a triangle normal and the
69 /// carrier normal at the triangle centroid, in degrees.
70 pub normal_tolerance_deg: f64,
71 /// Refinement pass: a triangle joins a seed plane only when its normal
72 /// is within this many degrees of the seed normal (and its vertices lie
73 /// within the distance gate of the seed plane).
74 pub planar_extraction_angle_deg: f64,
75 /// Refinement pass: an extracted planar group is kept only when its
76 /// area is at least this percentage of its parent region's area
77 /// (mirrors the app's planar minimum-area percent).
78 pub planar_min_area_percent: f64,
79 /// Vertex weld tolerance; `<= 0` derives it from the bounding-box
80 /// diagonal (`diagonal · 1e-6`), matching `mesh_to_faceted_brep`.
81 pub weld_tolerance: f64,
82}
83
84impl Default for SegmentOptions {
85 fn default() -> Self {
86 Self {
87 deflection_angle_deg: 30.0,
88 min_region_triangles: 1,
89 fit_tolerance: 1e-3,
90 normal_tolerance_deg: 15.0,
91 planar_extraction_angle_deg: 1.0,
92 planar_min_area_percent: 1.0,
93 weld_tolerance: 0.0,
94 }
95 }
96}
97
98/// Recognized analytic carrier of a region. Axis directions are unit
99/// vectors; `sense` is `+1` when the mesh normals point along the carrier's
100/// outward normal (away from the axis/center) and `-1` for a cavity.
101#[derive(Clone, Debug, Deserialize, Serialize)]
102#[serde(tag = "type")]
103pub enum RegionCarrier {
104 Plane {
105 /// A point on the plane (the region's vertex centroid).
106 origin: Vec3,
107 /// Unit normal oriented with the mesh triangle normals.
108 normal: Vec3,
109 },
110 Cylinder {
111 /// Point on the axis closest to the region's vertex centroid.
112 axis_point: Vec3,
113 axis_dir: Vec3,
114 radius: f64,
115 sense: i8,
116 },
117 Cone {
118 apex: Vec3,
119 /// Unit axis pointing from the apex into the region.
120 axis_dir: Vec3,
121 half_angle_rad: f64,
122 sense: i8,
123 },
124 Sphere {
125 center: Vec3,
126 radius: f64,
127 sense: i8,
128 },
129 Torus {
130 /// Center of the spine circle.
131 center: Vec3,
132 axis_dir: Vec3,
133 major_radius: f64,
134 minor_radius: f64,
135 sense: i8,
136 },
137 Freeform,
138}
139
140impl RegionCarrier {
141 pub fn kind(&self) -> &'static str {
142 match self {
143 RegionCarrier::Plane { .. } => "plane",
144 RegionCarrier::Cylinder { .. } => "cylinder",
145 RegionCarrier::Cone { .. } => "cone",
146 RegionCarrier::Sphere { .. } => "sphere",
147 RegionCarrier::Torus { .. } => "torus",
148 RegionCarrier::Freeform => "freeform",
149 }
150 }
151}
152
153/// One smooth region of the mesh with its recognized carrier and fit
154/// residuals (residuals are zero for `Freeform` regions — no fit).
155#[derive(Clone, Debug, Deserialize, Serialize)]
156pub struct MeshRegion {
157 pub id: u32,
158 pub triangle_count: usize,
159 pub area: f64,
160 pub bbox_min: Vec3,
161 pub bbox_max: Vec3,
162 pub carrier: RegionCarrier,
163 /// Maximum absolute vertex deviation from the accepted carrier.
164 pub max_deviation: f64,
165 /// Root-mean-square vertex deviation from the accepted carrier.
166 pub rms_deviation: f64,
167 /// Maximum angle between a triangle normal and the carrier normal, deg.
168 pub max_normal_angle_deg: f64,
169}
170
171/// Segmentation result: `triangle_region_ids[t]` is the region id of input
172/// triangle `t` (index into `regions`; `UNASSIGNED_REGION` for degenerate
173/// triangles with no assignable neighbor).
174#[derive(Clone, Debug, Deserialize, Serialize)]
175pub struct MeshSegmentation {
176 pub triangle_region_ids: Vec<u32>,
177 pub regions: Vec<MeshRegion>,
178 pub triangle_count: usize,
179 pub welded_vertex_count: usize,
180}
181
182#[path = "mesh_segment/mesh_data.rs"]
183mod mesh_data;
184#[path = "mesh_segment/carrier_fit.rs"]
185mod carrier_fit;
186#[path = "mesh_segment/segmentation.rs"]
187mod segmentation;
188#[path = "mesh_segment/brep_builder.rs"]
189mod brep_builder;
190#[path = "mesh_segment/face_build.rs"]
191mod face_build;
192
193use brep_builder::*;
194use carrier_fit::*;
195use face_build::*;
196use mesh_data::*;
197use segmentation::*;
198
199pub use brep_builder::mesh_regions_to_brep;
200pub use segmentation::segment_mesh_faces;
201
202// ---------------------------------------------------------------------------
203// Tests
204// ---------------------------------------------------------------------------
205