mesh_repair/
intersect.rs

1//! Self-intersection detection for meshes.
2//!
3//! This module provides tools for detecting self-intersecting triangles in a mesh.
4//! Self-intersections cause 3D printing failures and indicate mesh topology issues.
5
6use nalgebra::{Point3, Vector3};
7use rayon::prelude::*;
8use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
9use tracing::{debug, info, warn};
10
11use crate::types::{Mesh, Triangle};
12
13/// Result of self-intersection detection.
14#[derive(Debug, Clone)]
15pub struct SelfIntersectionResult {
16    /// Whether the mesh has any self-intersections.
17    pub has_intersections: bool,
18    /// Number of intersecting triangle pairs found.
19    pub intersection_count: usize,
20    /// List of intersecting triangle pairs as (face_idx_a, face_idx_b).
21    /// Limited to first `max_reported` pairs.
22    pub intersecting_pairs: Vec<(u32, u32)>,
23    /// Total faces checked.
24    pub faces_checked: usize,
25    /// Whether the search was terminated early due to reaching max_reported.
26    pub truncated: bool,
27}
28
29impl SelfIntersectionResult {
30    /// Check if the mesh is free of self-intersections.
31    pub fn is_clean(&self) -> bool {
32        !self.has_intersections
33    }
34}
35
36impl std::fmt::Display for SelfIntersectionResult {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        if self.has_intersections {
39            write!(
40                f,
41                "Self-intersections found: {} pair(s){}",
42                self.intersection_count,
43                if self.truncated { " (truncated)" } else { "" }
44            )
45        } else {
46            write!(f, "No self-intersections detected")
47        }
48    }
49}
50
51/// Parameters for self-intersection detection.
52#[derive(Debug, Clone)]
53pub struct IntersectionParams {
54    /// Maximum number of intersecting pairs to report.
55    /// Set to 0 for unlimited (but may be slow for highly self-intersecting meshes).
56    pub max_reported: usize,
57    /// Epsilon for geometric comparisons.
58    pub epsilon: f64,
59    /// Whether to skip adjacent triangles (sharing an edge or vertex).
60    /// Usually true since adjacent triangles touching at edges isn't a "self-intersection".
61    pub skip_adjacent: bool,
62    /// Use GPU acceleration if available (requires `gpu` feature in mesh-shell).
63    /// When enabled and a GPU is available, collision detection will use
64    /// GPU compute shaders for significant speedup on dense meshes.
65    /// Falls back to CPU if GPU is unavailable or initialization fails.
66    pub use_gpu: bool,
67}
68
69impl Default for IntersectionParams {
70    fn default() -> Self {
71        Self {
72            max_reported: 100,
73            epsilon: 1e-10,
74            skip_adjacent: true,
75            use_gpu: false,
76        }
77    }
78}
79
80impl IntersectionParams {
81    /// Create params with GPU acceleration enabled.
82    ///
83    /// Requires the `gpu` feature to be enabled in the calling crate.
84    /// Falls back to CPU automatically if no GPU is available.
85    pub fn with_gpu(mut self) -> Self {
86        self.use_gpu = true;
87        self
88    }
89}
90
91/// Axis-aligned bounding box for spatial acceleration.
92#[derive(Debug, Clone, Copy)]
93struct Aabb {
94    min: Point3<f64>,
95    max: Point3<f64>,
96}
97
98impl Aabb {
99    /// Create AABB from a triangle.
100    fn from_triangle(tri: &Triangle) -> Self {
101        let min = Point3::new(
102            tri.v0.x.min(tri.v1.x).min(tri.v2.x),
103            tri.v0.y.min(tri.v1.y).min(tri.v2.y),
104            tri.v0.z.min(tri.v1.z).min(tri.v2.z),
105        );
106        let max = Point3::new(
107            tri.v0.x.max(tri.v1.x).max(tri.v2.x),
108            tri.v0.y.max(tri.v1.y).max(tri.v2.y),
109            tri.v0.z.max(tri.v1.z).max(tri.v2.z),
110        );
111        Self { min, max }
112    }
113
114    /// Expand AABB by epsilon for numerical robustness.
115    fn expand(&self, epsilon: f64) -> Self {
116        Self {
117            min: Point3::new(
118                self.min.x - epsilon,
119                self.min.y - epsilon,
120                self.min.z - epsilon,
121            ),
122            max: Point3::new(
123                self.max.x + epsilon,
124                self.max.y + epsilon,
125                self.max.z + epsilon,
126            ),
127        }
128    }
129
130    /// Check if two AABBs overlap.
131    fn overlaps(&self, other: &Aabb) -> bool {
132        self.min.x <= other.max.x
133            && self.max.x >= other.min.x
134            && self.min.y <= other.max.y
135            && self.max.y >= other.min.y
136            && self.min.z <= other.max.z
137            && self.max.z >= other.min.z
138    }
139}
140
141/// Detect self-intersections in a mesh.
142///
143/// Uses bounding box culling to avoid O(n²) triangle-triangle tests where possible.
144///
145/// # Arguments
146/// * `mesh` - The mesh to check
147/// * `params` - Detection parameters
148///
149/// # Returns
150/// A `SelfIntersectionResult` with information about any intersections found.
151///
152/// # Example
153/// ```
154/// use mesh_repair::{Mesh, Vertex};
155/// use mesh_repair::intersect::{detect_self_intersections, IntersectionParams};
156///
157/// let mut mesh = Mesh::new();
158/// mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
159/// mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0));
160/// mesh.vertices.push(Vertex::from_coords(0.0, 1.0, 0.0));
161/// mesh.faces.push([0, 1, 2]);
162///
163/// let result = detect_self_intersections(&mesh, &IntersectionParams::default());
164/// assert!(result.is_clean());
165/// ```
166pub fn detect_self_intersections(
167    mesh: &Mesh,
168    params: &IntersectionParams,
169) -> SelfIntersectionResult {
170    let face_count = mesh.faces.len();
171
172    if face_count < 2 {
173        return SelfIntersectionResult {
174            has_intersections: false,
175            intersection_count: 0,
176            intersecting_pairs: Vec::new(),
177            faces_checked: face_count,
178            truncated: false,
179        };
180    }
181
182    info!("Checking {} faces for self-intersections", face_count);
183
184    // Precompute triangles and AABBs
185    let triangles: Vec<Triangle> = mesh.triangles().collect();
186    let aabbs: Vec<Aabb> = triangles
187        .iter()
188        .map(|t| Aabb::from_triangle(t).expand(params.epsilon))
189        .collect();
190
191    // Build adjacency info if we need to skip adjacent triangles
192    let adjacency = if params.skip_adjacent {
193        Some(build_face_adjacency(&mesh.faces))
194    } else {
195        None
196    };
197
198    let max_pairs = if params.max_reported == 0 {
199        usize::MAX
200    } else {
201        params.max_reported
202    };
203
204    // Shared atomics for counting and early termination
205    let intersection_count = AtomicUsize::new(0);
206    let should_stop = AtomicBool::new(false);
207
208    // Process triangle pairs in parallel
209    // We parallelize the outer loop and collect intersecting pairs from each chunk
210    let intersecting_pairs: Vec<(u32, u32)> = (0..face_count)
211        .into_par_iter()
212        .flat_map(|i| {
213            // Early termination check
214            if should_stop.load(Ordering::Relaxed) {
215                return Vec::new();
216            }
217
218            let mut local_pairs = Vec::new();
219
220            for j in (i + 1)..face_count {
221                // Early termination check inside inner loop
222                if should_stop.load(Ordering::Relaxed) {
223                    break;
224                }
225
226                // Skip if AABBs don't overlap
227                if !aabbs[i].overlaps(&aabbs[j]) {
228                    continue;
229                }
230
231                // Skip adjacent triangles if requested
232                if let Some(ref adj) = adjacency
233                    && adj[i].contains(&(j as u32))
234                {
235                    continue;
236                }
237
238                // Perform actual triangle-triangle intersection test
239                if triangles_intersect(&triangles[i], &triangles[j], params.epsilon) {
240                    let count = intersection_count.fetch_add(1, Ordering::Relaxed);
241
242                    if count < max_pairs {
243                        local_pairs.push((i as u32, j as u32));
244                    }
245
246                    if count + 1 >= max_pairs && params.max_reported > 0 {
247                        should_stop.store(true, Ordering::Relaxed);
248                        break;
249                    }
250                }
251            }
252
253            local_pairs
254        })
255        .collect();
256
257    let final_count = intersection_count.load(Ordering::Relaxed);
258    let truncated = params.max_reported > 0 && final_count >= max_pairs;
259
260    if truncated {
261        debug!(
262            "Stopping intersection search after {} pairs (max_reported limit)",
263            max_pairs
264        );
265    }
266
267    if final_count > 0 {
268        warn!("Found {} self-intersecting triangle pair(s)", final_count);
269    } else {
270        info!("No self-intersections found");
271    }
272
273    SelfIntersectionResult {
274        has_intersections: final_count > 0,
275        intersection_count: final_count,
276        intersecting_pairs,
277        faces_checked: face_count,
278        truncated,
279    }
280}
281
282/// Build face adjacency (faces sharing vertices).
283fn build_face_adjacency(faces: &[[u32; 3]]) -> Vec<hashbrown::HashSet<u32>> {
284    use hashbrown::{HashMap, HashSet};
285
286    // Map vertex -> faces using that vertex
287    let mut vertex_to_faces: HashMap<u32, Vec<u32>> = HashMap::new();
288    for (face_idx, face) in faces.iter().enumerate() {
289        for &v in face {
290            vertex_to_faces.entry(v).or_default().push(face_idx as u32);
291        }
292    }
293
294    // For each face, find all faces sharing at least one vertex
295    let mut adjacency: Vec<HashSet<u32>> = vec![HashSet::new(); faces.len()];
296    for (face_idx, face) in faces.iter().enumerate() {
297        for &v in face {
298            if let Some(neighbors) = vertex_to_faces.get(&v) {
299                for &neighbor in neighbors {
300                    if neighbor != face_idx as u32 {
301                        adjacency[face_idx].insert(neighbor);
302                    }
303                }
304            }
305        }
306    }
307
308    adjacency
309}
310
311/// Test if two triangles intersect.
312///
313/// Uses the Möller–Trumbore style separating axis test.
314/// Two triangles intersect if they share interior points.
315fn triangles_intersect(t1: &Triangle, t2: &Triangle, epsilon: f64) -> bool {
316    // Compute normals
317    let n1 = t1.normal_unnormalized();
318    let n2 = t2.normal_unnormalized();
319
320    // Degenerate triangles don't intersect meaningfully
321    if n1.norm_squared() < epsilon * epsilon || n2.norm_squared() < epsilon * epsilon {
322        return false;
323    }
324
325    // Get edges of both triangles
326    let edges1 = [t1.v1 - t1.v0, t1.v2 - t1.v1, t1.v0 - t1.v2];
327    let edges2 = [t2.v1 - t2.v0, t2.v2 - t2.v1, t2.v0 - t2.v2];
328
329    // Check if triangles are coplanar (or nearly so)
330    let cross_normals = n1.cross(&n2);
331    let is_coplanar =
332        cross_normals.norm_squared() < epsilon * epsilon * n1.norm_squared() * n2.norm_squared();
333
334    if is_coplanar {
335        // For coplanar triangles, use 2D SAT test
336        // Project onto the plane and test edge normals as separating axes
337
338        // Test separation using edges of triangle 1 (perpendicular in-plane)
339        for edge in &edges1 {
340            let axis = n1.cross(edge);
341            if axis.norm_squared() > epsilon * epsilon && separated_by_axis(&axis, t1, t2, epsilon)
342            {
343                return false;
344            }
345        }
346
347        // Test separation using edges of triangle 2 (perpendicular in-plane)
348        for edge in &edges2 {
349            let axis = n2.cross(edge);
350            if axis.norm_squared() > epsilon * epsilon && separated_by_axis(&axis, t1, t2, epsilon)
351            {
352                return false;
353            }
354        }
355
356        // No separating axis in-plane - coplanar triangles overlap
357        return true;
358    }
359
360    // Non-coplanar case: Use standard 3D SAT
361
362    // Test separation along triangle normals
363    if separated_by_axis(&n1, t1, t2, epsilon) {
364        return false;
365    }
366    if separated_by_axis(&n2, t1, t2, epsilon) {
367        return false;
368    }
369
370    // Test 9 edge-edge cross product axes
371    for e1 in &edges1 {
372        for e2 in &edges2 {
373            let axis = e1.cross(e2);
374            if axis.norm_squared() > epsilon * epsilon && separated_by_axis(&axis, t1, t2, epsilon)
375            {
376                return false;
377            }
378        }
379    }
380
381    // No separating axis found - triangles intersect
382    true
383}
384
385/// Check if two triangles are separated by a given axis.
386fn separated_by_axis(axis: &Vector3<f64>, t1: &Triangle, t2: &Triangle, epsilon: f64) -> bool {
387    // Project triangle 1 vertices onto axis
388    let p1_0 = axis.dot(&t1.v0.coords);
389    let p1_1 = axis.dot(&t1.v1.coords);
390    let p1_2 = axis.dot(&t1.v2.coords);
391    let min1 = p1_0.min(p1_1).min(p1_2);
392    let max1 = p1_0.max(p1_1).max(p1_2);
393
394    // Project triangle 2 vertices onto axis
395    let p2_0 = axis.dot(&t2.v0.coords);
396    let p2_1 = axis.dot(&t2.v1.coords);
397    let p2_2 = axis.dot(&t2.v2.coords);
398    let min2 = p2_0.min(p2_1).min(p2_2);
399    let max2 = p2_0.max(p2_1).max(p2_2);
400
401    // Check if projections are separated (with epsilon tolerance)
402    max1 + epsilon < min2 || max2 + epsilon < min1
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408    use crate::Vertex;
409
410    fn create_xy_triangle(x: f64, y: f64, size: f64) -> Triangle {
411        Triangle::new(
412            Point3::new(x, y, 0.0),
413            Point3::new(x + size, y, 0.0),
414            Point3::new(x + size / 2.0, y + size, 0.0),
415        )
416    }
417
418    #[test]
419    fn test_aabb_overlap() {
420        let aabb1 = Aabb {
421            min: Point3::new(0.0, 0.0, 0.0),
422            max: Point3::new(1.0, 1.0, 1.0),
423        };
424        let aabb2 = Aabb {
425            min: Point3::new(0.5, 0.5, 0.5),
426            max: Point3::new(1.5, 1.5, 1.5),
427        };
428        let aabb3 = Aabb {
429            min: Point3::new(2.0, 2.0, 2.0),
430            max: Point3::new(3.0, 3.0, 3.0),
431        };
432
433        assert!(aabb1.overlaps(&aabb2));
434        assert!(aabb2.overlaps(&aabb1));
435        assert!(!aabb1.overlaps(&aabb3));
436        assert!(!aabb3.overlaps(&aabb1));
437    }
438
439    #[test]
440    fn test_non_intersecting_triangles() {
441        // Two triangles far apart
442        let t1 = create_xy_triangle(0.0, 0.0, 1.0);
443        let t2 = create_xy_triangle(10.0, 10.0, 1.0);
444
445        assert!(!triangles_intersect(&t1, &t2, 1e-10));
446    }
447
448    #[test]
449    fn test_coplanar_non_intersecting() {
450        // Two coplanar triangles that don't overlap
451        let t1 = create_xy_triangle(0.0, 0.0, 1.0);
452        let t2 = create_xy_triangle(2.0, 0.0, 1.0);
453
454        assert!(!triangles_intersect(&t1, &t2, 1e-10));
455    }
456
457    #[test]
458    fn test_coplanar_intersecting() {
459        // Two coplanar triangles that overlap
460        let t1 = create_xy_triangle(0.0, 0.0, 2.0);
461        let t2 = create_xy_triangle(0.5, 0.5, 2.0);
462
463        assert!(triangles_intersect(&t1, &t2, 1e-10));
464    }
465
466    #[test]
467    fn test_perpendicular_intersecting() {
468        // Triangle in XY plane
469        let t1 = Triangle::new(
470            Point3::new(-1.0, -1.0, 0.0),
471            Point3::new(1.0, -1.0, 0.0),
472            Point3::new(0.0, 1.0, 0.0),
473        );
474        // Triangle in XZ plane, crossing through t1
475        let t2 = Triangle::new(
476            Point3::new(-1.0, 0.0, -1.0),
477            Point3::new(1.0, 0.0, -1.0),
478            Point3::new(0.0, 0.0, 1.0),
479        );
480
481        assert!(triangles_intersect(&t1, &t2, 1e-10));
482    }
483
484    #[test]
485    fn test_perpendicular_non_intersecting() {
486        // Triangle in XY plane at z=0
487        let t1 = Triangle::new(
488            Point3::new(-1.0, -1.0, 0.0),
489            Point3::new(1.0, -1.0, 0.0),
490            Point3::new(0.0, 1.0, 0.0),
491        );
492        // Triangle in XZ plane at y=5 (doesn't cross t1)
493        let t2 = Triangle::new(
494            Point3::new(-1.0, 5.0, -1.0),
495            Point3::new(1.0, 5.0, -1.0),
496            Point3::new(0.0, 5.0, 1.0),
497        );
498
499        assert!(!triangles_intersect(&t1, &t2, 1e-10));
500    }
501
502    #[test]
503    fn test_detect_clean_mesh() {
504        // Simple tetrahedron - no self-intersections
505        let mut mesh = Mesh::new();
506        mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
507        mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0));
508        mesh.vertices.push(Vertex::from_coords(0.5, 1.0, 0.0));
509        mesh.vertices.push(Vertex::from_coords(0.5, 0.5, 1.0));
510        mesh.faces.push([0, 1, 2]);
511        mesh.faces.push([0, 1, 3]);
512        mesh.faces.push([1, 2, 3]);
513        mesh.faces.push([2, 0, 3]);
514
515        let result = detect_self_intersections(&mesh, &IntersectionParams::default());
516        assert!(result.is_clean());
517        assert_eq!(result.intersection_count, 0);
518    }
519
520    #[test]
521    fn test_detect_self_intersecting_mesh() {
522        // Create a mesh with two triangles that intersect
523        let mut mesh = Mesh::new();
524
525        // Triangle 1 in XY plane
526        mesh.vertices.push(Vertex::from_coords(-1.0, -1.0, 0.0));
527        mesh.vertices.push(Vertex::from_coords(1.0, -1.0, 0.0));
528        mesh.vertices.push(Vertex::from_coords(0.0, 1.0, 0.0));
529
530        // Triangle 2 in XZ plane, passing through triangle 1
531        mesh.vertices.push(Vertex::from_coords(-1.0, 0.0, -1.0));
532        mesh.vertices.push(Vertex::from_coords(1.0, 0.0, -1.0));
533        mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 1.0));
534
535        mesh.faces.push([0, 1, 2]);
536        mesh.faces.push([3, 4, 5]);
537
538        let result = detect_self_intersections(&mesh, &IntersectionParams::default());
539        assert!(!result.is_clean());
540        assert_eq!(result.intersection_count, 1);
541        assert_eq!(result.intersecting_pairs.len(), 1);
542        assert_eq!(result.intersecting_pairs[0], (0, 1));
543    }
544
545    #[test]
546    fn test_skip_adjacent_triangles() {
547        // Two triangles sharing an edge - should not be flagged as intersecting
548        let mut mesh = Mesh::new();
549        mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
550        mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0));
551        mesh.vertices.push(Vertex::from_coords(0.5, 1.0, 0.0));
552        mesh.vertices.push(Vertex::from_coords(0.5, -1.0, 0.0));
553        mesh.faces.push([0, 1, 2]);
554        mesh.faces.push([0, 3, 1]); // Shares edge 0-1
555
556        let params = IntersectionParams {
557            skip_adjacent: true,
558            ..Default::default()
559        };
560        let result = detect_self_intersections(&mesh, &params);
561        assert!(result.is_clean());
562    }
563
564    #[test]
565    fn test_empty_mesh() {
566        let mesh = Mesh::new();
567        let result = detect_self_intersections(&mesh, &IntersectionParams::default());
568        assert!(result.is_clean());
569        assert_eq!(result.faces_checked, 0);
570    }
571
572    #[test]
573    fn test_single_triangle() {
574        let mut mesh = Mesh::new();
575        mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
576        mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0));
577        mesh.vertices.push(Vertex::from_coords(0.0, 1.0, 0.0));
578        mesh.faces.push([0, 1, 2]);
579
580        let result = detect_self_intersections(&mesh, &IntersectionParams::default());
581        assert!(result.is_clean());
582        assert_eq!(result.faces_checked, 1);
583    }
584
585    #[test]
586    fn test_result_display() {
587        let result = SelfIntersectionResult {
588            has_intersections: true,
589            intersection_count: 5,
590            intersecting_pairs: vec![(0, 1), (2, 3)],
591            faces_checked: 100,
592            truncated: false,
593        };
594        let output = format!("{}", result);
595        assert!(output.contains("5 pair(s)"));
596
597        let clean_result = SelfIntersectionResult {
598            has_intersections: false,
599            intersection_count: 0,
600            intersecting_pairs: Vec::new(),
601            faces_checked: 100,
602            truncated: false,
603        };
604        let clean_output = format!("{}", clean_result);
605        assert!(clean_output.contains("No self-intersections"));
606    }
607
608    #[test]
609    fn test_max_reported_limit() {
610        // Create mesh with multiple intersections
611        let mut mesh = Mesh::new();
612
613        // Create several intersecting triangle pairs
614        for i in 0..5 {
615            let offset = i as f64 * 0.1;
616            // Triangle in XY plane
617            mesh.vertices
618                .push(Vertex::from_coords(-1.0 + offset, -1.0, 0.0));
619            mesh.vertices
620                .push(Vertex::from_coords(1.0 + offset, -1.0, 0.0));
621            mesh.vertices
622                .push(Vertex::from_coords(0.0 + offset, 1.0, 0.0));
623            // Triangle in XZ plane passing through
624            mesh.vertices
625                .push(Vertex::from_coords(-1.0 + offset, 0.0, -1.0));
626            mesh.vertices
627                .push(Vertex::from_coords(1.0 + offset, 0.0, -1.0));
628            mesh.vertices
629                .push(Vertex::from_coords(0.0 + offset, 0.0, 1.0));
630
631            let base = (i * 6) as u32;
632            mesh.faces.push([base, base + 1, base + 2]);
633            mesh.faces.push([base + 3, base + 4, base + 5]);
634        }
635
636        let params = IntersectionParams {
637            max_reported: 2,
638            ..Default::default()
639        };
640        let result = detect_self_intersections(&mesh, &params);
641        assert!(!result.is_clean());
642        assert!(result.intersecting_pairs.len() <= 2);
643    }
644}