parry3d_f64/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(|a, b| a.id.cmp(&b.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(|a, b| a.id.cmp(&b.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    let cdt_triangulation = ConstrainedDelaunayTriangulation::bulk_load_cdt(planar_points, edges)?;
465    debug_assert!(cdt_triangulation.vertices().len() == points.len());
466
467    let points = points.into_iter().map(|p| Vector3::from(p.point)).collect();
468    Ok((cdt_triangulation, points))
469}
470
471// We heavily recommend that this is left here in case one needs to debug the above code.
472#[cfg(feature = "wavefront")]
473fn _points_to_obj(mesh: &[Vector3], path: &PathBuf) {
474    use std::io::Write;
475    let mut file = std::fs::File::create(path).unwrap();
476
477    for p in mesh {
478        writeln!(file, "v {} {} {}", p.x, p.y, p.z).unwrap();
479    }
480}
481
482// We heavily recommend that this is left here in case one needs to debug the above code.
483#[cfg(feature = "wavefront")]
484fn _points_and_edges_to_obj(mesh: &[Vector3], edges: &[[usize; 2]], path: &PathBuf) {
485    use std::io::Write;
486    let mut file = std::fs::File::create(path).unwrap();
487
488    for p in mesh {
489        writeln!(file, "v {} {} {}", p.x, p.y, p.z).unwrap();
490    }
491
492    for e in edges {
493        writeln!(file, "l {} {}", e[0] + 1, e[1] + 1).unwrap();
494    }
495}
496
497#[derive(Copy, Clone, PartialEq, Debug, Default)]
498struct TreeVector {
499    point: Vector3,
500    id: usize,
501}
502
503impl rstar::Point for TreeVector {
504    type Scalar = Real;
505    const DIMENSIONS: usize = 3;
506
507    fn generate(mut generator: impl FnMut(usize) -> Self::Scalar) -> Self {
508        TreeVector {
509            point: Vector3::new(generator(0), generator(1), generator(2)),
510            id: usize::MAX,
511        }
512    }
513
514    fn nth(&self, index: usize) -> Self::Scalar {
515        self.point[index]
516    }
517
518    fn nth_mut(&mut self, index: usize) -> &mut Self::Scalar {
519        &mut self.point[index]
520    }
521}
522
523fn insert_into_set(position: Vector3, point_set: &mut RTree<TreeVector>, epsilon: Real) -> usize {
524    let point_count = point_set.size();
525    let point_to_insert = TreeVector {
526        point: position,
527        id: point_count,
528    };
529
530    match point_set.nearest_neighbor(&point_to_insert) {
531        Some(tree_point) => {
532            if (tree_point.point - position).length_squared() <= epsilon {
533                tree_point.id
534            } else {
535                point_set.insert(point_to_insert);
536                debug_assert!(point_set.size() == point_count + 1);
537                point_count
538            }
539        }
540        None => {
541            point_set.insert(point_to_insert);
542            debug_assert!(point_set.size() == point_count + 1);
543            point_count
544        }
545    }
546}
547
548fn smallest_angle(points: &[Vector3]) -> Real {
549    let n = points.len();
550
551    let mut worst_cos: Real = -2.0;
552    for i in 0..points.len() {
553        let d1 = (points[i] - points[(i + 1) % n]).normalize();
554        let d2 = (points[(i + 2) % n] - points[(i + 1) % n]).normalize();
555
556        let cos = d1.dot(d2);
557        if cos > worst_cos {
558            worst_cos = cos;
559        }
560    }
561
562    worst_cos.acos()
563}
564
565fn planar_gram_schmidt(v1: Vector3, v2: Vector3) -> (Vector3, Vector3) {
566    let u1 = v1;
567    let u2 = v2 - (v2.dot(u1) / u1.length_squared()) * u1;
568
569    let e1 = u1.normalize();
570    let e2 = u2.normalize();
571
572    (e1, e2)
573}
574
575fn project_point_to_segment(point: Vector3, segment: &[Vector3; 2]) -> Vector3 {
576    let dir = segment[1] - segment[0];
577    let local = point - segment[0];
578
579    let norm = dir.length();
580    // Restrict the result to the segment portion of the line.
581    let coeff = (dir.dot(local) / norm).clamp(0., norm);
582
583    segment[0] + coeff * dir.normalize()
584}
585
586/// No matter how smart we are about computing intersections. It is always possible
587/// to create ultra thin triangles when a point lies on an edge of a triangle. These
588/// are degenerate and need to be removed.
589fn is_triangle_degenerate(
590    triangle: &[Vector3; 3],
591    epsilon_angle: Real,
592    epsilon_distance: Real,
593) -> bool {
594    if smallest_angle(triangle) < epsilon_angle {
595        return true;
596    }
597
598    for i in 0..3 {
599        let (dir, dist) = (triangle[(i + 1) % 3] - triangle[(i + 2) % 3]).normalize_and_length();
600        if dist < epsilon_distance {
601            return true;
602        }
603
604        let proj = triangle[(i + 2) % 3] + (triangle[i] - triangle[(i + 2) % 3]).dot(dir) * dir;
605
606        if (proj - triangle[i]).length() < epsilon_distance {
607            return true;
608        }
609    }
610
611    false
612}
613
614fn merge_triangle_sets(
615    mesh1: &TriMesh,
616    mesh2: &TriMesh,
617    triangle_constraints: &BTreeMap<&u32, Vec<[Vector3; 2]>>,
618    pos1: &Pose,
619    pos2: &Pose,
620    flip1: bool,
621    flip2: bool,
622    metadata: &MeshIntersectionTolerances,
623    point_set: &mut RTree<TreeVector>,
624    topology_indices: &mut HashMap<HashableTriangleIndices, [u32; 3]>,
625) -> Result<(), MeshIntersectionError> {
626    // For each triangle, and each constraint edge associated to that triangle,
627    // make a triangulation of the face and sort whether each generated
628    // sub-triangle is part of the intersection.
629    // For each sub-triangle that is part of the intersection, add them to the
630    // output mesh.
631    for (triangle_id, constraints) in triangle_constraints.iter() {
632        let tri = mesh1.triangle(**triangle_id).transformed(pos1);
633
634        let (delaunay, points) = triangulate_constraints_and_merge_duplicates(
635            &tri,
636            constraints,
637            metadata.global_insertion_epsilon * metadata.local_insertion_epsilon_scale,
638        )
639        .or(Err(MeshIntersectionError::TriangulationError))?;
640
641        for face in delaunay.inner_faces() {
642            let verts = face.vertices();
643            let p1 = points[verts[0].index()];
644            let p2 = points[verts[1].index()];
645            let p3 = points[verts[2].index()];
646
647            // Sometimes the triangulation is messed up due to numerical errors. If
648            // a triangle does not survive this test it should be deleted.
649            if is_triangle_degenerate(
650                &[p1, p2, p3],
651                metadata.angle_epsilon,
652                metadata.global_insertion_epsilon,
653            ) {
654                continue;
655            }
656
657            let center = Triangle {
658                a: p1,
659                b: p2,
660                c: p3,
661            }
662            .center();
663
664            let epsilon = metadata.global_insertion_epsilon;
665            let projection = mesh2
666                .project_local_point_and_get_location(pos2.inverse_transform_point(center), true)
667                .0;
668
669            if flip2 ^ (projection.is_inside_eps(center, epsilon)) {
670                let mut new_tri_idx = [
671                    insert_into_set(p1, point_set, epsilon) as u32,
672                    insert_into_set(p2, point_set, epsilon) as u32,
673                    insert_into_set(p3, point_set, epsilon) as u32,
674                ];
675
676                if flip1 {
677                    new_tri_idx.swap(0, 1)
678                }
679
680                // This should *never* trigger. If it does
681                // it means the code has created a triangle with duplicate vertices,
682                // which means we encountered an unaccounted for edge case.
683                if is_topologically_degenerate(new_tri_idx) {
684                    return Err(MeshIntersectionError::DuplicateVertices);
685                }
686
687                insert_topology_indices(topology_indices, new_tri_idx);
688            }
689        }
690    }
691
692    Ok(())
693}
694
695// Insert in the hashmap with sorted indices to avoid adding duplicates.
696//
697// We also check if we don’t keep pairs of triangles that have the same
698// set of indices but opposite orientations. If this happens, both the new triangle, and the one it
699// matched with are removed (because they describe a degenerate piece of volume).
700fn insert_topology_indices(
701    topology_indices: &mut HashMap<HashableTriangleIndices, [u32; 3]>,
702    new_tri_idx: [u32; 3],
703) {
704    match topology_indices.entry(new_tri_idx.into()) {
705        Entry::Vacant(e) => {
706            let _ = e.insert(new_tri_idx);
707        }
708        Entry::Occupied(e) => {
709            fn same_orientation(a: &[u32; 3], b: &[u32; 3]) -> bool {
710                let ib = if a[0] == b[0] {
711                    0
712                } else if a[0] == b[1] {
713                    1
714                } else {
715                    2
716                };
717                a[1] == b[(ib + 1) % 3]
718            }
719
720            if !same_orientation(e.get(), &new_tri_idx) {
721                // If we are inserting two identical triangles but with mismatching
722                // orientations, we can just ignore both because they cover a degenerate
723                // 2D plane.
724                #[cfg(feature = "enhanced-determinism")]
725                let _ = e.swap_remove();
726                #[cfg(not(feature = "enhanced-determinism"))]
727                let _ = e.remove();
728            }
729        }
730    }
731}
732
733fn is_topologically_degenerate(tri_idx: [u32; 3]) -> bool {
734    tri_idx[0] == tri_idx[1] || tri_idx[0] == tri_idx[2] || tri_idx[1] == tri_idx[2]
735}
736
737#[cfg(feature = "wavefront")]
738#[cfg(test)]
739mod tests {
740    use super::*;
741    use crate::math::Vector;
742    use crate::shape::{Ball, Cuboid, TriMeshFlags};
743    use obj::Obj;
744    use obj::ObjData;
745
746    #[test]
747    fn test_same_mesh_intersection() {
748        let Obj {
749            data: ObjData {
750                position, objects, ..
751            },
752            ..
753        } = Obj::load("../../assets/tests/low_poly_bunny.obj").unwrap();
754
755        let mesh = TriMesh::with_flags(
756            position
757                .iter()
758                .map(|v| Vector::new(v[0] as Real, v[1] as Real, v[2] as Real))
759                .collect::<Vec<_>>(),
760            objects[0].groups[0]
761                .polys
762                .iter()
763                .map(|p| [p.0[0].0 as u32, p.0[1].0 as u32, p.0[2].0 as u32])
764                .collect::<Vec<_>>(),
765            TriMeshFlags::all(),
766        )
767        .unwrap();
768
769        let _ = intersect_meshes(
770            &Pose::identity(),
771            &mesh,
772            false,
773            &Pose::identity(),
774            &mesh,
775            false,
776        )
777        .unwrap()
778        .unwrap();
779
780        let _ = mesh.to_obj_file(&PathBuf::from("same_test.obj"));
781    }
782
783    #[test]
784    fn test_non_origin_pos1_pos2_intersection() {
785        let ball = Ball::new(2f32 as Real).to_trimesh(10, 10);
786        let cuboid = Cuboid::new(Vector::new(2.0, 1.0, 1.0)).to_trimesh();
787        let mut sphere_mesh = TriMesh::new(ball.0, ball.1).unwrap();
788        sphere_mesh.set_flags(TriMeshFlags::all()).unwrap();
789        let mut cuboid_mesh = TriMesh::new(cuboid.0, cuboid.1).unwrap();
790        cuboid_mesh.set_flags(TriMeshFlags::all()).unwrap();
791
792        let res = intersect_meshes(
793            &Pose::translation(1.0, 0.0, 0.0),
794            &cuboid_mesh,
795            false,
796            &Pose::translation(2.0, 0.0, 0.0),
797            &sphere_mesh,
798            false,
799        )
800        .unwrap()
801        .unwrap();
802
803        let _ = res.to_obj_file(&PathBuf::from("test_non_origin_pos1_pos2_intersection.obj"));
804
805        let bounding_sphere = res.local_bounding_sphere();
806        assert!(bounding_sphere.center == Vector::new(1.5, 0.0, 0.0));
807        assert_relative_eq!(2.0615528, bounding_sphere.radius, epsilon = 1.0e-5);
808    }
809
810    #[test]
811    fn test_offset_cylinder_intersection() {
812        let Obj {
813            data: ObjData {
814                position, objects, ..
815            },
816            ..
817        } = Obj::load("../../assets/tests/offset_cylinder.obj").unwrap();
818
819        let offset_mesh = TriMesh::with_flags(
820            position
821                .iter()
822                .map(|v| Vector::new(v[0] as Real, v[1] as Real, v[2] as Real))
823                .collect::<Vec<_>>(),
824            objects[0].groups[0]
825                .polys
826                .iter()
827                .map(|p| [p.0[0].0 as u32, p.0[1].0 as u32, p.0[2].0 as u32])
828                .collect::<Vec<_>>(),
829            TriMeshFlags::all(),
830        )
831        .unwrap();
832
833        let Obj {
834            data: ObjData {
835                position, objects, ..
836            },
837            ..
838        } = Obj::load("../../assets/tests/center_cylinder.obj").unwrap();
839
840        let center_mesh = TriMesh::with_flags(
841            position
842                .iter()
843                .map(|v| Vector::new(v[0] as Real, v[1] as Real, v[2] as Real))
844                .collect::<Vec<_>>(),
845            objects[0].groups[0]
846                .polys
847                .iter()
848                .map(|p| [p.0[0].0 as u32, p.0[1].0 as u32, p.0[2].0 as u32])
849                .collect::<Vec<_>>(),
850            TriMeshFlags::all(),
851        )
852        .unwrap();
853
854        let res = intersect_meshes(
855            &Pose::identity(),
856            &center_mesh,
857            false,
858            &Pose::identity(),
859            &offset_mesh,
860            false,
861        )
862        .unwrap()
863        .unwrap();
864
865        let _ = res.to_obj_file(&PathBuf::from("offset_test.obj"));
866    }
867
868    #[test]
869    fn test_stair_bar_intersection() {
870        let Obj {
871            data: ObjData {
872                position, objects, ..
873            },
874            ..
875        } = Obj::load("../../assets/tests/stairs.obj").unwrap();
876
877        let stair_mesh = TriMesh::with_flags(
878            position
879                .iter()
880                .map(|v| Vector::new(v[0] as Real, v[1] as Real, v[2] as Real))
881                .collect::<Vec<_>>(),
882            objects[0].groups[0]
883                .polys
884                .iter()
885                .map(|p| [p.0[0].0 as u32, p.0[1].0 as u32, p.0[2].0 as u32])
886                .collect::<Vec<_>>(),
887            TriMeshFlags::all(),
888        )
889        .unwrap();
890
891        let Obj {
892            data: ObjData {
893                position, objects, ..
894            },
895            ..
896        } = Obj::load("../../assets/tests/bar.obj").unwrap();
897
898        let bar_mesh = TriMesh::with_flags(
899            position
900                .iter()
901                .map(|v| Vector::new(v[0] as Real, v[1] as Real, v[2] as Real))
902                .collect::<Vec<_>>(),
903            objects[0].groups[0]
904                .polys
905                .iter()
906                .map(|p| [p.0[0].0 as u32, p.0[1].0 as u32, p.0[2].0 as u32])
907                .collect::<Vec<_>>(),
908            TriMeshFlags::all(),
909        )
910        .unwrap();
911
912        let res = intersect_meshes(
913            &Pose::identity(),
914            &stair_mesh,
915            false,
916            &Pose::identity(),
917            &bar_mesh,
918            false,
919        )
920        .unwrap()
921        .unwrap();
922
923        let _ = res.to_obj_file(&PathBuf::from("stair_test.obj"));
924    }
925
926    #[test]
927    fn test_complex_intersection() {
928        let Obj {
929            data: ObjData {
930                position, objects, ..
931            },
932            ..
933        } = Obj::load("../../assets/tests/low_poly_bunny.obj").unwrap();
934
935        let bunny_mesh = TriMesh::with_flags(
936            position
937                .iter()
938                .map(|v| Vector::new(v[0] as Real, v[1] as Real, v[2] as Real))
939                .collect::<Vec<_>>(),
940            objects[0].groups[0]
941                .polys
942                .iter()
943                .map(|p| [p.0[0].0 as u32, p.0[1].0 as u32, p.0[2].0 as u32])
944                .collect::<Vec<_>>(),
945            TriMeshFlags::all(),
946        )
947        .unwrap();
948
949        let Obj {
950            data: ObjData {
951                position, objects, ..
952            },
953            ..
954        } = Obj::load("../../assets/tests/poly_cylinder.obj").unwrap();
955
956        let cylinder_mesh = TriMesh::with_flags(
957            position
958                .iter()
959                .map(|v| Vector::new(v[0] as Real, v[1] as Real, v[2] as Real))
960                .collect::<Vec<_>>(),
961            objects[0].groups[0]
962                .polys
963                .iter()
964                .map(|p| [p.0[0].0 as u32, p.0[1].0 as u32, p.0[2].0 as u32])
965                .collect::<Vec<_>>(),
966            TriMeshFlags::all(),
967        )
968        .unwrap();
969
970        let res = intersect_meshes(
971            &Pose::identity(),
972            &bunny_mesh,
973            false,
974            &Pose::identity(),
975            &cylinder_mesh,
976            true,
977        )
978        .unwrap()
979        .unwrap();
980
981        let _ = res.to_obj_file(&PathBuf::from("complex_test.obj"));
982    }
983}