Skip to main content

parry3d/transformation/mesh_intersection/
mesh_intersection.rs

1use super::{MeshIntersectionError, TriangleTriangleIntersection};
2use crate::bounding_volume::BoundingVolume;
3use crate::math::{Pose, Real, Vector3};
4use crate::partitioning::BvhNode;
5use crate::query::point::point_query::PointQueryWithLocation;
6use crate::query::PointQuery;
7use crate::shape::{TriMesh, Triangle};
8use crate::utils;
9use crate::utils::hashmap::Entry;
10use crate::utils::hashmap::HashMap;
11use crate::utils::hashset::HashSet;
12use alloc::collections::BTreeMap;
13use alloc::{vec, vec::Vec};
14use rstar::RTree;
15use spade::{ConstrainedDelaunayTriangulation, InsertionError, Triangulation as _};
16#[cfg(feature = "wavefront")]
17use std::path::PathBuf;
18
19/// A triangle with indices sorted in increasing order for deduplication in a hashmap.
20///
21/// Note that when converting a `[u32; 3]` into a `HashableTriangleIndices`, the result’s orientation
22/// might not match the input’s.
23#[derive(Copy, Clone, PartialEq, Eq, Hash)]
24struct HashableTriangleIndices([u32; 3]);
25
26impl From<[u32; 3]> for HashableTriangleIndices {
27    fn from([a, b, c]: [u32; 3]) -> Self {
28        let (sa, sb, sc) = utils::sort3(&a, &b, &c);
29        HashableTriangleIndices([*sa, *sb, *sc])
30    }
31}
32
33/// Metadata that specifies thresholds to use when making construction choices
34/// in mesh intersections.
35#[derive(Copy, Clone, PartialEq, Debug)]
36pub struct MeshIntersectionTolerances {
37    /// The smallest angle (in radians) that will be tolerated. A triangle with
38    /// a smaller angle is considered degenerate and will be deleted.
39    pub angle_epsilon: Real,
40    /// The maximum distance at which two points are considered to overlap in space.
41    /// If `||p1 - p2|| < global_insertion_epsilon` then p1 and p2 are considered
42    /// to be the same point.
43    pub global_insertion_epsilon: Real,
44    /// A multiplier coefficient to scale [`Self::global_insertion_epsilon`] when checking for
45    /// point duplication within a single triangle.
46    ///
47    /// Inside an individual triangle the distance at which two points are considered
48    /// to be the same is `global_insertion_epsilon * local_insertion_epsilon_mod`.
49    pub local_insertion_epsilon_scale: Real,
50    /// Three points forming a triangle with an area smaller than this epsilon are considered collinear.
51    pub collinearity_epsilon: Real,
52}
53
54impl Default for MeshIntersectionTolerances {
55    fn default() -> Self {
56        Self {
57            #[expect(clippy::unnecessary_cast)]
58            angle_epsilon: (0.005 as Real).to_radians(), // 0.005 degrees
59            global_insertion_epsilon: Real::EPSILON * 100.0,
60            local_insertion_epsilon_scale: 10.,
61            collinearity_epsilon: Real::EPSILON * 100.0,
62        }
63    }
64}
65
66/// Computes the intersection of two meshes.
67///
68/// The meshes must be oriented, have their half-edge topology computed, and must not be self-intersecting.
69/// The result mesh vertex coordinates are given in the local-space of `mesh1`.
70pub fn intersect_meshes(
71    pos1: &Pose,
72    mesh1: &TriMesh,
73    flip1: bool,
74    pos2: &Pose,
75    mesh2: &TriMesh,
76    flip2: bool,
77) -> Result<Option<TriMesh>, MeshIntersectionError> {
78    intersect_meshes_with_tolerances(
79        pos1,
80        mesh1,
81        flip1,
82        pos2,
83        mesh2,
84        flip2,
85        MeshIntersectionTolerances::default(),
86    )
87}
88
89/// Similar to `intersect_meshes`.
90///
91/// It allows to specify epsilons for how the algorithm will behave.
92/// See `MeshIntersectionTolerances` for details.
93pub fn intersect_meshes_with_tolerances(
94    pos1: &Pose,
95    mesh1: &TriMesh,
96    flip1: bool,
97    pos2: &Pose,
98    mesh2: &TriMesh,
99    flip2: bool,
100    tolerances: MeshIntersectionTolerances,
101) -> Result<Option<TriMesh>, MeshIntersectionError> {
102    if cfg!(debug_assertions) {
103        mesh1.assert_half_edge_topology_is_valid();
104        mesh2.assert_half_edge_topology_is_valid();
105    }
106
107    if mesh1.topology().is_none() || mesh2.topology().is_none() {
108        return Err(MeshIntersectionError::MissingTopology);
109    }
110
111    if mesh1.pseudo_normals_if_oriented().is_none() || mesh2.pseudo_normals_if_oriented().is_none()
112    {
113        return Err(MeshIntersectionError::MissingPseudoNormals);
114    }
115
116    let pos12 = pos1.inv_mul(pos2);
117
118    // 1: collect all the potential triangle-triangle intersections.
119    let intersection_candidates: Vec<_> = mesh1
120        .bvh()
121        .leaf_pairs(mesh2.bvh(), |node1: &BvhNode, node2: &BvhNode| {
122            let aabb1 = node1.aabb();
123            let aabb2 = node2.aabb().transform_by(&pos12);
124            aabb1.intersects(&aabb2)
125        })
126        .collect();
127
128    let mut deleted_faces1: HashSet<u32> = HashSet::default();
129    let mut deleted_faces2: HashSet<u32> = HashSet::default();
130    let mut new_indices1 = vec![];
131    let mut new_indices2 = vec![];
132
133    // 2: Identify all triangles that do actually intersect.
134    let mut intersections = vec![];
135    for (fid1, fid2) in &intersection_candidates {
136        let tri1 = mesh1.triangle(*fid1);
137        let tri2 = mesh2.triangle(*fid2).transformed(&pos12);
138
139        if super::triangle_triangle_intersection(&tri1, &tri2, tolerances.collinearity_epsilon)
140            .is_some()
141        {
142            intersections.push((*fid1, *fid2));
143            let _ = deleted_faces1.insert(*fid1);
144            let _ = deleted_faces2.insert(*fid2);
145        }
146    }
147
148    // 3: Grab all triangles that are inside the other mesh but do not intersect it.
149    extract_connected_components(
150        &pos12,
151        mesh1,
152        mesh2,
153        flip2,
154        &deleted_faces1,
155        &mut new_indices1,
156    );
157    extract_connected_components(
158        &pos12.inverse(),
159        mesh2,
160        mesh1,
161        flip1,
162        &deleted_faces2,
163        &mut new_indices2,
164    );
165
166    // 4: Initialize a new mesh by inserting points into a set. Duplicate points should
167    // hash to the same index.
168    let mut point_set = RTree::<TreeVector, _>::new();
169    let mut topology_indices = HashMap::default();
170    {
171        let mut insert_point = |position: Vector3| {
172            insert_into_set(
173                position,
174                &mut point_set,
175                tolerances.global_insertion_epsilon,
176            ) as u32
177        };
178        // Add the inside vertices and triangles from mesh1
179        for mut face in new_indices1 {
180            if flip1 {
181                face.swap(0, 1);
182            }
183
184            let idx = [
185                insert_point(pos1 * mesh1.vertices()[face[0] as usize]),
186                insert_point(pos1 * mesh1.vertices()[face[1] as usize]),
187                insert_point(pos1 * mesh1.vertices()[face[2] as usize]),
188            ];
189
190            if !is_topologically_degenerate(idx) {
191                insert_topology_indices(&mut topology_indices, idx);
192            }
193        }
194
195        // Add the inside vertices and triangles from mesh2
196        for mut face in new_indices2 {
197            if flip2 {
198                face.swap(0, 1);
199            }
200            let idx = [
201                insert_point(pos2 * mesh2.vertices()[face[0] as usize]),
202                insert_point(pos2 * mesh2.vertices()[face[1] as usize]),
203                insert_point(pos2 * mesh2.vertices()[face[2] as usize]),
204            ];
205
206            if !is_topologically_degenerate(idx) {
207                insert_topology_indices(&mut topology_indices, idx);
208            }
209        }
210    }
211
212    // 5: Associate constraint edges generated by a triangle-triangle intersection
213    // to each intersecting triangle where they occur.
214    let mut constraints1 = BTreeMap::<_, Vec<_>>::new();
215    let mut constraints2 = BTreeMap::<_, Vec<_>>::new();
216    for (fid1, fid2) in &intersections {
217        let tri1 = mesh1.triangle(*fid1).transformed(pos1);
218        let tri2 = mesh2.triangle(*fid2).transformed(pos2);
219
220        let list1 = constraints1.entry(fid1).or_default();
221        let list2 = constraints2.entry(fid2).or_default();
222
223        let intersection =
224            super::triangle_triangle_intersection(&tri1, &tri2, tolerances.collinearity_epsilon);
225        if let Some(intersection) = intersection {
226            match intersection {
227                TriangleTriangleIntersection::Segment { a, b } => {
228                    // For both triangles, add the points in the intersection
229                    // and their associated edge to the set.
230                    // Note this necessarily introduces duplicate points to the
231                    // set that need to be filtered out.
232                    list1.push([a.p1, b.p1]);
233                    list2.push([a.p1, b.p1]);
234                }
235                TriangleTriangleIntersection::Polygon(polygon) => {
236                    for i in 0..polygon.len() {
237                        let a = polygon[i];
238                        let b = polygon[(i + 1) % polygon.len()];
239
240                        // Triangles overlap in space, so only one constraint is needed.
241                        list1.push([a.p1, b.p1]);
242                    }
243                }
244            }
245        }
246    }
247
248    // 6: Collect all triangles that intersect and their associated constraint edges.
249    // For each such triangle, compute a CDT of its constraints. For each face in this CDT,
250    // if the face is contained in the opposite mesh, add it to the intersection mesh.
251    merge_triangle_sets(
252        mesh1,
253        mesh2,
254        &constraints1,
255        pos1,
256        pos2,
257        flip1,
258        flip2,
259        &tolerances,
260        &mut point_set,
261        &mut topology_indices,
262    )?;
263
264    merge_triangle_sets(
265        mesh2,
266        mesh1,
267        &constraints2,
268        pos2,
269        pos1,
270        flip2,
271        flip1,
272        &tolerances,
273        &mut point_set,
274        &mut topology_indices,
275    )?;
276
277    // 7: Sort the output points by insertion order.
278    let mut vertices: Vec<_> = point_set.iter().copied().collect();
279    vertices.sort_by_key(|a| a.id);
280    let vertices: Vec<_> = vertices.iter().map(|p| Vector3::from(p.point)).collect();
281
282    if !topology_indices.is_empty() {
283        Ok(Some(TriMesh::new(
284            vertices,
285            topology_indices.into_values().collect(),
286        )?))
287    } else {
288        Ok(None)
289    }
290}
291
292fn extract_connected_components(
293    pos12: &Pose,
294    mesh1: &TriMesh,
295    mesh2: &TriMesh,
296    flip2: bool,
297    deleted_faces1: &HashSet<u32>,
298    new_indices1: &mut Vec<[u32; 3]>,
299) {
300    let topo1 = mesh1.topology().unwrap();
301    let mut visited: HashSet<u32> = HashSet::default();
302    let mut to_visit = vec![];
303    let mut visited_conn_comp = if let Some(cc) = mesh1.connected_components() {
304        vec![false; cc.ranges.len()] // TODO: use a Vob instead?
305    } else {
306        vec![]
307    };
308
309    for face in deleted_faces1 {
310        if let Some(cc) = mesh1.connected_components() {
311            visited_conn_comp[cc.face_colors[*face as usize] as usize] = true;
312        }
313
314        let eid = topo1.faces[*face as usize].half_edge;
315        let edge_a = &topo1.half_edges[eid as usize];
316        let edge_b = &topo1.half_edges[edge_a.next as usize];
317        let edge_c = &topo1.half_edges[edge_b.next as usize];
318        let edges = [edge_a, edge_b, edge_c];
319
320        for edge in edges {
321            if let Some(twin) = topo1.half_edges.get(edge.twin as usize) {
322                if !deleted_faces1.contains(&twin.face) {
323                    let tri1 = mesh1.triangle(twin.face);
324
325                    if flip2
326                        ^ mesh2.contains_local_point(pos12.inverse_transform_point(tri1.center()))
327                    {
328                        to_visit.push(twin.face);
329                    }
330                }
331            }
332        }
333    }
334
335    // Propagate.
336    while let Some(face) = to_visit.pop() {
337        if !visited.insert(face) {
338            continue; // Already visited.
339        }
340
341        new_indices1.push(mesh1.indices()[face as usize]);
342
343        let eid = topo1.faces[face as usize].half_edge;
344        let edge_a = &topo1.half_edges[eid as usize];
345        let edge_b = &topo1.half_edges[edge_a.next as usize];
346        let edge_c = &topo1.half_edges[edge_b.next as usize];
347        let edges = [edge_a, edge_b, edge_c];
348
349        for edge in edges {
350            if let Some(twin) = topo1.half_edges.get(edge.twin as usize) {
351                if !deleted_faces1.contains(&twin.face) {
352                    to_visit.push(twin.face);
353                }
354            }
355        }
356    }
357
358    /*
359     * Deal with connected components that don’t intersect the other mesh.
360     */
361    if let Some(cc) = mesh1.connected_components() {
362        for (i, range) in cc.ranges.windows(2).enumerate() {
363            if !visited_conn_comp[i] {
364                // This connected component doesn’t intersect the second mesh.
365                // Classify one of its face (the "representative face", can be any
366                // face of the connected component) to determine
367                // if the whole thing is inside or outside.
368                let repr_face = cc.grouped_faces[range[0]];
369                let repr_pt = mesh1.triangle(repr_face).center();
370                let indices = mesh1.indices();
371
372                if flip2 ^ mesh2.contains_local_point(pos12.inverse_transform_point(repr_pt)) {
373                    new_indices1.extend(
374                        cc.grouped_faces[range[0]..range[1]]
375                            .iter()
376                            .map(|fid| indices[*fid as usize]),
377                    )
378                }
379            }
380        }
381    } else if deleted_faces1.is_empty() {
382        // Deal with the case where there is no intersection between the meshes.
383        let repr_pt = mesh1.triangle(0).center();
384
385        if flip2 ^ mesh2.contains_local_point(pos12.inverse_transform_point(repr_pt)) {
386            new_indices1.extend_from_slice(mesh1.indices());
387        }
388    }
389}
390
391fn triangulate_constraints_and_merge_duplicates(
392    tri: &Triangle,
393    constraints: &[[Vector3; 2]],
394    epsilon: Real,
395) -> Result<
396    (
397        ConstrainedDelaunayTriangulation<spade::Point2<Real>>,
398        Vec<Vector3>,
399    ),
400    InsertionError,
401> {
402    let mut constraints = constraints.to_vec();
403    // Add the triangle points to the triangulation.
404    let mut point_set = RTree::<TreeVector, _>::new();
405    let _ = insert_into_set(tri.a, &mut point_set, epsilon);
406    let _ = insert_into_set(tri.b, &mut point_set, epsilon);
407    let _ = insert_into_set(tri.c, &mut point_set, epsilon);
408
409    // Sometimes, points on the edge of a triangle are slightly off, and this makes
410    // spade think that there is a super thin triangle. Project points close to an edge
411    // onto the edge to get better results.
412    let tri_vtx = tri.vertices();
413    for point_pair in constraints.iter_mut() {
414        let p1 = point_pair[0];
415        let p2 = point_pair[1];
416
417        for i in 0..3 {
418            let q1 = tri_vtx[i];
419            let q2 = tri_vtx[(i + 1) % 3];
420
421            let proj1 = project_point_to_segment(p1, &[q1, q2]);
422            if (p1 - proj1).length() < epsilon {
423                point_pair[0] = Vector3::from(proj1);
424            }
425
426            let proj2 = project_point_to_segment(p2, &[q1, q2]);
427            if (p2 - proj2).length() < epsilon {
428                point_pair[1] = Vector3::from(proj2);
429            }
430        }
431    }
432
433    // Generate edge, taking care to merge duplicate vertices.
434    let mut edges = Vec::new();
435    for point_pair in constraints {
436        let p1_id = insert_into_set(point_pair[0], &mut point_set, epsilon);
437        let p2_id = insert_into_set(point_pair[1], &mut point_set, epsilon);
438
439        if p1_id != p2_id {
440            edges.push([p1_id, p2_id]);
441        }
442    }
443
444    let mut points: Vec<_> = point_set.iter().cloned().collect();
445    points.sort_by_key(|a| a.id);
446
447    let tri_points = tri.vertices();
448    let best_source = tri.angle_closest_to_90();
449    let d1 = tri_points[(best_source + 2) % 3] - tri_points[(best_source + 1) % 3];
450    let d2 = tri_points[best_source] - tri_points[(best_source + 1) % 3];
451    let (e1, e2) = planar_gram_schmidt(d1, d2);
452
453    let project = |p: Vector3| spade::Point2::new(e1.dot(p), e2.dot(p));
454
455    // Project points into 2D and triangulate the resulting set.
456    let planar_points: Vec<_> = points
457        .iter()
458        .copied()
459        .map(|point| {
460            let point_proj = project(point.point);
461            utils::sanitize_spade_point(point_proj)
462        })
463        .collect();
464    // NOTE: use the fallible `try_bulk_load_cdt` instead of `bulk_load_cdt`: the latter panics
465    //       when constraint edges overlap, which can legitimately happen for degenerate inputs
466    //       (e.g. co-planar triangles). Overlapping constraints are simply skipped.
467    let cdt_triangulation =
468        ConstrainedDelaunayTriangulation::try_bulk_load_cdt(planar_points, edges, |_conflict| {})?;
469    debug_assert!(cdt_triangulation.vertices().len() == points.len());
470
471    let points = points.into_iter().map(|p| Vector3::from(p.point)).collect();
472    Ok((cdt_triangulation, points))
473}
474
475// We heavily recommend that this is left here in case one needs to debug the above code.
476#[cfg(feature = "wavefront")]
477fn _points_to_obj(mesh: &[Vector3], path: &PathBuf) {
478    use std::io::Write;
479    let mut file = std::fs::File::create(path).unwrap();
480
481    for p in mesh {
482        writeln!(file, "v {} {} {}", p.x, p.y, p.z).unwrap();
483    }
484}
485
486// We heavily recommend that this is left here in case one needs to debug the above code.
487#[cfg(feature = "wavefront")]
488fn _points_and_edges_to_obj(mesh: &[Vector3], edges: &[[usize; 2]], path: &PathBuf) {
489    use std::io::Write;
490    let mut file = std::fs::File::create(path).unwrap();
491
492    for p in mesh {
493        writeln!(file, "v {} {} {}", p.x, p.y, p.z).unwrap();
494    }
495
496    for e in edges {
497        writeln!(file, "l {} {}", e[0] + 1, e[1] + 1).unwrap();
498    }
499}
500
501#[derive(Copy, Clone, PartialEq, Debug, Default)]
502struct TreeVector {
503    point: Vector3,
504    id: usize,
505}
506
507impl rstar::Point for TreeVector {
508    type Scalar = Real;
509    const DIMENSIONS: usize = 3;
510
511    fn generate(mut generator: impl FnMut(usize) -> Self::Scalar) -> Self {
512        TreeVector {
513            point: Vector3::new(generator(0), generator(1), generator(2)),
514            id: usize::MAX,
515        }
516    }
517
518    fn nth(&self, index: usize) -> Self::Scalar {
519        self.point[index]
520    }
521
522    fn nth_mut(&mut self, index: usize) -> &mut Self::Scalar {
523        &mut self.point[index]
524    }
525}
526
527fn insert_into_set(position: Vector3, point_set: &mut RTree<TreeVector>, epsilon: Real) -> usize {
528    let point_count = point_set.size();
529    let point_to_insert = TreeVector {
530        point: position,
531        id: point_count,
532    };
533
534    match point_set.nearest_neighbor(point_to_insert) {
535        Some(tree_point) if (tree_point.point - position).length_squared() <= epsilon => {
536            tree_point.id
537        }
538        _ => {
539            point_set.insert(point_to_insert);
540            debug_assert!(point_set.size() == point_count + 1);
541            point_count
542        }
543    }
544}
545
546fn smallest_angle(points: &[Vector3]) -> Real {
547    let n = points.len();
548
549    let mut worst_cos: Real = -2.0;
550    for i in 0..points.len() {
551        let d1 = (points[i] - points[(i + 1) % n]).normalize();
552        let d2 = (points[(i + 2) % n] - points[(i + 1) % n]).normalize();
553
554        let cos = d1.dot(d2);
555        if cos > worst_cos {
556            worst_cos = cos;
557        }
558    }
559
560    worst_cos.acos()
561}
562
563fn planar_gram_schmidt(v1: Vector3, v2: Vector3) -> (Vector3, Vector3) {
564    let u1 = v1;
565    let u2 = v2 - (v2.dot(u1) / u1.length_squared()) * u1;
566
567    let e1 = u1.normalize();
568    let e2 = u2.normalize();
569
570    (e1, e2)
571}
572
573fn project_point_to_segment(point: Vector3, segment: &[Vector3; 2]) -> Vector3 {
574    let dir = segment[1] - segment[0];
575    let local = point - segment[0];
576
577    let norm = dir.length();
578    // Restrict the result to the segment portion of the line.
579    let coeff = (dir.dot(local) / norm).clamp(0., norm);
580
581    segment[0] + coeff * dir.normalize()
582}
583
584/// No matter how smart we are about computing intersections. It is always possible
585/// to create ultra thin triangles when a point lies on an edge of a triangle. These
586/// are degenerate and need to be removed.
587fn is_triangle_degenerate(
588    triangle: &[Vector3; 3],
589    epsilon_angle: Real,
590    epsilon_distance: Real,
591) -> bool {
592    if smallest_angle(triangle) < epsilon_angle {
593        return true;
594    }
595
596    for i in 0..3 {
597        let (dir, dist) = (triangle[(i + 1) % 3] - triangle[(i + 2) % 3]).normalize_and_length();
598        if dist < epsilon_distance {
599            return true;
600        }
601
602        let proj = triangle[(i + 2) % 3] + (triangle[i] - triangle[(i + 2) % 3]).dot(dir) * dir;
603
604        if (proj - triangle[i]).length() < epsilon_distance {
605            return true;
606        }
607    }
608
609    false
610}
611
612fn merge_triangle_sets(
613    mesh1: &TriMesh,
614    mesh2: &TriMesh,
615    triangle_constraints: &BTreeMap<&u32, Vec<[Vector3; 2]>>,
616    pos1: &Pose,
617    pos2: &Pose,
618    flip1: bool,
619    flip2: bool,
620    metadata: &MeshIntersectionTolerances,
621    point_set: &mut RTree<TreeVector>,
622    topology_indices: &mut HashMap<HashableTriangleIndices, [u32; 3]>,
623) -> Result<(), MeshIntersectionError> {
624    // For each triangle, and each constraint edge associated to that triangle,
625    // make a triangulation of the face and sort whether each generated
626    // sub-triangle is part of the intersection.
627    // For each sub-triangle that is part of the intersection, add them to the
628    // output mesh.
629    for (triangle_id, constraints) in triangle_constraints.iter() {
630        let tri = mesh1.triangle(**triangle_id).transformed(pos1);
631
632        let (delaunay, points) = triangulate_constraints_and_merge_duplicates(
633            &tri,
634            constraints,
635            metadata.global_insertion_epsilon * metadata.local_insertion_epsilon_scale,
636        )
637        .or(Err(MeshIntersectionError::TriangulationError))?;
638
639        for face in delaunay.inner_faces() {
640            let verts = face.vertices();
641            let p1 = points[verts[0].index()];
642            let p2 = points[verts[1].index()];
643            let p3 = points[verts[2].index()];
644
645            // Sometimes the triangulation is messed up due to numerical errors. If
646            // a triangle does not survive this test it should be deleted.
647            if is_triangle_degenerate(
648                &[p1, p2, p3],
649                metadata.angle_epsilon,
650                metadata.global_insertion_epsilon,
651            ) {
652                continue;
653            }
654
655            let center = Triangle {
656                a: p1,
657                b: p2,
658                c: p3,
659            }
660            .center();
661
662            let epsilon = metadata.global_insertion_epsilon;
663            let projection = mesh2
664                .project_local_point_and_get_location(pos2.inverse_transform_point(center), true)
665                .0;
666
667            if flip2 ^ (projection.is_inside_eps(center, epsilon)) {
668                let mut new_tri_idx = [
669                    insert_into_set(p1, point_set, epsilon) as u32,
670                    insert_into_set(p2, point_set, epsilon) as u32,
671                    insert_into_set(p3, point_set, epsilon) as u32,
672                ];
673
674                if flip1 {
675                    new_tri_idx.swap(0, 1)
676                }
677
678                // This should *never* trigger. If it does
679                // it means the code has created a triangle with duplicate vertices,
680                // which means we encountered an unaccounted for edge case.
681                if is_topologically_degenerate(new_tri_idx) {
682                    return Err(MeshIntersectionError::DuplicateVertices);
683                }
684
685                insert_topology_indices(topology_indices, new_tri_idx);
686            }
687        }
688    }
689
690    Ok(())
691}
692
693// Insert in the hashmap with sorted indices to avoid adding duplicates.
694//
695// We also check if we don’t keep pairs of triangles that have the same
696// set of indices but opposite orientations. If this happens, both the new triangle, and the one it
697// matched with are removed (because they describe a degenerate piece of volume).
698fn insert_topology_indices(
699    topology_indices: &mut HashMap<HashableTriangleIndices, [u32; 3]>,
700    new_tri_idx: [u32; 3],
701) {
702    match topology_indices.entry(new_tri_idx.into()) {
703        Entry::Vacant(e) => {
704            let _ = e.insert(new_tri_idx);
705        }
706        Entry::Occupied(e) => {
707            fn same_orientation(a: &[u32; 3], b: &[u32; 3]) -> bool {
708                let ib = if a[0] == b[0] {
709                    0
710                } else if a[0] == b[1] {
711                    1
712                } else {
713                    2
714                };
715                a[1] == b[(ib + 1) % 3]
716            }
717
718            if !same_orientation(e.get(), &new_tri_idx) {
719                // If we are inserting two identical triangles but with mismatching
720                // orientations, we can just ignore both because they cover a degenerate
721                // 2D plane.
722                #[cfg(feature = "enhanced-determinism")]
723                let _ = e.swap_remove();
724                #[cfg(not(feature = "enhanced-determinism"))]
725                let _ = e.remove();
726            }
727        }
728    }
729}
730
731fn is_topologically_degenerate(tri_idx: [u32; 3]) -> bool {
732    tri_idx[0] == tri_idx[1] || tri_idx[0] == tri_idx[2] || tri_idx[1] == tri_idx[2]
733}
734
735#[cfg(feature = "wavefront")]
736#[cfg(test)]
737mod tests {
738    use super::*;
739    use crate::math::Vector;
740    use crate::shape::{Ball, Cuboid, TriMeshFlags};
741    use obj::Obj;
742    use obj::ObjData;
743
744    #[test]
745    fn test_same_mesh_intersection() {
746        let Obj {
747            data: ObjData {
748                position, objects, ..
749            },
750            ..
751        } = Obj::load("../../assets/tests/low_poly_bunny.obj").unwrap();
752
753        let mesh = TriMesh::with_flags(
754            position
755                .iter()
756                .map(|v| Vector::new(v[0] as Real, v[1] as Real, v[2] as Real))
757                .collect::<Vec<_>>(),
758            objects[0].groups[0]
759                .polys
760                .iter()
761                .map(|p| [p.0[0].0 as u32, p.0[1].0 as u32, p.0[2].0 as u32])
762                .collect::<Vec<_>>(),
763            TriMeshFlags::all(),
764        )
765        .unwrap();
766
767        let _ = intersect_meshes(
768            &Pose::identity(),
769            &mesh,
770            false,
771            &Pose::identity(),
772            &mesh,
773            false,
774        )
775        .unwrap()
776        .unwrap();
777
778        let _ = mesh.to_obj_file(&PathBuf::from("same_test.obj"));
779    }
780
781    #[test]
782    fn test_non_origin_pos1_pos2_intersection() {
783        let ball = Ball::new(2f32 as Real).to_trimesh(10, 10);
784        let cuboid = Cuboid::new(Vector::new(2.0, 1.0, 1.0)).to_trimesh();
785        let mut sphere_mesh = TriMesh::new(ball.0, ball.1).unwrap();
786        sphere_mesh.set_flags(TriMeshFlags::all()).unwrap();
787        let mut cuboid_mesh = TriMesh::new(cuboid.0, cuboid.1).unwrap();
788        cuboid_mesh.set_flags(TriMeshFlags::all()).unwrap();
789
790        let res = intersect_meshes(
791            &Pose::translation(1.0, 0.0, 0.0),
792            &cuboid_mesh,
793            false,
794            &Pose::translation(2.0, 0.0, 0.0),
795            &sphere_mesh,
796            false,
797        )
798        .unwrap()
799        .unwrap();
800
801        let _ = res.to_obj_file(&PathBuf::from("test_non_origin_pos1_pos2_intersection.obj"));
802
803        let bounding_sphere = res.local_bounding_sphere();
804        assert!(bounding_sphere.center == Vector::new(1.5, 0.0, 0.0));
805        assert_relative_eq!(2.0615528, bounding_sphere.radius, epsilon = 1.0e-5);
806    }
807
808    #[test]
809    fn test_offset_cylinder_intersection() {
810        let Obj {
811            data: ObjData {
812                position, objects, ..
813            },
814            ..
815        } = Obj::load("../../assets/tests/offset_cylinder.obj").unwrap();
816
817        let offset_mesh = TriMesh::with_flags(
818            position
819                .iter()
820                .map(|v| Vector::new(v[0] as Real, v[1] as Real, v[2] as Real))
821                .collect::<Vec<_>>(),
822            objects[0].groups[0]
823                .polys
824                .iter()
825                .map(|p| [p.0[0].0 as u32, p.0[1].0 as u32, p.0[2].0 as u32])
826                .collect::<Vec<_>>(),
827            TriMeshFlags::all(),
828        )
829        .unwrap();
830
831        let Obj {
832            data: ObjData {
833                position, objects, ..
834            },
835            ..
836        } = Obj::load("../../assets/tests/center_cylinder.obj").unwrap();
837
838        let center_mesh = TriMesh::with_flags(
839            position
840                .iter()
841                .map(|v| Vector::new(v[0] as Real, v[1] as Real, v[2] as Real))
842                .collect::<Vec<_>>(),
843            objects[0].groups[0]
844                .polys
845                .iter()
846                .map(|p| [p.0[0].0 as u32, p.0[1].0 as u32, p.0[2].0 as u32])
847                .collect::<Vec<_>>(),
848            TriMeshFlags::all(),
849        )
850        .unwrap();
851
852        let res = intersect_meshes(
853            &Pose::identity(),
854            &center_mesh,
855            false,
856            &Pose::identity(),
857            &offset_mesh,
858            false,
859        )
860        .unwrap()
861        .unwrap();
862
863        let _ = res.to_obj_file(&PathBuf::from("offset_test.obj"));
864    }
865
866    #[test]
867    fn test_stair_bar_intersection() {
868        let Obj {
869            data: ObjData {
870                position, objects, ..
871            },
872            ..
873        } = Obj::load("../../assets/tests/stairs.obj").unwrap();
874
875        let stair_mesh = TriMesh::with_flags(
876            position
877                .iter()
878                .map(|v| Vector::new(v[0] as Real, v[1] as Real, v[2] as Real))
879                .collect::<Vec<_>>(),
880            objects[0].groups[0]
881                .polys
882                .iter()
883                .map(|p| [p.0[0].0 as u32, p.0[1].0 as u32, p.0[2].0 as u32])
884                .collect::<Vec<_>>(),
885            TriMeshFlags::all(),
886        )
887        .unwrap();
888
889        let Obj {
890            data: ObjData {
891                position, objects, ..
892            },
893            ..
894        } = Obj::load("../../assets/tests/bar.obj").unwrap();
895
896        let bar_mesh = TriMesh::with_flags(
897            position
898                .iter()
899                .map(|v| Vector::new(v[0] as Real, v[1] as Real, v[2] as Real))
900                .collect::<Vec<_>>(),
901            objects[0].groups[0]
902                .polys
903                .iter()
904                .map(|p| [p.0[0].0 as u32, p.0[1].0 as u32, p.0[2].0 as u32])
905                .collect::<Vec<_>>(),
906            TriMeshFlags::all(),
907        )
908        .unwrap();
909
910        let res = intersect_meshes(
911            &Pose::identity(),
912            &stair_mesh,
913            false,
914            &Pose::identity(),
915            &bar_mesh,
916            false,
917        )
918        .unwrap()
919        .unwrap();
920
921        let _ = res.to_obj_file(&PathBuf::from("stair_test.obj"));
922    }
923
924    #[test]
925    fn test_complex_intersection() {
926        let Obj {
927            data: ObjData {
928                position, objects, ..
929            },
930            ..
931        } = Obj::load("../../assets/tests/low_poly_bunny.obj").unwrap();
932
933        let bunny_mesh = TriMesh::with_flags(
934            position
935                .iter()
936                .map(|v| Vector::new(v[0] as Real, v[1] as Real, v[2] as Real))
937                .collect::<Vec<_>>(),
938            objects[0].groups[0]
939                .polys
940                .iter()
941                .map(|p| [p.0[0].0 as u32, p.0[1].0 as u32, p.0[2].0 as u32])
942                .collect::<Vec<_>>(),
943            TriMeshFlags::all(),
944        )
945        .unwrap();
946
947        let Obj {
948            data: ObjData {
949                position, objects, ..
950            },
951            ..
952        } = Obj::load("../../assets/tests/poly_cylinder.obj").unwrap();
953
954        let cylinder_mesh = TriMesh::with_flags(
955            position
956                .iter()
957                .map(|v| Vector::new(v[0] as Real, v[1] as Real, v[2] as Real))
958                .collect::<Vec<_>>(),
959            objects[0].groups[0]
960                .polys
961                .iter()
962                .map(|p| [p.0[0].0 as u32, p.0[1].0 as u32, p.0[2].0 as u32])
963                .collect::<Vec<_>>(),
964            TriMeshFlags::all(),
965        )
966        .unwrap();
967
968        let res = intersect_meshes(
969            &Pose::identity(),
970            &bunny_mesh,
971            false,
972            &Pose::identity(),
973            &cylinder_mesh,
974            true,
975        )
976        .unwrap()
977        .unwrap();
978
979        let _ = res.to_obj_file(&PathBuf::from("complex_test.obj"));
980    }
981}