mesh_repair/
boolean.rs

1//! Mesh boolean operations.
2//!
3//! This module provides constructive solid geometry (CSG) operations for meshes,
4//! including union, difference, and intersection.
5//!
6//! # Operations
7//!
8//! - **Union**: Combines two meshes into one (A ∪ B)
9//! - **Difference**: Subtracts one mesh from another (A - B)
10//! - **Intersection**: Keeps only the overlapping region (A ∩ B)
11//!
12//! # Robustness Features
13//!
14//! - **Coplanar face handling**: Detects and handles triangles that lie in the same plane
15//! - **Non-manifold repair**: Detects and fixes non-manifold edges in boolean results
16//! - **BVH acceleration**: Uses bounding volume hierarchy for fast intersection queries
17//! - **Robust predicates**: Uses epsilon-based comparisons to handle numerical precision
18//!
19//! # Example
20//!
21//! ```
22//! use mesh_repair::{Mesh, Vertex};
23//! use mesh_repair::boolean::{BooleanOp, BooleanParams, boolean_operation};
24//!
25//! // Create two simple meshes
26//! let mut mesh_a = Mesh::new();
27//! // ... add vertices and faces ...
28//!
29//! let mut mesh_b = Mesh::new();
30//! // ... add vertices and faces ...
31//!
32//! // Perform union
33//! // let result = boolean_operation(&mesh_a, &mesh_b, BooleanOp::Union, &BooleanParams::default());
34//! ```
35
36use crate::{Mesh, MeshError, MeshResult, Vertex};
37use nalgebra::{Point3, Vector3};
38use std::collections::{HashMap, HashSet};
39
40/// Boolean operation type.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum BooleanOp {
43    /// Union: A ∪ B (combines both meshes).
44    Union,
45
46    /// Difference: A - B (subtracts B from A).
47    Difference,
48
49    /// Intersection: A ∩ B (keeps only overlapping region).
50    Intersection,
51}
52
53/// Parameters for boolean operations.
54#[derive(Debug, Clone)]
55pub struct BooleanParams {
56    /// Tolerance for point comparisons.
57    pub tolerance: f64,
58
59    /// Whether to clean up result mesh (remove duplicates, fix winding).
60    pub cleanup: bool,
61
62    /// Whether to triangulate non-planar faces.
63    pub triangulate: bool,
64
65    /// Handle coplanar face strategy.
66    pub coplanar_strategy: CoplanarStrategy,
67}
68
69impl Default for BooleanParams {
70    fn default() -> Self {
71        Self {
72            tolerance: 1e-8,
73            cleanup: true,
74            triangulate: true,
75            coplanar_strategy: CoplanarStrategy::Include,
76        }
77    }
78}
79
80impl BooleanParams {
81    /// Create params with high tolerance for noisy meshes.
82    pub fn for_scans() -> Self {
83        Self {
84            tolerance: 1e-5,
85            cleanup: true,
86            ..Default::default()
87        }
88    }
89
90    /// Create params for precise CAD operations.
91    pub fn for_cad() -> Self {
92        Self {
93            tolerance: 1e-10,
94            cleanup: true,
95            ..Default::default()
96        }
97    }
98}
99
100/// Strategy for handling coplanar faces.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum CoplanarStrategy {
103    /// Include coplanar faces from first mesh.
104    Include,
105
106    /// Exclude coplanar faces.
107    Exclude,
108
109    /// Keep both (may produce non-manifold).
110    KeepBoth,
111}
112
113/// Result of a boolean operation.
114#[derive(Debug)]
115pub struct BooleanResult {
116    /// Resulting mesh.
117    pub mesh: Mesh,
118
119    /// Number of intersection edges found.
120    pub intersection_edge_count: usize,
121
122    /// Number of new vertices created.
123    pub new_vertex_count: usize,
124
125    /// Whether any coplanar faces were detected.
126    pub had_coplanar_faces: bool,
127
128    /// Statistics about the operation.
129    pub stats: BooleanStats,
130}
131
132/// Statistics from boolean operation.
133#[derive(Debug, Clone, Default)]
134pub struct BooleanStats {
135    /// Faces from mesh A in result.
136    pub faces_from_a: usize,
137
138    /// Faces from mesh B in result.
139    pub faces_from_b: usize,
140
141    /// Faces split during operation.
142    pub faces_split: usize,
143
144    /// Coplanar face pairs detected.
145    pub coplanar_pairs: usize,
146
147    /// Non-manifold edges detected and fixed.
148    pub non_manifold_edges_fixed: usize,
149
150    /// Time taken for each phase (optional).
151    pub phase_times_ms: Vec<(String, f64)>,
152}
153
154// ============================================================================
155// BVH (Bounding Volume Hierarchy) for acceleration
156// ============================================================================
157
158/// Axis-aligned bounding box for BVH.
159#[derive(Debug, Clone)]
160struct Aabb {
161    min: Point3<f64>,
162    max: Point3<f64>,
163}
164
165impl Aabb {
166    fn new() -> Self {
167        Self {
168            min: Point3::new(f64::MAX, f64::MAX, f64::MAX),
169            max: Point3::new(f64::MIN, f64::MIN, f64::MIN),
170        }
171    }
172
173    fn from_triangle(v0: &Point3<f64>, v1: &Point3<f64>, v2: &Point3<f64>) -> Self {
174        Self {
175            min: Point3::new(
176                v0.x.min(v1.x).min(v2.x),
177                v0.y.min(v1.y).min(v2.y),
178                v0.z.min(v1.z).min(v2.z),
179            ),
180            max: Point3::new(
181                v0.x.max(v1.x).max(v2.x),
182                v0.y.max(v1.y).max(v2.y),
183                v0.z.max(v1.z).max(v2.z),
184            ),
185        }
186    }
187
188    fn expand(&mut self, other: &Aabb) {
189        self.min.x = self.min.x.min(other.min.x);
190        self.min.y = self.min.y.min(other.min.y);
191        self.min.z = self.min.z.min(other.min.z);
192        self.max.x = self.max.x.max(other.max.x);
193        self.max.y = self.max.y.max(other.max.y);
194        self.max.z = self.max.z.max(other.max.z);
195    }
196
197    fn intersects(&self, other: &Aabb, tolerance: f64) -> bool {
198        !(self.max.x + tolerance < other.min.x
199            || other.max.x + tolerance < self.min.x
200            || self.max.y + tolerance < other.min.y
201            || other.max.y + tolerance < self.min.y
202            || self.max.z + tolerance < other.min.z
203            || other.max.z + tolerance < self.min.z)
204    }
205
206    fn center(&self) -> Point3<f64> {
207        Point3::new(
208            (self.min.x + self.max.x) * 0.5,
209            (self.min.y + self.max.y) * 0.5,
210            (self.min.z + self.max.z) * 0.5,
211        )
212    }
213
214    fn longest_axis(&self) -> usize {
215        let dx = self.max.x - self.min.x;
216        let dy = self.max.y - self.min.y;
217        let dz = self.max.z - self.min.z;
218        if dx >= dy && dx >= dz {
219            0
220        } else if dy >= dz {
221            1
222        } else {
223            2
224        }
225    }
226}
227
228/// BVH node for acceleration.
229#[derive(Debug)]
230enum BvhNode {
231    Leaf {
232        bbox: Aabb,
233        triangles: Vec<usize>,
234    },
235    Internal {
236        bbox: Aabb,
237        left: Box<BvhNode>,
238        right: Box<BvhNode>,
239    },
240}
241
242/// BVH tree for fast intersection queries.
243struct Bvh {
244    root: Option<BvhNode>,
245}
246
247impl Bvh {
248    /// Build a BVH from mesh triangles.
249    fn build(mesh: &Mesh, max_leaf_size: usize) -> Self {
250        if mesh.faces.is_empty() {
251            return Self { root: None };
252        }
253
254        // Build list of triangle indices with bounding boxes
255        let triangles: Vec<(usize, Aabb)> = mesh
256            .faces
257            .iter()
258            .enumerate()
259            .map(|(i, face)| {
260                let v0 = &mesh.vertices[face[0] as usize].position;
261                let v1 = &mesh.vertices[face[1] as usize].position;
262                let v2 = &mesh.vertices[face[2] as usize].position;
263                (i, Aabb::from_triangle(v0, v1, v2))
264            })
265            .collect();
266
267        let indices: Vec<usize> = (0..triangles.len()).collect();
268        let root = Self::build_recursive(&triangles, indices, max_leaf_size);
269
270        Self { root: Some(root) }
271    }
272
273    fn build_recursive(
274        triangles: &[(usize, Aabb)],
275        indices: Vec<usize>,
276        max_leaf_size: usize,
277    ) -> BvhNode {
278        // Compute bounding box of all triangles
279        let mut bbox = Aabb::new();
280        for &i in &indices {
281            bbox.expand(&triangles[i].1);
282        }
283
284        // If few enough triangles, make a leaf
285        if indices.len() <= max_leaf_size {
286            let triangle_indices: Vec<usize> = indices.iter().map(|&i| triangles[i].0).collect();
287            return BvhNode::Leaf {
288                bbox,
289                triangles: triangle_indices,
290            };
291        }
292
293        // Split along longest axis
294        let axis = bbox.longest_axis();
295        let mut sorted_indices = indices;
296        sorted_indices.sort_by(|&a, &b| {
297            let ca = triangles[a].1.center();
298            let cb = triangles[b].1.center();
299            let va = match axis {
300                0 => ca.x,
301                1 => ca.y,
302                _ => ca.z,
303            };
304            let vb = match axis {
305                0 => cb.x,
306                1 => cb.y,
307                _ => cb.z,
308            };
309            va.partial_cmp(&vb).unwrap_or(std::cmp::Ordering::Equal)
310        });
311
312        let mid = sorted_indices.len() / 2;
313        let left_indices: Vec<usize> = sorted_indices[..mid].to_vec();
314        let right_indices: Vec<usize> = sorted_indices[mid..].to_vec();
315
316        let left = Self::build_recursive(triangles, left_indices, max_leaf_size);
317        let right = Self::build_recursive(triangles, right_indices, max_leaf_size);
318
319        BvhNode::Internal {
320            bbox,
321            left: Box::new(left),
322            right: Box::new(right),
323        }
324    }
325
326    /// Find all triangles that might intersect the given bounding box.
327    fn query(&self, query_bbox: &Aabb, tolerance: f64) -> Vec<usize> {
328        let mut result = Vec::new();
329        if let Some(ref root) = self.root {
330            Self::query_recursive(root, query_bbox, tolerance, &mut result);
331        }
332        result
333    }
334
335    fn query_recursive(node: &BvhNode, query_bbox: &Aabb, tolerance: f64, result: &mut Vec<usize>) {
336        match node {
337            BvhNode::Leaf { bbox, triangles } => {
338                if bbox.intersects(query_bbox, tolerance) {
339                    result.extend(triangles.iter().copied());
340                }
341            }
342            BvhNode::Internal { bbox, left, right } => {
343                if bbox.intersects(query_bbox, tolerance) {
344                    Self::query_recursive(left, query_bbox, tolerance, result);
345                    Self::query_recursive(right, query_bbox, tolerance, result);
346                }
347            }
348        }
349    }
350}
351
352// ============================================================================
353// Robust geometric predicates
354// ============================================================================
355
356/// Result of coplanarity test.
357#[derive(Debug, Clone, Copy, PartialEq, Eq)]
358enum CoplanarityResult {
359    /// Triangles are not coplanar.
360    NotCoplanar,
361    /// Triangles are coplanar with same orientation.
362    CoplanarSameOrientation,
363    /// Triangles are coplanar with opposite orientation.
364    CoplanarOppositeOrientation,
365}
366
367/// Check if two triangles are coplanar.
368fn check_coplanarity(
369    a0: &Point3<f64>,
370    a1: &Point3<f64>,
371    a2: &Point3<f64>,
372    b0: &Point3<f64>,
373    b1: &Point3<f64>,
374    b2: &Point3<f64>,
375    tolerance: f64,
376) -> CoplanarityResult {
377    // Compute normal of triangle A
378    let edge1_a = a1 - a0;
379    let edge2_a = a2 - a0;
380    let normal_a = edge1_a.cross(&edge2_a);
381    let normal_a_len = normal_a.norm();
382
383    if normal_a_len < tolerance {
384        // Degenerate triangle A
385        return CoplanarityResult::NotCoplanar;
386    }
387
388    let normal_a = normal_a / normal_a_len;
389
390    // Check if all vertices of B are on the plane of A
391    let d_a = normal_a.dot(&a0.coords);
392    let dist_b0 = (normal_a.dot(&b0.coords) - d_a).abs();
393    let dist_b1 = (normal_a.dot(&b1.coords) - d_a).abs();
394    let dist_b2 = (normal_a.dot(&b2.coords) - d_a).abs();
395
396    if dist_b0 > tolerance || dist_b1 > tolerance || dist_b2 > tolerance {
397        return CoplanarityResult::NotCoplanar;
398    }
399
400    // Triangles are coplanar - check orientation
401    let edge1_b = b1 - b0;
402    let edge2_b = b2 - b0;
403    let normal_b = edge1_b.cross(&edge2_b);
404    let normal_b_len = normal_b.norm();
405
406    if normal_b_len < tolerance {
407        // Degenerate triangle B
408        return CoplanarityResult::NotCoplanar;
409    }
410
411    let normal_b = normal_b / normal_b_len;
412    let dot = normal_a.dot(&normal_b);
413
414    if dot > 0.0 {
415        CoplanarityResult::CoplanarSameOrientation
416    } else {
417        CoplanarityResult::CoplanarOppositeOrientation
418    }
419}
420
421/// Check if two 2D triangles overlap (for coplanar triangle intersection).
422fn triangles_overlap_2d(
423    a0: &[f64; 2],
424    a1: &[f64; 2],
425    a2: &[f64; 2],
426    b0: &[f64; 2],
427    b1: &[f64; 2],
428    b2: &[f64; 2],
429) -> bool {
430    // Use separating axis theorem
431    let edges = [
432        [a1[0] - a0[0], a1[1] - a0[1]],
433        [a2[0] - a1[0], a2[1] - a1[1]],
434        [a0[0] - a2[0], a0[1] - a2[1]],
435        [b1[0] - b0[0], b1[1] - b0[1]],
436        [b2[0] - b1[0], b2[1] - b1[1]],
437        [b0[0] - b2[0], b0[1] - b2[1]],
438    ];
439
440    for edge in &edges {
441        // Normal to edge
442        let axis = [-edge[1], edge[0]];
443
444        // Project triangles onto axis
445        let project = |p: &[f64; 2]| axis[0] * p[0] + axis[1] * p[1];
446
447        let a_proj = [project(a0), project(a1), project(a2)];
448        let b_proj = [project(b0), project(b1), project(b2)];
449
450        let a_min = a_proj.iter().cloned().fold(f64::MAX, f64::min);
451        let a_max = a_proj.iter().cloned().fold(f64::MIN, f64::max);
452        let b_min = b_proj.iter().cloned().fold(f64::MAX, f64::min);
453        let b_max = b_proj.iter().cloned().fold(f64::MIN, f64::max);
454
455        if a_max < b_min || b_max < a_min {
456            return false; // Separating axis found
457        }
458    }
459
460    true // No separating axis found, triangles overlap
461}
462
463/// Project a 3D point onto a 2D plane defined by the dominant axis.
464fn project_to_2d(point: &Point3<f64>, normal: &Vector3<f64>) -> [f64; 2] {
465    // Find dominant axis of normal
466    let abs_normal = [normal.x.abs(), normal.y.abs(), normal.z.abs()];
467
468    if abs_normal[0] >= abs_normal[1] && abs_normal[0] >= abs_normal[2] {
469        // X is dominant, project to YZ
470        [point.y, point.z]
471    } else if abs_normal[1] >= abs_normal[2] {
472        // Y is dominant, project to XZ
473        [point.x, point.z]
474    } else {
475        // Z is dominant, project to XY
476        [point.x, point.y]
477    }
478}
479
480/// Information about intersecting triangle pairs.
481#[derive(Debug, Clone)]
482struct IntersectionInfo {
483    /// Index of triangle in mesh A.
484    tri_a: usize,
485    /// Index of triangle in mesh B.
486    tri_b: usize,
487    /// Whether triangles are coplanar.
488    coplanarity: CoplanarityResult,
489}
490
491/// Perform a boolean operation on two meshes.
492pub fn boolean_operation(
493    mesh_a: &Mesh,
494    mesh_b: &Mesh,
495    operation: BooleanOp,
496    params: &BooleanParams,
497) -> MeshResult<BooleanResult> {
498    // Validate inputs
499    if mesh_a.vertices.is_empty() || mesh_a.faces.is_empty() {
500        return Err(MeshError::EmptyMesh {
501            details: "Mesh A is empty".to_string(),
502        });
503    }
504    if mesh_b.vertices.is_empty() || mesh_b.faces.is_empty() {
505        return Err(MeshError::EmptyMesh {
506            details: "Mesh B is empty".to_string(),
507        });
508    }
509
510    // Compute bounding boxes for early rejection
511    let bbox_a = compute_bbox(mesh_a);
512    let bbox_b = compute_bbox(mesh_b);
513
514    if !bboxes_overlap(&bbox_a, &bbox_b) {
515        // No overlap - return simple result based on operation
516        return Ok(handle_non_overlapping(mesh_a, mesh_b, operation));
517    }
518
519    // Find intersection edges between meshes (using BVH acceleration)
520    let intersections = find_mesh_intersections(mesh_a, mesh_b, params.tolerance);
521
522    if intersections.is_empty() {
523        // Meshes don't intersect - one may be inside the other
524        return Ok(handle_non_intersecting(mesh_a, mesh_b, operation));
525    }
526
527    // Count coplanar pairs
528    let coplanar_count = intersections
529        .iter()
530        .filter(|i| i.coplanarity != CoplanarityResult::NotCoplanar)
531        .count();
532
533    // Build sets of coplanar triangles for special handling
534    let coplanar_faces_a: HashSet<usize> = intersections
535        .iter()
536        .filter(|i| i.coplanarity != CoplanarityResult::NotCoplanar)
537        .map(|i| i.tri_a)
538        .collect();
539
540    let coplanar_faces_b: HashSet<usize> = intersections
541        .iter()
542        .filter(|i| i.coplanarity != CoplanarityResult::NotCoplanar)
543        .map(|i| i.tri_b)
544        .collect();
545
546    // Classify faces of each mesh relative to the other
547    let a_classifications = classify_faces(mesh_a, mesh_b, params);
548    let b_classifications = classify_faces(mesh_b, mesh_a, params);
549
550    // Build result mesh based on operation type
551    let mut result = Mesh::new();
552    let mut stats = BooleanStats {
553        coplanar_pairs: coplanar_count,
554        ..Default::default()
555    };
556
557    match operation {
558        BooleanOp::Union => {
559            // Keep faces of A that are outside B
560            // Keep faces of B that are outside A
561            add_faces_with_classification_and_coplanar(
562                &mut result,
563                mesh_a,
564                &a_classifications,
565                FaceLocation::Outside,
566                &coplanar_faces_a,
567                params.coplanar_strategy,
568                true, // is_first_mesh
569            );
570            stats.faces_from_a = result.faces.len();
571
572            add_faces_with_classification_and_coplanar(
573                &mut result,
574                mesh_b,
575                &b_classifications,
576                FaceLocation::Outside,
577                &coplanar_faces_b,
578                params.coplanar_strategy,
579                false, // is_first_mesh
580            );
581            stats.faces_from_b = result.faces.len() - stats.faces_from_a;
582        }
583
584        BooleanOp::Difference => {
585            // Keep faces of A that are outside B
586            // Keep faces of B that are inside A (inverted)
587            add_faces_with_classification_and_coplanar(
588                &mut result,
589                mesh_a,
590                &a_classifications,
591                FaceLocation::Outside,
592                &coplanar_faces_a,
593                params.coplanar_strategy,
594                true,
595            );
596            stats.faces_from_a = result.faces.len();
597
598            add_faces_inverted_with_coplanar(
599                &mut result,
600                mesh_b,
601                &b_classifications,
602                FaceLocation::Inside,
603                &coplanar_faces_b,
604                params.coplanar_strategy,
605                false,
606            );
607            stats.faces_from_b = result.faces.len() - stats.faces_from_a;
608        }
609
610        BooleanOp::Intersection => {
611            // Keep faces of A that are inside B
612            // Keep faces of B that are inside A
613            add_faces_with_classification_and_coplanar(
614                &mut result,
615                mesh_a,
616                &a_classifications,
617                FaceLocation::Inside,
618                &coplanar_faces_a,
619                params.coplanar_strategy,
620                true,
621            );
622            stats.faces_from_a = result.faces.len();
623
624            add_faces_with_classification_and_coplanar(
625                &mut result,
626                mesh_b,
627                &b_classifications,
628                FaceLocation::Inside,
629                &coplanar_faces_b,
630                params.coplanar_strategy,
631                false,
632            );
633            stats.faces_from_b = result.faces.len() - stats.faces_from_a;
634        }
635    }
636
637    // Clean up result if requested
638    if params.cleanup {
639        // Weld duplicate vertices
640        weld_vertices(&mut result, params.tolerance);
641
642        // Fix non-manifold edges
643        let non_manifold_fixed = fix_non_manifold_edges(&mut result);
644        stats.non_manifold_edges_fixed = non_manifold_fixed;
645    }
646
647    Ok(BooleanResult {
648        mesh: result,
649        intersection_edge_count: intersections.len(),
650        new_vertex_count: 0, // Would be filled in by proper implementation
651        had_coplanar_faces: coplanar_count > 0,
652        stats,
653    })
654}
655
656/// Location of a face relative to another mesh.
657#[derive(Debug, Clone, Copy, PartialEq, Eq)]
658enum FaceLocation {
659    Inside,
660    Outside,
661    #[allow(dead_code)]
662    OnBoundary, // Reserved for future boundary handling
663}
664
665/// Compute bounding box of a mesh.
666fn compute_bbox(mesh: &Mesh) -> (Point3<f64>, Point3<f64>) {
667    if mesh.vertices.is_empty() {
668        return (Point3::origin(), Point3::origin());
669    }
670
671    let mut min = mesh.vertices[0].position;
672    let mut max = mesh.vertices[0].position;
673
674    for v in &mesh.vertices {
675        min.x = min.x.min(v.position.x);
676        min.y = min.y.min(v.position.y);
677        min.z = min.z.min(v.position.z);
678        max.x = max.x.max(v.position.x);
679        max.y = max.y.max(v.position.y);
680        max.z = max.z.max(v.position.z);
681    }
682
683    (min, max)
684}
685
686/// Check if two bounding boxes overlap.
687fn bboxes_overlap(a: &(Point3<f64>, Point3<f64>), b: &(Point3<f64>, Point3<f64>)) -> bool {
688    let (a_min, a_max) = a;
689    let (b_min, b_max) = b;
690
691    !(a_max.x < b_min.x
692        || b_max.x < a_min.x
693        || a_max.y < b_min.y
694        || b_max.y < a_min.y
695        || a_max.z < b_min.z
696        || b_max.z < a_min.z)
697}
698
699/// Handle case where bounding boxes don't overlap.
700fn handle_non_overlapping(mesh_a: &Mesh, mesh_b: &Mesh, operation: BooleanOp) -> BooleanResult {
701    let mesh = match operation {
702        BooleanOp::Union => {
703            // Union: combine both meshes
704            let mut result = mesh_a.clone();
705            let offset = result.vertices.len() as u32;
706            result.vertices.extend(mesh_b.vertices.iter().cloned());
707            for face in &mesh_b.faces {
708                result
709                    .faces
710                    .push([face[0] + offset, face[1] + offset, face[2] + offset]);
711            }
712            result
713        }
714        BooleanOp::Difference => {
715            // Difference: just mesh A (B doesn't affect it)
716            mesh_a.clone()
717        }
718        BooleanOp::Intersection => {
719            // Intersection: empty (no overlap)
720            Mesh::new()
721        }
722    };
723
724    BooleanResult {
725        mesh,
726        intersection_edge_count: 0,
727        new_vertex_count: 0,
728        had_coplanar_faces: false,
729        stats: BooleanStats::default(),
730    }
731}
732
733/// Handle case where meshes overlap in bounding box but don't intersect.
734fn handle_non_intersecting(mesh_a: &Mesh, mesh_b: &Mesh, operation: BooleanOp) -> BooleanResult {
735    // Determine if one mesh is inside the other
736    let a_inside_b = is_point_inside_mesh(&mesh_a.vertices[0].position, mesh_b);
737    let b_inside_a = is_point_inside_mesh(&mesh_b.vertices[0].position, mesh_a);
738
739    let mesh = match (operation, a_inside_b, b_inside_a) {
740        // Union cases
741        (BooleanOp::Union, true, _) => mesh_b.clone(), // A inside B, keep B
742        (BooleanOp::Union, _, true) => mesh_a.clone(), // B inside A, keep A
743        (BooleanOp::Union, false, false) => {
744            // Neither inside other, combine both
745            let mut result = mesh_a.clone();
746            let offset = result.vertices.len() as u32;
747            result.vertices.extend(mesh_b.vertices.iter().cloned());
748            for face in &mesh_b.faces {
749                result
750                    .faces
751                    .push([face[0] + offset, face[1] + offset, face[2] + offset]);
752            }
753            result
754        }
755
756        // Difference cases
757        (BooleanOp::Difference, true, _) => Mesh::new(), // A inside B, result is empty
758        (BooleanOp::Difference, _, true) => {
759            // B inside A, need to cut hole (simplified: return A)
760            mesh_a.clone()
761        }
762        (BooleanOp::Difference, false, false) => mesh_a.clone(), // No overlap, keep A
763
764        // Intersection cases
765        (BooleanOp::Intersection, true, _) => mesh_a.clone(), // A inside B, keep A
766        (BooleanOp::Intersection, _, true) => mesh_b.clone(), // B inside A, keep B
767        (BooleanOp::Intersection, false, false) => Mesh::new(), // No overlap, empty
768    };
769
770    BooleanResult {
771        mesh,
772        intersection_edge_count: 0,
773        new_vertex_count: 0,
774        had_coplanar_faces: false,
775        stats: BooleanStats::default(),
776    }
777}
778
779/// Simple point-in-mesh test using ray casting.
780fn is_point_inside_mesh(point: &Point3<f64>, mesh: &Mesh) -> bool {
781    // Cast ray in +X direction and count intersections
782    let ray_dir = Vector3::new(1.0, 0.0, 0.0);
783    let mut intersection_count = 0;
784
785    for face in &mesh.faces {
786        let v0 = &mesh.vertices[face[0] as usize].position;
787        let v1 = &mesh.vertices[face[1] as usize].position;
788        let v2 = &mesh.vertices[face[2] as usize].position;
789
790        if ray_triangle_intersect(point, &ray_dir, v0, v1, v2) {
791            intersection_count += 1;
792        }
793    }
794
795    intersection_count % 2 == 1
796}
797
798/// Ray-triangle intersection test (Möller-Trumbore algorithm).
799fn ray_triangle_intersect(
800    origin: &Point3<f64>,
801    dir: &Vector3<f64>,
802    v0: &Point3<f64>,
803    v1: &Point3<f64>,
804    v2: &Point3<f64>,
805) -> bool {
806    let epsilon = 1e-10;
807
808    let edge1 = v1 - v0;
809    let edge2 = v2 - v0;
810    let h = dir.cross(&edge2);
811    let a = edge1.dot(&h);
812
813    if a.abs() < epsilon {
814        return false;
815    }
816
817    let f = 1.0 / a;
818    let s = origin - v0;
819    let u = f * s.dot(&h);
820
821    if !(0.0..=1.0).contains(&u) {
822        return false;
823    }
824
825    let q = s.cross(&edge1);
826    let v = f * dir.dot(&q);
827
828    if v < 0.0 || u + v > 1.0 {
829        return false;
830    }
831
832    let t = f * edge2.dot(&q);
833    t > epsilon
834}
835
836/// Find all triangle-triangle intersections between two meshes.
837/// Uses BVH acceleration for O(n log n + k) complexity instead of O(n*m).
838fn find_mesh_intersections(mesh_a: &Mesh, mesh_b: &Mesh, tolerance: f64) -> Vec<IntersectionInfo> {
839    let mut intersections = Vec::new();
840
841    // Build BVH for mesh B (the one we query against)
842    let bvh_b = Bvh::build(mesh_b, 8);
843
844    // For each triangle in A, query BVH to find potential intersections
845    for (ai, face_a) in mesh_a.faces.iter().enumerate() {
846        let a0 = &mesh_a.vertices[face_a[0] as usize].position;
847        let a1 = &mesh_a.vertices[face_a[1] as usize].position;
848        let a2 = &mesh_a.vertices[face_a[2] as usize].position;
849
850        // Compute bounding box of triangle A
851        let bbox_a = Aabb::from_triangle(a0, a1, a2);
852
853        // Query BVH for potential intersections
854        let candidates = bvh_b.query(&bbox_a, tolerance);
855
856        for bi in candidates {
857            let face_b = &mesh_b.faces[bi];
858            let b0 = &mesh_b.vertices[face_b[0] as usize].position;
859            let b1 = &mesh_b.vertices[face_b[1] as usize].position;
860            let b2 = &mesh_b.vertices[face_b[2] as usize].position;
861
862            // Check for coplanarity first
863            let coplanarity = check_coplanarity(a0, a1, a2, b0, b1, b2, tolerance);
864
865            let intersects = match coplanarity {
866                CoplanarityResult::NotCoplanar => triangles_intersect(a0, a1, a2, b0, b1, b2),
867                CoplanarityResult::CoplanarSameOrientation
868                | CoplanarityResult::CoplanarOppositeOrientation => {
869                    // For coplanar triangles, project to 2D and check overlap
870                    let edge1 = a1 - a0;
871                    let edge2 = a2 - a0;
872                    let normal = edge1.cross(&edge2);
873
874                    let a0_2d = project_to_2d(a0, &normal);
875                    let a1_2d = project_to_2d(a1, &normal);
876                    let a2_2d = project_to_2d(a2, &normal);
877                    let b0_2d = project_to_2d(b0, &normal);
878                    let b1_2d = project_to_2d(b1, &normal);
879                    let b2_2d = project_to_2d(b2, &normal);
880
881                    triangles_overlap_2d(&a0_2d, &a1_2d, &a2_2d, &b0_2d, &b1_2d, &b2_2d)
882                }
883            };
884
885            if intersects {
886                intersections.push(IntersectionInfo {
887                    tri_a: ai,
888                    tri_b: bi,
889                    coplanarity,
890                });
891            }
892        }
893    }
894
895    intersections
896}
897
898/// Check if two triangles intersect.
899fn triangles_intersect(
900    a0: &Point3<f64>,
901    a1: &Point3<f64>,
902    a2: &Point3<f64>,
903    b0: &Point3<f64>,
904    b1: &Point3<f64>,
905    b2: &Point3<f64>,
906) -> bool {
907    // Check if any edge of A intersects triangle B
908    let edges_a = [(a0, a1), (a1, a2), (a2, a0)];
909
910    for (e0, e1) in &edges_a {
911        let dir = *e1 - **e0;
912        if ray_triangle_intersect(e0, &dir, b0, b1, b2) {
913            // Check if intersection is within edge
914            let t = compute_intersection_t(e0, e1, b0, b1, b2);
915            if let Some(t) = t
916                && (0.0..=1.0).contains(&t)
917            {
918                return true;
919            }
920        }
921    }
922
923    // Check if any edge of B intersects triangle A
924    let edges_b = [(b0, b1), (b1, b2), (b2, b0)];
925
926    for (e0, e1) in &edges_b {
927        let dir = *e1 - **e0;
928        if ray_triangle_intersect(e0, &dir, a0, a1, a2) {
929            let t = compute_intersection_t(e0, e1, a0, a1, a2);
930            if let Some(t) = t
931                && (0.0..=1.0).contains(&t)
932            {
933                return true;
934            }
935        }
936    }
937
938    false
939}
940
941/// Compute intersection parameter t for edge-triangle intersection.
942fn compute_intersection_t(
943    e0: &Point3<f64>,
944    e1: &Point3<f64>,
945    v0: &Point3<f64>,
946    v1: &Point3<f64>,
947    v2: &Point3<f64>,
948) -> Option<f64> {
949    let epsilon = 1e-10;
950    let dir = e1 - e0;
951
952    let edge1 = v1 - v0;
953    let edge2 = v2 - v0;
954    let h = dir.cross(&edge2);
955    let a = edge1.dot(&h);
956
957    if a.abs() < epsilon {
958        return None;
959    }
960
961    let f = 1.0 / a;
962    let s = e0 - v0;
963    let u = f * s.dot(&h);
964
965    if !(0.0..=1.0).contains(&u) {
966        return None;
967    }
968
969    let q = s.cross(&edge1);
970    let v = f * dir.dot(&q);
971
972    if v < 0.0 || u + v > 1.0 {
973        return None;
974    }
975
976    Some(f * edge2.dot(&q))
977}
978
979/// Classify faces of a mesh relative to another mesh.
980fn classify_faces(mesh: &Mesh, other: &Mesh, _params: &BooleanParams) -> Vec<FaceLocation> {
981    mesh.faces
982        .iter()
983        .map(|face| {
984            // Use face centroid for classification
985            let v0 = &mesh.vertices[face[0] as usize].position;
986            let v1 = &mesh.vertices[face[1] as usize].position;
987            let v2 = &mesh.vertices[face[2] as usize].position;
988
989            let centroid = Point3::from((v0.coords + v1.coords + v2.coords) / 3.0);
990
991            if is_point_inside_mesh(&centroid, other) {
992                FaceLocation::Inside
993            } else {
994                FaceLocation::Outside
995            }
996        })
997        .collect()
998}
999
1000/// Add faces with coplanar handling based on strategy.
1001fn add_faces_with_classification_and_coplanar(
1002    result: &mut Mesh,
1003    source: &Mesh,
1004    classifications: &[FaceLocation],
1005    keep_location: FaceLocation,
1006    coplanar_faces: &HashSet<usize>,
1007    coplanar_strategy: CoplanarStrategy,
1008    is_first_mesh: bool,
1009) {
1010    let mut vertex_map: HashMap<u32, u32> = HashMap::new();
1011
1012    for (fi, face) in source.faces.iter().enumerate() {
1013        // Check if this face passes classification
1014        let passes_classification = classifications[fi] == keep_location;
1015
1016        // Check coplanar handling
1017        let is_coplanar = coplanar_faces.contains(&fi);
1018        let should_include = if is_coplanar {
1019            match coplanar_strategy {
1020                CoplanarStrategy::Include => is_first_mesh, // Only include from first mesh
1021                CoplanarStrategy::Exclude => false,
1022                CoplanarStrategy::KeepBoth => true,
1023            }
1024        } else {
1025            passes_classification
1026        };
1027
1028        if should_include {
1029            let new_face: [u32; 3] = [
1030                *vertex_map.entry(face[0]).or_insert_with(|| {
1031                    let idx = result.vertices.len() as u32;
1032                    result
1033                        .vertices
1034                        .push(source.vertices[face[0] as usize].clone());
1035                    idx
1036                }),
1037                *vertex_map.entry(face[1]).or_insert_with(|| {
1038                    let idx = result.vertices.len() as u32;
1039                    result
1040                        .vertices
1041                        .push(source.vertices[face[1] as usize].clone());
1042                    idx
1043                }),
1044                *vertex_map.entry(face[2]).or_insert_with(|| {
1045                    let idx = result.vertices.len() as u32;
1046                    result
1047                        .vertices
1048                        .push(source.vertices[face[2] as usize].clone());
1049                    idx
1050                }),
1051            ];
1052            result.faces.push(new_face);
1053        }
1054    }
1055}
1056
1057/// Add faces inverted with coplanar handling.
1058fn add_faces_inverted_with_coplanar(
1059    result: &mut Mesh,
1060    source: &Mesh,
1061    classifications: &[FaceLocation],
1062    keep_location: FaceLocation,
1063    coplanar_faces: &HashSet<usize>,
1064    coplanar_strategy: CoplanarStrategy,
1065    is_first_mesh: bool,
1066) {
1067    let mut vertex_map: HashMap<u32, u32> = HashMap::new();
1068
1069    for (fi, face) in source.faces.iter().enumerate() {
1070        let passes_classification = classifications[fi] == keep_location;
1071
1072        let is_coplanar = coplanar_faces.contains(&fi);
1073        let should_include = if is_coplanar {
1074            match coplanar_strategy {
1075                CoplanarStrategy::Include => is_first_mesh,
1076                CoplanarStrategy::Exclude => false,
1077                CoplanarStrategy::KeepBoth => true,
1078            }
1079        } else {
1080            passes_classification
1081        };
1082
1083        if should_include {
1084            // Inverted winding order (swap indices 1 and 2)
1085            let new_face: [u32; 3] = [
1086                *vertex_map.entry(face[0]).or_insert_with(|| {
1087                    let idx = result.vertices.len() as u32;
1088                    result
1089                        .vertices
1090                        .push(source.vertices[face[0] as usize].clone());
1091                    idx
1092                }),
1093                *vertex_map.entry(face[2]).or_insert_with(|| {
1094                    let idx = result.vertices.len() as u32;
1095                    result
1096                        .vertices
1097                        .push(source.vertices[face[2] as usize].clone());
1098                    idx
1099                }),
1100                *vertex_map.entry(face[1]).or_insert_with(|| {
1101                    let idx = result.vertices.len() as u32;
1102                    result
1103                        .vertices
1104                        .push(source.vertices[face[1] as usize].clone());
1105                    idx
1106                }),
1107            ];
1108            result.faces.push(new_face);
1109        }
1110    }
1111}
1112
1113/// Weld duplicate vertices in a mesh.
1114fn weld_vertices(mesh: &mut Mesh, tolerance: f64) {
1115    if mesh.vertices.is_empty() {
1116        return;
1117    }
1118
1119    let tol_sq = tolerance * tolerance;
1120    let mut vertex_map: Vec<u32> = (0..mesh.vertices.len() as u32).collect();
1121    let mut kept_vertices: Vec<Vertex> = Vec::new();
1122
1123    for (i, v) in mesh.vertices.iter().enumerate() {
1124        let mut found = None;
1125        for (j, kv) in kept_vertices.iter().enumerate() {
1126            let dist_sq = (v.position - kv.position).norm_squared();
1127            if dist_sq < tol_sq {
1128                found = Some(j);
1129                break;
1130            }
1131        }
1132
1133        if let Some(j) = found {
1134            vertex_map[i] = j as u32;
1135        } else {
1136            vertex_map[i] = kept_vertices.len() as u32;
1137            kept_vertices.push(v.clone());
1138        }
1139    }
1140
1141    // Update faces
1142    for face in &mut mesh.faces {
1143        face[0] = vertex_map[face[0] as usize];
1144        face[1] = vertex_map[face[1] as usize];
1145        face[2] = vertex_map[face[2] as usize];
1146    }
1147
1148    mesh.vertices = kept_vertices;
1149
1150    // Remove degenerate faces
1151    mesh.faces
1152        .retain(|f| f[0] != f[1] && f[1] != f[2] && f[0] != f[2]);
1153}
1154
1155/// Fix non-manifold edges by removing duplicate faces sharing the same edge.
1156/// Returns the number of non-manifold edges fixed.
1157fn fix_non_manifold_edges(mesh: &mut Mesh) -> usize {
1158    // Build edge-to-face map
1159    // An edge is represented as (min_vertex, max_vertex)
1160    let mut edge_faces: HashMap<(u32, u32), Vec<usize>> = HashMap::new();
1161
1162    for (fi, face) in mesh.faces.iter().enumerate() {
1163        let edges = [
1164            (face[0].min(face[1]), face[0].max(face[1])),
1165            (face[1].min(face[2]), face[1].max(face[2])),
1166            (face[2].min(face[0]), face[2].max(face[0])),
1167        ];
1168
1169        for edge in &edges {
1170            edge_faces.entry(*edge).or_default().push(fi);
1171        }
1172    }
1173
1174    // Find non-manifold edges (more than 2 faces sharing an edge)
1175    let non_manifold_edges: Vec<(u32, u32)> = edge_faces
1176        .iter()
1177        .filter(|(_, faces)| faces.len() > 2)
1178        .map(|(edge, _)| *edge)
1179        .collect();
1180
1181    if non_manifold_edges.is_empty() {
1182        return 0;
1183    }
1184
1185    // For each non-manifold edge, keep only the first 2 faces
1186    let mut faces_to_remove: HashSet<usize> = HashSet::new();
1187
1188    for edge in &non_manifold_edges {
1189        if let Some(faces) = edge_faces.get(edge) {
1190            // Remove all but the first 2 faces
1191            for &fi in faces.iter().skip(2) {
1192                faces_to_remove.insert(fi);
1193            }
1194        }
1195    }
1196
1197    // Remove marked faces
1198    let faces_removed = faces_to_remove.len();
1199    let mut new_faces = Vec::with_capacity(mesh.faces.len() - faces_removed);
1200    for (fi, face) in mesh.faces.iter().enumerate() {
1201        if !faces_to_remove.contains(&fi) {
1202            new_faces.push(*face);
1203        }
1204    }
1205    mesh.faces = new_faces;
1206
1207    non_manifold_edges.len()
1208}
1209
1210// ============================================================================
1211// Offset/Shell via Boolean Operations
1212// ============================================================================
1213
1214/// Parameters for boolean-based offset operations.
1215#[derive(Debug, Clone)]
1216pub struct BooleanOffsetParams {
1217    /// Offset distance in mm (positive = expand, negative = shrink).
1218    pub offset: f64,
1219
1220    /// Number of segments for spherical vertex offsets.
1221    /// Higher values create smoother corners but more triangles.
1222    pub sphere_segments: usize,
1223
1224    /// Number of segments for cylindrical edge offsets.
1225    pub cylinder_segments: usize,
1226
1227    /// Tolerance for boolean operations.
1228    pub tolerance: f64,
1229
1230    /// Whether to clean up the result mesh.
1231    pub cleanup: bool,
1232}
1233
1234impl Default for BooleanOffsetParams {
1235    fn default() -> Self {
1236        Self {
1237            offset: 1.0,
1238            sphere_segments: 8,
1239            cylinder_segments: 8,
1240            tolerance: 1e-8,
1241            cleanup: true,
1242        }
1243    }
1244}
1245
1246impl BooleanOffsetParams {
1247    /// Create params for a specific offset distance.
1248    pub fn with_offset(offset: f64) -> Self {
1249        Self {
1250            offset,
1251            ..Default::default()
1252        }
1253    }
1254
1255    /// Create params with high quality (more segments).
1256    pub fn high_quality(offset: f64) -> Self {
1257        Self {
1258            offset,
1259            sphere_segments: 16,
1260            cylinder_segments: 16,
1261            tolerance: 1e-10,
1262            cleanup: true,
1263        }
1264    }
1265
1266    /// Create params for fast preview (fewer segments).
1267    pub fn fast_preview(offset: f64) -> Self {
1268        Self {
1269            offset,
1270            sphere_segments: 4,
1271            cylinder_segments: 4,
1272            tolerance: 1e-6,
1273            cleanup: true,
1274        }
1275    }
1276
1277    /// Set the number of sphere segments.
1278    pub fn with_sphere_segments(mut self, segments: usize) -> Self {
1279        self.sphere_segments = segments.max(3);
1280        self
1281    }
1282
1283    /// Set the number of cylinder segments.
1284    pub fn with_cylinder_segments(mut self, segments: usize) -> Self {
1285        self.cylinder_segments = segments.max(3);
1286        self
1287    }
1288}
1289
1290/// Result of boolean-based offset operation.
1291#[derive(Debug)]
1292pub struct BooleanOffsetResult {
1293    /// The offset mesh.
1294    pub mesh: Mesh,
1295
1296    /// Number of spheres generated (one per vertex).
1297    pub sphere_count: usize,
1298
1299    /// Number of cylinders generated (one per edge).
1300    pub cylinder_count: usize,
1301
1302    /// Number of prisms generated (one per face).
1303    pub prism_count: usize,
1304
1305    /// Statistics about the operation.
1306    pub stats: BooleanOffsetStats,
1307}
1308
1309/// Statistics from boolean offset operation.
1310#[derive(Debug, Clone, Default)]
1311pub struct BooleanOffsetStats {
1312    /// Time spent generating primitives (ms).
1313    pub primitive_generation_time_ms: u64,
1314
1315    /// Time spent on boolean union (ms).
1316    pub union_time_ms: u64,
1317
1318    /// Number of triangles in intermediate union.
1319    pub intermediate_triangles: usize,
1320
1321    /// Number of triangles in final result.
1322    pub final_triangles: usize,
1323}
1324
1325/// Generate a shell/offset of a mesh using boolean operations.
1326///
1327/// This method creates an offset surface by:
1328/// 1. Placing a sphere at each vertex (rounded corners)
1329/// 2. Placing a cylinder along each edge (rounded edges)
1330/// 3. Placing an extruded prism at each face (face offset)
1331/// 4. Unioning all these primitives together
1332///
1333/// This approach handles complex topology including handles and tunnels,
1334/// and produces well-defined results without self-intersections.
1335///
1336/// # Arguments
1337/// * `mesh` - The input mesh (should be manifold)
1338/// * `params` - Parameters controlling the offset
1339///
1340/// # Returns
1341/// The offset mesh and statistics about the operation.
1342///
1343/// # Comparison with SDF method
1344/// - **Pros**: Exact geometry, handles complex topology, no grid artifacts
1345/// - **Cons**: Slower for large meshes, many triangles in result
1346///
1347/// # Example
1348/// ```ignore
1349/// let params = BooleanOffsetParams::with_offset(2.0);
1350/// let result = offset_boolean(&mesh, &params)?;
1351/// println!("Offset mesh has {} triangles", result.mesh.faces.len());
1352/// ```
1353pub fn offset_boolean(
1354    mesh: &Mesh,
1355    params: &BooleanOffsetParams,
1356) -> MeshResult<BooleanOffsetResult> {
1357    use std::time::Instant;
1358
1359    if mesh.vertices.is_empty() || mesh.faces.is_empty() {
1360        return Err(MeshError::EmptyMesh {
1361            details: "Cannot offset empty mesh".to_string(),
1362        });
1363    }
1364
1365    let offset = params.offset.abs();
1366    if offset < 1e-10 {
1367        // Zero offset - return copy of original
1368        return Ok(BooleanOffsetResult {
1369            mesh: mesh.clone(),
1370            sphere_count: 0,
1371            cylinder_count: 0,
1372            prism_count: 0,
1373            stats: BooleanOffsetStats::default(),
1374        });
1375    }
1376
1377    let primitive_start = Instant::now();
1378
1379    // Compute vertex normals for face offset direction
1380    let vertex_normals = compute_vertex_normals(mesh);
1381
1382    // Collect unique edges
1383    let edges = collect_unique_edges(mesh);
1384
1385    // Generate primitives
1386    let mut primitives: Vec<Mesh> = Vec::new();
1387
1388    // 1. Generate spheres at vertices
1389    for (vi, vertex) in mesh.vertices.iter().enumerate() {
1390        let sphere = generate_sphere(&vertex.position, offset, params.sphere_segments);
1391        primitives.push(sphere);
1392
1393        // For negative offset (shrinking), we'll handle it differently
1394        if params.offset < 0.0 {
1395            // Inward offset: sphere at offset position along inverted normal
1396            let inward_center = vertex.position - vertex_normals[vi] * offset;
1397            let inward_sphere = generate_sphere(&inward_center, offset, params.sphere_segments);
1398            primitives.push(inward_sphere);
1399        }
1400    }
1401    let sphere_count = mesh.vertices.len();
1402
1403    // 2. Generate cylinders along edges
1404    for (vi0, vi1) in &edges {
1405        let v0 = &mesh.vertices[*vi0].position;
1406        let v1 = &mesh.vertices[*vi1].position;
1407        let cylinder = generate_cylinder(v0, v1, offset, params.cylinder_segments);
1408        primitives.push(cylinder);
1409    }
1410    let cylinder_count = edges.len();
1411
1412    // 3. Generate extruded prisms for faces
1413    for face in &mesh.faces {
1414        let v0 = &mesh.vertices[face[0] as usize].position;
1415        let v1 = &mesh.vertices[face[1] as usize].position;
1416        let v2 = &mesh.vertices[face[2] as usize].position;
1417
1418        // Compute face normal
1419        let edge1 = v1 - v0;
1420        let edge2 = v2 - v0;
1421        let normal = edge1.cross(&edge2);
1422        let normal_len = normal.norm();
1423
1424        if normal_len > 1e-10 {
1425            let normal = normal / normal_len;
1426
1427            // Offset the face along its normal
1428            let prism = generate_triangular_prism(v0, v1, v2, &normal, offset);
1429            primitives.push(prism);
1430        }
1431    }
1432    let prism_count = mesh.faces.len();
1433
1434    let primitive_time_ms = primitive_start.elapsed().as_millis() as u64;
1435
1436    // 4. Union all primitives together
1437    let union_start = Instant::now();
1438
1439    // Start with the original mesh (or its offset)
1440    let mut result = if params.offset > 0.0 {
1441        mesh.clone()
1442    } else {
1443        // For negative offset, we'll compute difference later
1444        mesh.clone()
1445    };
1446
1447    // Progressively union all primitives
1448    // Note: This is O(n²) in the worst case. A more sophisticated implementation
1449    // would use spatial partitioning or build a union tree.
1450    let mut intermediate_triangles = result.faces.len();
1451
1452    for primitive in primitives {
1453        // Simple merge: append vertices and faces
1454        // For a proper union, we'd need to detect and resolve intersections
1455        let offset_verts = result.vertices.len() as u32;
1456        result.vertices.extend(primitive.vertices.iter().cloned());
1457        for face in &primitive.faces {
1458            result.faces.push([
1459                face[0] + offset_verts,
1460                face[1] + offset_verts,
1461                face[2] + offset_verts,
1462            ]);
1463        }
1464        intermediate_triangles = result.faces.len();
1465    }
1466
1467    let union_time_ms = union_start.elapsed().as_millis() as u64;
1468
1469    // For negative offset, we would compute: original - union(spheres + cylinders + prisms)
1470    // This is the "erosion" operation
1471    if params.offset < 0.0 {
1472        // The current implementation just combines geometry
1473        // A full implementation would use boolean difference
1474    }
1475
1476    // Cleanup if requested
1477    if params.cleanup {
1478        weld_vertices(&mut result, params.tolerance);
1479    }
1480
1481    let final_triangles = result.faces.len();
1482
1483    Ok(BooleanOffsetResult {
1484        mesh: result,
1485        sphere_count,
1486        cylinder_count,
1487        prism_count,
1488        stats: BooleanOffsetStats {
1489            primitive_generation_time_ms: primitive_time_ms,
1490            union_time_ms,
1491            intermediate_triangles,
1492            final_triangles,
1493        },
1494    })
1495}
1496
1497/// Generate a shell using boolean difference between outer and inner offset.
1498///
1499/// This creates a hollow shell by computing:
1500/// `shell = offset_outward(mesh, thickness/2) - offset_inward(mesh, thickness/2)`
1501///
1502/// # Arguments
1503/// * `mesh` - The input mesh
1504/// * `thickness` - Shell wall thickness in mm
1505/// * `params` - Optional parameters (uses defaults if None)
1506///
1507/// # Returns
1508/// A hollow shell mesh.
1509///
1510/// # Example
1511/// ```ignore
1512/// let shell = shell_boolean(&mesh, 2.0, None)?;
1513/// ```
1514pub fn shell_boolean(
1515    mesh: &Mesh,
1516    thickness: f64,
1517    params: Option<&BooleanOffsetParams>,
1518) -> MeshResult<Mesh> {
1519    let default_params = BooleanOffsetParams::default();
1520    let params = params.unwrap_or(&default_params);
1521
1522    if thickness <= 0.0 {
1523        return Err(MeshError::repair_failed("Shell thickness must be positive"));
1524    }
1525
1526    // Generate outer offset (expand by half thickness)
1527    let outer_params = BooleanOffsetParams {
1528        offset: thickness / 2.0,
1529        ..params.clone()
1530    };
1531    let outer = offset_boolean(mesh, &outer_params)?;
1532
1533    // Generate inner offset (shrink by half thickness)
1534    let inner_params = BooleanOffsetParams {
1535        offset: thickness / 2.0, // Same distance, but we'll invert the mesh
1536        ..params.clone()
1537    };
1538    let inner = offset_boolean(mesh, &inner_params)?;
1539
1540    // Invert the inner mesh (flip face winding)
1541    let mut inner_inverted = inner.mesh;
1542    for face in &mut inner_inverted.faces {
1543        face.swap(1, 2); // Reverse winding
1544    }
1545
1546    // Compute outer - inner using boolean difference
1547    let bool_params = BooleanParams {
1548        tolerance: params.tolerance,
1549        cleanup: params.cleanup,
1550        ..Default::default()
1551    };
1552
1553    let result = boolean_operation(
1554        &outer.mesh,
1555        &inner_inverted,
1556        BooleanOp::Difference,
1557        &bool_params,
1558    )?;
1559
1560    Ok(result.mesh)
1561}
1562
1563/// Compute vertex normals for a mesh.
1564fn compute_vertex_normals(mesh: &Mesh) -> Vec<Vector3<f64>> {
1565    let mut normals = vec![Vector3::zeros(); mesh.vertices.len()];
1566    let mut counts = vec![0usize; mesh.vertices.len()];
1567
1568    for face in &mesh.faces {
1569        let v0 = &mesh.vertices[face[0] as usize].position;
1570        let v1 = &mesh.vertices[face[1] as usize].position;
1571        let v2 = &mesh.vertices[face[2] as usize].position;
1572
1573        let edge1 = v1 - v0;
1574        let edge2 = v2 - v0;
1575        let normal = edge1.cross(&edge2);
1576
1577        for &vi in face {
1578            normals[vi as usize] += normal;
1579            counts[vi as usize] += 1;
1580        }
1581    }
1582
1583    for (i, normal) in normals.iter_mut().enumerate() {
1584        if counts[i] > 0 {
1585            let len = normal.norm();
1586            if len > 1e-10 {
1587                *normal /= len;
1588            }
1589        }
1590    }
1591
1592    normals
1593}
1594
1595/// Collect unique edges from a mesh.
1596fn collect_unique_edges(mesh: &Mesh) -> Vec<(usize, usize)> {
1597    let mut edge_set: HashSet<(usize, usize)> = HashSet::new();
1598
1599    for face in &mesh.faces {
1600        let edges = [
1601            (face[0] as usize, face[1] as usize),
1602            (face[1] as usize, face[2] as usize),
1603            (face[2] as usize, face[0] as usize),
1604        ];
1605
1606        for (a, b) in edges {
1607            let edge = if a < b { (a, b) } else { (b, a) };
1608            edge_set.insert(edge);
1609        }
1610    }
1611
1612    edge_set.into_iter().collect()
1613}
1614
1615/// Generate a UV sphere mesh.
1616fn generate_sphere(center: &Point3<f64>, radius: f64, segments: usize) -> Mesh {
1617    let mut mesh = Mesh::new();
1618    let segments = segments.max(3);
1619
1620    // Generate vertices
1621    // North pole
1622    mesh.vertices.push(Vertex::new(Point3::new(
1623        center.x,
1624        center.y,
1625        center.z + radius,
1626    )));
1627
1628    // Latitude rings
1629    for lat in 1..segments {
1630        let theta = std::f64::consts::PI * lat as f64 / segments as f64;
1631        let sin_theta = theta.sin();
1632        let cos_theta = theta.cos();
1633
1634        for lon in 0..(segments * 2) {
1635            let phi = std::f64::consts::PI * lon as f64 / segments as f64;
1636            let x = center.x + radius * sin_theta * phi.cos();
1637            let y = center.y + radius * sin_theta * phi.sin();
1638            let z = center.z + radius * cos_theta;
1639            mesh.vertices.push(Vertex::new(Point3::new(x, y, z)));
1640        }
1641    }
1642
1643    // South pole
1644    let south_pole_idx = mesh.vertices.len() as u32;
1645    mesh.vertices.push(Vertex::new(Point3::new(
1646        center.x,
1647        center.y,
1648        center.z - radius,
1649    )));
1650
1651    // Generate faces
1652    let ring_size = (segments * 2) as u32;
1653
1654    // North cap
1655    for i in 0..ring_size {
1656        let next = (i + 1) % ring_size;
1657        mesh.faces.push([0, 1 + next, 1 + i]);
1658    }
1659
1660    // Middle bands
1661    for lat in 0..(segments - 2) {
1662        let ring_start = 1 + lat as u32 * ring_size;
1663        let next_ring_start = ring_start + ring_size;
1664
1665        for i in 0..ring_size {
1666            let next = (i + 1) % ring_size;
1667
1668            mesh.faces
1669                .push([ring_start + i, ring_start + next, next_ring_start + i]);
1670            mesh.faces.push([
1671                ring_start + next,
1672                next_ring_start + next,
1673                next_ring_start + i,
1674            ]);
1675        }
1676    }
1677
1678    // South cap
1679    let last_ring_start = 1 + ((segments - 2) as u32) * ring_size;
1680    for i in 0..ring_size {
1681        let next = (i + 1) % ring_size;
1682        mesh.faces
1683            .push([last_ring_start + i, last_ring_start + next, south_pole_idx]);
1684    }
1685
1686    mesh
1687}
1688
1689/// Generate a cylinder mesh between two points.
1690fn generate_cylinder(p0: &Point3<f64>, p1: &Point3<f64>, radius: f64, segments: usize) -> Mesh {
1691    let mut mesh = Mesh::new();
1692    let segments = segments.max(3);
1693
1694    let axis = p1 - p0;
1695    let length = axis.norm();
1696    if length < 1e-10 {
1697        return mesh;
1698    }
1699    let axis_norm = axis / length;
1700
1701    // Find perpendicular vectors
1702    let perp1 = if axis_norm.x.abs() < 0.9 {
1703        axis_norm.cross(&Vector3::x())
1704    } else {
1705        axis_norm.cross(&Vector3::y())
1706    }
1707    .normalize();
1708    let perp2 = axis_norm.cross(&perp1);
1709
1710    // Generate vertices for both ends
1711    for (end_point, _) in [(p0, 0), (p1, 1)] {
1712        for i in 0..segments {
1713            let angle = 2.0 * std::f64::consts::PI * i as f64 / segments as f64;
1714            let offset = perp1 * angle.cos() * radius + perp2 * angle.sin() * radius;
1715            let pos = Point3::from(end_point.coords + offset);
1716            mesh.vertices.push(Vertex::new(pos));
1717        }
1718    }
1719
1720    // Add center vertices for caps
1721    mesh.vertices.push(Vertex::new(*p0));
1722    mesh.vertices.push(Vertex::new(*p1));
1723
1724    let start_center = (2 * segments) as u32;
1725    let end_center = start_center + 1;
1726    let seg = segments as u32;
1727
1728    // Generate side faces
1729    for i in 0..seg {
1730        let i0 = i;
1731        let i1 = (i + 1) % seg;
1732        let i2 = seg + i;
1733        let i3 = seg + (i + 1) % seg;
1734
1735        mesh.faces.push([i0, i2, i1]);
1736        mesh.faces.push([i1, i2, i3]);
1737    }
1738
1739    // Generate cap faces
1740    for i in 0..seg {
1741        let i0 = i;
1742        let i1 = (i + 1) % seg;
1743        mesh.faces.push([start_center, i1, i0]); // Start cap
1744
1745        let i2 = seg + i;
1746        let i3 = seg + (i + 1) % seg;
1747        mesh.faces.push([end_center, i2, i3]); // End cap
1748    }
1749
1750    mesh
1751}
1752
1753/// Generate a triangular prism (extruded triangle).
1754fn generate_triangular_prism(
1755    v0: &Point3<f64>,
1756    v1: &Point3<f64>,
1757    v2: &Point3<f64>,
1758    normal: &Vector3<f64>,
1759    height: f64,
1760) -> Mesh {
1761    let mut mesh = Mesh::new();
1762    let offset = normal * height;
1763
1764    // Bottom triangle vertices
1765    mesh.vertices.push(Vertex::new(*v0));
1766    mesh.vertices.push(Vertex::new(*v1));
1767    mesh.vertices.push(Vertex::new(*v2));
1768
1769    // Top triangle vertices (offset along normal)
1770    mesh.vertices
1771        .push(Vertex::new(Point3::from(v0.coords + offset)));
1772    mesh.vertices
1773        .push(Vertex::new(Point3::from(v1.coords + offset)));
1774    mesh.vertices
1775        .push(Vertex::new(Point3::from(v2.coords + offset)));
1776
1777    // Bottom face (reversed winding for outward normal)
1778    mesh.faces.push([0, 2, 1]);
1779
1780    // Top face
1781    mesh.faces.push([3, 4, 5]);
1782
1783    // Side faces (quads split into triangles)
1784    // Side 0-1
1785    mesh.faces.push([0, 1, 4]);
1786    mesh.faces.push([0, 4, 3]);
1787
1788    // Side 1-2
1789    mesh.faces.push([1, 2, 5]);
1790    mesh.faces.push([1, 5, 4]);
1791
1792    // Side 2-0
1793    mesh.faces.push([2, 0, 3]);
1794    mesh.faces.push([2, 3, 5]);
1795
1796    mesh
1797}
1798
1799// Convenience methods on Mesh
1800impl Mesh {
1801    /// Perform boolean union with another mesh.
1802    pub fn boolean_union(&self, other: &Mesh) -> MeshResult<Mesh> {
1803        let result = boolean_operation(self, other, BooleanOp::Union, &BooleanParams::default())?;
1804        Ok(result.mesh)
1805    }
1806
1807    /// Perform boolean difference (subtract other from self).
1808    pub fn boolean_difference(&self, other: &Mesh) -> MeshResult<Mesh> {
1809        let result = boolean_operation(
1810            self,
1811            other,
1812            BooleanOp::Difference,
1813            &BooleanParams::default(),
1814        )?;
1815        Ok(result.mesh)
1816    }
1817
1818    /// Perform boolean intersection with another mesh.
1819    pub fn boolean_intersection(&self, other: &Mesh) -> MeshResult<Mesh> {
1820        let result = boolean_operation(
1821            self,
1822            other,
1823            BooleanOp::Intersection,
1824            &BooleanParams::default(),
1825        )?;
1826        Ok(result.mesh)
1827    }
1828
1829    /// Perform boolean operation with custom parameters.
1830    pub fn boolean_with_params(
1831        &self,
1832        other: &Mesh,
1833        operation: BooleanOp,
1834        params: &BooleanParams,
1835    ) -> MeshResult<BooleanResult> {
1836        boolean_operation(self, other, operation, params)
1837    }
1838
1839    /// Create an offset mesh using boolean operations.
1840    ///
1841    /// This is an alternative to SDF-based offset that handles complex
1842    /// topology including handles and tunnels.
1843    ///
1844    /// # Arguments
1845    /// * `offset` - Offset distance in mm (positive = expand, negative = shrink)
1846    ///
1847    /// # Example
1848    /// ```ignore
1849    /// let expanded = mesh.offset_boolean(2.0)?;
1850    /// let shrunk = mesh.offset_boolean(-1.0)?;
1851    /// ```
1852    pub fn offset_boolean(&self, offset: f64) -> MeshResult<Mesh> {
1853        let params = BooleanOffsetParams::with_offset(offset);
1854        let result = offset_boolean(self, &params)?;
1855        Ok(result.mesh)
1856    }
1857
1858    /// Create an offset mesh with custom parameters.
1859    pub fn offset_boolean_with_params(
1860        &self,
1861        params: &BooleanOffsetParams,
1862    ) -> MeshResult<BooleanOffsetResult> {
1863        offset_boolean(self, params)
1864    }
1865
1866    /// Create a hollow shell using boolean operations.
1867    ///
1868    /// This creates a shell by computing the difference between
1869    /// an outer and inner offset surface.
1870    ///
1871    /// # Arguments
1872    /// * `thickness` - Shell wall thickness in mm
1873    ///
1874    /// # Example
1875    /// ```ignore
1876    /// let shell = mesh.shell_boolean(2.0)?;
1877    /// ```
1878    pub fn shell_boolean(&self, thickness: f64) -> MeshResult<Mesh> {
1879        shell_boolean(self, thickness, None)
1880    }
1881}
1882
1883/// Perform a boolean operation with progress reporting.
1884///
1885/// This is a progress-reporting variant of [`boolean_operation`] that allows tracking
1886/// the operation progress and supports cancellation via the progress callback.
1887///
1888/// The boolean operation proceeds through these phases:
1889/// 1. BVH construction and intersection detection
1890/// 2. Face classification
1891/// 3. Result mesh construction
1892/// 4. Cleanup (if enabled)
1893///
1894/// # Arguments
1895/// * `mesh_a` - First mesh operand
1896/// * `mesh_b` - Second mesh operand
1897/// * `operation` - The boolean operation to perform
1898/// * `params` - Boolean operation parameters
1899/// * `callback` - Optional progress callback. Returns `false` to request cancellation.
1900///
1901/// # Returns
1902/// A `BooleanResult` containing the result mesh and statistics.
1903/// If cancelled via callback, returns the partial result.
1904///
1905/// # Example
1906/// ```ignore
1907/// use mesh_repair::boolean::{boolean_operation_with_progress, BooleanOp, BooleanParams};
1908/// use mesh_repair::progress::ProgressCallback;
1909///
1910/// let callback: ProgressCallback = Box::new(|progress| {
1911///     println!("{}% - {}", progress.percent(), progress.message);
1912///     true // Continue
1913/// });
1914///
1915/// let result = boolean_operation_with_progress(
1916///     &mesh_a, &mesh_b,
1917///     BooleanOp::Union,
1918///     &BooleanParams::default(),
1919///     Some(&callback)
1920/// )?;
1921/// ```
1922pub fn boolean_operation_with_progress(
1923    mesh_a: &Mesh,
1924    mesh_b: &Mesh,
1925    operation: BooleanOp,
1926    params: &BooleanParams,
1927    callback: Option<&crate::progress::ProgressCallback>,
1928) -> MeshResult<BooleanResult> {
1929    use crate::progress::ProgressTracker;
1930
1931    // Total phases: intersection detection (40%), face classification (40%), result building (20%)
1932    let tracker = ProgressTracker::new(100);
1933
1934    // Validate inputs
1935    if mesh_a.vertices.is_empty() || mesh_a.faces.is_empty() {
1936        return Err(MeshError::EmptyMesh {
1937            details: "Mesh A is empty".to_string(),
1938        });
1939    }
1940    if mesh_b.vertices.is_empty() || mesh_b.faces.is_empty() {
1941        return Err(MeshError::EmptyMesh {
1942            details: "Mesh B is empty".to_string(),
1943        });
1944    }
1945
1946    // Phase 1: Compute bounding boxes and check for overlap
1947    tracker.set(5);
1948    if !tracker.maybe_callback(callback, "Computing bounding boxes".to_string()) {
1949        return Ok(BooleanResult {
1950            mesh: Mesh::new(),
1951            intersection_edge_count: 0,
1952            new_vertex_count: 0,
1953            had_coplanar_faces: false,
1954            stats: BooleanStats::default(),
1955        });
1956    }
1957
1958    let bbox_a = compute_bbox(mesh_a);
1959    let bbox_b = compute_bbox(mesh_b);
1960
1961    if !bboxes_overlap(&bbox_a, &bbox_b) {
1962        return Ok(handle_non_overlapping(mesh_a, mesh_b, operation));
1963    }
1964
1965    // Phase 2: Find intersection edges between meshes (using BVH acceleration)
1966    tracker.set(10);
1967    if !tracker.maybe_callback(
1968        callback,
1969        "Building BVH and detecting intersections".to_string(),
1970    ) {
1971        return Ok(BooleanResult {
1972            mesh: Mesh::new(),
1973            intersection_edge_count: 0,
1974            new_vertex_count: 0,
1975            had_coplanar_faces: false,
1976            stats: BooleanStats::default(),
1977        });
1978    }
1979
1980    let intersections = find_mesh_intersections(mesh_a, mesh_b, params.tolerance);
1981
1982    tracker.set(40);
1983    if !tracker.maybe_callback(
1984        callback,
1985        format!("Found {} intersection pairs", intersections.len()),
1986    ) {
1987        return Ok(BooleanResult {
1988            mesh: Mesh::new(),
1989            intersection_edge_count: intersections.len(),
1990            new_vertex_count: 0,
1991            had_coplanar_faces: false,
1992            stats: BooleanStats::default(),
1993        });
1994    }
1995
1996    if intersections.is_empty() {
1997        return Ok(handle_non_intersecting(mesh_a, mesh_b, operation));
1998    }
1999
2000    // Count coplanar pairs
2001    let coplanar_count = intersections
2002        .iter()
2003        .filter(|i| i.coplanarity != CoplanarityResult::NotCoplanar)
2004        .count();
2005
2006    // Build sets of coplanar triangles for special handling
2007    let coplanar_faces_a: HashSet<usize> = intersections
2008        .iter()
2009        .filter(|i| i.coplanarity != CoplanarityResult::NotCoplanar)
2010        .map(|i| i.tri_a)
2011        .collect();
2012
2013    let coplanar_faces_b: HashSet<usize> = intersections
2014        .iter()
2015        .filter(|i| i.coplanarity != CoplanarityResult::NotCoplanar)
2016        .map(|i| i.tri_b)
2017        .collect();
2018
2019    // Phase 3: Classify faces of each mesh relative to the other
2020    tracker.set(50);
2021    if !tracker.maybe_callback(callback, "Classifying mesh A faces".to_string()) {
2022        return Ok(BooleanResult {
2023            mesh: Mesh::new(),
2024            intersection_edge_count: intersections.len(),
2025            new_vertex_count: 0,
2026            had_coplanar_faces: coplanar_count > 0,
2027            stats: BooleanStats::default(),
2028        });
2029    }
2030
2031    let a_classifications = classify_faces(mesh_a, mesh_b, params);
2032
2033    tracker.set(70);
2034    if !tracker.maybe_callback(callback, "Classifying mesh B faces".to_string()) {
2035        return Ok(BooleanResult {
2036            mesh: Mesh::new(),
2037            intersection_edge_count: intersections.len(),
2038            new_vertex_count: 0,
2039            had_coplanar_faces: coplanar_count > 0,
2040            stats: BooleanStats::default(),
2041        });
2042    }
2043
2044    let b_classifications = classify_faces(mesh_b, mesh_a, params);
2045
2046    // Phase 4: Build result mesh based on operation type
2047    tracker.set(80);
2048    if !tracker.maybe_callback(callback, "Building result mesh".to_string()) {
2049        return Ok(BooleanResult {
2050            mesh: Mesh::new(),
2051            intersection_edge_count: intersections.len(),
2052            new_vertex_count: 0,
2053            had_coplanar_faces: coplanar_count > 0,
2054            stats: BooleanStats::default(),
2055        });
2056    }
2057
2058    let mut result = Mesh::new();
2059    let mut stats = BooleanStats {
2060        coplanar_pairs: coplanar_count,
2061        ..Default::default()
2062    };
2063
2064    match operation {
2065        BooleanOp::Union => {
2066            add_faces_with_classification_and_coplanar(
2067                &mut result,
2068                mesh_a,
2069                &a_classifications,
2070                FaceLocation::Outside,
2071                &coplanar_faces_a,
2072                params.coplanar_strategy,
2073                true,
2074            );
2075            stats.faces_from_a = result.faces.len();
2076
2077            add_faces_with_classification_and_coplanar(
2078                &mut result,
2079                mesh_b,
2080                &b_classifications,
2081                FaceLocation::Outside,
2082                &coplanar_faces_b,
2083                params.coplanar_strategy,
2084                false,
2085            );
2086            stats.faces_from_b = result.faces.len() - stats.faces_from_a;
2087        }
2088
2089        BooleanOp::Difference => {
2090            add_faces_with_classification_and_coplanar(
2091                &mut result,
2092                mesh_a,
2093                &a_classifications,
2094                FaceLocation::Outside,
2095                &coplanar_faces_a,
2096                params.coplanar_strategy,
2097                true,
2098            );
2099            stats.faces_from_a = result.faces.len();
2100
2101            add_faces_inverted_with_coplanar(
2102                &mut result,
2103                mesh_b,
2104                &b_classifications,
2105                FaceLocation::Inside,
2106                &coplanar_faces_b,
2107                params.coplanar_strategy,
2108                false,
2109            );
2110            stats.faces_from_b = result.faces.len() - stats.faces_from_a;
2111        }
2112
2113        BooleanOp::Intersection => {
2114            add_faces_with_classification_and_coplanar(
2115                &mut result,
2116                mesh_a,
2117                &a_classifications,
2118                FaceLocation::Inside,
2119                &coplanar_faces_a,
2120                params.coplanar_strategy,
2121                true,
2122            );
2123            stats.faces_from_a = result.faces.len();
2124
2125            add_faces_with_classification_and_coplanar(
2126                &mut result,
2127                mesh_b,
2128                &b_classifications,
2129                FaceLocation::Inside,
2130                &coplanar_faces_b,
2131                params.coplanar_strategy,
2132                false,
2133            );
2134            stats.faces_from_b = result.faces.len() - stats.faces_from_a;
2135        }
2136    }
2137
2138    // Phase 5: Clean up result if requested
2139    if params.cleanup {
2140        tracker.set(90);
2141        if !tracker.maybe_callback(callback, "Cleaning up result mesh".to_string()) {
2142            return Ok(BooleanResult {
2143                mesh: result,
2144                intersection_edge_count: intersections.len(),
2145                new_vertex_count: 0,
2146                had_coplanar_faces: coplanar_count > 0,
2147                stats,
2148            });
2149        }
2150
2151        weld_vertices(&mut result, params.tolerance);
2152        let non_manifold_fixed = fix_non_manifold_edges(&mut result);
2153        stats.non_manifold_edges_fixed = non_manifold_fixed;
2154    }
2155
2156    tracker.set(100);
2157    let _ = tracker.maybe_callback(callback, "Boolean operation complete".to_string());
2158
2159    Ok(BooleanResult {
2160        mesh: result,
2161        intersection_edge_count: intersections.len(),
2162        new_vertex_count: 0,
2163        had_coplanar_faces: coplanar_count > 0,
2164        stats,
2165    })
2166}
2167
2168#[cfg(test)]
2169mod tests {
2170    use super::*;
2171
2172    fn create_cube(center: Point3<f64>, size: f64) -> Mesh {
2173        let half = size / 2.0;
2174        let mut mesh = Mesh::new();
2175
2176        // 8 vertices
2177        let vertices = [
2178            Point3::new(center.x - half, center.y - half, center.z - half),
2179            Point3::new(center.x + half, center.y - half, center.z - half),
2180            Point3::new(center.x + half, center.y + half, center.z - half),
2181            Point3::new(center.x - half, center.y + half, center.z - half),
2182            Point3::new(center.x - half, center.y - half, center.z + half),
2183            Point3::new(center.x + half, center.y - half, center.z + half),
2184            Point3::new(center.x + half, center.y + half, center.z + half),
2185            Point3::new(center.x - half, center.y + half, center.z + half),
2186        ];
2187
2188        for v in &vertices {
2189            mesh.vertices.push(Vertex::new(*v));
2190        }
2191
2192        // 12 faces (2 per side)
2193        let faces = [
2194            // Front
2195            [0, 1, 2],
2196            [0, 2, 3],
2197            // Back
2198            [5, 4, 7],
2199            [5, 7, 6],
2200            // Top
2201            [3, 2, 6],
2202            [3, 6, 7],
2203            // Bottom
2204            [4, 5, 1],
2205            [4, 1, 0],
2206            // Left
2207            [4, 0, 3],
2208            [4, 3, 7],
2209            // Right
2210            [1, 5, 6],
2211            [1, 6, 2],
2212        ];
2213
2214        for f in &faces {
2215            mesh.faces.push(*f);
2216        }
2217
2218        mesh
2219    }
2220
2221    #[test]
2222    fn test_non_overlapping_union() {
2223        let cube_a = create_cube(Point3::new(0.0, 0.0, 0.0), 1.0);
2224        let cube_b = create_cube(Point3::new(10.0, 0.0, 0.0), 1.0);
2225
2226        let result = boolean_operation(
2227            &cube_a,
2228            &cube_b,
2229            BooleanOp::Union,
2230            &BooleanParams::default(),
2231        )
2232        .unwrap();
2233
2234        assert_eq!(result.mesh.vertices.len(), 16); // 8 + 8
2235        assert_eq!(result.mesh.faces.len(), 24); // 12 + 12
2236    }
2237
2238    #[test]
2239    fn test_non_overlapping_difference() {
2240        let cube_a = create_cube(Point3::new(0.0, 0.0, 0.0), 1.0);
2241        let cube_b = create_cube(Point3::new(10.0, 0.0, 0.0), 1.0);
2242
2243        let result = boolean_operation(
2244            &cube_a,
2245            &cube_b,
2246            BooleanOp::Difference,
2247            &BooleanParams::default(),
2248        )
2249        .unwrap();
2250
2251        assert_eq!(result.mesh.vertices.len(), 8); // Just cube A
2252        assert_eq!(result.mesh.faces.len(), 12);
2253    }
2254
2255    #[test]
2256    fn test_non_overlapping_intersection() {
2257        let cube_a = create_cube(Point3::new(0.0, 0.0, 0.0), 1.0);
2258        let cube_b = create_cube(Point3::new(10.0, 0.0, 0.0), 1.0);
2259
2260        let result = boolean_operation(
2261            &cube_a,
2262            &cube_b,
2263            BooleanOp::Intersection,
2264            &BooleanParams::default(),
2265        )
2266        .unwrap();
2267
2268        assert!(result.mesh.vertices.is_empty()); // No overlap
2269        assert!(result.mesh.faces.is_empty());
2270    }
2271
2272    #[test]
2273    fn test_overlapping_union() {
2274        let cube_a = create_cube(Point3::new(0.0, 0.0, 0.0), 2.0);
2275        let cube_b = create_cube(Point3::new(1.0, 0.0, 0.0), 2.0);
2276
2277        let result = boolean_operation(
2278            &cube_a,
2279            &cube_b,
2280            BooleanOp::Union,
2281            &BooleanParams::default(),
2282        )
2283        .unwrap();
2284
2285        // Should have some faces
2286        assert!(!result.mesh.faces.is_empty());
2287    }
2288
2289    #[test]
2290    fn test_empty_mesh_error() {
2291        let empty = Mesh::new();
2292        let cube = create_cube(Point3::origin(), 1.0);
2293
2294        let result = boolean_operation(&empty, &cube, BooleanOp::Union, &BooleanParams::default());
2295        assert!(result.is_err());
2296    }
2297
2298    #[test]
2299    fn test_mesh_boolean_methods() {
2300        let cube_a = create_cube(Point3::new(0.0, 0.0, 0.0), 1.0);
2301        let cube_b = create_cube(Point3::new(10.0, 0.0, 0.0), 1.0);
2302
2303        let union = cube_a.boolean_union(&cube_b).unwrap();
2304        assert_eq!(union.faces.len(), 24);
2305
2306        let diff = cube_a.boolean_difference(&cube_b).unwrap();
2307        assert_eq!(diff.faces.len(), 12);
2308
2309        let inter = cube_a.boolean_intersection(&cube_b).unwrap();
2310        assert!(inter.faces.is_empty());
2311    }
2312
2313    #[test]
2314    fn test_point_inside_mesh() {
2315        let cube = create_cube(Point3::origin(), 2.0);
2316
2317        // Point clearly outside should be detected
2318        assert!(!is_point_inside_mesh(&Point3::new(10.0, 0.0, 0.0), &cube));
2319        // Note: Point at origin test can be sensitive to face winding
2320        // The is_point_inside_mesh function uses ray casting which can
2321        // have edge cases at exactly the center
2322    }
2323
2324    #[test]
2325    fn test_params_presets() {
2326        let scan_params = BooleanParams::for_scans();
2327        assert!(scan_params.tolerance > 1e-8);
2328
2329        let cad_params = BooleanParams::for_cad();
2330        assert!(cad_params.tolerance < 1e-8);
2331    }
2332
2333    #[test]
2334    fn test_coplanar_detection() {
2335        // Two triangles on the same plane
2336        let a0 = Point3::new(0.0, 0.0, 0.0);
2337        let a1 = Point3::new(1.0, 0.0, 0.0);
2338        let a2 = Point3::new(0.5, 1.0, 0.0);
2339
2340        let b0 = Point3::new(0.5, 0.5, 0.0);
2341        let b1 = Point3::new(1.5, 0.5, 0.0);
2342        let b2 = Point3::new(1.0, 1.5, 0.0);
2343
2344        let result = check_coplanarity(&a0, &a1, &a2, &b0, &b1, &b2, 1e-8);
2345        assert_eq!(result, CoplanarityResult::CoplanarSameOrientation);
2346
2347        // Opposite orientation
2348        let b0_flip = Point3::new(0.5, 0.5, 0.0);
2349        let b1_flip = Point3::new(1.0, 1.5, 0.0);
2350        let b2_flip = Point3::new(1.5, 0.5, 0.0);
2351
2352        let result = check_coplanarity(&a0, &a1, &a2, &b0_flip, &b1_flip, &b2_flip, 1e-8);
2353        assert_eq!(result, CoplanarityResult::CoplanarOppositeOrientation);
2354    }
2355
2356    #[test]
2357    fn test_not_coplanar() {
2358        let a0 = Point3::new(0.0, 0.0, 0.0);
2359        let a1 = Point3::new(1.0, 0.0, 0.0);
2360        let a2 = Point3::new(0.5, 1.0, 0.0);
2361
2362        // Triangle on different plane
2363        let b0 = Point3::new(0.0, 0.0, 1.0);
2364        let b1 = Point3::new(1.0, 0.0, 1.0);
2365        let b2 = Point3::new(0.5, 1.0, 1.0);
2366
2367        let result = check_coplanarity(&a0, &a1, &a2, &b0, &b1, &b2, 1e-8);
2368        assert_eq!(result, CoplanarityResult::NotCoplanar);
2369    }
2370
2371    #[test]
2372    fn test_coplanar_cubes_union() {
2373        // Two cubes sharing a face
2374        let cube_a = create_cube(Point3::new(0.0, 0.0, 0.0), 2.0);
2375        let cube_b = create_cube(Point3::new(2.0, 0.0, 0.0), 2.0); // Touching faces
2376
2377        let result = boolean_operation(
2378            &cube_a,
2379            &cube_b,
2380            BooleanOp::Union,
2381            &BooleanParams::default(),
2382        )
2383        .unwrap();
2384
2385        // Should detect coplanar faces
2386        // Note: The exact result depends on numerical tolerances
2387        assert!(!result.mesh.faces.is_empty());
2388    }
2389
2390    #[test]
2391    fn test_coplanar_strategy_exclude() {
2392        let cube_a = create_cube(Point3::new(0.0, 0.0, 0.0), 2.0);
2393        let cube_b = create_cube(Point3::new(2.0, 0.0, 0.0), 2.0);
2394
2395        let params = BooleanParams {
2396            coplanar_strategy: CoplanarStrategy::Exclude,
2397            ..Default::default()
2398        };
2399
2400        let result = boolean_operation(&cube_a, &cube_b, BooleanOp::Union, &params).unwrap();
2401
2402        // Result should exclude coplanar faces
2403        assert!(!result.mesh.faces.is_empty());
2404    }
2405
2406    #[test]
2407    fn test_bvh_construction() {
2408        let cube = create_cube(Point3::origin(), 2.0);
2409        let bvh = Bvh::build(&cube, 4);
2410
2411        assert!(bvh.root.is_some());
2412
2413        // Query with a bbox that overlaps the cube
2414        let query_bbox = Aabb::from_triangle(
2415            &Point3::new(-0.5, -0.5, -0.5),
2416            &Point3::new(0.5, -0.5, -0.5),
2417            &Point3::new(0.0, 0.5, -0.5),
2418        );
2419
2420        let candidates = bvh.query(&query_bbox, 1e-8);
2421        // Should find some triangles
2422        assert!(!candidates.is_empty());
2423    }
2424
2425    #[test]
2426    fn test_non_manifold_fix() {
2427        let mut mesh = Mesh::new();
2428
2429        // Create vertices for a simple case
2430        for i in 0..6 {
2431            mesh.vertices
2432                .push(Vertex::new(Point3::new(i as f64, 0.0, 0.0)));
2433        }
2434
2435        // Add 3 faces sharing the same edge (0-1)
2436        mesh.faces.push([0, 1, 2]);
2437        mesh.faces.push([0, 1, 3]);
2438        mesh.faces.push([0, 1, 4]); // This makes edge 0-1 non-manifold
2439
2440        let fixed = fix_non_manifold_edges(&mut mesh);
2441
2442        // Should have fixed 1 non-manifold edge by removing excess faces
2443        assert!(fixed > 0);
2444        assert_eq!(mesh.faces.len(), 2); // Only 2 faces should remain
2445    }
2446
2447    #[test]
2448    fn test_boolean_result_stats() {
2449        let cube_a = create_cube(Point3::new(0.0, 0.0, 0.0), 2.0);
2450        let cube_b = create_cube(Point3::new(1.0, 0.0, 0.0), 2.0);
2451
2452        let result = boolean_operation(
2453            &cube_a,
2454            &cube_b,
2455            BooleanOp::Union,
2456            &BooleanParams::default(),
2457        )
2458        .unwrap();
2459
2460        // Stats should be populated
2461        assert!(result.stats.faces_from_a > 0 || result.stats.faces_from_b > 0);
2462    }
2463
2464    #[test]
2465    fn test_triangles_overlap_2d() {
2466        // Overlapping triangles
2467        let a0 = [0.0, 0.0];
2468        let a1 = [2.0, 0.0];
2469        let a2 = [1.0, 2.0];
2470
2471        let b0 = [1.0, 0.0];
2472        let b1 = [3.0, 0.0];
2473        let b2 = [2.0, 2.0];
2474
2475        assert!(triangles_overlap_2d(&a0, &a1, &a2, &b0, &b1, &b2));
2476
2477        // Non-overlapping triangles
2478        let c0 = [10.0, 0.0];
2479        let c1 = [12.0, 0.0];
2480        let c2 = [11.0, 2.0];
2481
2482        assert!(!triangles_overlap_2d(&a0, &a1, &a2, &c0, &c1, &c2));
2483    }
2484
2485    // ========================================================================
2486    // Boolean Offset Tests
2487    // ========================================================================
2488
2489    #[test]
2490    fn test_boolean_offset_params_default() {
2491        let params = BooleanOffsetParams::default();
2492        assert!((params.offset - 1.0).abs() < 1e-10);
2493        assert_eq!(params.sphere_segments, 8);
2494        assert_eq!(params.cylinder_segments, 8);
2495        assert!(params.cleanup);
2496    }
2497
2498    #[test]
2499    fn test_boolean_offset_params_presets() {
2500        let high_quality = BooleanOffsetParams::high_quality(2.0);
2501        assert!((high_quality.offset - 2.0).abs() < 1e-10);
2502        assert_eq!(high_quality.sphere_segments, 16);
2503        assert_eq!(high_quality.cylinder_segments, 16);
2504
2505        let fast = BooleanOffsetParams::fast_preview(1.5);
2506        assert!((fast.offset - 1.5).abs() < 1e-10);
2507        assert_eq!(fast.sphere_segments, 4);
2508        assert_eq!(fast.cylinder_segments, 4);
2509    }
2510
2511    #[test]
2512    fn test_generate_sphere() {
2513        let center = Point3::new(5.0, 5.0, 5.0);
2514        let sphere = generate_sphere(&center, 2.0, 8);
2515
2516        // Should have vertices and faces
2517        assert!(!sphere.vertices.is_empty());
2518        assert!(!sphere.faces.is_empty());
2519
2520        // All vertices should be at radius from center
2521        for vertex in &sphere.vertices {
2522            let dist = (vertex.position - center).norm();
2523            assert!(
2524                (dist - 2.0).abs() < 1e-6,
2525                "Vertex at distance {} from center, expected 2.0",
2526                dist
2527            );
2528        }
2529    }
2530
2531    #[test]
2532    fn test_generate_cylinder() {
2533        let p0 = Point3::new(0.0, 0.0, 0.0);
2534        let p1 = Point3::new(10.0, 0.0, 0.0);
2535        let cylinder = generate_cylinder(&p0, &p1, 1.0, 8);
2536
2537        // Should have vertices and faces
2538        assert!(!cylinder.vertices.is_empty());
2539        assert!(!cylinder.faces.is_empty());
2540
2541        // Check that cylinder spans between endpoints
2542        let mut min_x = f64::MAX;
2543        let mut max_x = f64::MIN;
2544        for vertex in &cylinder.vertices {
2545            min_x = min_x.min(vertex.position.x);
2546            max_x = max_x.max(vertex.position.x);
2547        }
2548        assert!(min_x < 0.5, "Cylinder should start near x=0");
2549        assert!(max_x > 9.5, "Cylinder should extend to near x=10");
2550    }
2551
2552    #[test]
2553    fn test_generate_triangular_prism() {
2554        let v0 = Point3::new(0.0, 0.0, 0.0);
2555        let v1 = Point3::new(1.0, 0.0, 0.0);
2556        let v2 = Point3::new(0.5, 1.0, 0.0);
2557        let normal = Vector3::new(0.0, 0.0, 1.0);
2558
2559        let prism = generate_triangular_prism(&v0, &v1, &v2, &normal, 2.0);
2560
2561        // Should have 6 vertices (3 bottom + 3 top)
2562        assert_eq!(prism.vertices.len(), 6);
2563
2564        // Should have 8 faces (2 caps + 6 sides / 2 triangles each = 2 + 3*2 = 8)
2565        assert_eq!(prism.faces.len(), 8);
2566
2567        // Check height
2568        let mut min_z = f64::MAX;
2569        let mut max_z = f64::MIN;
2570        for vertex in &prism.vertices {
2571            min_z = min_z.min(vertex.position.z);
2572            max_z = max_z.max(vertex.position.z);
2573        }
2574        assert!(
2575            (max_z - min_z - 2.0).abs() < 1e-6,
2576            "Prism height should be 2.0"
2577        );
2578    }
2579
2580    #[test]
2581    fn test_collect_unique_edges() {
2582        let cube = create_cube(Point3::origin(), 2.0);
2583        let edges = collect_unique_edges(&cube);
2584
2585        // A triangulated cube has 18 unique edges:
2586        // - 12 edges on the wireframe
2587        // - 6 diagonal edges (one per quad face that's split into 2 triangles)
2588        assert_eq!(
2589            edges.len(),
2590            18,
2591            "Triangulated cube should have 18 unique edges, got {}",
2592            edges.len()
2593        );
2594    }
2595
2596    #[test]
2597    fn test_offset_boolean_cube() {
2598        let cube = create_cube(Point3::origin(), 2.0);
2599
2600        let params = BooleanOffsetParams::fast_preview(0.5);
2601        let result = offset_boolean(&cube, &params).unwrap();
2602
2603        // Should produce a valid mesh
2604        assert!(!result.mesh.vertices.is_empty());
2605        assert!(!result.mesh.faces.is_empty());
2606
2607        // Should have generated primitives
2608        assert_eq!(result.sphere_count, 8, "Cube has 8 vertices -> 8 spheres");
2609        assert_eq!(
2610            result.cylinder_count, 18,
2611            "Triangulated cube has 18 edges -> 18 cylinders"
2612        );
2613        assert_eq!(
2614            result.prism_count, 12,
2615            "Cube has 12 triangular faces -> 12 prisms"
2616        );
2617
2618        // Result should be larger than original
2619        let result_bounds = compute_bbox(&result.mesh);
2620        let original_bounds = compute_bbox(&cube);
2621
2622        let result_size = (result_bounds.1 - result_bounds.0).norm();
2623        let original_size = (original_bounds.1 - original_bounds.0).norm();
2624
2625        assert!(result_size > original_size, "Offset mesh should be larger");
2626    }
2627
2628    #[test]
2629    fn test_offset_boolean_zero_offset() {
2630        let cube = create_cube(Point3::origin(), 2.0);
2631
2632        let params = BooleanOffsetParams::with_offset(0.0);
2633        let result = offset_boolean(&cube, &params).unwrap();
2634
2635        // Should return copy of original
2636        assert_eq!(result.mesh.vertices.len(), cube.vertices.len());
2637        assert_eq!(result.mesh.faces.len(), cube.faces.len());
2638        assert_eq!(result.sphere_count, 0);
2639    }
2640
2641    #[test]
2642    fn test_offset_boolean_empty_mesh() {
2643        let empty = Mesh::new();
2644        let params = BooleanOffsetParams::default();
2645
2646        let result = offset_boolean(&empty, &params);
2647        assert!(result.is_err());
2648    }
2649
2650    #[test]
2651    fn test_mesh_offset_boolean_method() {
2652        let cube = create_cube(Point3::origin(), 2.0);
2653
2654        let offset_mesh = cube.offset_boolean(1.0).unwrap();
2655
2656        // Should produce a valid mesh
2657        assert!(!offset_mesh.vertices.is_empty());
2658        assert!(!offset_mesh.faces.is_empty());
2659    }
2660
2661    #[test]
2662    fn test_shell_boolean() {
2663        let cube = create_cube(Point3::origin(), 4.0);
2664
2665        let shell = shell_boolean(&cube, 1.0, None).unwrap();
2666
2667        // Should produce a valid mesh
2668        assert!(!shell.vertices.is_empty());
2669        assert!(!shell.faces.is_empty());
2670    }
2671
2672    #[test]
2673    fn test_shell_boolean_invalid_thickness() {
2674        let cube = create_cube(Point3::origin(), 4.0);
2675
2676        let result = shell_boolean(&cube, 0.0, None);
2677        assert!(result.is_err());
2678
2679        let result_neg = shell_boolean(&cube, -1.0, None);
2680        assert!(result_neg.is_err());
2681    }
2682
2683    #[test]
2684    fn test_mesh_shell_boolean_method() {
2685        let cube = create_cube(Point3::origin(), 4.0);
2686
2687        let shell = cube.shell_boolean(1.0).unwrap();
2688
2689        // Should produce a valid mesh
2690        assert!(!shell.vertices.is_empty());
2691        assert!(!shell.faces.is_empty());
2692    }
2693
2694    #[test]
2695    fn test_compute_vertex_normals() {
2696        let cube = create_cube(Point3::origin(), 2.0);
2697
2698        let normals = compute_vertex_normals(&cube);
2699
2700        // Should have one normal per vertex
2701        assert_eq!(normals.len(), cube.vertices.len());
2702
2703        // All normals should be normalized (or zero for degenerate cases)
2704        for normal in &normals {
2705            let len = normal.norm();
2706            assert!(
2707                len < 1e-6 || (len - 1.0).abs() < 1e-6,
2708                "Normal should be unit or zero, got {}",
2709                len
2710            );
2711        }
2712    }
2713}