Skip to main content

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_arc, make_extrusion, 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