ifc_lite_geometry/
csg.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! CSG (Constructive Solid Geometry) Operations
6//!
7//! Fast triangle clipping and boolean operations.
8
9use crate::error::Result;
10use crate::mesh::Mesh;
11use crate::triangulation::{calculate_polygon_normal, project_to_2d, triangulate_polygon};
12use nalgebra::{Point3, Vector3};
13use rustc_hash::FxHashMap;
14
15/// Plane definition for clipping
16#[derive(Debug, Clone, Copy)]
17pub struct Plane {
18    /// Point on the plane
19    pub point: Point3<f64>,
20    /// Normal vector (must be normalized)
21    pub normal: Vector3<f64>,
22}
23
24impl Plane {
25    /// Create a new plane
26    pub fn new(point: Point3<f64>, normal: Vector3<f64>) -> Self {
27        Self {
28            point,
29            normal: normal.normalize(),
30        }
31    }
32
33    /// Calculate signed distance from point to plane
34    /// Positive = in front, Negative = behind
35    pub fn signed_distance(&self, point: &Point3<f64>) -> f64 {
36        (point - self.point).dot(&self.normal)
37    }
38
39    /// Check if point is in front of plane
40    pub fn is_front(&self, point: &Point3<f64>) -> bool {
41        self.signed_distance(point) >= 0.0
42    }
43}
44
45/// Triangle clipping result
46#[derive(Debug, Clone)]
47pub enum ClipResult {
48    /// Triangle is completely in front (keep it)
49    AllFront(Triangle),
50    /// Triangle is completely behind (discard it)
51    AllBehind,
52    /// Triangle intersects plane - returns new triangles
53    Split(Vec<Triangle>),
54}
55
56/// Triangle definition
57#[derive(Debug, Clone)]
58pub struct Triangle {
59    pub v0: Point3<f64>,
60    pub v1: Point3<f64>,
61    pub v2: Point3<f64>,
62}
63
64impl Triangle {
65    /// Create a new triangle
66    pub fn new(v0: Point3<f64>, v1: Point3<f64>, v2: Point3<f64>) -> Self {
67        Self { v0, v1, v2 }
68    }
69
70    /// Calculate triangle normal
71    pub fn normal(&self) -> Vector3<f64> {
72        let edge1 = self.v1 - self.v0;
73        let edge2 = self.v2 - self.v0;
74        edge1.cross(&edge2).normalize()
75    }
76
77    /// Calculate triangle area
78    pub fn area(&self) -> f64 {
79        let edge1 = self.v1 - self.v0;
80        let edge2 = self.v2 - self.v0;
81        edge1.cross(&edge2).norm() * 0.5
82    }
83}
84
85/// CSG Clipping Processor
86pub struct ClippingProcessor {
87    /// Epsilon for floating point comparisons
88    pub epsilon: f64,
89}
90
91/// Create a box mesh from AABB min/max bounds
92/// Returns a mesh with 12 triangles (2 per face, 6 faces)
93fn aabb_to_mesh(min: Point3<f64>, max: Point3<f64>) -> Mesh {
94    let mut mesh = Mesh::with_capacity(8, 36);
95
96    // Define the 8 vertices of the box
97    let v0 = Point3::new(min.x, min.y, min.z); // 0: front-bottom-left
98    let v1 = Point3::new(max.x, min.y, min.z); // 1: front-bottom-right
99    let v2 = Point3::new(max.x, max.y, min.z); // 2: front-top-right
100    let v3 = Point3::new(min.x, max.y, min.z); // 3: front-top-left
101    let v4 = Point3::new(min.x, min.y, max.z); // 4: back-bottom-left
102    let v5 = Point3::new(max.x, min.y, max.z); // 5: back-bottom-right
103    let v6 = Point3::new(max.x, max.y, max.z); // 6: back-top-right
104    let v7 = Point3::new(min.x, max.y, max.z); // 7: back-top-left
105
106    // Add triangles for each face (counter-clockwise winding when viewed from outside)
107    // Front face (z = min.z) - normal points toward -Z
108    add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v2, v1));
109    add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v3, v2));
110
111    // Back face (z = max.z) - normal points toward +Z
112    add_triangle_to_mesh(&mut mesh, &Triangle::new(v4, v5, v6));
113    add_triangle_to_mesh(&mut mesh, &Triangle::new(v4, v6, v7));
114
115    // Left face (x = min.x) - normal points toward -X
116    add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v4, v7));
117    add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v7, v3));
118
119    // Right face (x = max.x) - normal points toward +X
120    add_triangle_to_mesh(&mut mesh, &Triangle::new(v1, v2, v6));
121    add_triangle_to_mesh(&mut mesh, &Triangle::new(v1, v6, v5));
122
123    // Bottom face (y = min.y) - normal points toward -Y
124    add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v1, v5));
125    add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v5, v4));
126
127    // Top face (y = max.y) - normal points toward +Y
128    add_triangle_to_mesh(&mut mesh, &Triangle::new(v3, v7, v6));
129    add_triangle_to_mesh(&mut mesh, &Triangle::new(v3, v6, v2));
130
131    mesh
132}
133
134impl ClippingProcessor {
135    /// Create a new clipping processor
136    pub fn new() -> Self {
137        Self { epsilon: 1e-6 }
138    }
139
140    /// Clip a triangle against a plane
141    /// Returns triangles that are in front of the plane
142    pub fn clip_triangle(&self, triangle: &Triangle, plane: &Plane) -> ClipResult {
143        // Calculate signed distances for all vertices
144        let d0 = plane.signed_distance(&triangle.v0);
145        let d1 = plane.signed_distance(&triangle.v1);
146        let d2 = plane.signed_distance(&triangle.v2);
147
148        // Count vertices in front of plane
149        let mut front_count = 0;
150        if d0 >= -self.epsilon {
151            front_count += 1;
152        }
153        if d1 >= -self.epsilon {
154            front_count += 1;
155        }
156        if d2 >= -self.epsilon {
157            front_count += 1;
158        }
159
160        match front_count {
161            // All vertices behind - discard triangle
162            0 => ClipResult::AllBehind,
163
164            // All vertices in front - keep triangle
165            3 => ClipResult::AllFront(triangle.clone()),
166
167            // One vertex in front - create 1 smaller triangle
168            1 => {
169                let (front, back1, back2) = if d0 >= -self.epsilon {
170                    (triangle.v0, triangle.v1, triangle.v2)
171                } else if d1 >= -self.epsilon {
172                    (triangle.v1, triangle.v2, triangle.v0)
173                } else {
174                    (triangle.v2, triangle.v0, triangle.v1)
175                };
176
177                // Interpolate to find intersection points
178                let d_front = if d0 >= -self.epsilon {
179                    d0
180                } else if d1 >= -self.epsilon {
181                    d1
182                } else {
183                    d2
184                };
185                let d_back1 = if d0 >= -self.epsilon {
186                    d1
187                } else if d1 >= -self.epsilon {
188                    d2
189                } else {
190                    d0
191                };
192                let d_back2 = if d0 >= -self.epsilon {
193                    d2
194                } else if d1 >= -self.epsilon {
195                    d0
196                } else {
197                    d1
198                };
199
200                let t1 = d_front / (d_front - d_back1);
201                let t2 = d_front / (d_front - d_back2);
202
203                let p1 = front + (back1 - front) * t1;
204                let p2 = front + (back2 - front) * t2;
205
206                ClipResult::Split(vec![Triangle::new(front, p1, p2)])
207            }
208
209            // Two vertices in front - create 2 triangles
210            2 => {
211                let (front1, front2, back) = if d0 < -self.epsilon {
212                    (triangle.v1, triangle.v2, triangle.v0)
213                } else if d1 < -self.epsilon {
214                    (triangle.v2, triangle.v0, triangle.v1)
215                } else {
216                    (triangle.v0, triangle.v1, triangle.v2)
217                };
218
219                // Interpolate to find intersection points
220                let d_back = if d0 < -self.epsilon {
221                    d0
222                } else if d1 < -self.epsilon {
223                    d1
224                } else {
225                    d2
226                };
227                let d_front1 = if d0 < -self.epsilon {
228                    d1
229                } else if d1 < -self.epsilon {
230                    d2
231                } else {
232                    d0
233                };
234                let d_front2 = if d0 < -self.epsilon {
235                    d2
236                } else if d1 < -self.epsilon {
237                    d0
238                } else {
239                    d1
240                };
241
242                let t1 = d_front1 / (d_front1 - d_back);
243                let t2 = d_front2 / (d_front2 - d_back);
244
245                let p1 = front1 + (back - front1) * t1;
246                let p2 = front2 + (back - front2) * t2;
247
248                ClipResult::Split(vec![
249                    Triangle::new(front1, front2, p1),
250                    Triangle::new(front2, p2, p1),
251                ])
252            }
253
254            _ => unreachable!(),
255        }
256    }
257
258    /// Box subtraction - removes everything inside the box from the mesh
259    /// Uses proper CSG difference operation via subtract_mesh
260    pub fn subtract_box(&self, mesh: &Mesh, min: Point3<f64>, max: Point3<f64>) -> Result<Mesh> {
261        // Fast path: if mesh is empty, return empty mesh
262        if mesh.is_empty() {
263            return Ok(Mesh::new());
264        }
265
266        // Create a box mesh from the AABB bounds
267        let box_mesh = aabb_to_mesh(min, max);
268
269        // Use the CSG difference operation (mesh - box)
270        self.subtract_mesh(mesh, &box_mesh)
271    }
272
273    /// Extract opening profile from mesh (find largest face)
274    /// Returns profile points as an ordered contour and the face normal
275    /// Uses boundary extraction via edge counting to produce stable results
276    #[allow(dead_code)]
277    fn extract_opening_profile(
278        &self,
279        opening_mesh: &Mesh,
280    ) -> Option<(Vec<Point3<f64>>, Vector3<f64>)> {
281        if opening_mesh.is_empty() {
282            return None;
283        }
284
285        // Group triangles by normal to find faces
286        let mut face_groups: FxHashMap<u64, Vec<(Point3<f64>, Point3<f64>, Point3<f64>)>> =
287            FxHashMap::default();
288        let normal_epsilon = 0.01; // Tolerance for normal comparison
289
290        for i in (0..opening_mesh.indices.len()).step_by(3) {
291            let i0 = opening_mesh.indices[i] as usize;
292            let i1 = opening_mesh.indices[i + 1] as usize;
293            let i2 = opening_mesh.indices[i + 2] as usize;
294
295            let v0 = Point3::new(
296                opening_mesh.positions[i0 * 3] as f64,
297                opening_mesh.positions[i0 * 3 + 1] as f64,
298                opening_mesh.positions[i0 * 3 + 2] as f64,
299            );
300            let v1 = Point3::new(
301                opening_mesh.positions[i1 * 3] as f64,
302                opening_mesh.positions[i1 * 3 + 1] as f64,
303                opening_mesh.positions[i1 * 3 + 2] as f64,
304            );
305            let v2 = Point3::new(
306                opening_mesh.positions[i2 * 3] as f64,
307                opening_mesh.positions[i2 * 3 + 1] as f64,
308                opening_mesh.positions[i2 * 3 + 2] as f64,
309            );
310
311            let edge1 = v1 - v0;
312            let edge2 = v2 - v0;
313            // Use try_normalize to handle degenerate triangles
314            let normal = match edge1.cross(&edge2).try_normalize(1e-10) {
315                Some(n) => n,
316                None => continue, // Skip degenerate triangles
317            };
318
319            // Quantize normal for grouping (round to nearest 0.01)
320            let nx = (normal.x / normal_epsilon).round() as i32;
321            let ny = (normal.y / normal_epsilon).round() as i32;
322            let nz = (normal.z / normal_epsilon).round() as i32;
323            let key = ((nx as u64) << 32) | ((ny as u32 as u64) << 16) | (nz as u32 as u64);
324
325            face_groups.entry(key).or_default().push((v0, v1, v2));
326        }
327
328        // Find largest face group (most triangles = largest face)
329        let largest_face = face_groups
330            .iter()
331            .max_by_key(|(_, triangles)| triangles.len())?;
332
333        let triangles = largest_face.1;
334        if triangles.is_empty() {
335            return None;
336        }
337
338        // Build edge count map to find boundary edges
339        // An edge is a boundary if it appears exactly once (not shared between triangles)
340        // Use quantized vertex positions as keys
341        let quantize = |p: &Point3<f64>| -> (i64, i64, i64) {
342            let scale = 1e6; // Quantize to micrometer precision
343            (
344                (p.x * scale).round() as i64,
345                (p.y * scale).round() as i64,
346                (p.z * scale).round() as i64,
347            )
348        };
349
350        // Edge key: ordered pair of quantized vertices (smaller first for consistency)
351        let make_edge_key =
352            |a: (i64, i64, i64), b: (i64, i64, i64)| -> ((i64, i64, i64), (i64, i64, i64)) {
353                if a < b {
354                    (a, b)
355                } else {
356                    (b, a)
357                }
358            };
359
360        // Count edges and store original vertices
361        let mut edge_count: FxHashMap<
362            ((i64, i64, i64), (i64, i64, i64)),
363            (usize, Point3<f64>, Point3<f64>),
364        > = FxHashMap::default();
365
366        for (v0, v1, v2) in triangles {
367            let q0 = quantize(v0);
368            let q1 = quantize(v1);
369            let q2 = quantize(v2);
370
371            // Three edges per triangle
372            for (qa, qb, pa, pb) in [
373                (q0, q1, *v0, *v1),
374                (q1, q2, *v1, *v2),
375                (q2, q0, *v2, *v0),
376            ] {
377                let key = make_edge_key(qa, qb);
378                edge_count
379                    .entry(key)
380                    .and_modify(|(count, _, _)| *count += 1)
381                    .or_insert((1, pa, pb));
382            }
383        }
384
385        // Collect boundary edges (count == 1)
386        let mut boundary_edges: Vec<(Point3<f64>, Point3<f64>)> = Vec::new();
387        for (_, (count, pa, pb)) in &edge_count {
388            if *count == 1 {
389                boundary_edges.push((*pa, *pb));
390            }
391        }
392
393        if boundary_edges.is_empty() {
394            // No boundary found (closed surface with no edges) - fall back to using centroid
395            return None;
396        }
397
398        // Build vertex adjacency map for boundary traversal
399        let mut adjacency: FxHashMap<(i64, i64, i64), Vec<(i64, i64, i64, Point3<f64>)>> =
400            FxHashMap::default();
401        for (pa, pb) in &boundary_edges {
402            let qa = quantize(pa);
403            let qb = quantize(pb);
404            adjacency.entry(qa).or_default().push((qb.0, qb.1, qb.2, *pb));
405            adjacency.entry(qb).or_default().push((qa.0, qa.1, qa.2, *pa));
406        }
407
408        // Build ordered contour by walking the boundary
409        let mut contour: Vec<Point3<f64>> = Vec::new();
410        let mut visited: FxHashMap<(i64, i64, i64), bool> = FxHashMap::default();
411
412        // Start from first boundary edge
413        if let Some((start_p, _)) = boundary_edges.first() {
414            let start_q = quantize(start_p);
415            contour.push(*start_p);
416            visited.insert(start_q, true);
417
418            let mut current_q = start_q;
419
420            // Walk around the boundary
421            loop {
422                let neighbors = match adjacency.get(&current_q) {
423                    Some(n) => n,
424                    None => break,
425                };
426
427                // Find unvisited neighbor
428                let mut found_next = false;
429                for (nqx, nqy, nqz, np) in neighbors {
430                    let nq = (*nqx, *nqy, *nqz);
431                    if !visited.get(&nq).unwrap_or(&false) {
432                        contour.push(*np);
433                        visited.insert(nq, true);
434                        current_q = nq;
435                        found_next = true;
436                        break;
437                    }
438                }
439
440                if !found_next {
441                    break; // Closed loop or no more unvisited neighbors
442                }
443            }
444        }
445
446        if contour.len() < 3 {
447            // Not enough points for a valid polygon
448            return None;
449        }
450
451        // Calculate normal from the ordered contour
452        let normal = calculate_polygon_normal(&contour);
453
454        // Normalize the result
455        let normalized_normal = match normal.try_normalize(1e-10) {
456            Some(n) => n,
457            None => return None, // Degenerate polygon
458        };
459
460        Some((contour, normalized_normal))
461    }
462
463    /// Convert our Mesh format to csgrs Mesh format
464    fn mesh_to_csgrs(mesh: &Mesh) -> Result<csgrs::mesh::Mesh<()>> {
465        use csgrs::mesh::{polygon::Polygon, vertex::Vertex, Mesh as CSGMesh};
466        use std::sync::OnceLock;
467
468        if mesh.is_empty() {
469            return Ok(CSGMesh {
470                polygons: Vec::new(),
471                bounding_box: OnceLock::new(),
472                metadata: None,
473            });
474        }
475
476        let mut polygons = Vec::new();
477
478        // Process each triangle
479        for i in (0..mesh.indices.len()).step_by(3) {
480            let i0 = mesh.indices[i] as usize;
481            let i1 = mesh.indices[i + 1] as usize;
482            let i2 = mesh.indices[i + 2] as usize;
483
484            // Get triangle vertices
485            let v0 = Point3::new(
486                mesh.positions[i0 * 3] as f64,
487                mesh.positions[i0 * 3 + 1] as f64,
488                mesh.positions[i0 * 3 + 2] as f64,
489            );
490            let v1 = Point3::new(
491                mesh.positions[i1 * 3] as f64,
492                mesh.positions[i1 * 3 + 1] as f64,
493                mesh.positions[i1 * 3 + 2] as f64,
494            );
495            let v2 = Point3::new(
496                mesh.positions[i2 * 3] as f64,
497                mesh.positions[i2 * 3 + 1] as f64,
498                mesh.positions[i2 * 3 + 2] as f64,
499            );
500
501            // Calculate face normal from triangle edges
502            // Use try_normalize to handle degenerate (zero-area/collinear) triangles
503            let edge1 = v1 - v0;
504            let edge2 = v2 - v0;
505            let face_normal = match edge1.cross(&edge2).try_normalize(1e-10) {
506                Some(n) => n,
507                None => continue, // Skip degenerate triangles to avoid NaN propagation
508            };
509
510            // Create csgrs vertices (use face normal for all vertices)
511            let vertices = vec![
512                Vertex::new(v0, face_normal),
513                Vertex::new(v1, face_normal),
514                Vertex::new(v2, face_normal),
515            ];
516
517            polygons.push(Polygon::new(vertices, None));
518        }
519
520        Ok(CSGMesh::from_polygons(&polygons, None))
521    }
522
523    /// Convert csgrs Mesh format back to our Mesh format
524    fn csgrs_to_mesh(csg_mesh: &csgrs::mesh::Mesh<()>) -> Result<Mesh> {
525        let mut mesh = Mesh::new();
526
527        for polygon in &csg_mesh.polygons {
528            let vertices = &polygon.vertices;
529            if vertices.len() < 3 {
530                continue;
531            }
532
533            // Extract 3D positions
534            let points_3d: Vec<Point3<f64>> = vertices
535                .iter()
536                .map(|v| Point3::new(v.pos[0], v.pos[1], v.pos[2]))
537                .collect();
538
539            // Get the CSG polygon's intended normal (from first vertex)
540            // Validate and normalize to avoid NaN propagation in project_to_2d
541            let raw_normal = Vector3::new(
542                vertices[0].normal[0],
543                vertices[0].normal[1],
544                vertices[0].normal[2],
545            );
546
547            // Try to normalize the CSG normal; if it fails (zero or NaN), compute from points
548            let csg_normal = match raw_normal.try_normalize(1e-10) {
549                Some(n) if n.x.is_finite() && n.y.is_finite() && n.z.is_finite() => n,
550                _ => {
551                    // Fall back to computing normal from polygon points
552                    let computed = calculate_polygon_normal(&points_3d);
553                    match computed.try_normalize(1e-10) {
554                        Some(n) => n,
555                        None => continue, // Skip degenerate polygon
556                    }
557                }
558            };
559
560            // FAST PATH: Triangle - no triangulation needed
561            if points_3d.len() == 3 {
562                let base_idx = mesh.vertex_count();
563                for v in vertices {
564                    mesh.add_vertex(v.pos, v.normal);
565                }
566                mesh.add_triangle(
567                    base_idx as u32,
568                    (base_idx + 1) as u32,
569                    (base_idx + 2) as u32,
570                );
571                continue;
572            }
573
574            // Project 3D polygon to 2D using CSG normal (preserves winding intent)
575            let (points_2d, _, _, _) = project_to_2d(&points_3d, &csg_normal);
576
577            // Triangulate (handles convex AND concave polygons)
578            let indices = match triangulate_polygon(&points_2d) {
579                Ok(idx) => idx,
580                Err(_) => continue, // Skip degenerate polygons
581            };
582
583            // Add vertices and create triangles (winding is correct from projection)
584            let base_idx = mesh.vertex_count();
585            for v in vertices {
586                mesh.add_vertex(v.pos, v.normal);
587            }
588
589            for tri in indices.chunks(3) {
590                if tri.len() == 3 {
591                    mesh.add_triangle(
592                        (base_idx + tri[0]) as u32,
593                        (base_idx + tri[1]) as u32,
594                        (base_idx + tri[2]) as u32,
595                    );
596                }
597            }
598        }
599
600        Ok(mesh)
601    }
602
603    /// Subtract opening mesh from host mesh using csgrs CSG boolean operations
604    pub fn subtract_mesh(&self, host_mesh: &Mesh, opening_mesh: &Mesh) -> Result<Mesh> {
605        use csgrs::traits::CSG;
606
607        // Fast path: if opening is empty, return host unchanged
608        if opening_mesh.is_empty() {
609            return Ok(host_mesh.clone());
610        }
611
612        // Convert meshes to csgrs format
613        let host_csg = Self::mesh_to_csgrs(host_mesh)?;
614        let opening_csg = Self::mesh_to_csgrs(opening_mesh)?;
615
616        // Perform CSG difference (host - opening)
617        let result_csg = host_csg.difference(&opening_csg);
618
619        // Convert back to our Mesh format
620        Self::csgrs_to_mesh(&result_csg)
621    }
622
623    /// Union two meshes together using csgrs CSG boolean operations
624    pub fn union_mesh(&self, mesh_a: &Mesh, mesh_b: &Mesh) -> Result<Mesh> {
625        use csgrs::traits::CSG;
626
627        // Fast paths
628        if mesh_a.is_empty() {
629            return Ok(mesh_b.clone());
630        }
631        if mesh_b.is_empty() {
632            return Ok(mesh_a.clone());
633        }
634
635        // Convert meshes to csgrs format
636        let csg_a = Self::mesh_to_csgrs(mesh_a)?;
637        let csg_b = Self::mesh_to_csgrs(mesh_b)?;
638
639        // Perform CSG union
640        let result_csg = csg_a.union(&csg_b);
641
642        // Convert back to our Mesh format
643        Self::csgrs_to_mesh(&result_csg)
644    }
645
646    /// Union multiple meshes together
647    ///
648    /// Convenience method that sequentially unions all non-empty meshes.
649    /// Skips empty meshes to avoid unnecessary CSG operations.
650    pub fn union_meshes(&self, meshes: &[Mesh]) -> Result<Mesh> {
651        if meshes.is_empty() {
652            return Ok(Mesh::new());
653        }
654
655        if meshes.len() == 1 {
656            return Ok(meshes[0].clone());
657        }
658
659        // Start with first non-empty mesh
660        let mut result = Mesh::new();
661        let mut found_first = false;
662
663        for mesh in meshes {
664            if mesh.is_empty() {
665                continue;
666            }
667
668            if !found_first {
669                result = mesh.clone();
670                found_first = true;
671                continue;
672            }
673
674            result = self.union_mesh(&result, mesh)?;
675        }
676
677        Ok(result)
678    }
679
680    /// Subtract multiple meshes efficiently
681    ///
682    /// When void count exceeds threshold, unions all voids first
683    /// then performs a single subtraction. This is much more efficient
684    /// for elements with many openings (e.g., floors with many penetrations).
685    ///
686    /// # Arguments
687    /// * `host` - The host mesh to subtract from
688    /// * `voids` - List of void meshes to subtract
689    ///
690    /// # Returns
691    /// The host mesh with all voids subtracted
692    pub fn subtract_meshes_batched(&self, host: &Mesh, voids: &[Mesh]) -> Result<Mesh> {
693        // Filter out empty meshes
694        let non_empty_voids: Vec<&Mesh> = voids.iter().filter(|m| !m.is_empty()).collect();
695
696        if non_empty_voids.is_empty() {
697            return Ok(host.clone());
698        }
699
700        if non_empty_voids.len() == 1 {
701            return self.subtract_mesh(host, non_empty_voids[0]);
702        }
703
704        // Threshold for batching: if more than 10 voids, union them first
705        const BATCH_THRESHOLD: usize = 10;
706
707        if non_empty_voids.len() > BATCH_THRESHOLD {
708            // Union all voids into a single mesh first
709            let void_refs: Vec<Mesh> = non_empty_voids.iter().map(|m| (*m).clone()).collect();
710            let combined = self.union_meshes(&void_refs)?;
711
712            // Single subtraction
713            self.subtract_mesh(host, &combined)
714        } else {
715            // Sequential subtraction for small counts
716            let mut result = host.clone();
717
718            for void in non_empty_voids {
719                result = self.subtract_mesh(&result, void)?;
720            }
721
722            Ok(result)
723        }
724    }
725
726    /// Subtract meshes with fallback on failure
727    ///
728    /// Attempts batched subtraction, but if it fails, returns the host mesh
729    /// unchanged rather than propagating the error. This provides graceful
730    /// degradation for problematic void geometries.
731    pub fn subtract_meshes_with_fallback(&self, host: &Mesh, voids: &[Mesh]) -> Mesh {
732        match self.subtract_meshes_batched(host, voids) {
733            Ok(result) => {
734                // Validate result
735                if result.is_empty() || !self.validate_mesh(&result) {
736                    host.clone()
737                } else {
738                    result
739                }
740            }
741            Err(_) => host.clone(),
742        }
743    }
744
745    /// Validate mesh for common issues
746    fn validate_mesh(&self, mesh: &Mesh) -> bool {
747        // Check for NaN/Inf in positions
748        if mesh.positions.iter().any(|v| !v.is_finite()) {
749            return false;
750        }
751
752        // Check for NaN/Inf in normals
753        if mesh.normals.iter().any(|v| !v.is_finite()) {
754            return false;
755        }
756
757        // Check for valid triangle indices
758        let vertex_count = mesh.vertex_count();
759        for idx in &mesh.indices {
760            if *idx as usize >= vertex_count {
761                return false;
762            }
763        }
764
765        true
766    }
767
768    /// Clip mesh using bounding box (6 planes) - DEPRECATED: use subtract_box() instead
769    /// Subtracts everything inside the box from the mesh
770    #[deprecated(note = "Use subtract_box() for better performance")]
771    pub fn clip_mesh_with_box(
772        &self,
773        mesh: &Mesh,
774        min: Point3<f64>,
775        max: Point3<f64>,
776    ) -> Result<Mesh> {
777        self.subtract_box(mesh, min, max)
778    }
779
780    /// Clip an entire mesh against a plane
781    pub fn clip_mesh(&self, mesh: &Mesh, plane: &Plane) -> Result<Mesh> {
782        let mut result = Mesh::new();
783
784        // Process each triangle
785        for i in (0..mesh.indices.len()).step_by(3) {
786            let i0 = mesh.indices[i] as usize;
787            let i1 = mesh.indices[i + 1] as usize;
788            let i2 = mesh.indices[i + 2] as usize;
789
790            // Get triangle vertices
791            let v0 = Point3::new(
792                mesh.positions[i0 * 3] as f64,
793                mesh.positions[i0 * 3 + 1] as f64,
794                mesh.positions[i0 * 3 + 2] as f64,
795            );
796            let v1 = Point3::new(
797                mesh.positions[i1 * 3] as f64,
798                mesh.positions[i1 * 3 + 1] as f64,
799                mesh.positions[i1 * 3 + 2] as f64,
800            );
801            let v2 = Point3::new(
802                mesh.positions[i2 * 3] as f64,
803                mesh.positions[i2 * 3 + 1] as f64,
804                mesh.positions[i2 * 3 + 2] as f64,
805            );
806
807            let triangle = Triangle::new(v0, v1, v2);
808
809            // Clip triangle
810            match self.clip_triangle(&triangle, plane) {
811                ClipResult::AllFront(tri) => {
812                    // Keep original triangle
813                    add_triangle_to_mesh(&mut result, &tri);
814                }
815                ClipResult::AllBehind => {
816                    // Discard triangle
817                }
818                ClipResult::Split(triangles) => {
819                    // Add clipped triangles
820                    for tri in triangles {
821                        add_triangle_to_mesh(&mut result, &tri);
822                    }
823                }
824            }
825        }
826
827        Ok(result)
828    }
829}
830
831impl Default for ClippingProcessor {
832    fn default() -> Self {
833        Self::new()
834    }
835}
836
837/// Add a triangle to a mesh
838fn add_triangle_to_mesh(mesh: &mut Mesh, triangle: &Triangle) {
839    let base_idx = mesh.vertex_count() as u32;
840
841    // Calculate normal
842    let normal = triangle.normal();
843
844    // Add vertices
845    mesh.add_vertex(triangle.v0, normal);
846    mesh.add_vertex(triangle.v1, normal);
847    mesh.add_vertex(triangle.v2, normal);
848
849    // Add triangle
850    mesh.add_triangle(base_idx, base_idx + 1, base_idx + 2);
851}
852
853/// Calculate smooth normals for a mesh
854#[inline]
855pub fn calculate_normals(mesh: &mut Mesh) {
856    let vertex_count = mesh.vertex_count();
857    if vertex_count == 0 {
858        return;
859    }
860
861    // Initialize normals to zero
862    let mut normals = vec![Vector3::zeros(); vertex_count];
863
864    // Accumulate face normals
865    for i in (0..mesh.indices.len()).step_by(3) {
866        let i0 = mesh.indices[i] as usize;
867        let i1 = mesh.indices[i + 1] as usize;
868        let i2 = mesh.indices[i + 2] as usize;
869
870        // Get triangle vertices
871        let v0 = Point3::new(
872            mesh.positions[i0 * 3] as f64,
873            mesh.positions[i0 * 3 + 1] as f64,
874            mesh.positions[i0 * 3 + 2] as f64,
875        );
876        let v1 = Point3::new(
877            mesh.positions[i1 * 3] as f64,
878            mesh.positions[i1 * 3 + 1] as f64,
879            mesh.positions[i1 * 3 + 2] as f64,
880        );
881        let v2 = Point3::new(
882            mesh.positions[i2 * 3] as f64,
883            mesh.positions[i2 * 3 + 1] as f64,
884            mesh.positions[i2 * 3 + 2] as f64,
885        );
886
887        // Calculate face normal
888        let edge1 = v1 - v0;
889        let edge2 = v2 - v0;
890        let normal = edge1.cross(&edge2);
891
892        // Accumulate normal for each vertex
893        normals[i0] += normal;
894        normals[i1] += normal;
895        normals[i2] += normal;
896    }
897
898    // Normalize and write back
899    mesh.normals.clear();
900    mesh.normals.reserve(vertex_count * 3);
901
902    for normal in normals {
903        let normalized = normal.try_normalize(1e-6).unwrap_or_else(|| Vector3::new(0.0, 0.0, 1.0));
904        mesh.normals.push(normalized.x as f32);
905        mesh.normals.push(normalized.y as f32);
906        mesh.normals.push(normalized.z as f32);
907    }
908}
909
910#[cfg(test)]
911mod tests {
912    use super::*;
913
914    #[test]
915    fn test_plane_signed_distance() {
916        let plane = Plane::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
917
918        assert_eq!(plane.signed_distance(&Point3::new(0.0, 0.0, 5.0)), 5.0);
919        assert_eq!(plane.signed_distance(&Point3::new(0.0, 0.0, -5.0)), -5.0);
920        assert_eq!(plane.signed_distance(&Point3::new(5.0, 5.0, 0.0)), 0.0);
921    }
922
923    #[test]
924    fn test_clip_triangle_all_front() {
925        let processor = ClippingProcessor::new();
926        let triangle = Triangle::new(
927            Point3::new(0.0, 0.0, 1.0),
928            Point3::new(1.0, 0.0, 1.0),
929            Point3::new(0.5, 1.0, 1.0),
930        );
931        let plane = Plane::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
932
933        match processor.clip_triangle(&triangle, &plane) {
934            ClipResult::AllFront(_) => {}
935            _ => panic!("Expected AllFront"),
936        }
937    }
938
939    #[test]
940    fn test_clip_triangle_all_behind() {
941        let processor = ClippingProcessor::new();
942        let triangle = Triangle::new(
943            Point3::new(0.0, 0.0, -1.0),
944            Point3::new(1.0, 0.0, -1.0),
945            Point3::new(0.5, 1.0, -1.0),
946        );
947        let plane = Plane::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
948
949        match processor.clip_triangle(&triangle, &plane) {
950            ClipResult::AllBehind => {}
951            _ => panic!("Expected AllBehind"),
952        }
953    }
954
955    #[test]
956    fn test_clip_triangle_split_one_front() {
957        let processor = ClippingProcessor::new();
958        let triangle = Triangle::new(
959            Point3::new(0.0, 0.0, 1.0),  // Front
960            Point3::new(1.0, 0.0, -1.0), // Behind
961            Point3::new(0.5, 1.0, -1.0), // Behind
962        );
963        let plane = Plane::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
964
965        match processor.clip_triangle(&triangle, &plane) {
966            ClipResult::Split(triangles) => {
967                assert_eq!(triangles.len(), 1);
968            }
969            _ => panic!("Expected Split"),
970        }
971    }
972
973    #[test]
974    fn test_clip_triangle_split_two_front() {
975        let processor = ClippingProcessor::new();
976        let triangle = Triangle::new(
977            Point3::new(0.0, 0.0, 1.0),  // Front
978            Point3::new(1.0, 0.0, 1.0),  // Front
979            Point3::new(0.5, 1.0, -1.0), // Behind
980        );
981        let plane = Plane::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
982
983        match processor.clip_triangle(&triangle, &plane) {
984            ClipResult::Split(triangles) => {
985                assert_eq!(triangles.len(), 2);
986            }
987            _ => panic!("Expected Split with 2 triangles"),
988        }
989    }
990
991    #[test]
992    fn test_triangle_normal() {
993        let triangle = Triangle::new(
994            Point3::new(0.0, 0.0, 0.0),
995            Point3::new(1.0, 0.0, 0.0),
996            Point3::new(0.0, 1.0, 0.0),
997        );
998
999        let normal = triangle.normal();
1000        assert!((normal.z - 1.0).abs() < 1e-6);
1001    }
1002
1003    #[test]
1004    fn test_triangle_area() {
1005        let triangle = Triangle::new(
1006            Point3::new(0.0, 0.0, 0.0),
1007            Point3::new(1.0, 0.0, 0.0),
1008            Point3::new(0.0, 1.0, 0.0),
1009        );
1010
1011        let area = triangle.area();
1012        assert!((area - 0.5).abs() < 1e-6);
1013    }
1014}