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