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