Skip to main content

brep_cam/mesh/
mod.rs

1//! The immutable triangle snapshot every CAM algorithm queries.
2//!
3//! A [`MeshSnapshot`] is built once per generation from the targets' and
4//! fixtures' tessellations, in MACHINE coordinates, and never mutated. Each
5//! triangle keeps the body and face it came from so drive-face regions and
6//! per-face diagnostics resolve by stable identity, and triangle ids are the
7//! positions in `triangles`, so an index built on a snapshot is valid for the
8//! snapshot's lifetime.
9
10pub(crate) mod core;
11
12use crate::model::{Bounds3, Point3, RigidTransform, TargetRef};
13use crate::numeric::Tolerance;
14use serde::{Deserialize, Serialize};
15
16/// Which body and face a triangle came from.
17#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct FaceSource {
20    /// Persistent body name.
21    pub solid: String,
22    /// Persistent face name within the body.
23    pub face: String,
24    /// `true` for fixtures and other bodies that are protected but never machined.
25    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
26    pub protected_only: bool,
27}
28
29/// One triangle with its cached bounds, unit normal and source face.
30#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
31#[serde(rename_all = "camelCase")]
32pub struct Triangle {
33    /// Position in the snapshot's `triangles`.
34    pub id: u32,
35    pub a: Point3,
36    pub b: Point3,
37    pub c: Point3,
38    pub bounds: Bounds3,
39    /// Unit normal `(b - a) × (c - a)`, normalized. Degenerate triangles never enter a snapshot.
40    pub normal: Point3,
41    /// Index into [`MeshSnapshot::faces`].
42    pub face: u32,
43}
44
45impl Triangle {
46    pub fn vertices(&self) -> [Point3; 3] {
47        [self.a, self.b, self.c]
48    }
49
50    /// Twice the signed area vector; its length is twice the area.
51    pub fn area_vector(&self) -> Point3 {
52        self.b.sub(self.a).cross(self.c.sub(self.a))
53    }
54
55    pub fn area(&self) -> f64 {
56        0.5 * self.area_vector().length()
57    }
58
59    pub fn centroid(&self) -> Point3 {
60        self.a.add(self.b).add(self.c).scale(1.0 / 3.0)
61    }
62
63    /// The triangle's plane is vertical (normal perpendicular to Z) within the tolerance.
64    pub fn is_vertical(&self, tol: &Tolerance) -> bool {
65        tol.is_horizontal(self.normal)
66    }
67
68    /// The triangle's plane is horizontal within the tolerance.
69    pub fn is_horizontal(&self, tol: &Tolerance) -> bool {
70        tol.is_vertical(self.normal)
71    }
72
73    pub fn min_z(&self) -> f64 {
74        self.bounds.min.z
75    }
76
77    pub fn max_z(&self) -> f64 {
78        self.bounds.max.z
79    }
80}
81
82/// Why a triangle or buffer was refused while building a snapshot.
83#[derive(Clone, Debug, PartialEq, Eq)]
84pub enum MeshError {
85    /// A coordinate was NaN or infinite.
86    NonFinite { triangle: usize },
87    /// `positions` is not a multiple of three, `indices` not a multiple of three,
88    /// or `face_ids` does not have one entry per triangle.
89    BufferShape(String),
90    /// An index points past the vertex count.
91    IndexOutOfRange { triangle: usize, index: u32 },
92    /// A per-triangle face id is not in the face table.
93    FaceOutOfRange { triangle: usize, face: u32 },
94}
95
96impl std::fmt::Display for MeshError {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        match self {
99            MeshError::NonFinite { triangle } => write!(f, "triangle {triangle} has a non-finite coordinate"),
100            MeshError::BufferShape(msg) => write!(f, "mesh buffers: {msg}"),
101            MeshError::IndexOutOfRange { triangle, index } => {
102                write!(f, "triangle {triangle} references vertex {index} past the buffer")
103            }
104            MeshError::FaceOutOfRange { triangle, face } => {
105                write!(f, "triangle {triangle} references face {face} past the face table")
106            }
107        }
108    }
109}
110
111impl std::error::Error for MeshError {}
112
113/// What the snapshot builder dropped or flagged. Zero-area triangles are
114/// dropped silently (they contribute no surface); the count is reported so a
115/// badly tessellated body is visible in the operation report.
116#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct MeshBuildReport {
119    pub triangles_in: u64,
120    pub triangles_kept: u64,
121    pub degenerate_dropped: u64,
122}
123
124/// Builds a [`MeshSnapshot`] body by body, face by face.
125#[derive(Debug, Default)]
126pub struct MeshBuilder {
127    faces: Vec<FaceSource>,
128    triangles: Vec<Triangle>,
129    bounds: Bounds3,
130    report: MeshBuildReport,
131    tolerance: Tolerance,
132}
133
134impl MeshBuilder {
135    /// A builder using `tolerance.area_eps` to drop degenerate triangles.
136    pub fn new(tolerance: Tolerance) -> Self {
137        Self { tolerance, bounds: Bounds3::empty(), ..Default::default() }
138    }
139
140    /// Register a face and get its index; triangles are added against it.
141    pub fn add_face(&mut self, source: FaceSource) -> u32 {
142        self.faces.push(source);
143        (self.faces.len() - 1) as u32
144    }
145
146    /// Add one triangle against a registered face. Degenerate triangles are
147    /// counted and dropped; non-finite coordinates are an error.
148    pub fn add_triangle(&mut self, face: u32, a: Point3, b: Point3, c: Point3) -> Result<(), MeshError> {
149        let i = self.report.triangles_in as usize;
150        self.report.triangles_in += 1;
151        if face as usize >= self.faces.len() {
152            return Err(MeshError::FaceOutOfRange { triangle: i, face });
153        }
154        if !a.is_finite() || !b.is_finite() || !c.is_finite() {
155            return Err(MeshError::NonFinite { triangle: i });
156        }
157        let area_vector = b.sub(a).cross(c.sub(a));
158        let twice_area = area_vector.length();
159        if self.tolerance.is_zero_area(0.5 * twice_area) {
160            self.report.degenerate_dropped += 1;
161            return Ok(());
162        }
163        let normal = area_vector.scale(1.0 / twice_area);
164        let bounds = Bounds3::from_points([a, b, c]);
165        self.bounds = self.bounds.union(&bounds);
166        let id = self.triangles.len() as u32;
167        self.triangles.push(Triangle { id, a, b, c, bounds, normal, face });
168        self.report.triangles_kept += 1;
169        Ok(())
170    }
171
172    /// Add a whole body from flat buffers: `positions` is `xyz` per vertex,
173    /// `indices` three per triangle, `face_ids` one per triangle indexing
174    /// `face_names`. This is the shape the kernel's tessellation produces.
175    pub fn add_body_buffers(
176        &mut self,
177        solid: &str,
178        protected_only: bool,
179        positions: &[f64],
180        indices: &[u32],
181        face_ids: &[u32],
182        face_names: &[String],
183    ) -> Result<(), MeshError> {
184        if positions.len() % 3 != 0 {
185            return Err(MeshError::BufferShape("positions length is not a multiple of three".into()));
186        }
187        if indices.len() % 3 != 0 {
188            return Err(MeshError::BufferShape("indices length is not a multiple of three".into()));
189        }
190        let triangle_count = indices.len() / 3;
191        if face_ids.len() != triangle_count {
192            return Err(MeshError::BufferShape(format!(
193                "face_ids has {} entries for {} triangles",
194                face_ids.len(),
195                triangle_count
196            )));
197        }
198        let base_face = self.faces.len() as u32;
199        for name in face_names {
200            self.add_face(FaceSource { solid: solid.to_string(), face: name.clone(), protected_only });
201        }
202        let vertex_count = (positions.len() / 3) as u32;
203        let point = |index: u32| -> Point3 {
204            let i = index as usize * 3;
205            Point3::new(positions[i], positions[i + 1], positions[i + 2])
206        };
207        for (t, tri) in indices.chunks_exact(3).enumerate() {
208            for &index in tri {
209                if index >= vertex_count {
210                    return Err(MeshError::IndexOutOfRange { triangle: self.report.triangles_in as usize, index });
211                }
212            }
213            let face_local = face_ids[t];
214            if face_local as usize >= face_names.len() {
215                return Err(MeshError::FaceOutOfRange { triangle: self.report.triangles_in as usize, face: face_local });
216            }
217            self.add_triangle(base_face + face_local, point(tri[0]), point(tri[1]), point(tri[2]))?;
218        }
219        Ok(())
220    }
221
222    pub fn finish(self) -> MeshSnapshot {
223        MeshSnapshot { faces: self.faces, triangles: self.triangles, bounds: self.bounds, report: self.report }
224    }
225}
226
227/// The immutable mesh every algorithm reads. Triangle `id == position`.
228#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
229#[serde(rename_all = "camelCase")]
230pub struct MeshSnapshot {
231    pub faces: Vec<FaceSource>,
232    pub triangles: Vec<Triangle>,
233    pub bounds: Bounds3,
234    #[serde(default)]
235    pub report: MeshBuildReport,
236}
237
238impl MeshSnapshot {
239    pub fn empty() -> Self {
240        Self::default()
241    }
242
243    pub fn builder(tolerance: Tolerance) -> MeshBuilder {
244        MeshBuilder::new(tolerance)
245    }
246
247    /// Same mesh in another coordinate frame. Triangle ids and face indices
248    /// are unchanged; normals rotate; bounds are recomputed exactly.
249    pub fn transformed(&self, transform: &RigidTransform) -> MeshSnapshot {
250        if transform.is_identity() {
251            return self.clone();
252        }
253        let mut bounds = Bounds3::empty();
254        let triangles = self
255            .triangles
256            .iter()
257            .map(|t| {
258                let a = transform.apply_point(t.a);
259                let b = transform.apply_point(t.b);
260                let c = transform.apply_point(t.c);
261                let tb = Bounds3::from_points([a, b, c]);
262                bounds = bounds.union(&tb);
263                Triangle { id: t.id, a, b, c, bounds: tb, normal: transform.apply_vector(t.normal), face: t.face }
264            })
265            .collect();
266        MeshSnapshot { faces: self.faces.clone(), triangles, bounds, report: self.report }
267    }
268
269    pub fn triangle_count(&self) -> usize {
270        self.triangles.len()
271    }
272
273    pub fn is_empty(&self) -> bool {
274        self.triangles.is_empty()
275    }
276
277    pub fn triangle(&self, id: u32) -> &Triangle {
278        &self.triangles[id as usize]
279    }
280
281    pub fn face_of(&self, id: u32) -> &FaceSource {
282        &self.faces[self.triangles[id as usize].face as usize]
283    }
284
285    /// Face indices whose source matches `target` (whole body: every face of it).
286    pub fn resolve_faces(&self, target: &TargetRef) -> Vec<u32> {
287        self.faces
288            .iter()
289            .enumerate()
290            .filter(|(_, f)| f.solid == target.solid && target.face.as_ref().map_or(true, |name| *name == f.face))
291            .map(|(i, _)| i as u32)
292            .collect()
293    }
294
295    /// Triangle ids whose face is in `faces` (ascending).
296    pub fn triangles_of_faces(&self, faces: &[u32]) -> Vec<u32> {
297        let mut mark = vec![false; self.faces.len()];
298        for &f in faces {
299            if let Some(m) = mark.get_mut(f as usize) {
300                *m = true;
301            }
302        }
303        self.triangles.iter().filter(|t| mark[t.face as usize]).map(|t| t.id).collect()
304    }
305
306    /// Triangle ids of a body or face, ascending; empty when unresolved.
307    pub fn triangles_of(&self, target: &TargetRef) -> Vec<u32> {
308        self.triangles_of_faces(&self.resolve_faces(target))
309    }
310
311    /// Sorted, de-duplicated body names present in the snapshot.
312    pub fn solids(&self) -> Vec<String> {
313        let mut names: Vec<String> = self.faces.iter().map(|f| f.solid.clone()).collect();
314        names.sort();
315        names.dedup();
316        names
317    }
318
319    /// Bounds of the triangles whose ids are given.
320    pub fn bounds_of(&self, ids: &[u32]) -> Bounds3 {
321        ids.iter().fold(Bounds3::empty(), |b, &id| b.union(&self.triangles[id as usize].bounds))
322    }
323}
324
325/// THE place model coordinates become machine coordinates for a mesh: the
326/// setup's `model_to_machine` applied to a model-space snapshot. Algorithms
327/// never transform geometry themselves; display conversion back goes through
328/// [`crate::model::Setup::machine_to_model`].
329pub fn to_machine(setup: &crate::model::Setup, model: &MeshSnapshot) -> MeshSnapshot {
330    model.transformed(&setup.model_to_machine)
331}
332
333/// Read access other lanes code against, so an algorithm can run on a full
334/// snapshot or on any other triangle source (a sub-mesh, a test fixture).
335pub trait TriangleSource {
336    fn triangle_count(&self) -> usize;
337    fn triangle(&self, id: u32) -> &Triangle;
338    fn bounds(&self) -> Bounds3;
339    fn face_source(&self, face: u32) -> &FaceSource;
340}
341
342impl TriangleSource for MeshSnapshot {
343    fn triangle_count(&self) -> usize {
344        self.triangles.len()
345    }
346
347    fn triangle(&self, id: u32) -> &Triangle {
348        &self.triangles[id as usize]
349    }
350
351    fn bounds(&self) -> Bounds3 {
352        self.bounds
353    }
354
355    fn face_source(&self, face: u32) -> &FaceSource {
356        &self.faces[face as usize]
357    }
358}
359