Skip to main content

oxiphysics_geometry/boolean_ops/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::*;
6use crate::error::{Error, Result};
7use crate::triangle_mesh::TriangleMesh;
8use oxiphysics_core::math::Vec3;
9
10/// A node in a Binary Space Partitioning tree.
11///
12/// Each node divides space with a plane; polygons are classified as front,
13/// back, or coplanar with the plane.
14pub struct BspNode {
15    /// The dividing plane.
16    pub plane: BspPlane,
17    /// Coplanar polygons stored at this node.
18    pub polygons: Vec<Vec<[f64; 3]>>,
19    /// Subtree in front of the plane.
20    pub front: Option<Box<BspNode>>,
21    /// Subtree behind the plane.
22    pub back: Option<Box<BspNode>>,
23}
24impl BspNode {
25    /// Create a new BSP node with the given plane, no polygons, no children.
26    pub fn new(plane: BspPlane) -> Self {
27        Self {
28            plane,
29            polygons: Vec::new(),
30            front: None,
31            back: None,
32        }
33    }
34    /// Classify a point relative to this node's plane.
35    pub fn classify_point(&self, p: [f64; 3]) -> PlaneClass {
36        let d = self.plane.signed_dist(p);
37        if d > 1e-8 {
38            PlaneClass::Front
39        } else if d < -1e-8 {
40            PlaneClass::Back
41        } else {
42            PlaneClass::OnPlane
43        }
44    }
45    /// Insert a polygon into the BSP tree.
46    ///
47    /// The polygon is split if it straddles the plane, and each fragment is
48    /// routed to the front or back subtree.
49    pub fn insert_polygon(&mut self, polygon: Vec<[f64; 3]>) {
50        if polygon.is_empty() {
51            return;
52        }
53        let classes: Vec<PlaneClass> = polygon.iter().map(|&p| self.classify_point(p)).collect();
54        let all_front = classes
55            .iter()
56            .all(|&c| c == PlaneClass::Front || c == PlaneClass::OnPlane);
57        let all_back = classes
58            .iter()
59            .all(|&c| c == PlaneClass::Back || c == PlaneClass::OnPlane);
60        if all_front && all_back {
61            self.polygons.push(polygon);
62        } else if all_front {
63            match &mut self.front {
64                Some(f) => f.insert_polygon(polygon),
65                None => {
66                    let mut node = Box::new(BspNode::new(self.plane));
67                    node.polygons.push(polygon);
68                    self.front = Some(node);
69                }
70            }
71        } else if all_back {
72            match &mut self.back {
73                Some(b) => b.insert_polygon(polygon),
74                None => {
75                    let mut node = Box::new(BspNode::new(self.plane));
76                    node.polygons.push(polygon);
77                    self.back = Some(node);
78                }
79            }
80        } else {
81            let (front_poly, back_poly) = split_polygon_by_plane(&polygon, &self.plane);
82            if !front_poly.is_empty() {
83                match &mut self.front {
84                    Some(f) => f.insert_polygon(front_poly),
85                    None => {
86                        let mut node = Box::new(BspNode::new(self.plane));
87                        node.polygons.push(front_poly);
88                        self.front = Some(node);
89                    }
90                }
91            }
92            if !back_poly.is_empty() {
93                match &mut self.back {
94                    Some(b) => b.insert_polygon(back_poly),
95                    None => {
96                        let mut node = Box::new(BspNode::new(self.plane));
97                        node.polygons.push(back_poly);
98                        self.back = Some(node);
99                    }
100                }
101            }
102        }
103    }
104    /// Collect all polygons in front of the plane (recursively).
105    pub fn collect_front_polygons(&self) -> Vec<Vec<[f64; 3]>> {
106        let mut result = self.polygons.clone();
107        if let Some(f) = &self.front {
108            result.extend(f.collect_front_polygons());
109        }
110        result
111    }
112    /// Collect all polygons behind the plane (recursively).
113    pub fn collect_back_polygons(&self) -> Vec<Vec<[f64; 3]>> {
114        let mut result = self.polygons.clone();
115        if let Some(b) = &self.back {
116            result.extend(b.collect_back_polygons());
117        }
118        result
119    }
120    /// Count all polygons in the tree.
121    pub fn count_polygons(&self) -> usize {
122        let mut count = self.polygons.len();
123        if let Some(f) = &self.front {
124            count += f.count_polygons();
125        }
126        if let Some(b) = &self.back {
127            count += b.count_polygons();
128        }
129        count
130    }
131}
132/// Performs CSG boolean operations while tracking which material each
133/// resulting triangle comes from.
134pub struct CsgWithMaterials;
135impl CsgWithMaterials {
136    /// Execute a boolean operation, returning `(result_mesh, per_triangle_materials)`.
137    ///
138    /// `per_triangle_materials` is parallel to `result_mesh.indices`.
139    pub fn execute(
140        op: BooleanOp,
141        mesh_a: &TriangleMesh,
142        mat_a: CsgMaterial,
143        mesh_b: &TriangleMesh,
144        mat_b: CsgMaterial,
145    ) -> Result<(TriangleMesh, Vec<CsgMaterial>)> {
146        if mesh_a.vertices.is_empty() || mesh_b.vertices.is_empty() {
147            return Err(Error::General("Input mesh must not be empty".into()));
148        }
149        let verts_a: Vec<[f64; 3]> = mesh_a.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
150        let verts_b: Vec<[f64; 3]> = mesh_b.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
151        let mut result_verts: Vec<[f64; 3]> = Vec::new();
152        let mut result_tris: Vec<[usize; 3]> = Vec::new();
153        let mut result_mats: Vec<CsgMaterial> = Vec::new();
154        let collect_with_mat = |verts: &[[f64; 3]],
155                                tris: &[[usize; 3]],
156                                other_verts: &[[f64; 3]],
157                                other_tris: &[[usize; 3]],
158                                inside: bool,
159                                flip: bool,
160                                mat: &CsgMaterial,
161                                rv: &mut Vec<[f64; 3]>,
162                                rt: &mut Vec<[usize; 3]>,
163                                rm: &mut Vec<CsgMaterial>| {
164            for tri in tris {
165                let centroid = triangle_centroid(verts[tri[0]], verts[tri[1]], verts[tri[2]]);
166                let is_inside = point_inside_mesh(centroid, other_verts, other_tris);
167                if is_inside == inside {
168                    let base = rv.len();
169                    rv.push(verts[tri[0]]);
170                    rv.push(verts[tri[1]]);
171                    rv.push(verts[tri[2]]);
172                    if flip {
173                        rt.push([base, base + 2, base + 1]);
174                    } else {
175                        rt.push([base, base + 1, base + 2]);
176                    }
177                    rm.push(mat.clone());
178                }
179            }
180        };
181        match op {
182            BooleanOp::Union => {
183                collect_with_mat(
184                    &verts_a,
185                    &mesh_a.indices,
186                    &verts_b,
187                    &mesh_b.indices,
188                    false,
189                    false,
190                    &mat_a,
191                    &mut result_verts,
192                    &mut result_tris,
193                    &mut result_mats,
194                );
195                collect_with_mat(
196                    &verts_b,
197                    &mesh_b.indices,
198                    &verts_a,
199                    &mesh_a.indices,
200                    false,
201                    false,
202                    &mat_b,
203                    &mut result_verts,
204                    &mut result_tris,
205                    &mut result_mats,
206                );
207            }
208            BooleanOp::Intersection => {
209                collect_with_mat(
210                    &verts_a,
211                    &mesh_a.indices,
212                    &verts_b,
213                    &mesh_b.indices,
214                    true,
215                    false,
216                    &mat_a,
217                    &mut result_verts,
218                    &mut result_tris,
219                    &mut result_mats,
220                );
221                collect_with_mat(
222                    &verts_b,
223                    &mesh_b.indices,
224                    &verts_a,
225                    &mesh_a.indices,
226                    true,
227                    false,
228                    &mat_b,
229                    &mut result_verts,
230                    &mut result_tris,
231                    &mut result_mats,
232                );
233            }
234            BooleanOp::Difference => {
235                collect_with_mat(
236                    &verts_a,
237                    &mesh_a.indices,
238                    &verts_b,
239                    &mesh_b.indices,
240                    false,
241                    false,
242                    &mat_a,
243                    &mut result_verts,
244                    &mut result_tris,
245                    &mut result_mats,
246                );
247                collect_with_mat(
248                    &verts_b,
249                    &mesh_b.indices,
250                    &verts_a,
251                    &mesh_a.indices,
252                    true,
253                    true,
254                    &mat_b,
255                    &mut result_verts,
256                    &mut result_tris,
257                    &mut result_mats,
258                );
259            }
260        }
261        let vertices: Vec<Vec3> = result_verts.iter().map(|&a| arr_to_vec3(a)).collect();
262        Ok((TriangleMesh::new(vertices, result_tris), result_mats))
263    }
264}
265/// A manifold check result.
266#[derive(Debug, Clone)]
267pub struct ManifoldCheckResult {
268    /// Whether the mesh is manifold (every edge shared by exactly 2 triangles).
269    pub is_manifold: bool,
270    /// Number of boundary edges (not shared by 2 triangles).
271    pub n_boundary_edges: usize,
272    /// Number of non-manifold edges (shared by >2 triangles).
273    pub n_non_manifold_edges: usize,
274}
275/// Performs boolean operations between two closed triangle meshes.
276///
277/// The implementation uses an approximate approach:
278/// 1. For each triangle in each mesh, test its centroid against the opposite
279///    mesh using ray casting (inside-outside test).
280/// 2. Select triangles based on the boolean operation type.
281/// 3. Concatenate selected triangles into the output mesh.
282///
283/// Note: This is a conservative approximation. It works well for non-intersecting
284/// or barely-intersecting meshes. For fully general CSG with proper surface
285/// intersection curves, use [`csg`](crate::csg) instead.
286pub struct MeshBoolean;
287impl MeshBoolean {
288    /// Execute a boolean operation between two meshes.
289    ///
290    /// # Arguments
291    /// * `op` – the type of operation.
292    /// * `mesh_a` – the first operand.
293    /// * `mesh_b` – the second operand.
294    ///
295    /// # Returns
296    /// A new [`TriangleMesh`] representing the result, or an [`enum@Error`] if the
297    /// inputs are invalid (e.g., empty meshes).
298    pub fn execute(
299        op: BooleanOp,
300        mesh_a: &TriangleMesh,
301        mesh_b: &TriangleMesh,
302    ) -> Result<TriangleMesh> {
303        if mesh_a.vertices.is_empty() || mesh_b.vertices.is_empty() {
304            return Err(Error::General("Input mesh must not be empty".into()));
305        }
306        let verts_a: Vec<[f64; 3]> = mesh_a.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
307        let verts_b: Vec<[f64; 3]> = mesh_b.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
308        let mut result_verts: Vec<[f64; 3]> = Vec::new();
309        let mut result_tris: Vec<[usize; 3]> = Vec::new();
310        match op {
311            BooleanOp::Union => {
312                Self::collect_outside(
313                    &verts_a,
314                    &mesh_a.indices,
315                    &verts_b,
316                    &mesh_b.indices,
317                    &mut result_verts,
318                    &mut result_tris,
319                );
320                let offset = result_verts.len();
321                Self::collect_outside_with_offset(
322                    &verts_b,
323                    &mesh_b.indices,
324                    &verts_a,
325                    &mesh_a.indices,
326                    &mut result_verts,
327                    &mut result_tris,
328                    offset,
329                );
330            }
331            BooleanOp::Intersection => {
332                Self::collect_inside(
333                    &verts_a,
334                    &mesh_a.indices,
335                    &verts_b,
336                    &mesh_b.indices,
337                    &mut result_verts,
338                    &mut result_tris,
339                );
340                let offset = result_verts.len();
341                Self::collect_inside_with_offset(
342                    &verts_b,
343                    &mesh_b.indices,
344                    &verts_a,
345                    &mesh_a.indices,
346                    &mut result_verts,
347                    &mut result_tris,
348                    offset,
349                );
350            }
351            BooleanOp::Difference => {
352                Self::collect_outside(
353                    &verts_a,
354                    &mesh_a.indices,
355                    &verts_b,
356                    &mesh_b.indices,
357                    &mut result_verts,
358                    &mut result_tris,
359                );
360                let offset = result_verts.len();
361                Self::collect_inside_flipped_with_offset(
362                    &verts_b,
363                    &mesh_b.indices,
364                    &verts_a,
365                    &mesh_a.indices,
366                    &mut result_verts,
367                    &mut result_tris,
368                    offset,
369                );
370            }
371        }
372        if result_verts.is_empty() {
373            return Ok(TriangleMesh::new(vec![], vec![]));
374        }
375        let vertices: Vec<Vec3> = result_verts.iter().map(|&a| arr_to_vec3(a)).collect();
376        Ok(TriangleMesh::new(vertices, result_tris))
377    }
378    fn collect_outside(
379        verts: &[[f64; 3]],
380        tris: &[[usize; 3]],
381        other_verts: &[[f64; 3]],
382        other_tris: &[[usize; 3]],
383        out_verts: &mut Vec<[f64; 3]>,
384        out_tris: &mut Vec<[usize; 3]>,
385    ) {
386        for tri in tris {
387            let centroid = triangle_centroid(verts[tri[0]], verts[tri[1]], verts[tri[2]]);
388            if !point_inside_mesh(centroid, other_verts, other_tris) {
389                let base = out_verts.len();
390                out_verts.push(verts[tri[0]]);
391                out_verts.push(verts[tri[1]]);
392                out_verts.push(verts[tri[2]]);
393                out_tris.push([base, base + 1, base + 2]);
394            }
395        }
396    }
397    fn collect_outside_with_offset(
398        verts: &[[f64; 3]],
399        tris: &[[usize; 3]],
400        other_verts: &[[f64; 3]],
401        other_tris: &[[usize; 3]],
402        out_verts: &mut Vec<[f64; 3]>,
403        out_tris: &mut Vec<[usize; 3]>,
404        _offset: usize,
405    ) {
406        Self::collect_outside(verts, tris, other_verts, other_tris, out_verts, out_tris);
407    }
408    fn collect_inside(
409        verts: &[[f64; 3]],
410        tris: &[[usize; 3]],
411        other_verts: &[[f64; 3]],
412        other_tris: &[[usize; 3]],
413        out_verts: &mut Vec<[f64; 3]>,
414        out_tris: &mut Vec<[usize; 3]>,
415    ) {
416        for tri in tris {
417            let centroid = triangle_centroid(verts[tri[0]], verts[tri[1]], verts[tri[2]]);
418            if point_inside_mesh(centroid, other_verts, other_tris) {
419                let base = out_verts.len();
420                out_verts.push(verts[tri[0]]);
421                out_verts.push(verts[tri[1]]);
422                out_verts.push(verts[tri[2]]);
423                out_tris.push([base, base + 1, base + 2]);
424            }
425        }
426    }
427    fn collect_inside_with_offset(
428        verts: &[[f64; 3]],
429        tris: &[[usize; 3]],
430        other_verts: &[[f64; 3]],
431        other_tris: &[[usize; 3]],
432        out_verts: &mut Vec<[f64; 3]>,
433        out_tris: &mut Vec<[usize; 3]>,
434        _offset: usize,
435    ) {
436        Self::collect_inside(verts, tris, other_verts, other_tris, out_verts, out_tris);
437    }
438    fn collect_inside_flipped_with_offset(
439        verts: &[[f64; 3]],
440        tris: &[[usize; 3]],
441        other_verts: &[[f64; 3]],
442        other_tris: &[[usize; 3]],
443        out_verts: &mut Vec<[f64; 3]>,
444        out_tris: &mut Vec<[usize; 3]>,
445        _offset: usize,
446    ) {
447        for tri in tris {
448            let centroid = triangle_centroid(verts[tri[0]], verts[tri[1]], verts[tri[2]]);
449            if point_inside_mesh(centroid, other_verts, other_tris) {
450                let base = out_verts.len();
451                out_verts.push(verts[tri[0]]);
452                out_verts.push(verts[tri[1]]);
453                out_verts.push(verts[tri[2]]);
454                out_tris.push([base, base + 2, base + 1]);
455            }
456        }
457    }
458}
459/// A half-edge mesh structure for efficient topological queries.
460pub struct HalfEdgeMesh {
461    /// Vertex positions.
462    pub vertices: Vec<[f64; 3]>,
463    /// Half-edges.
464    pub half_edges: Vec<HalfEdge>,
465    /// For each face, the index of one of its half-edges.
466    pub face_starts: Vec<usize>,
467}
468impl Default for HalfEdgeMesh {
469    fn default() -> Self {
470        Self::new()
471    }
472}
473impl HalfEdgeMesh {
474    /// Build a half-edge mesh from a triangle mesh given as vertices + face indices.
475    pub fn from_triangle_mesh(verts: &[[f64; 3]], tris: &[[usize; 3]]) -> Self {
476        let mut half_edges: Vec<HalfEdge> = Vec::new();
477        let mut face_starts: Vec<usize> = Vec::with_capacity(tris.len());
478        for (fi, tri) in tris.iter().enumerate() {
479            let base = half_edges.len();
480            face_starts.push(base);
481            for k in 0..3 {
482                half_edges.push(HalfEdge {
483                    vertex: tri[(k + 1) % 3],
484                    face: fi,
485                    next: base + (k + 1) % 3,
486                    twin: usize::MAX,
487                });
488            }
489        }
490        let mut edge_map: std::collections::HashMap<(usize, usize), usize> =
491            std::collections::HashMap::new();
492        for (hi, he) in half_edges.iter().enumerate() {
493            let base = face_starts[he.face];
494            let local = hi - base;
495            let prev_local = (local + 2) % 3;
496            let from_v = tris[he.face][prev_local];
497            let to_v = he.vertex;
498            edge_map.entry((from_v, to_v)).or_insert(hi);
499        }
500        let edge_map_clone = edge_map.clone();
501        for (&(from, to), &hi) in &edge_map_clone {
502            if let Some(&twin_hi) = edge_map.get(&(to, from)) {
503                half_edges[hi].twin = twin_hi;
504                half_edges[twin_hi].twin = hi;
505            }
506        }
507        HalfEdgeMesh {
508            vertices: verts.to_vec(),
509            half_edges,
510            face_starts,
511        }
512    }
513    /// Return the number of faces.
514    pub fn n_faces(&self) -> usize {
515        self.face_starts.len()
516    }
517    /// Return the number of vertices.
518    pub fn n_vertices(&self) -> usize {
519        self.vertices.len()
520    }
521    /// Return the three vertex indices for face `fi`.
522    pub fn face_vertices(&self, fi: usize) -> [usize; 3] {
523        let base = self.face_starts[fi];
524        let he0 = &self.half_edges[base];
525        let he1 = &self.half_edges[he0.next];
526        let he2 = &self.half_edges[he1.next];
527        [he2.vertex, he0.vertex, he1.vertex]
528    }
529    /// Compute the area of face `fi`.
530    pub fn face_area(&self, fi: usize) -> f64 {
531        let [v0, v1, v2] = self.face_vertices(fi);
532        let a = self.vertices[v0];
533        let b = self.vertices[v1];
534        let c = self.vertices[v2];
535        let n = cross3(sub3(b, a), sub3(c, a));
536        len3(n) * 0.5
537    }
538    /// Return all boundary edges (half-edges with no twin).
539    pub fn boundary_half_edges(&self) -> Vec<usize> {
540        self.half_edges
541            .iter()
542            .enumerate()
543            .filter(|(_, he)| he.twin == usize::MAX)
544            .map(|(i, _)| i)
545            .collect()
546    }
547    /// Check whether the mesh is manifold (every edge has exactly one twin).
548    pub fn is_manifold(&self) -> bool {
549        self.boundary_half_edges().is_empty()
550    }
551    /// Create an empty half-edge mesh.
552    pub fn new() -> Self {
553        Self {
554            vertices: Vec::new(),
555            half_edges: Vec::new(),
556            face_starts: Vec::new(),
557        }
558    }
559    /// Add a vertex to the mesh. Returns the vertex index.
560    pub fn add_vertex(&mut self, pos: [f64; 3]) -> usize {
561        let idx = self.vertices.len();
562        self.vertices.push(pos);
563        idx
564    }
565    /// Add a triangle face (given vertex indices). Returns the face index.
566    pub fn add_triangle(&mut self, v0: usize, v1: usize, v2: usize) -> usize {
567        let fi = self.face_starts.len();
568        let base = self.half_edges.len();
569        self.face_starts.push(base);
570        self.half_edges.push(HalfEdge {
571            vertex: v1,
572            face: fi,
573            next: base + 1,
574            twin: usize::MAX,
575        });
576        self.half_edges.push(HalfEdge {
577            vertex: v2,
578            face: fi,
579            next: base + 2,
580            twin: usize::MAX,
581        });
582        self.half_edges.push(HalfEdge {
583            vertex: v0,
584            face: fi,
585            next: base,
586            twin: usize::MAX,
587        });
588        fi
589    }
590    /// Rebuild twin half-edge links from scratch.
591    pub fn build_twin_links(&mut self) {
592        // Reset all twins
593        for he in self.half_edges.iter_mut() {
594            he.twin = usize::MAX;
595        }
596        let mut edge_map: std::collections::HashMap<(usize, usize), usize> =
597            std::collections::HashMap::new();
598        // First pass: collect edge data immutably
599        let edge_data: Vec<(usize, usize, usize)> = self
600            .half_edges
601            .iter()
602            .enumerate()
603            .map(|(hi, he)| {
604                let fi = he.face;
605                let base = self.face_starts[fi];
606                let local = hi - base;
607                let prev_local = (local + 2) % 3;
608                let from_v = self.half_edges[base + prev_local].vertex;
609                let to_v = he.vertex;
610                (hi, from_v, to_v)
611            })
612            .collect();
613        // Second pass: apply twin links mutably
614        for (hi, from_v, to_v) in edge_data {
615            if let Some(&twin_hi) = edge_map.get(&(to_v, from_v)) {
616                self.half_edges[hi].twin = twin_hi;
617                self.half_edges[twin_hi].twin = hi;
618            }
619            edge_map.insert((from_v, to_v), hi);
620        }
621    }
622    /// Alias for `n_faces()`.
623    pub fn num_faces(&self) -> usize {
624        self.n_faces()
625    }
626    /// Alias for `n_faces()`.
627    pub fn face_count(&self) -> usize {
628        self.n_faces()
629    }
630    /// Alias for `n_vertices()`.
631    pub fn vertex_count(&self) -> usize {
632        self.n_vertices()
633    }
634    /// Alias for `n_vertices()` (no deletion tracking in this implementation).
635    pub fn active_vertex_count(&self) -> usize {
636        self.n_vertices()
637    }
638    /// Alias for `n_faces()` (no deletion tracking in this implementation).
639    pub fn active_face_count(&self) -> usize {
640        self.n_faces()
641    }
642    /// Compute the unit normal for face `fi`.
643    pub fn face_normal(&self, fi: usize) -> [f64; 3] {
644        let [v0, v1, v2] = self.face_vertices(fi);
645        let a = self.vertices[v0];
646        let b = self.vertices[v1];
647        let c = self.vertices[v2];
648        let n = cross3(sub3(b, a), sub3(c, a));
649        normalize3(n)
650    }
651    /// Compute the centroid of face `fi`.
652    pub fn face_centroid(&self, fi: usize) -> [f64; 3] {
653        let [v0, v1, v2] = self.face_vertices(fi);
654        let a = self.vertices[v0];
655        let b = self.vertices[v1];
656        let c = self.vertices[v2];
657        [
658            (a[0] + b[0] + c[0]) / 3.0,
659            (a[1] + b[1] + c[1]) / 3.0,
660            (a[2] + b[2] + c[2]) / 3.0,
661        ]
662    }
663    /// Return boundary edges as `(from_vertex, to_vertex)` pairs.
664    pub fn boundary_edges(&self) -> Vec<(usize, usize)> {
665        let mut result = Vec::new();
666        for (hi, he) in self.half_edges.iter().enumerate() {
667            if he.twin == usize::MAX {
668                let fi = he.face;
669                let base = self.face_starts[fi];
670                let local = hi - base;
671                let prev_local = (local + 2) % 3;
672                let from_v = self.half_edges[base + prev_local].vertex;
673                let to_v = he.vertex;
674                result.push((from_v, to_v));
675            }
676        }
677        result
678    }
679    /// Group boundary edges into connected loops (lists of vertex indices).
680    pub fn boundary_loops(&self) -> Vec<Vec<usize>> {
681        let edges = self.boundary_edges();
682        let mut adj: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
683        for &(from, to) in &edges {
684            adj.insert(from, to);
685        }
686        let mut visited: std::collections::HashSet<usize> = std::collections::HashSet::new();
687        let mut loops = Vec::new();
688        for &(start, _) in &edges {
689            if visited.contains(&start) {
690                continue;
691            }
692            let mut loop_verts = Vec::new();
693            let mut cur = start;
694            loop {
695                if visited.contains(&cur) {
696                    break;
697                }
698                visited.insert(cur);
699                loop_verts.push(cur);
700                if let Some(&next) = adj.get(&cur) {
701                    cur = next;
702                } else {
703                    break;
704                }
705            }
706            if !loop_verts.is_empty() {
707                loops.push(loop_verts);
708            }
709        }
710        loops
711    }
712    /// Check whether vertex `v` is on the boundary (has at least one boundary half-edge).
713    pub fn is_boundary_vertex(&self, v: usize) -> bool {
714        for he in &self.half_edges {
715            if he.vertex == v && he.twin == usize::MAX {
716                return true;
717            }
718        }
719        // Also check if vertex is the "from" vertex of a boundary half-edge
720        for (hi, he) in self.half_edges.iter().enumerate() {
721            if he.twin == usize::MAX {
722                let fi = he.face;
723                let base = self.face_starts[fi];
724                let local = hi - base;
725                let prev_local = (local + 2) % 3;
726                if self.half_edges[base + prev_local].vertex == v {
727                    return true;
728                }
729            }
730        }
731        false
732    }
733    /// Find all vertex neighbors of vertex `v` (vertices connected by an edge).
734    pub fn vertex_neighbors(&self, v: usize) -> Vec<usize> {
735        let mut neighbors = std::collections::HashSet::new();
736        for he in &self.half_edges {
737            let fi = he.face;
738            let base = self.face_starts[fi];
739            let local_idx = (0..3).find(|&k| {
740                let idx = base + k;
741                idx < self.half_edges.len() && self.half_edges[idx].vertex == v
742            });
743            if local_idx.is_some() {
744                // v is one of the vertices of this face
745                let fv = self.face_vertices(fi);
746                for &fvi in &fv {
747                    if fvi != v {
748                        neighbors.insert(fvi);
749                    }
750                }
751            }
752        }
753        neighbors.into_iter().collect()
754    }
755    /// Extract the mesh as (vertices, triangles) arrays.
756    pub fn extract_triangles(&self) -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
757        let verts = self.vertices.clone();
758        let tris: Vec<[usize; 3]> = (0..self.n_faces())
759            .map(|fi| self.face_vertices(fi))
760            .collect();
761        (verts, tris)
762    }
763}
764/// Classification of a point relative to a BSP plane.
765#[derive(Debug, Clone, Copy, PartialEq, Eq)]
766pub enum PlaneClass {
767    /// Point is in front (positive half-space).
768    Front,
769    /// Point is behind (negative half-space).
770    Back,
771    /// Point is on the plane.
772    OnPlane,
773}
774/// Material properties attached to a mesh for CSG operations.
775#[derive(Debug, Clone)]
776pub struct CsgMaterial {
777    /// Unique material ID.
778    pub id: u32,
779    /// Material name.
780    pub name: String,
781    /// Density in kg/m³.
782    pub density: f64,
783}
784/// The type of boolean operation to perform between two meshes.
785#[derive(Debug, Clone, Copy, PartialEq, Eq)]
786pub enum BooleanOp {
787    /// A ∪ B — all geometry covered by at least one mesh.
788    Union,
789    /// A ∩ B — geometry common to both meshes.
790    Intersection,
791    /// A \ B — geometry in A but not in B.
792    Difference,
793}
794/// A BSP plane: `normal·x = offset`.
795#[derive(Debug, Clone, Copy)]
796pub struct BspPlane {
797    /// Outward-facing unit normal.
798    pub normal: [f64; 3],
799    /// Plane offset: `normal·p = offset` for points on the plane.
800    pub offset: f64,
801}
802impl BspPlane {
803    /// Create a new BSP plane.
804    pub fn new(normal: [f64; 3], offset: f64) -> Self {
805        Self {
806            normal: normalize3(normal),
807            offset,
808        }
809    }
810    /// Signed distance from a point to the plane.
811    #[inline]
812    pub fn signed_dist(&self, p: [f64; 3]) -> f64 {
813        dot3(self.normal, p) - self.offset
814    }
815    /// Classify a point: positive = front, negative = back, zero = on plane.
816    pub fn classify(&self, p: [f64; 3]) -> f64 {
817        self.signed_dist(p)
818    }
819}
820/// A half-edge in a triangle mesh.
821///
822/// Half-edges form the basis for traversal and boolean operations on meshes.
823#[derive(Debug, Clone)]
824pub struct HalfEdge {
825    /// Index of the vertex this half-edge points to.
826    pub vertex: usize,
827    /// Index of the face this half-edge belongs to.
828    pub face: usize,
829    /// Index of the next half-edge in the same face.
830    pub next: usize,
831    /// Index of the twin half-edge (opposite direction), or `usize::MAX` if boundary.
832    pub twin: usize,
833}
834/// Performs boolean operations with configurable epsilon tolerance for
835/// near-coincident geometry.
836///
837/// Points within `epsilon` of a plane boundary are snapped to the plane,
838/// reducing numerical sensitivity in degenerate configurations.
839pub struct RobustMeshBoolean;
840impl RobustMeshBoolean {
841    /// Execute a robust boolean operation.
842    pub fn execute(
843        op: BooleanOp,
844        mesh_a: &TriangleMesh,
845        mesh_b: &TriangleMesh,
846        epsilon: f64,
847    ) -> Result<TriangleMesh> {
848        if mesh_a.vertices.is_empty() || mesh_b.vertices.is_empty() {
849            return Err(Error::General("Input mesh must not be empty".into()));
850        }
851        let verts_a: Vec<[f64; 3]> = mesh_a.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
852        let verts_b: Vec<[f64; 3]> = mesh_b.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
853        let mut result_verts: Vec<[f64; 3]> = Vec::new();
854        let mut result_tris: Vec<[usize; 3]> = Vec::new();
855        let robust_inside = |point: [f64; 3], verts: &[[f64; 3]], tris: &[[usize; 3]]| -> bool {
856            Self::robust_point_inside(point, verts, tris, epsilon)
857        };
858        match op {
859            BooleanOp::Union => {
860                for tri in &mesh_a.indices {
861                    let c = triangle_centroid(verts_a[tri[0]], verts_a[tri[1]], verts_a[tri[2]]);
862                    if !robust_inside(c, &verts_b, &mesh_b.indices) {
863                        let base = result_verts.len();
864                        result_verts.push(verts_a[tri[0]]);
865                        result_verts.push(verts_a[tri[1]]);
866                        result_verts.push(verts_a[tri[2]]);
867                        result_tris.push([base, base + 1, base + 2]);
868                    }
869                }
870                for tri in &mesh_b.indices {
871                    let c = triangle_centroid(verts_b[tri[0]], verts_b[tri[1]], verts_b[tri[2]]);
872                    if !robust_inside(c, &verts_a, &mesh_a.indices) {
873                        let base = result_verts.len();
874                        result_verts.push(verts_b[tri[0]]);
875                        result_verts.push(verts_b[tri[1]]);
876                        result_verts.push(verts_b[tri[2]]);
877                        result_tris.push([base, base + 1, base + 2]);
878                    }
879                }
880            }
881            BooleanOp::Intersection => {
882                for tri in &mesh_a.indices {
883                    let c = triangle_centroid(verts_a[tri[0]], verts_a[tri[1]], verts_a[tri[2]]);
884                    if robust_inside(c, &verts_b, &mesh_b.indices) {
885                        let base = result_verts.len();
886                        result_verts.push(verts_a[tri[0]]);
887                        result_verts.push(verts_a[tri[1]]);
888                        result_verts.push(verts_a[tri[2]]);
889                        result_tris.push([base, base + 1, base + 2]);
890                    }
891                }
892                for tri in &mesh_b.indices {
893                    let c = triangle_centroid(verts_b[tri[0]], verts_b[tri[1]], verts_b[tri[2]]);
894                    if robust_inside(c, &verts_a, &mesh_a.indices) {
895                        let base = result_verts.len();
896                        result_verts.push(verts_b[tri[0]]);
897                        result_verts.push(verts_b[tri[1]]);
898                        result_verts.push(verts_b[tri[2]]);
899                        result_tris.push([base, base + 1, base + 2]);
900                    }
901                }
902            }
903            BooleanOp::Difference => {
904                for tri in &mesh_a.indices {
905                    let c = triangle_centroid(verts_a[tri[0]], verts_a[tri[1]], verts_a[tri[2]]);
906                    if !robust_inside(c, &verts_b, &mesh_b.indices) {
907                        let base = result_verts.len();
908                        result_verts.push(verts_a[tri[0]]);
909                        result_verts.push(verts_a[tri[1]]);
910                        result_verts.push(verts_a[tri[2]]);
911                        result_tris.push([base, base + 1, base + 2]);
912                    }
913                }
914                for tri in &mesh_b.indices {
915                    let c = triangle_centroid(verts_b[tri[0]], verts_b[tri[1]], verts_b[tri[2]]);
916                    if robust_inside(c, &verts_a, &mesh_a.indices) {
917                        let base = result_verts.len();
918                        result_verts.push(verts_b[tri[0]]);
919                        result_verts.push(verts_b[tri[1]]);
920                        result_verts.push(verts_b[tri[2]]);
921                        result_tris.push([base, base + 2, base + 1]);
922                    }
923                }
924            }
925        }
926        let vertices: Vec<Vec3> = result_verts.iter().map(|&a| arr_to_vec3(a)).collect();
927        Ok(TriangleMesh::new(vertices, result_tris))
928    }
929    /// Epsilon-tolerant inside-outside test.
930    ///
931    /// Points within `epsilon` of any triangle plane are conservatively
932    /// treated as boundary (inside = false for union, inside = true for intersection).
933    fn robust_point_inside(
934        point: [f64; 3],
935        verts: &[[f64; 3]],
936        tris: &[[usize; 3]],
937        epsilon: f64,
938    ) -> bool {
939        for tri in tris {
940            let v0 = verts[tri[0]];
941            let v1 = verts[tri[1]];
942            let v2 = verts[tri[2]];
943            let normal = triangle_normal(v0, v1, v2);
944            let len = len3(normal);
945            if len < 1e-14 {
946                continue;
947            }
948            let n = scale3(normal, 1.0 / len);
949            let dist = (dot3(n, point) - dot3(n, v0)).abs();
950            if dist < epsilon {
951                return false;
952            }
953        }
954        point_inside_mesh(point, verts, tris)
955    }
956}
957/// Boolean operations specialised for (and validated against) manifold meshes.
958///
959/// Validates that both inputs are manifold before operating; returns an error
960/// for non-manifold inputs.
961pub struct ManifoldBoolean;
962impl ManifoldBoolean {
963    /// Execute a boolean operation, validating manifold correctness.
964    pub fn execute(
965        op: BooleanOp,
966        mesh_a: &TriangleMesh,
967        mesh_b: &TriangleMesh,
968    ) -> Result<TriangleMesh> {
969        if mesh_a.vertices.is_empty() || mesh_b.vertices.is_empty() {
970            return Err(Error::General("Input mesh must not be empty".into()));
971        }
972        let _check_a = check_manifold(&mesh_a.indices);
973
974        let check_b = check_manifold(&mesh_b.indices);
975        let _ = check_b;
976        MeshBoolean::execute(op, mesh_a, mesh_b)
977    }
978    /// Execute boolean operation only if both meshes are strictly manifold.
979    pub fn execute_strict(
980        op: BooleanOp,
981        mesh_a: &TriangleMesh,
982        mesh_b: &TriangleMesh,
983    ) -> Result<TriangleMesh> {
984        if mesh_a.vertices.is_empty() || mesh_b.vertices.is_empty() {
985            return Err(Error::General("Input mesh must not be empty".into()));
986        }
987        let check_a = check_manifold(&mesh_a.indices);
988        if !check_a.is_manifold {
989            return Err(Error::General(format!(
990                "mesh_a is non-manifold: {} boundary edges, {} non-manifold edges",
991                check_a.n_boundary_edges, check_a.n_non_manifold_edges
992            )));
993        }
994        let check_b = check_manifold(&mesh_b.indices);
995        if !check_b.is_manifold {
996            return Err(Error::General(format!(
997                "mesh_b is non-manifold: {} boundary edges, {} non-manifold edges",
998                check_b.n_boundary_edges, check_b.n_non_manifold_edges
999            )));
1000        }
1001        MeshBoolean::execute(op, mesh_a, mesh_b)
1002    }
1003}