Skip to main content

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;
14use smallvec::SmallVec;
15
16/// Type alias for small triangle collections (typically 1-2 triangles from clipping)
17pub type TriangleVec = SmallVec<[Triangle; 4]>;
18
19/// Plane definition for clipping
20#[derive(Debug, Clone, Copy)]
21pub struct Plane {
22    /// Point on the plane
23    pub point: Point3<f64>,
24    /// Normal vector (must be normalized)
25    pub normal: Vector3<f64>,
26}
27
28impl Plane {
29    /// Create a new plane
30    pub fn new(point: Point3<f64>, normal: Vector3<f64>) -> Self {
31        Self {
32            point,
33            normal: normal.normalize(),
34        }
35    }
36
37    /// Calculate signed distance from point to plane
38    /// Positive = in front, Negative = behind
39    pub fn signed_distance(&self, point: &Point3<f64>) -> f64 {
40        (point - self.point).dot(&self.normal)
41    }
42
43    /// Check if point is in front of plane
44    pub fn is_front(&self, point: &Point3<f64>) -> bool {
45        self.signed_distance(point) >= 0.0
46    }
47}
48
49/// Triangle clipping result
50#[derive(Debug, Clone)]
51pub enum ClipResult {
52    /// Triangle is completely in front (keep it)
53    AllFront(Triangle),
54    /// Triangle is completely behind (discard it)
55    AllBehind,
56    /// Triangle intersects plane - returns new triangles (uses SmallVec to avoid heap allocation)
57    Split(TriangleVec),
58}
59
60/// Triangle definition
61#[derive(Debug, Clone)]
62pub struct Triangle {
63    pub v0: Point3<f64>,
64    pub v1: Point3<f64>,
65    pub v2: Point3<f64>,
66}
67
68impl Triangle {
69    /// Create a new triangle
70    #[inline]
71    pub fn new(v0: Point3<f64>, v1: Point3<f64>, v2: Point3<f64>) -> Self {
72        Self { v0, v1, v2 }
73    }
74
75    /// Calculate triangle normal
76    #[inline]
77    pub fn normal(&self) -> Vector3<f64> {
78        let edge1 = self.v1 - self.v0;
79        let edge2 = self.v2 - self.v0;
80        edge1.cross(&edge2).normalize()
81    }
82
83    /// Calculate the cross product of edges, which is twice the area vector.
84    ///
85    /// Returns a `Vector3<f64>` perpendicular to the triangle plane.
86    /// For degenerate/collinear triangles, returns the zero vector.
87    /// Use `is_degenerate()` or `try_normalize()` on the result if you need
88    /// to detect and handle degenerate cases.
89    #[inline]
90    pub fn cross_product(&self) -> Vector3<f64> {
91        let edge1 = self.v1 - self.v0;
92        let edge2 = self.v2 - self.v0;
93        edge1.cross(&edge2)
94    }
95
96    /// Calculate triangle area (half the magnitude of the cross product).
97    #[inline]
98    pub fn area(&self) -> f64 {
99        self.cross_product().norm() * 0.5
100    }
101
102    /// Check if triangle is degenerate (zero area, collinear vertices).
103    ///
104    /// Uses `try_normalize` on the cross product with the specified epsilon.
105    /// Returns `true` if the cross product cannot be normalized (i.e., degenerate).
106    #[inline]
107    pub fn is_degenerate(&self, epsilon: f64) -> bool {
108        self.cross_product().try_normalize(epsilon).is_none()
109    }
110}
111
112/// Maximum combined polygon count for CSG operations.
113/// The csgrs BSP tree can infinite-recurse on certain polygon configurations
114/// (coplanar/near-coplanar faces cause repeated splitting with exponential growth).
115/// This limit prevents stack overflow in both native and WASM builds.
116const MAX_CSG_POLYGONS: usize = 2000;
117
118/// CSG Clipping Processor
119pub struct ClippingProcessor {
120    /// Epsilon for floating point comparisons
121    pub epsilon: f64,
122}
123
124/// Create a box mesh from AABB min/max bounds
125/// Returns a mesh with 12 triangles (2 per face, 6 faces)
126fn aabb_to_mesh(min: Point3<f64>, max: Point3<f64>) -> Mesh {
127    let mut mesh = Mesh::with_capacity(8, 36);
128
129    // Define the 8 vertices of the box
130    let v0 = Point3::new(min.x, min.y, min.z); // 0: front-bottom-left
131    let v1 = Point3::new(max.x, min.y, min.z); // 1: front-bottom-right
132    let v2 = Point3::new(max.x, max.y, min.z); // 2: front-top-right
133    let v3 = Point3::new(min.x, max.y, min.z); // 3: front-top-left
134    let v4 = Point3::new(min.x, min.y, max.z); // 4: back-bottom-left
135    let v5 = Point3::new(max.x, min.y, max.z); // 5: back-bottom-right
136    let v6 = Point3::new(max.x, max.y, max.z); // 6: back-top-right
137    let v7 = Point3::new(min.x, max.y, max.z); // 7: back-top-left
138
139    // Add triangles for each face (counter-clockwise winding when viewed from outside)
140    // Front face (z = min.z) - normal points toward -Z
141    add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v2, v1));
142    add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v3, v2));
143
144    // Back face (z = max.z) - normal points toward +Z
145    add_triangle_to_mesh(&mut mesh, &Triangle::new(v4, v5, v6));
146    add_triangle_to_mesh(&mut mesh, &Triangle::new(v4, v6, v7));
147
148    // Left face (x = min.x) - normal points toward -X
149    add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v4, v7));
150    add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v7, v3));
151
152    // Right face (x = max.x) - normal points toward +X
153    add_triangle_to_mesh(&mut mesh, &Triangle::new(v1, v2, v6));
154    add_triangle_to_mesh(&mut mesh, &Triangle::new(v1, v6, v5));
155
156    // Bottom face (y = min.y) - normal points toward -Y
157    add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v1, v5));
158    add_triangle_to_mesh(&mut mesh, &Triangle::new(v0, v5, v4));
159
160    // Top face (y = max.y) - normal points toward +Y
161    add_triangle_to_mesh(&mut mesh, &Triangle::new(v3, v7, v6));
162    add_triangle_to_mesh(&mut mesh, &Triangle::new(v3, v6, v2));
163
164    mesh
165}
166
167impl ClippingProcessor {
168    /// Create a new clipping processor
169    pub fn new() -> Self {
170        Self { epsilon: 1e-6 }
171    }
172
173    /// Clip a triangle against a plane
174    /// Returns triangles that are in front of the plane
175    pub fn clip_triangle(&self, triangle: &Triangle, plane: &Plane) -> ClipResult {
176        // Calculate signed distances for all vertices
177        let d0 = plane.signed_distance(&triangle.v0);
178        let d1 = plane.signed_distance(&triangle.v1);
179        let d2 = plane.signed_distance(&triangle.v2);
180
181        // Count vertices in front of plane
182        let mut front_count = 0;
183        if d0 >= -self.epsilon {
184            front_count += 1;
185        }
186        if d1 >= -self.epsilon {
187            front_count += 1;
188        }
189        if d2 >= -self.epsilon {
190            front_count += 1;
191        }
192
193        match front_count {
194            // All vertices behind - discard triangle
195            0 => ClipResult::AllBehind,
196
197            // All vertices in front - keep triangle
198            3 => ClipResult::AllFront(triangle.clone()),
199
200            // One vertex in front - create 1 smaller triangle
201            1 => {
202                let (front, back1, back2) = if d0 >= -self.epsilon {
203                    (triangle.v0, triangle.v1, triangle.v2)
204                } else if d1 >= -self.epsilon {
205                    (triangle.v1, triangle.v2, triangle.v0)
206                } else {
207                    (triangle.v2, triangle.v0, triangle.v1)
208                };
209
210                // Interpolate to find intersection points
211                let d_front = if d0 >= -self.epsilon {
212                    d0
213                } else if d1 >= -self.epsilon {
214                    d1
215                } else {
216                    d2
217                };
218                let d_back1 = if d0 >= -self.epsilon {
219                    d1
220                } else if d1 >= -self.epsilon {
221                    d2
222                } else {
223                    d0
224                };
225                let d_back2 = if d0 >= -self.epsilon {
226                    d2
227                } else if d1 >= -self.epsilon {
228                    d0
229                } else {
230                    d1
231                };
232
233                let t1 = d_front / (d_front - d_back1);
234                let t2 = d_front / (d_front - d_back2);
235
236                let p1 = front + (back1 - front) * t1;
237                let p2 = front + (back2 - front) * t2;
238
239                ClipResult::Split(smallvec::smallvec![Triangle::new(front, p1, p2)])
240            }
241
242            // Two vertices in front - create 2 triangles
243            2 => {
244                let (front1, front2, back) = if d0 < -self.epsilon {
245                    (triangle.v1, triangle.v2, triangle.v0)
246                } else if d1 < -self.epsilon {
247                    (triangle.v2, triangle.v0, triangle.v1)
248                } else {
249                    (triangle.v0, triangle.v1, triangle.v2)
250                };
251
252                // Interpolate to find intersection points
253                let d_back = if d0 < -self.epsilon {
254                    d0
255                } else if d1 < -self.epsilon {
256                    d1
257                } else {
258                    d2
259                };
260                let d_front1 = if d0 < -self.epsilon {
261                    d1
262                } else if d1 < -self.epsilon {
263                    d2
264                } else {
265                    d0
266                };
267                let d_front2 = if d0 < -self.epsilon {
268                    d2
269                } else if d1 < -self.epsilon {
270                    d0
271                } else {
272                    d1
273                };
274
275                let t1 = d_front1 / (d_front1 - d_back);
276                let t2 = d_front2 / (d_front2 - d_back);
277
278                let p1 = front1 + (back - front1) * t1;
279                let p2 = front2 + (back - front2) * t2;
280
281                ClipResult::Split(smallvec::smallvec![
282                    Triangle::new(front1, front2, p1),
283                    Triangle::new(front2, p2, p1),
284                ])
285            }
286
287            _ => unreachable!(),
288        }
289    }
290
291    /// Box subtraction - removes everything inside the box from the mesh
292    /// Uses proper CSG difference operation via subtract_mesh
293    pub fn subtract_box(&self, mesh: &Mesh, min: Point3<f64>, max: Point3<f64>) -> Result<Mesh> {
294        // Fast path: if mesh is empty, return empty mesh
295        if mesh.is_empty() {
296            return Ok(Mesh::new());
297        }
298
299        // Create a box mesh from the AABB bounds
300        let box_mesh = aabb_to_mesh(min, max);
301
302        // Use the CSG difference operation (mesh - box)
303        self.subtract_mesh(mesh, &box_mesh)
304    }
305
306    /// Extract opening profile from mesh (find largest face)
307    /// Returns profile points as an ordered contour and the face normal
308    /// Uses boundary extraction via edge counting to produce stable results
309    #[allow(dead_code)]
310    fn extract_opening_profile(
311        &self,
312        opening_mesh: &Mesh,
313    ) -> Option<(Vec<Point3<f64>>, Vector3<f64>)> {
314        if opening_mesh.is_empty() {
315            return None;
316        }
317
318        // Group triangles by normal to find faces
319        let mut face_groups: FxHashMap<u64, Vec<(Point3<f64>, Point3<f64>, Point3<f64>)>> =
320            FxHashMap::default();
321        let normal_epsilon = 0.01; // Tolerance for normal comparison
322
323        let vertex_count = opening_mesh.positions.len() / 3;
324        for i in (0..opening_mesh.indices.len()).step_by(3) {
325            if i + 2 >= opening_mesh.indices.len() {
326                break;
327            }
328            let i0 = opening_mesh.indices[i] as usize;
329            let i1 = opening_mesh.indices[i + 1] as usize;
330            let i2 = opening_mesh.indices[i + 2] as usize;
331
332            // Bounds check vertex indices against positions
333            if i0 >= vertex_count || i1 >= vertex_count || i2 >= vertex_count {
334                continue;
335            }
336
337            let v0 = Point3::new(
338                opening_mesh.positions[i0 * 3] as f64,
339                opening_mesh.positions[i0 * 3 + 1] as f64,
340                opening_mesh.positions[i0 * 3 + 2] as f64,
341            );
342            let v1 = Point3::new(
343                opening_mesh.positions[i1 * 3] as f64,
344                opening_mesh.positions[i1 * 3 + 1] as f64,
345                opening_mesh.positions[i1 * 3 + 2] as f64,
346            );
347            let v2 = Point3::new(
348                opening_mesh.positions[i2 * 3] as f64,
349                opening_mesh.positions[i2 * 3 + 1] as f64,
350                opening_mesh.positions[i2 * 3 + 2] as f64,
351            );
352
353            let edge1 = v1 - v0;
354            let edge2 = v2 - v0;
355            // Use try_normalize to handle degenerate triangles
356            let normal = match edge1.cross(&edge2).try_normalize(1e-10) {
357                Some(n) => n,
358                None => continue, // Skip degenerate triangles
359            };
360
361            // Quantize normal for grouping (round to nearest 0.01)
362            let nx = (normal.x / normal_epsilon).round() as i32;
363            let ny = (normal.y / normal_epsilon).round() as i32;
364            let nz = (normal.z / normal_epsilon).round() as i32;
365            let key = ((nx as u64) << 32) | ((ny as u32 as u64) << 16) | (nz as u32 as u64);
366
367            face_groups.entry(key).or_default().push((v0, v1, v2));
368        }
369
370        // Find largest face group (most triangles = largest face)
371        let largest_face = face_groups
372            .iter()
373            .max_by_key(|(_, triangles)| triangles.len())?;
374
375        let triangles = largest_face.1;
376        if triangles.is_empty() {
377            return None;
378        }
379
380        // Build edge count map to find boundary edges
381        // An edge is a boundary if it appears exactly once (not shared between triangles)
382        // Use quantized vertex positions as keys
383        let quantize = |p: &Point3<f64>| -> (i64, i64, i64) {
384            let scale = 1e6; // Quantize to micrometer precision
385            (
386                (p.x * scale).round() as i64,
387                (p.y * scale).round() as i64,
388                (p.z * scale).round() as i64,
389            )
390        };
391
392        // Edge key: ordered pair of quantized vertices (smaller first for consistency)
393        let make_edge_key =
394            |a: (i64, i64, i64), b: (i64, i64, i64)| -> ((i64, i64, i64), (i64, i64, i64)) {
395                if a < b {
396                    (a, b)
397                } else {
398                    (b, a)
399                }
400            };
401
402        // Count edges and store original vertices
403        let mut edge_count: FxHashMap<
404            ((i64, i64, i64), (i64, i64, i64)),
405            (usize, Point3<f64>, Point3<f64>),
406        > = FxHashMap::default();
407
408        for (v0, v1, v2) in triangles {
409            let q0 = quantize(v0);
410            let q1 = quantize(v1);
411            let q2 = quantize(v2);
412
413            // Three edges per triangle
414            for (qa, qb, pa, pb) in [
415                (q0, q1, *v0, *v1),
416                (q1, q2, *v1, *v2),
417                (q2, q0, *v2, *v0),
418            ] {
419                let key = make_edge_key(qa, qb);
420                edge_count
421                    .entry(key)
422                    .and_modify(|(count, _, _)| *count += 1)
423                    .or_insert((1, pa, pb));
424            }
425        }
426
427        // Collect boundary edges (count == 1)
428        let mut boundary_edges: Vec<(Point3<f64>, Point3<f64>)> = Vec::new();
429        for (_, (count, pa, pb)) in &edge_count {
430            if *count == 1 {
431                boundary_edges.push((*pa, *pb));
432            }
433        }
434
435        if boundary_edges.is_empty() {
436            // No boundary found (closed surface with no edges) - fall back to using centroid
437            return None;
438        }
439
440        // Build vertex adjacency map for boundary traversal
441        let mut adjacency: FxHashMap<(i64, i64, i64), Vec<(i64, i64, i64, Point3<f64>)>> =
442            FxHashMap::default();
443        for (pa, pb) in &boundary_edges {
444            let qa = quantize(pa);
445            let qb = quantize(pb);
446            adjacency.entry(qa).or_default().push((qb.0, qb.1, qb.2, *pb));
447            adjacency.entry(qb).or_default().push((qa.0, qa.1, qa.2, *pa));
448        }
449
450        // Build ordered contour by walking the boundary
451        let mut contour: Vec<Point3<f64>> = Vec::new();
452        let mut visited: FxHashMap<(i64, i64, i64), bool> = FxHashMap::default();
453
454        // Start from first boundary edge
455        if let Some((start_p, _)) = boundary_edges.first() {
456            let start_q = quantize(start_p);
457            contour.push(*start_p);
458            visited.insert(start_q, true);
459
460            let mut current_q = start_q;
461
462            // Walk around the boundary
463            loop {
464                let neighbors = match adjacency.get(&current_q) {
465                    Some(n) => n,
466                    None => break,
467                };
468
469                // Find unvisited neighbor
470                let mut found_next = false;
471                for (nqx, nqy, nqz, np) in neighbors {
472                    let nq = (*nqx, *nqy, *nqz);
473                    if !visited.get(&nq).unwrap_or(&false) {
474                        contour.push(*np);
475                        visited.insert(nq, true);
476                        current_q = nq;
477                        found_next = true;
478                        break;
479                    }
480                }
481
482                if !found_next {
483                    break; // Closed loop or no more unvisited neighbors
484                }
485            }
486        }
487
488        if contour.len() < 3 {
489            // Not enough points for a valid polygon
490            return None;
491        }
492
493        // Calculate normal from the ordered contour
494        let normal = calculate_polygon_normal(&contour);
495
496        // Normalize the result
497        let normalized_normal = match normal.try_normalize(1e-10) {
498            Some(n) => n,
499            None => return None, // Degenerate polygon
500        };
501
502        Some((contour, normalized_normal))
503    }
504
505    /// Convert our Mesh format to csgrs Mesh format
506    fn mesh_to_csgrs(mesh: &Mesh) -> Result<csgrs::mesh::Mesh<()>> {
507        use csgrs::mesh::{polygon::Polygon, vertex::Vertex, Mesh as CSGMesh};
508        use std::sync::OnceLock;
509
510        if mesh.is_empty() {
511            return Ok(CSGMesh {
512                polygons: Vec::new(),
513                bounding_box: OnceLock::new(),
514                metadata: None,
515            });
516        }
517
518        // Validate mesh has enough indices for at least one triangle
519        if mesh.indices.len() < 3 {
520            return Ok(CSGMesh {
521                polygons: Vec::new(),
522                bounding_box: OnceLock::new(),
523                metadata: None,
524            });
525        }
526
527        let vertex_count = mesh.positions.len() / 3;
528        let triangle_count = mesh.indices.len() / 3;
529
530        // Pre-allocate for expected number of triangles (avoids reallocations)
531        let mut polygons = Vec::with_capacity(triangle_count);
532
533        // Process each triangle using chunks_exact to ensure bounds safety
534        // (handles the case where indices.len() is not divisible by 3)
535        for chunk in mesh.indices.chunks_exact(3) {
536            let i0 = chunk[0] as usize;
537            let i1 = chunk[1] as usize;
538            let i2 = chunk[2] as usize;
539
540            // Bounds check for vertex indices - skip invalid triangles
541            if i0 >= vertex_count || i1 >= vertex_count || i2 >= vertex_count {
542                continue;
543            }
544
545            // Get triangle vertices
546            // Note: bounds are guaranteed by the vertex_count check above
547            let p0_idx = i0 * 3;
548            let p1_idx = i1 * 3;
549            let p2_idx = i2 * 3;
550
551            let v0 = Point3::new(
552                mesh.positions[p0_idx] as f64,
553                mesh.positions[p0_idx + 1] as f64,
554                mesh.positions[p0_idx + 2] as f64,
555            );
556            let v1 = Point3::new(
557                mesh.positions[p1_idx] as f64,
558                mesh.positions[p1_idx + 1] as f64,
559                mesh.positions[p1_idx + 2] as f64,
560            );
561            let v2 = Point3::new(
562                mesh.positions[p2_idx] as f64,
563                mesh.positions[p2_idx + 1] as f64,
564                mesh.positions[p2_idx + 2] as f64,
565            );
566
567            // Skip triangles with NaN or Infinity values
568            if !v0.x.is_finite()
569                || !v0.y.is_finite()
570                || !v0.z.is_finite()
571                || !v1.x.is_finite()
572                || !v1.y.is_finite()
573                || !v1.z.is_finite()
574                || !v2.x.is_finite()
575                || !v2.y.is_finite()
576                || !v2.z.is_finite()
577            {
578                continue;
579            }
580
581            // Calculate face normal from triangle edges
582            // Use try_normalize to handle degenerate (zero-area/collinear) triangles
583            let edge1 = v1 - v0;
584            let edge2 = v2 - v0;
585            let face_normal = match edge1.cross(&edge2).try_normalize(1e-10) {
586                Some(n) => n,
587                None => continue, // Skip degenerate triangles to avoid NaN propagation
588            };
589            // Note: try_normalize returns a unit vector, which is always finite
590
591            // Create csgrs vertices (use face normal for all vertices)
592            let vertices = vec![
593                Vertex::new(v0, face_normal),
594                Vertex::new(v1, face_normal),
595                Vertex::new(v2, face_normal),
596            ];
597
598            polygons.push(Polygon::new(vertices, None));
599        }
600
601        Ok(CSGMesh::from_polygons(&polygons, None))
602    }
603
604    /// Convert csgrs Mesh format back to our Mesh format
605    fn csgrs_to_mesh(csg_mesh: &csgrs::mesh::Mesh<()>) -> Result<Mesh> {
606        let mut mesh = Mesh::new();
607
608        for polygon in &csg_mesh.polygons {
609            let vertices = &polygon.vertices;
610            if vertices.len() < 3 {
611                continue;
612            }
613
614            // Extract 3D positions
615            let points_3d: Vec<Point3<f64>> = vertices
616                .iter()
617                .map(|v| Point3::new(v.pos[0], v.pos[1], v.pos[2]))
618                .collect();
619
620            // Get the CSG polygon's intended normal (from first vertex)
621            // Validate and normalize to avoid NaN propagation in project_to_2d
622            let raw_normal = Vector3::new(
623                vertices[0].normal[0],
624                vertices[0].normal[1],
625                vertices[0].normal[2],
626            );
627
628            // Try to normalize the CSG normal; if it fails (zero or NaN), compute from points
629            let csg_normal = match raw_normal.try_normalize(1e-10) {
630                Some(n) if n.x.is_finite() && n.y.is_finite() && n.z.is_finite() => n,
631                _ => {
632                    // Fall back to computing normal from polygon points
633                    let computed = calculate_polygon_normal(&points_3d);
634                    match computed.try_normalize(1e-10) {
635                        Some(n) => n,
636                        None => continue, // Skip degenerate polygon
637                    }
638                }
639            };
640
641            // FAST PATH: Triangle - no triangulation needed
642            if points_3d.len() == 3 {
643                let base_idx = mesh.vertex_count();
644                for v in vertices {
645                    mesh.add_vertex(v.pos, v.normal);
646                }
647                mesh.add_triangle(
648                    base_idx as u32,
649                    (base_idx + 1) as u32,
650                    (base_idx + 2) as u32,
651                );
652                continue;
653            }
654
655            // Project 3D polygon to 2D using CSG normal (preserves winding intent)
656            let (points_2d, _, _, _) = project_to_2d(&points_3d, &csg_normal);
657
658            // Triangulate (handles convex AND concave polygons)
659            let indices = match triangulate_polygon(&points_2d) {
660                Ok(idx) => idx,
661                Err(_) => continue, // Skip degenerate polygons
662            };
663
664            // Add vertices and create triangles (winding is correct from projection)
665            let base_idx = mesh.vertex_count();
666            for v in vertices {
667                mesh.add_vertex(v.pos, v.normal);
668            }
669
670            for tri in indices.chunks(3) {
671                if tri.len() == 3 {
672                    mesh.add_triangle(
673                        (base_idx + tri[0]) as u32,
674                        (base_idx + tri[1]) as u32,
675                        (base_idx + tri[2]) as u32,
676                    );
677                }
678            }
679        }
680
681        Ok(mesh)
682    }
683
684    /// Check if two meshes' bounding boxes overlap
685    fn bounds_overlap(host_mesh: &Mesh, opening_mesh: &Mesh) -> bool {
686        let (host_min, host_max) = host_mesh.bounds();
687        let (open_min, open_max) = opening_mesh.bounds();
688
689        // Check for overlap in all three dimensions
690        let overlap_x = open_min.x < host_max.x && open_max.x > host_min.x;
691        let overlap_y = open_min.y < host_max.y && open_max.y > host_min.y;
692        let overlap_z = open_min.z < host_max.z && open_max.z > host_min.z;
693
694        overlap_x && overlap_y && overlap_z
695    }
696
697    /// Subtract opening mesh from host mesh using csgrs CSG boolean operations
698    pub fn subtract_mesh(&self, host_mesh: &Mesh, opening_mesh: &Mesh) -> Result<Mesh> {
699        use csgrs::traits::CSG;
700
701        // Validate input meshes - early exit for empty host (no clone needed)
702        if host_mesh.is_empty() {
703            return Ok(Mesh::new());
704        }
705
706        if opening_mesh.is_empty() {
707            return Ok(host_mesh.clone());
708        }
709
710        // Check bounds overlap - early exit if no intersection possible
711        if !Self::bounds_overlap(host_mesh, opening_mesh) {
712            return Ok(host_mesh.clone());
713        }
714
715        // Convert meshes to csgrs format
716        let host_csg = match Self::mesh_to_csgrs(host_mesh) {
717            Ok(csg) => csg,
718            Err(_) => return Ok(host_mesh.clone()),
719        };
720
721        let opening_csg = match Self::mesh_to_csgrs(opening_mesh) {
722            Ok(csg) => csg,
723            Err(_) => return Ok(host_mesh.clone()),
724        };
725
726        // Validate CSG meshes have enough polygons for a valid operation
727        // Empty or near-empty meshes can cause panics in csgrs
728        if host_csg.polygons.is_empty() || opening_csg.polygons.is_empty() {
729            return Ok(host_mesh.clone());
730        }
731
732        // Additional validation: check for degenerate polygons that could cause panics
733        // Skip CSG if either mesh has suspicious polygon counts (too few for a solid)
734        if host_csg.polygons.len() < 4 || opening_csg.polygons.len() < 4 {
735            return Ok(host_mesh.clone());
736        }
737
738        // Safety: skip CSG if combined polygon count risks BSP infinite recursion
739        if host_csg.polygons.len() + opening_csg.polygons.len() > MAX_CSG_POLYGONS {
740            return Ok(host_mesh.clone());
741        }
742
743        // Perform CSG difference (host - opening)
744        let result_csg = host_csg.difference(&opening_csg);
745
746        // Check if result is empty
747        if result_csg.polygons.is_empty() {
748            return Ok(host_mesh.clone());
749        }
750
751        // Convert back to our Mesh format
752        match Self::csgrs_to_mesh(&result_csg) {
753            Ok(result) => {
754                // Clean up degenerate triangles (thin slivers from CSG numerical issues)
755                // Note: We don't use remove_triangles_inside_bounds here because it uses
756                // the opening's bounding box, which can incorrectly remove valid triangles
757                // for complex non-rectangular openings.
758                let cleaned = Self::remove_degenerate_triangles(&result, host_mesh);
759                Ok(cleaned)
760            }
761            Err(_) => Ok(host_mesh.clone())
762        }
763    }
764    
765    /// Remove degenerate triangles from CSG result
766    /// 
767    /// CSG operations can create thin "sliver" triangles at intersection boundaries
768    /// due to numerical precision issues. This function removes triangles that:
769    /// 1. Have very small area (thin slivers)
770    /// 2. Are located inside the original host mesh bounds (not on the surface)
771    fn remove_degenerate_triangles(mesh: &Mesh, host_mesh: &Mesh) -> Mesh {
772        let (host_min, host_max) = host_mesh.bounds();
773        
774        // Convert host bounds to f64 for calculations
775        let host_min_x = host_min.x as f64;
776        let host_min_y = host_min.y as f64;
777        let host_min_z = host_min.z as f64;
778        let host_max_x = host_max.x as f64;
779        let host_max_y = host_max.y as f64;
780        let host_max_z = host_max.z as f64;
781        
782        // Calculate host dimensions to determine appropriate thresholds
783        let host_size_x = (host_max_x - host_min_x).abs();
784        let host_size_y = (host_max_y - host_min_y).abs();
785        let host_size_z = (host_max_z - host_min_z).abs();
786        let min_dim = host_size_x.min(host_size_y).min(host_size_z);
787        
788        // Minimum area threshold - triangles smaller than this are likely artifacts
789        // Use 0.1% of the smallest host dimension squared
790        let min_area = (min_dim * 0.001).powi(2);
791        
792        // Distance threshold for "inside" detection
793        let epsilon = min_dim * 0.01;
794
795        let mut cleaned = Mesh::new();
796
797        // Process each triangle
798        let vert_count = mesh.positions.len() / 3;
799        for i in (0..mesh.indices.len()).step_by(3) {
800            if i + 2 >= mesh.indices.len() {
801                break;
802            }
803            let i0 = mesh.indices[i] as usize;
804            let i1 = mesh.indices[i + 1] as usize;
805            let i2 = mesh.indices[i + 2] as usize;
806
807            // Bounds check vertex indices
808            if i0 >= vert_count || i1 >= vert_count || i2 >= vert_count {
809                continue;
810            }
811
812            // Get vertex positions
813            let v0 = Point3::new(
814                mesh.positions[i0 * 3] as f64,
815                mesh.positions[i0 * 3 + 1] as f64,
816                mesh.positions[i0 * 3 + 2] as f64,
817            );
818            let v1 = Point3::new(
819                mesh.positions[i1 * 3] as f64,
820                mesh.positions[i1 * 3 + 1] as f64,
821                mesh.positions[i1 * 3 + 2] as f64,
822            );
823            let v2 = Point3::new(
824                mesh.positions[i2 * 3] as f64,
825                mesh.positions[i2 * 3 + 1] as f64,
826                mesh.positions[i2 * 3 + 2] as f64,
827            );
828            
829            // Calculate triangle area using cross product
830            let edge1 = v1 - v0;
831            let edge2 = v2 - v0;
832            let cross = edge1.cross(&edge2);
833            let area = cross.norm() / 2.0;
834            
835            // Check if triangle is degenerate (very small area)
836            if area < min_area {
837                continue;
838            }
839            
840            // Check if any vertex is significantly OUTSIDE the host bounds
841            // This catches CSG artifacts that create long thin triangles extending far from the model
842            let expansion = min_dim.max(1.0); // At least 1 meter expansion allowed
843            let far_outside = 
844                v0.x < (host_min_x - expansion) || v0.x > (host_max_x + expansion) ||
845                v0.y < (host_min_y - expansion) || v0.y > (host_max_y + expansion) ||
846                v0.z < (host_min_z - expansion) || v0.z > (host_max_z + expansion) ||
847                v1.x < (host_min_x - expansion) || v1.x > (host_max_x + expansion) ||
848                v1.y < (host_min_y - expansion) || v1.y > (host_max_y + expansion) ||
849                v1.z < (host_min_z - expansion) || v1.z > (host_max_z + expansion) ||
850                v2.x < (host_min_x - expansion) || v2.x > (host_max_x + expansion) ||
851                v2.y < (host_min_y - expansion) || v2.y > (host_max_y + expansion) ||
852                v2.z < (host_min_z - expansion) || v2.z > (host_max_z + expansion);
853            
854            if far_outside {
855                continue;
856            }
857            
858            // Check if triangle center is strictly inside the host bounds
859            // (not on the surface) - these are likely CSG artifacts
860            let center = Point3::new(
861                (v0.x + v1.x + v2.x) / 3.0,
862                (v0.y + v1.y + v2.y) / 3.0,
863                (v0.z + v1.z + v2.z) / 3.0,
864            );
865            
866            // Check if center is inside host bounds (with epsilon margin)
867            let inside_x = center.x > (host_min_x + epsilon) && center.x < (host_max_x - epsilon);
868            let inside_y = center.y > (host_min_y + epsilon) && center.y < (host_max_y - epsilon);
869            let inside_z = center.z > (host_min_z + epsilon) && center.z < (host_max_z - epsilon);
870            
871            // If triangle is strictly inside the host in ALL dimensions, it's likely an artifact
872            // Only remove if it's also relatively small
873            let max_area = min_dim * min_dim * 0.1; // 10% of smallest dimension squared
874            if inside_x && inside_y && inside_z && area < max_area {
875                continue;
876            }
877            
878            // Get normals
879            let n0 = Vector3::new(
880                mesh.normals[i0 * 3] as f64,
881                mesh.normals[i0 * 3 + 1] as f64,
882                mesh.normals[i0 * 3 + 2] as f64,
883            );
884            let n1 = Vector3::new(
885                mesh.normals[i1 * 3] as f64,
886                mesh.normals[i1 * 3 + 1] as f64,
887                mesh.normals[i1 * 3 + 2] as f64,
888            );
889            let n2 = Vector3::new(
890                mesh.normals[i2 * 3] as f64,
891                mesh.normals[i2 * 3 + 1] as f64,
892                mesh.normals[i2 * 3 + 2] as f64,
893            );
894            
895            // Add valid triangle to cleaned mesh
896            let base_idx = cleaned.vertex_count() as u32;
897            cleaned.add_vertex(v0, n0);
898            cleaned.add_vertex(v1, n1);
899            cleaned.add_vertex(v2, n2);
900            cleaned.add_triangle(base_idx, base_idx + 1, base_idx + 2);
901        }
902        
903        cleaned
904    }
905
906    /// Remove triangles that are completely inside the opening bounds
907    ///
908    /// This removes artifact faces that CSG operations may leave inside circular/curved openings.
909    /// Note: Currently unused because it can incorrectly remove valid triangles for complex
910    /// non-rectangular openings. Kept for potential future use with simple rectangular openings.
911    #[allow(dead_code)]
912    fn remove_triangles_inside_bounds(
913        mesh: &Mesh,
914        open_min: Point3<f64>,
915        open_max: Point3<f64>,
916    ) -> Mesh {
917        let mut cleaned = Mesh::new();
918
919        // Process each triangle
920        let vert_count = mesh.positions.len() / 3;
921        for i in (0..mesh.indices.len()).step_by(3) {
922            if i + 2 >= mesh.indices.len() {
923                break;
924            }
925            let i0 = mesh.indices[i] as usize;
926            let i1 = mesh.indices[i + 1] as usize;
927            let i2 = mesh.indices[i + 2] as usize;
928
929            // Bounds check vertex indices
930            if i0 >= vert_count || i1 >= vert_count || i2 >= vert_count {
931                continue;
932            }
933
934            // Get vertex positions
935            let v0 = Point3::new(
936                mesh.positions[i0 * 3] as f64,
937                mesh.positions[i0 * 3 + 1] as f64,
938                mesh.positions[i0 * 3 + 2] as f64,
939            );
940            let v1 = Point3::new(
941                mesh.positions[i1 * 3] as f64,
942                mesh.positions[i1 * 3 + 1] as f64,
943                mesh.positions[i1 * 3 + 2] as f64,
944            );
945            let v2 = Point3::new(
946                mesh.positions[i2 * 3] as f64,
947                mesh.positions[i2 * 3 + 1] as f64,
948                mesh.positions[i2 * 3 + 2] as f64,
949            );
950
951            // Calculate triangle bounding box
952            let tri_min_x = v0.x.min(v1.x).min(v2.x);
953            let tri_max_x = v0.x.max(v1.x).max(v2.x);
954            let tri_min_y = v0.y.min(v1.y).min(v2.y);
955            let tri_max_y = v0.y.max(v1.y).max(v2.y);
956            let tri_min_z = v0.z.min(v1.z).min(v2.z);
957            let tri_max_z = v0.z.max(v1.z).max(v2.z);
958            
959            // Check if triangle is completely inside opening bounds (remove it)
960            if tri_min_x >= open_min.x && tri_max_x <= open_max.x &&
961               tri_min_y >= open_min.y && tri_max_y <= open_max.y &&
962               tri_min_z >= open_min.z && tri_max_z <= open_max.z {
963                // Triangle is inside opening - remove it
964                continue;
965            }
966            
967            // Triangle is not completely inside - keep it
968            let n0 = Vector3::new(
969                mesh.normals[i0 * 3] as f64,
970                mesh.normals[i0 * 3 + 1] as f64,
971                mesh.normals[i0 * 3 + 2] as f64,
972            );
973            let n1 = Vector3::new(
974                mesh.normals[i1 * 3] as f64,
975                mesh.normals[i1 * 3 + 1] as f64,
976                mesh.normals[i1 * 3 + 2] as f64,
977            );
978            let n2 = Vector3::new(
979                mesh.normals[i2 * 3] as f64,
980                mesh.normals[i2 * 3 + 1] as f64,
981                mesh.normals[i2 * 3 + 2] as f64,
982            );
983            
984            let base_idx = cleaned.vertex_count() as u32;
985            cleaned.add_vertex(v0, n0);
986            cleaned.add_vertex(v1, n1);
987            cleaned.add_vertex(v2, n2);
988            cleaned.add_triangle(base_idx, base_idx + 1, base_idx + 2);
989        }
990        
991        cleaned
992    }
993
994    /// Union two meshes together using csgrs CSG boolean operations
995    pub fn union_mesh(&self, mesh_a: &Mesh, mesh_b: &Mesh) -> Result<Mesh> {
996        use csgrs::traits::CSG;
997
998        // Fast paths
999        if mesh_a.is_empty() {
1000            return Ok(mesh_b.clone());
1001        }
1002        if mesh_b.is_empty() {
1003            return Ok(mesh_a.clone());
1004        }
1005
1006        // Convert meshes to csgrs format
1007        let csg_a = Self::mesh_to_csgrs(mesh_a)?;
1008        let csg_b = Self::mesh_to_csgrs(mesh_b)?;
1009
1010        // Validate CSG meshes - fall back to simple merge if invalid
1011        if csg_a.polygons.is_empty() || csg_b.polygons.is_empty() {
1012            let mut merged = mesh_a.clone();
1013            merged.merge(mesh_b);
1014            return Ok(merged);
1015        }
1016
1017        // Skip CSG if either mesh has too few polygons for a valid solid
1018        if csg_a.polygons.len() < 4 || csg_b.polygons.len() < 4 {
1019            let mut merged = mesh_a.clone();
1020            merged.merge(mesh_b);
1021            return Ok(merged);
1022        }
1023
1024        // Safety: skip CSG if combined polygon count risks BSP infinite recursion
1025        if csg_a.polygons.len() + csg_b.polygons.len() > MAX_CSG_POLYGONS {
1026            let mut merged = mesh_a.clone();
1027            merged.merge(mesh_b);
1028            return Ok(merged);
1029        }
1030
1031        // Perform CSG union
1032        let result_csg = csg_a.union(&csg_b);
1033
1034        // Convert back to our Mesh format
1035        Self::csgrs_to_mesh(&result_csg)
1036    }
1037
1038    /// Intersect two meshes using csgrs CSG boolean operations
1039    ///
1040    /// Returns the intersection of two meshes (the volume where both overlap).
1041    pub fn intersection_mesh(&self, mesh_a: &Mesh, mesh_b: &Mesh) -> Result<Mesh> {
1042        use csgrs::traits::CSG;
1043
1044        // Fast paths: intersection with empty mesh is empty
1045        if mesh_a.is_empty() || mesh_b.is_empty() {
1046            return Ok(Mesh::new());
1047        }
1048
1049        // Convert meshes to csgrs format
1050        let csg_a = Self::mesh_to_csgrs(mesh_a)?;
1051        let csg_b = Self::mesh_to_csgrs(mesh_b)?;
1052
1053        // Validate CSG meshes - return empty if invalid
1054        if csg_a.polygons.is_empty() || csg_b.polygons.is_empty() {
1055            return Ok(Mesh::new());
1056        }
1057
1058        // Skip CSG if either mesh has too few polygons for a valid solid
1059        if csg_a.polygons.len() < 4 || csg_b.polygons.len() < 4 {
1060            return Ok(Mesh::new());
1061        }
1062
1063        // Safety: skip CSG if combined polygon count risks BSP infinite recursion
1064        if csg_a.polygons.len() + csg_b.polygons.len() > MAX_CSG_POLYGONS {
1065            return Ok(mesh_a.clone());
1066        }
1067
1068        // Perform CSG intersection
1069        let result_csg = csg_a.intersection(&csg_b);
1070
1071        // Convert back to our Mesh format
1072        Self::csgrs_to_mesh(&result_csg)
1073    }
1074
1075    /// Union multiple meshes together
1076    ///
1077    /// Convenience method that sequentially unions all non-empty meshes.
1078    /// Skips empty meshes to avoid unnecessary CSG operations.
1079    pub fn union_meshes(&self, meshes: &[Mesh]) -> Result<Mesh> {
1080        if meshes.is_empty() {
1081            return Ok(Mesh::new());
1082        }
1083
1084        if meshes.len() == 1 {
1085            return Ok(meshes[0].clone());
1086        }
1087
1088        // Start with first non-empty mesh
1089        let mut result = Mesh::new();
1090        let mut found_first = false;
1091
1092        for mesh in meshes {
1093            if mesh.is_empty() {
1094                continue;
1095            }
1096
1097            if !found_first {
1098                result = mesh.clone();
1099                found_first = true;
1100                continue;
1101            }
1102
1103            result = self.union_mesh(&result, mesh)?;
1104        }
1105
1106        Ok(result)
1107    }
1108
1109    /// Subtract multiple meshes efficiently
1110    ///
1111    /// When void count exceeds threshold, unions all voids first
1112    /// then performs a single subtraction. This is much more efficient
1113    /// for elements with many openings (e.g., floors with many penetrations).
1114    ///
1115    /// # Arguments
1116    /// * `host` - The host mesh to subtract from
1117    /// * `voids` - List of void meshes to subtract
1118    ///
1119    /// # Returns
1120    /// The host mesh with all voids subtracted
1121    pub fn subtract_meshes_batched(&self, host: &Mesh, voids: &[Mesh]) -> Result<Mesh> {
1122        // Filter out empty meshes
1123        let non_empty_voids: Vec<&Mesh> = voids.iter().filter(|m| !m.is_empty()).collect();
1124
1125        if non_empty_voids.is_empty() {
1126            return Ok(host.clone());
1127        }
1128
1129        if non_empty_voids.len() == 1 {
1130            return self.subtract_mesh(host, non_empty_voids[0]);
1131        }
1132
1133        // Threshold for batching: if more than 10 voids, union them first
1134        const BATCH_THRESHOLD: usize = 10;
1135
1136        if non_empty_voids.len() > BATCH_THRESHOLD {
1137            // Union all voids into a single mesh first
1138            let void_refs: Vec<Mesh> = non_empty_voids.iter().map(|m| (*m).clone()).collect();
1139            let combined = self.union_meshes(&void_refs)?;
1140
1141            // Single subtraction
1142            self.subtract_mesh(host, &combined)
1143        } else {
1144            // Sequential subtraction for small counts
1145            let mut result = host.clone();
1146
1147            for void in non_empty_voids {
1148                result = self.subtract_mesh(&result, void)?;
1149            }
1150
1151            Ok(result)
1152        }
1153    }
1154
1155    /// Subtract meshes with fallback on failure
1156    ///
1157    /// Attempts batched subtraction, but if it fails, returns the host mesh
1158    /// unchanged rather than propagating the error. This provides graceful
1159    /// degradation for problematic void geometries.
1160    pub fn subtract_meshes_with_fallback(&self, host: &Mesh, voids: &[Mesh]) -> Mesh {
1161        match self.subtract_meshes_batched(host, voids) {
1162            Ok(result) => {
1163                // Validate result
1164                if result.is_empty() || !self.validate_mesh(&result) {
1165                    host.clone()
1166                } else {
1167                    result
1168                }
1169            }
1170            Err(_) => host.clone(),
1171        }
1172    }
1173
1174    /// Validate mesh for common issues
1175    fn validate_mesh(&self, mesh: &Mesh) -> bool {
1176        // Check for NaN/Inf in positions
1177        if mesh.positions.iter().any(|v| !v.is_finite()) {
1178            return false;
1179        }
1180
1181        // Check for NaN/Inf in normals
1182        if mesh.normals.iter().any(|v| !v.is_finite()) {
1183            return false;
1184        }
1185
1186        // Check for valid triangle indices
1187        let vertex_count = mesh.vertex_count();
1188        for idx in &mesh.indices {
1189            if *idx as usize >= vertex_count {
1190                return false;
1191            }
1192        }
1193
1194        true
1195    }
1196
1197    /// Clip mesh using bounding box (6 planes) - DEPRECATED: use subtract_box() instead
1198    /// Subtracts everything inside the box from the mesh
1199    #[deprecated(note = "Use subtract_box() for better performance")]
1200    pub fn clip_mesh_with_box(
1201        &self,
1202        mesh: &Mesh,
1203        min: Point3<f64>,
1204        max: Point3<f64>,
1205    ) -> Result<Mesh> {
1206        self.subtract_box(mesh, min, max)
1207    }
1208
1209    /// Clip an entire mesh against a plane
1210    pub fn clip_mesh(&self, mesh: &Mesh, plane: &Plane) -> Result<Mesh> {
1211        let mut result = Mesh::new();
1212
1213        // Process each triangle
1214        let vert_count = mesh.positions.len() / 3;
1215        for i in (0..mesh.indices.len()).step_by(3) {
1216            if i + 2 >= mesh.indices.len() {
1217                break;
1218            }
1219            let i0 = mesh.indices[i] as usize;
1220            let i1 = mesh.indices[i + 1] as usize;
1221            let i2 = mesh.indices[i + 2] as usize;
1222
1223            // Bounds check vertex indices
1224            if i0 >= vert_count || i1 >= vert_count || i2 >= vert_count {
1225                continue;
1226            }
1227
1228            // Get triangle vertices
1229            let v0 = Point3::new(
1230                mesh.positions[i0 * 3] as f64,
1231                mesh.positions[i0 * 3 + 1] as f64,
1232                mesh.positions[i0 * 3 + 2] as f64,
1233            );
1234            let v1 = Point3::new(
1235                mesh.positions[i1 * 3] as f64,
1236                mesh.positions[i1 * 3 + 1] as f64,
1237                mesh.positions[i1 * 3 + 2] as f64,
1238            );
1239            let v2 = Point3::new(
1240                mesh.positions[i2 * 3] as f64,
1241                mesh.positions[i2 * 3 + 1] as f64,
1242                mesh.positions[i2 * 3 + 2] as f64,
1243            );
1244
1245            let triangle = Triangle::new(v0, v1, v2);
1246
1247            // Clip triangle
1248            match self.clip_triangle(&triangle, plane) {
1249                ClipResult::AllFront(tri) => {
1250                    // Keep original triangle
1251                    add_triangle_to_mesh(&mut result, &tri);
1252                }
1253                ClipResult::AllBehind => {
1254                    // Discard triangle
1255                }
1256                ClipResult::Split(triangles) => {
1257                    // Add clipped triangles
1258                    for tri in triangles {
1259                        add_triangle_to_mesh(&mut result, &tri);
1260                    }
1261                }
1262            }
1263        }
1264
1265        Ok(result)
1266    }
1267}
1268
1269impl Default for ClippingProcessor {
1270    fn default() -> Self {
1271        Self::new()
1272    }
1273}
1274
1275/// Add a triangle to a mesh
1276fn add_triangle_to_mesh(mesh: &mut Mesh, triangle: &Triangle) {
1277    let base_idx = mesh.vertex_count() as u32;
1278
1279    // Calculate normal
1280    let normal = triangle.normal();
1281
1282    // Add vertices
1283    mesh.add_vertex(triangle.v0, normal);
1284    mesh.add_vertex(triangle.v1, normal);
1285    mesh.add_vertex(triangle.v2, normal);
1286
1287    // Add triangle
1288    mesh.add_triangle(base_idx, base_idx + 1, base_idx + 2);
1289}
1290
1291/// Calculate smooth normals for a mesh
1292#[inline]
1293pub fn calculate_normals(mesh: &mut Mesh) {
1294    let vertex_count = mesh.vertex_count();
1295    if vertex_count == 0 {
1296        return;
1297    }
1298
1299    let positions_len = mesh.positions.len();
1300
1301    // Initialize normals to zero
1302    let mut normals = vec![Vector3::zeros(); vertex_count];
1303
1304    // Accumulate face normals
1305    for i in (0..mesh.indices.len()).step_by(3) {
1306        // Bounds check for indices array
1307        if i + 2 >= mesh.indices.len() {
1308            break;
1309        }
1310
1311        let i0 = mesh.indices[i] as usize;
1312        let i1 = mesh.indices[i + 1] as usize;
1313        let i2 = mesh.indices[i + 2] as usize;
1314
1315        // Bounds check for vertex indices - skip invalid triangles
1316        if i0 >= vertex_count || i1 >= vertex_count || i2 >= vertex_count {
1317            continue;
1318        }
1319        if i0 * 3 + 2 >= positions_len || i1 * 3 + 2 >= positions_len || i2 * 3 + 2 >= positions_len
1320        {
1321            continue;
1322        }
1323
1324        // Get triangle vertices
1325        let v0 = Point3::new(
1326            mesh.positions[i0 * 3] as f64,
1327            mesh.positions[i0 * 3 + 1] as f64,
1328            mesh.positions[i0 * 3 + 2] as f64,
1329        );
1330        let v1 = Point3::new(
1331            mesh.positions[i1 * 3] as f64,
1332            mesh.positions[i1 * 3 + 1] as f64,
1333            mesh.positions[i1 * 3 + 2] as f64,
1334        );
1335        let v2 = Point3::new(
1336            mesh.positions[i2 * 3] as f64,
1337            mesh.positions[i2 * 3 + 1] as f64,
1338            mesh.positions[i2 * 3 + 2] as f64,
1339        );
1340
1341        // Calculate face normal
1342        let edge1 = v1 - v0;
1343        let edge2 = v2 - v0;
1344        let normal = edge1.cross(&edge2);
1345
1346        // Accumulate normal for each vertex
1347        normals[i0] += normal;
1348        normals[i1] += normal;
1349        normals[i2] += normal;
1350    }
1351
1352    // Normalize and write back
1353    mesh.normals.clear();
1354    mesh.normals.reserve(vertex_count * 3);
1355
1356    for normal in normals {
1357        let normalized = normal.try_normalize(1e-6).unwrap_or_else(|| Vector3::new(0.0, 0.0, 1.0));
1358        mesh.normals.push(normalized.x as f32);
1359        mesh.normals.push(normalized.y as f32);
1360        mesh.normals.push(normalized.z as f32);
1361    }
1362}
1363
1364#[cfg(test)]
1365mod tests {
1366    use super::*;
1367
1368    #[test]
1369    fn test_plane_signed_distance() {
1370        let plane = Plane::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
1371
1372        assert_eq!(plane.signed_distance(&Point3::new(0.0, 0.0, 5.0)), 5.0);
1373        assert_eq!(plane.signed_distance(&Point3::new(0.0, 0.0, -5.0)), -5.0);
1374        assert_eq!(plane.signed_distance(&Point3::new(5.0, 5.0, 0.0)), 0.0);
1375    }
1376
1377    #[test]
1378    fn test_clip_triangle_all_front() {
1379        let processor = ClippingProcessor::new();
1380        let triangle = Triangle::new(
1381            Point3::new(0.0, 0.0, 1.0),
1382            Point3::new(1.0, 0.0, 1.0),
1383            Point3::new(0.5, 1.0, 1.0),
1384        );
1385        let plane = Plane::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
1386
1387        match processor.clip_triangle(&triangle, &plane) {
1388            ClipResult::AllFront(_) => {}
1389            _ => panic!("Expected AllFront"),
1390        }
1391    }
1392
1393    #[test]
1394    fn test_clip_triangle_all_behind() {
1395        let processor = ClippingProcessor::new();
1396        let triangle = Triangle::new(
1397            Point3::new(0.0, 0.0, -1.0),
1398            Point3::new(1.0, 0.0, -1.0),
1399            Point3::new(0.5, 1.0, -1.0),
1400        );
1401        let plane = Plane::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
1402
1403        match processor.clip_triangle(&triangle, &plane) {
1404            ClipResult::AllBehind => {}
1405            _ => panic!("Expected AllBehind"),
1406        }
1407    }
1408
1409    #[test]
1410    fn test_clip_triangle_split_one_front() {
1411        let processor = ClippingProcessor::new();
1412        let triangle = Triangle::new(
1413            Point3::new(0.0, 0.0, 1.0),  // Front
1414            Point3::new(1.0, 0.0, -1.0), // Behind
1415            Point3::new(0.5, 1.0, -1.0), // Behind
1416        );
1417        let plane = Plane::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
1418
1419        match processor.clip_triangle(&triangle, &plane) {
1420            ClipResult::Split(triangles) => {
1421                assert_eq!(triangles.len(), 1);
1422            }
1423            _ => panic!("Expected Split"),
1424        }
1425    }
1426
1427    #[test]
1428    fn test_clip_triangle_split_two_front() {
1429        let processor = ClippingProcessor::new();
1430        let triangle = Triangle::new(
1431            Point3::new(0.0, 0.0, 1.0),  // Front
1432            Point3::new(1.0, 0.0, 1.0),  // Front
1433            Point3::new(0.5, 1.0, -1.0), // Behind
1434        );
1435        let plane = Plane::new(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
1436
1437        match processor.clip_triangle(&triangle, &plane) {
1438            ClipResult::Split(triangles) => {
1439                assert_eq!(triangles.len(), 2);
1440            }
1441            _ => panic!("Expected Split with 2 triangles"),
1442        }
1443    }
1444
1445    #[test]
1446    fn test_triangle_normal() {
1447        let triangle = Triangle::new(
1448            Point3::new(0.0, 0.0, 0.0),
1449            Point3::new(1.0, 0.0, 0.0),
1450            Point3::new(0.0, 1.0, 0.0),
1451        );
1452
1453        let normal = triangle.normal();
1454        assert!((normal.z - 1.0).abs() < 1e-6);
1455    }
1456
1457    #[test]
1458    fn test_triangle_area() {
1459        let triangle = Triangle::new(
1460            Point3::new(0.0, 0.0, 0.0),
1461            Point3::new(1.0, 0.0, 0.0),
1462            Point3::new(0.0, 1.0, 0.0),
1463        );
1464
1465        let area = triangle.area();
1466        assert!((area - 0.5).abs() < 1e-6);
1467    }
1468}