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