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.
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// BREP private tests: 70cb6bf52947feea