Skip to main content

manifold_rust/robust/
intersection_graph.rs

1// robust/intersection_graph.rs — From two triangle soups to classified-ready
2// pieces (paper §6).
3//
4// Pipeline stage between the narrow phase (robust/tri_tri.rs) and
5// classification (robust/classify.rs):
6//   1. AABB broad phase over the P×Q triangle pairs, exact narrow phase.
7//   2. Distribute each pair's intersection primitives to both triangles.
8//   3. For coplanar overlaps, cross-copy each side's other primitives
9//      (clipped to the overlap region) so both sides subdivide the shared
10//      region identically.
11//   4. Global registries force a common subdivision of (a) every original
12//      mesh edge and (b) every intersection segment, by feeding all split
13//      points to every arrangement that sees the same geometry — exact-key
14//      edge matching downstream depends on this.
15//   5. Build the per-triangle arrangements (robust/arrangement.rs +
16//      robust/cdt.rs) and emit `Piece`s: outward-oriented sub-triangles (or
17//      whole untouched triangles) tagged with their origin.
18//
19// Everything is exact; broad-phase boxes are conservative f64.
20
21use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
22
23use num_rational::BigRational;
24use num_traits::{One, Zero};
25
26use crate::linalg::Vec3;
27use crate::types::Box;
28
29use super::arrangement::{self, ArrangementInput};
30use super::exact::rational::{r3_eq, R3, R3Key};
31use super::exact::Sign;
32use super::tri_tri::{tri_tri_intersect, TriTriIsect};
33
34/// Canonical (sorted) edge between two interned vertex ids. Downstream
35/// stages (classify rings, propagate flood fill) key their maps on these
36/// integers instead of exact rational point pairs — vertex interning at
37/// piece-emission time makes id equality coincide with exact geometric
38/// identity.
39pub type EdgeKey = (u32, u32);
40
41pub fn edge_key(a: u32, b: u32) -> EdgeKey {
42    if a <= b {
43        (a, b)
44    } else {
45        (b, a)
46    }
47}
48
49/// Canonical (lexicographically sorted) exact edge between two points —
50/// local key for the split-point registries built before interning exists.
51type GeoEdgeKey = (R3, R3);
52
53fn geo_edge_key(a: &R3, b: &R3) -> GeoEdgeKey {
54    if a <= b {
55        (a.clone(), b.clone())
56    } else {
57        (b.clone(), a.clone())
58    }
59}
60
61/// Canonical original-mesh edge keyed by raw coordinate bits — original
62/// edges always join exact f64 vertices, so the boundary-split registry
63/// never needs rational keys (and untouched triangles probe it for free).
64type BitEdgeKey = ([u64; 3], [u64; 3]);
65
66fn bit_edge_key(a: Vec3, b: Vec3) -> BitEdgeKey {
67    let (ka, kb) = (f64_key(a), f64_key(b));
68    if ka <= kb {
69        (ka, kb)
70    } else {
71        (kb, ka)
72    }
73}
74
75/// One output fragment: a sub-triangle of an arranged input triangle, or an
76/// untouched whole triangle. `v` is wound to match the input mesh's outward
77/// orientation; `vi` are the interned ids of the same three vertices.
78#[derive(Clone, Copy, Debug)]
79pub struct Piece {
80    /// 0 = first operand (P), 1 = second operand (Q).
81    pub mesh: u8,
82    /// Index of the originating triangle in its soup.
83    pub tri: usize,
84    /// Interned vertex ids (indices into `IntersectionGraph::verts`), wound
85    /// to the input mesh's outward orientation. Pieces carry no coordinates
86    /// of their own — the shared tables keep untouched triangles free of
87    /// rational clones entirely.
88    pub vi: [u32; 3],
89}
90
91/// Everything classification and assembly need.
92pub struct IntersectionGraph {
93    pub pieces: Vec<Piece>,
94    /// Interned unique vertices; `Piece::vi` and `EdgeKey` index into this.
95    pub verts: Vec<R3>,
96    /// Correctly rounded f64 approximation per interned vertex (exact for
97    /// input vertices) — float filters and output assembly read these
98    /// instead of re-rounding rationals.
99    pub verts_f64: Vec<Vec3>,
100    /// Canonical keys of every arrangement constraint edge — the exact
101    /// intersection sub-segments the classification rings live on.
102    pub isect_edges: HashSet<EdgeKey>,
103    /// True when any P×Q pair intersected at all.
104    pub any_intersections: bool,
105}
106
107impl IntersectionGraph {
108    /// The three exact vertices of a piece.
109    pub fn piece_verts(&self, pi: usize) -> [&R3; 3] {
110        let vi = self.pieces[pi].vi;
111        [
112            &self.verts[vi[0] as usize],
113            &self.verts[vi[1] as usize],
114            &self.verts[vi[2] as usize],
115        ]
116    }
117}
118
119/// Exact-point interner: one id per distinct point, with two disjoint key
120/// spaces. f64-representable points (all input vertices, and any constructed
121/// point that rounds exactly) key on their coordinate bits — no rational
122/// hashing, so untouched input triangles intern for the cost of a HashMap
123/// probe. Only genuinely non-representable constructed points use the
124/// rational map. `verts_f64` caches the correctly rounded approximation of
125/// every id (exact for bit-keyed points), which downstream float filters
126/// and output assembly reuse instead of re-rounding.
127#[derive(Default)]
128pub struct VertInterner {
129    map: HashMap<R3Key, u32>,
130    fmap: HashMap<[u64; 3], u32>,
131    pub verts: Vec<R3>,
132    pub verts_f64: Vec<Vec3>,
133}
134
135fn f64_key(v: Vec3) -> [u64; 3] {
136    // Normalize -0.0 so it shares an id with +0.0 (they are the same
137    // rational point).
138    let norm = |x: f64| if x == 0.0 { 0.0f64 } else { x }.to_bits();
139    [norm(v.x), norm(v.y), norm(v.z)]
140}
141
142impl VertInterner {
143    /// Intern an exact-f64 point (input mesh vertices): zero rational work
144    /// on hits; one `R3::from_vec3` on first sight, for the exact table.
145    pub fn intern_f64(&mut self, v: Vec3) -> u32 {
146        let key = f64_key(v);
147        if let Some(&id) = self.fmap.get(&key) {
148            return id;
149        }
150        let id = self.verts.len() as u32;
151        self.fmap.insert(key, id);
152        self.verts.push(R3::from_vec3(v));
153        self.verts_f64.push(v);
154        id
155    }
156
157    /// Intern an exact rational point. Representable points route to the
158    /// f64 key space so both paths agree on ids.
159    pub fn intern(&mut self, p: &R3) -> u32 {
160        let rounded = p.to_vec3_rounded();
161        if r3_eq(&R3::from_vec3(rounded), p) {
162            return self.intern_f64(rounded);
163        }
164        let next = self.verts.len() as u32;
165        match self.map.entry(R3Key(p.clone())) {
166            std::collections::hash_map::Entry::Occupied(e) => *e.get(),
167            std::collections::hash_map::Entry::Vacant(e) => {
168                e.insert(next);
169                self.verts.push(p.clone());
170                self.verts_f64.push(rounded);
171                next
172            }
173        }
174    }
175}
176
177/// A pair's primitives after distribution: segments (including coplanar
178/// boundary edges) and isolated points.
179#[derive(Clone, Debug, Default)]
180struct TriPrims {
181    points: Vec<(R3, usize)>,
182    segments: Vec<(R3, R3, usize)>,
183}
184
185fn tri_box(t: &[Vec3; 3]) -> Box {
186    let mut b = Box::from_points(t[0], t[1]);
187    b.union_point(t[2]);
188    b
189}
190
191fn is_degenerate(t: &[Vec3; 3]) -> bool {
192    // Certified-nonzero f64 cross first (magnitude-permanent bound, matching
193    // exact/approx.rs conventions); only near-degenerate triangles pay for
194    // the rational cross.
195    const EPS: f64 = f64::EPSILON * 0.5;
196    let u = t[1] - t[0];
197    let v = t[2] - t[0];
198    let n = crate::linalg::cross(u, v);
199    let m = |k: usize| t[0][k].abs() + t[1][k].abs() + t[2][k].abs();
200    let (mx, my, mz) = (m(0), m(1), m(2));
201    if n.x.abs() > 16.0 * EPS * my * mz
202        || n.y.abs() > 16.0 * EPS * mz * mx
203        || n.z.abs() > 16.0 * EPS * mx * my
204    {
205        return false;
206    }
207    use super::exact::predicates::tri_normal_r;
208    tri_normal_r(
209        &R3::from_vec3(t[0]),
210        &R3::from_vec3(t[1]),
211        &R3::from_vec3(t[2]),
212    )
213    .is_zero()
214}
215
216/// Exact: p collinear with (a,b) and within the closed segment.
217fn point_on_segment(p: &R3, a: &R3, b: &R3) -> bool {
218    super::exact::predicates::point_on_segment_r(p, a, b)
219}
220
221/// Correctly rounded f64 approximation of an exact point (relative error
222/// ≤ ε per coordinate) for the semi-static prefilters in exact/approx.rs.
223fn approx3(p: &R3) -> [f64; 3] {
224    use super::exact::rational::rat_to_f64;
225    [rat_to_f64(&p.x), rat_to_f64(&p.y), rat_to_f64(&p.z)]
226}
227
228/// Filtered point-on-segment: the approx prefilter rejects the generic case
229/// without touching BigInt; only near-incidences run the exact test.
230fn point_on_segment_f(p_approx: [f64; 3], p: &R3, a_approx: [f64; 3], a: &R3, b_approx: [f64; 3], b: &R3) -> bool {
231    match super::exact::approx::not_on_segment_a(p_approx, a_approx, b_approx) {
232        Some(false) => false,
233        _ => point_on_segment(p, a, b),
234    }
235}
236
237/// Clip segment (a,b) to a convex coplanar polygon (2D test via projection
238/// on the polygon's own plane). Returns a positive-length sub-segment or
239/// None. Used to cross-copy primitives into coplanar overlap regions.
240fn clip_segment_to_polygon(a: &R3, b: &R3, poly: &[R3]) -> Option<(R3, R3)> {
241    use super::exact::predicates::{orient2d_r, tri_normal_r};
242    use super::exact::rational::R2;
243    use super::exact::Sign;
244    use super::tri_tri::dominant_axis;
245
246    debug_assert!(poly.len() >= 3);
247    let n = tri_normal_r(&poly[0], &poly[1], &poly[2]);
248    let axis = dominant_axis(&n);
249    let mut pts2: Vec<R2> = poly.iter().map(|p| p.project_drop(axis)).collect();
250    if orient2d_r(&pts2[0], &pts2[1], &pts2[2]) == Sign::Neg {
251        pts2.reverse();
252    }
253    let a2 = a.project_drop(axis);
254    let b2 = b.project_drop(axis);
255    let dir = b2.sub(&a2);
256
257    // Parametric clip of [0,1] against each CCW edge halfplane.
258    let mut t0 = BigRational::zero();
259    let mut t1 = BigRational::one();
260    for i in 0..pts2.len() {
261        let e0 = &pts2[i];
262        let e1 = &pts2[(i + 1) % pts2.len()];
263        let edge = e1.sub(e0);
264        // Signed distance numerators of a2 + t*dir against the edge line:
265        // f(t) = cross(edge, a2 + t*dir - e0) = fa + t * fd.
266        let fa = edge.cross(&a2.sub(e0));
267        let fd = edge.cross(&dir);
268        if fd.is_zero() {
269            if fa < BigRational::zero() {
270                return None; // parallel and strictly outside
271            }
272            continue;
273        }
274        let t_hit = -&fa / &fd;
275        if fd > BigRational::zero() {
276            // entering: f grows with t → require t >= t_hit
277            if t_hit > t0 {
278                t0 = t_hit;
279            }
280        } else if t_hit < t1 {
281            t1 = t_hit;
282        }
283        if t0 >= t1 {
284            return None;
285        }
286    }
287    if t0 >= t1 {
288        return None;
289    }
290    let seg = |t: &BigRational| a.add(&b.sub(a).scale(t));
291    Some((seg(&t0), seg(&t1)))
292}
293
294/// Build the intersection graph for soups `p` and `q` (each triangle wound
295/// outward; degenerate triangles are dropped here, paper §5).
296pub fn build_graph(p: &[[Vec3; 3]], q: &[[Vec3; 3]]) -> IntersectionGraph {
297    let t_all = crate::timing::start();
298    let meshes: [&[[Vec3; 3]]; 2] = [p, q];
299    let live: [Vec<bool>; 2] = [
300        p.iter().map(|t| !is_degenerate(t)).collect(),
301        q.iter().map(|t| !is_degenerate(t)).collect(),
302    ];
303
304    // 1. Broad + narrow phase. The broad phase is a BVH (the same Collider
305    // the exact engine uses) over Q's triangle boxes, queried with each P
306    // triangle's box — O((|P|+|Q|)·log|Q|) instead of the all-pairs box
307    // sweep. Candidates are re-sorted to ascending qi per pi, so the pair
308    // provenance ids match the exhaustive loop exactly (only genuinely
309    // intersecting pairs consume an id, and the exact narrow phase decides
310    // those identically regardless of broad-phase method).
311    let p_boxes: Vec<Box> = p.iter().map(tri_box).collect();
312    let q_boxes: Vec<Box> = q.iter().map(tri_box).collect();
313
314    let scene_box = q_boxes
315        .iter()
316        .enumerate()
317        .filter(|(qi, _)| live[1][*qi])
318        .fold(Box::new(), |acc, (_, b)| acc.union_box(b));
319    let mut q_order: Vec<usize> = (0..q.len()).filter(|&qi| live[1][qi]).collect();
320    q_order.sort_by_key(|&qi| crate::sort::morton_code(q_boxes[qi].center(), &scene_box));
321    let leaf_boxes: Vec<Box> = q_order.iter().map(|&qi| q_boxes[qi]).collect();
322    let leaf_morton: Vec<u32> = q_order
323        .iter()
324        .map(|&qi| crate::sort::morton_code(q_boxes[qi].center(), &scene_box))
325        .collect();
326    let collider = crate::collider::Collider::new(leaf_boxes, leaf_morton);
327
328    // Per-(mesh, tri) primitive lists; provenance = pair index.
329    let mut prims: [Vec<TriPrims>; 2] = [
330        vec![TriPrims::default(); p.len()],
331        vec![TriPrims::default(); q.len()],
332    ];
333    // Coplanar overlap regions per pair, for the cross-copy step:
334    // (p_tri, q_tri, polygon).
335    let mut coplanar_regions: Vec<(usize, usize, Vec<R3>)> = Vec::new();
336    let mut any_intersections = false;
337    let mut pair_count = 0usize;
338
339    let mut candidates_q: Vec<usize> = Vec::new();
340    for (pi, pt) in p.iter().enumerate() {
341        if !live[0][pi] {
342            continue;
343        }
344        candidates_q.clear();
345        collider.collisions_one(&p_boxes[pi], pi, |_, leaf| {
346            candidates_q.push(q_order[leaf]);
347        });
348        candidates_q.sort_unstable();
349        for &qi in &candidates_q {
350            let qt = &q[qi];
351            if !p_boxes[pi].does_overlap_box(&q_boxes[qi]) {
352                continue;
353            }
354            let isect = tri_tri_intersect(*pt, *qt);
355            let pair = pair_count;
356            match isect {
357                TriTriIsect::None => continue,
358                TriTriIsect::Point(x) => {
359                    prims[0][pi].points.push((x.clone(), pair));
360                    prims[1][qi].points.push((x, pair));
361                }
362                TriTriIsect::Segment(x, y) => {
363                    prims[0][pi].segments.push((x.clone(), y.clone(), pair));
364                    prims[1][qi].segments.push((x, y, pair));
365                }
366                TriTriIsect::Coplanar { polygon, .. } => {
367                    for i in 0..polygon.len() {
368                        let a = polygon[i].clone();
369                        let b = polygon[(i + 1) % polygon.len()].clone();
370                        prims[0][pi].segments.push((a.clone(), b.clone(), pair));
371                        prims[1][qi].segments.push((a, b, pair));
372                    }
373                    coplanar_regions.push((pi, qi, polygon));
374                }
375            }
376            any_intersections = true;
377            pair_count += 1;
378        }
379    }
380
381    crate::timing::print("robust: pair narrow phase", t_all);
382    let t_self = crate::timing::start();
383
384    // 2b. Self-intersections: cut each mesh along its own P×P / Q×Q contact
385    // segments (beyond ordinary adjacency). Without these cuts a piece could
386    // straddle a fold of a self-overlapping operand, making "is this piece
387    // an interior wall of its own solid" ill-defined; with them, both
388    // winding numbers the classification needs are constant per flood-fill
389    // component (robust/propagate.rs never crosses constraint edges).
390    // Broad phase: per-mesh BVH, same approach as the cross-mesh loop above
391    // (candidates re-sorted so provenance ids stay deterministic).
392    for m in 0..2 {
393        let (tris, boxes) = if m == 0 {
394            (p, &p_boxes)
395        } else {
396            (q, &q_boxes)
397        };
398        let self_scene = boxes
399            .iter()
400            .enumerate()
401            .filter(|(i, _)| live[m][*i])
402            .fold(Box::new(), |acc, (_, b)| acc.union_box(b));
403        let mut order: Vec<usize> = (0..tris.len()).filter(|&i| live[m][i]).collect();
404        order.sort_by_key(|&i| crate::sort::morton_code(boxes[i].center(), &self_scene));
405        let self_collider = crate::collider::Collider::new(
406            order.iter().map(|&i| boxes[i]).collect(),
407            order
408                .iter()
409                .map(|&i| crate::sort::morton_code(boxes[i].center(), &self_scene))
410                .collect(),
411        );
412        let mut cands: Vec<usize> = Vec::new();
413        let mut n_pairs = 0usize;
414        let mut n_cut = 0usize;
415        let mut stats = SelfCutStats::default();
416        for i in 0..tris.len() {
417            if !live[m][i] {
418                continue;
419            }
420            cands.clear();
421            self_collider.collisions_one(&boxes[i], i, |_, leaf| {
422                cands.push(order[leaf]);
423            });
424            cands.sort_unstable();
425            for &j in &cands {
426                if j <= i || !boxes[i].does_overlap_box(&boxes[j]) {
427                    continue;
428                }
429                n_pairs += 1;
430                let Some(segs) = real_self_contact(tris[i], tris[j], &mut stats) else {
431                    continue;
432                };
433                n_cut += 1;
434                for (x, y) in segs {
435                    let pair = pair_count;
436                    prims[m][i].segments.push((x.clone(), y.clone(), pair));
437                    prims[m][j].segments.push((x, y, pair));
438                    pair_count += 1;
439                }
440            }
441        }
442        crate::timing::print_count(
443            &format!("robust: self-cut mesh {m}: {n_pairs} box pairs, {n_cut} cutting"),
444        );
445        crate::timing::print_count(&format!(
446            "robust: self-cut mesh {m} tri_tri exits: {}",
447            super::tri_tri::stats::snapshot_and_reset()
448        ));
449        crate::timing::print_count(&format!(
450            "robust: self-cut mesh {m} paths: identical {}, edge-benign {}, vert-benign {}, \
451             full {} ({:.3}s: none {}, point {}, seg-benign {})",
452            stats.identical,
453            stats.edge_benign,
454            stats.vert_benign,
455            stats.full,
456            stats.full_secs,
457            stats.full_none,
458            stats.full_point,
459            stats.full_seg_benign,
460        ));
461    }
462
463    crate::timing::print("robust: self-intersection cuts", t_self);
464    let t_cross = crate::timing::start();
465
466    // 3. Cross-copy primitives through coplanar overlap regions so both
467    // sides see identical geometry inside the shared area. Clip against the
468    // region to avoid dragging unrelated geometry across.
469    for (pi, qi, poly) in &coplanar_regions {
470        let from_p: TriPrims = prims[0][*pi].clone();
471        let from_q: TriPrims = prims[1][*qi].clone();
472        let copy = |src: &TriPrims, dst: &mut TriPrims| {
473            for (a, b, prov) in &src.segments {
474                if let Some((ca, cb)) = clip_segment_to_polygon(a, b, poly) {
475                    if !dst
476                        .segments
477                        .iter()
478                        .any(|(x, y, pv)| pv == prov && ((x, y) == (&ca, &cb) || (x, y) == (&cb, &ca)))
479                    {
480                        dst.segments.push((ca, cb, *prov));
481                    }
482                }
483            }
484            for (pt, prov) in &src.points {
485                if clip_segment_to_polygon(pt, pt, poly).is_some()
486                    || point_in_polygon_coplanar(pt, poly)
487                {
488                    if !dst.points.iter().any(|(x, pv)| pv == prov && x == pt) {
489                        dst.points.push((pt.clone(), *prov));
490                    }
491                }
492            }
493        };
494        copy(&from_p, &mut prims[1][*qi]);
495        copy(&from_q, &mut prims[0][*pi]);
496    }
497
498    crate::timing::print("robust: coplanar cross-copy", t_cross);
499    let t_cand = crate::timing::start();
500
501    // 4a. Candidate points per intersected triangle.
502    let mut candidates: [Vec<Option<Vec<R3>>>; 2] = [
503        vec![None; p.len()],
504        vec![None; q.len()],
505    ];
506    for m in 0..2 {
507        for ti in 0..meshes[m].len() {
508            let pr = &prims[m][ti];
509            if pr.points.is_empty() && pr.segments.is_empty() {
510                continue;
511            }
512            let input = ArrangementInput {
513                points: pr.points.clone(),
514                segments: pr.segments.clone(),
515            };
516            candidates[m][ti] = Some(arrangement::candidate_points(meshes[m][ti], &input));
517        }
518    }
519
520    crate::timing::print("robust: candidate points", t_cand);
521    let t_reg = crate::timing::start();
522
523    // 4b. Original-edge registry: split points on each mesh edge (geometric
524    // identity — soups have no reliable connectivity). Bit-keyed: original
525    // edges join exact f64 vertices.
526    let mut edge_registry: [HashMap<BitEdgeKey, BTreeSet<R3>>; 2] =
527        [HashMap::new(), HashMap::new()];
528    for m in 0..2 {
529        for ti in 0..meshes[m].len() {
530            let Some(cands) = &candidates[m][ti] else { continue };
531            let t = meshes[m][ti];
532            let corners = [
533                R3::from_vec3(t[0]),
534                R3::from_vec3(t[1]),
535                R3::from_vec3(t[2]),
536            ];
537            let ca: [[f64; 3]; 3] = [
538                [t[0].x, t[0].y, t[0].z],
539                [t[1].x, t[1].y, t[1].z],
540                [t[2].x, t[2].y, t[2].z],
541            ];
542            let cands_a: Vec<[f64; 3]> = cands.iter().map(approx3).collect();
543            for e in 0..3 {
544                let a = &corners[e];
545                let b = &corners[(e + 1) % 3];
546                let key = bit_edge_key(t[e], t[(e + 1) % 3]);
547                for (pt, pt_a) in cands.iter().zip(&cands_a) {
548                    if !r3_eq(pt, a)
549                        && !r3_eq(pt, b)
550                        && point_on_segment_f(*pt_a, pt, ca[e], a, ca[(e + 1) % 3], b)
551                    {
552                        edge_registry[m].entry(key).or_default().insert(pt.clone());
553                    }
554                }
555            }
556        }
557    }
558
559    // 4c. Intersection-segment registry: for every pair segment, gather the
560    // split points both sides know about.
561    let mut seg_splits: BTreeMap<GeoEdgeKey, BTreeSet<R3>> = BTreeMap::new();
562    for m in 0..2 {
563        for ti in 0..meshes[m].len() {
564            let Some(cands) = &candidates[m][ti] else { continue };
565            let cands_a: Vec<[f64; 3]> = cands.iter().map(approx3).collect();
566            for (a, b, _prov) in &prims[m][ti].segments {
567                let key = geo_edge_key(a, b);
568                let (aa, ba) = (approx3(a), approx3(b));
569                for (pt, pt_a) in cands.iter().zip(&cands_a) {
570                    if !r3_eq(pt, a) && !r3_eq(pt, b) && point_on_segment_f(*pt_a, pt, aa, a, ba, b) {
571                        seg_splits.entry(key.clone()).or_default().insert(pt.clone());
572                    }
573                }
574            }
575        }
576    }
577
578    crate::timing::print("robust: split registries", t_reg);
579    let t_arr = crate::timing::start();
580
581    // 5. Build arrangements and emit pieces.
582    let mut pieces: Vec<Piece> = Vec::new();
583    let mut isect_edges: HashSet<EdgeKey> = HashSet::new();
584    let mut interner = VertInterner::default();
585
586    for m in 0..2 {
587        for ti in 0..meshes[m].len() {
588            if !live[m][ti] {
589                continue;
590            }
591            let t = meshes[m][ti];
592            let pr = &prims[m][ti];
593            // Boundary split points for this triangle (bit-keyed: uncut
594            // triangles probe with zero rational work).
595            let mut extra: BTreeSet<R3> = BTreeSet::new();
596            for e in 0..3 {
597                if let Some(set) = edge_registry[m].get(&bit_edge_key(t[e], t[(e + 1) % 3])) {
598                    extra.extend(set.iter().cloned());
599                }
600            }
601            // Split points along this triangle's intersection segments
602            // discovered by the other side.
603            for (a, b, _) in &pr.segments {
604                if let Some(set) = seg_splits.get(&geo_edge_key(a, b)) {
605                    extra.extend(set.iter().cloned());
606                }
607            }
608
609            if pr.points.is_empty() && pr.segments.is_empty() && extra.is_empty() {
610                // Untouched triangle → whole piece, interned by f64 bits.
611                pieces.push(Piece {
612                    mesh: m as u8,
613                    tri: ti,
614                    vi: [
615                        interner.intern_f64(t[0]),
616                        interner.intern_f64(t[1]),
617                        interner.intern_f64(t[2]),
618                    ],
619                });
620                continue;
621            }
622
623            let mut input = ArrangementInput {
624                points: pr.points.clone(),
625                segments: pr.segments.clone(),
626            };
627            for pt in extra {
628                input.points.push((pt, usize::MAX));
629            }
630            let arr = arrangement::build(t, &input);
631            // Intern each arrangement point once; sub-triangles and
632            // constraint edges then only shuffle ids.
633            let ids: Vec<u32> = arr.points3.iter().map(|p| interner.intern(p)).collect();
634            for (u, w) in arr.constraints.keys() {
635                isect_edges.insert(edge_key(ids[*u], ids[*w]));
636            }
637            for st in &arr.tris {
638                let (a, b, c) = (st[0], st[1], st[2]);
639                let vi = if arr.flipped {
640                    [ids[a], ids[c], ids[b]]
641                } else {
642                    [ids[a], ids[b], ids[c]]
643                };
644                pieces.push(Piece {
645                    mesh: m as u8,
646                    tri: ti,
647                    vi,
648                });
649            }
650        }
651    }
652
653    crate::timing::print("robust: arrangements", t_arr);
654    crate::timing::print_count(&format!(
655        "robust: arrangement phases: {}",
656        arrangement::stats::snapshot_and_reset()
657    ));
658
659    IntersectionGraph {
660        pieces,
661        verts: interner.verts,
662        verts_f64: interner.verts_f64,
663        isect_edges,
664        any_intersections,
665    }
666}
667
668/// Real self-intersection of one triangle pair from the same mesh: the
669/// contact of `t1` and `t2` reduced by ordinary mesh adjacency. Shared-vertex
670/// point contacts and (sub-)segments of a shared edge are the normal way
671/// neighboring triangles of a closed mesh touch and yield `None`; anything
672/// else is a genuine self-intersection whose segments must cut the surface,
673/// so that every emitted piece lies on a single sheet level of its own mesh
674/// (robust/mod.rs classifies own-mesh winding per component).
675/// orient3d(t[0], t[1], t[2], v): float filter first, exact integer
676/// determinant on escalation. This replaced a cached exact-plane structure
677/// (TriPlane) — with intpred's division-free fallback, building planes
678/// eagerly per triangle cost more than it ever saved.
679fn orient3d_plane(t: &[Vec3; 3], v: Vec3) -> Sign {
680    if let Some(s) = super::exact::approx::orient3d_a(
681        [t[0].x, t[0].y, t[0].z],
682        [t[1].x, t[1].y, t[1].z],
683        [t[2].x, t[2].y, t[2].z],
684        [v.x, v.y, v.z],
685    ) {
686        return s;
687    }
688    super::exact::intpred::orient3d_i(
689        [t[0].x, t[0].y, t[0].z],
690        [t[1].x, t[1].y, t[1].z],
691        [t[2].x, t[2].y, t[2].z],
692        [v.x, v.y, v.z],
693    )
694}
695
696/// Axis of the largest |component| of the (f64) triangle normal. Only a
697/// projection *choice*: when the exact normal's chosen component happens to
698/// be zero, the projected points go collinear, the exact 2D signs come back
699/// Zero, and every shortcut below falls through — sound, just unoptimized.
700fn dominant_axis_f64(t: [Vec3; 3]) -> usize {
701    let n = crate::linalg::cross(t[1] - t[0], t[2] - t[0]);
702    let (ax, ay, az) = (n.x.abs(), n.y.abs(), n.z.abs());
703    if az >= ax && az >= ay {
704        2
705    } else if ay >= ax {
706        1
707    } else {
708        0
709    }
710}
711
712/// `R3::project_drop` for raw f64 points (same cyclic axis convention).
713fn project_f64(v: Vec3, axis: usize) -> crate::linalg::Vec2 {
714    match axis {
715        0 => crate::linalg::Vec2::new(v.y, v.z),
716        1 => crate::linalg::Vec2::new(v.z, v.x),
717        _ => crate::linalg::Vec2::new(v.x, v.y),
718    }
719}
720
721/// Per-path counters for the self-cut narrow phase, printed under
722/// MANIFOLD_TIMING to show where box-pair time goes (shortcut hits vs full
723/// tri_tri calls and their outcomes).
724#[derive(Default)]
725struct SelfCutStats {
726    identical: usize,
727    edge_benign: usize,
728    vert_benign: usize,
729    full: usize,
730    full_none: usize,
731    full_point: usize,
732    full_seg_benign: usize,
733    full_secs: f64,
734}
735
736fn real_self_contact(
737    t1: [Vec3; 3],
738    t2: [Vec3; 3],
739    stats: &mut SelfCutStats,
740) -> Option<Vec<(R3, R3)>> {
741    use super::exact::Sign;
742
743    // Shared vertex positions (exact f64 identity) between the pair. Kept in
744    // f64: hundreds of thousands of benign pairs pass through here, and the
745    // rational form is only needed by the final Segment-benign check.
746    //
747    // Exactly identical triangles (all three vertices coincide — doubled
748    // surfaces, which some scans apply to their whole mesh) need no cut:
749    // both emit whole pieces with identical interned ids, and the global
750    // coincident binding in classify::bind_coincident reduces the stack
751    // (same winding keeps one representative, opposite windings cancel).
752    // Cutting them instead would drag every such triangle through the full
753    // arrangement pipeline along its own boundary, for nothing.
754
755    // Adjacency fast paths — the overwhelming bulk of same-mesh box-overlap
756    // pairs are edge- or vertex-neighbors whose only contact is that shared
757    // simplex, which never needs a cut. All shortcuts are exact (filtered
758    // predicates escalate to rationals when uncertain); flat triangulated
759    // regions make the *coplanar* neighbor cases as common as the generic
760    // ones, and without their 2D shortcuts every such pair pays for a full
761    // rational coplanar-overlap clip.
762    // Stack-allocated shared-vertex list: this runs per box pair (hundreds
763    // of thousands on dense meshes) and a heap Vec here is measurable.
764    let mut shared_f = [Vec3::default(); 3];
765    let mut n_shared = 0usize;
766    for &v in &t1 {
767        if t2.contains(&v) {
768            shared_f[n_shared] = v;
769            n_shared += 1;
770        }
771    }
772    let shared_f = &shared_f[..n_shared];
773    if shared_f.len() == 3 {
774        stats.identical += 1;
775        return None;
776    }
777    if shared_f.len() == 2 {
778        if let Some(&opp) = t2.iter().find(|v| !t1.contains(v)) {
779            // Non-coplanar edge-neighbors only meet along the shared edge.
780            if orient3d_plane(&t1, opp) != Sign::Zero {
781                stats.edge_benign += 1;
782                return None;
783            }
784            // Coplanar edge-neighbors: benign exactly when the two opposite
785            // corners strictly straddle the shared edge's line within the
786            // plane (the flat-plate case) — then the closed half-plane
787            // intersection is the shared edge itself.
788            if let Some(&own) = t1.iter().find(|v| !t2.contains(v)) {
789                let axis = dominant_axis_f64(t1);
790                let p2 = |v: Vec3| project_f64(v, axis);
791                let s_own =
792                    super::exact::filtered::orient2d(p2(shared_f[0]), p2(shared_f[1]), p2(own));
793                let s_opp =
794                    super::exact::filtered::orient2d(p2(shared_f[0]), p2(shared_f[1]), p2(opp));
795                if s_own != Sign::Zero && s_opp != Sign::Zero && s_own != s_opp {
796                    stats.edge_benign += 1;
797                    return None;
798                }
799            }
800        }
801    } else if shared_f.len() == 1 {
802        // Vertex-adjacent: if t2's two non-shared corners lie strictly on
803        // one side of t1's plane, the contact is exactly the shared vertex —
804        // an isolated point, no cut.
805        let mut others = [(Vec3::default(), Sign::Zero); 3];
806        let mut n_others = 0usize;
807        for &v in &t2 {
808            if !t1.contains(&v) {
809                others[n_others] = (v, orient3d_plane(&t1, v));
810                n_others += 1;
811            }
812        }
813        let others = &others[..n_others];
814        if others.len() == 2 && others[0].1 != Sign::Zero && others[0].1 == others[1].1 {
815            stats.vert_benign += 1;
816            return None;
817        }
818        // Fully coplanar vertex-neighbors (triangle fans on flat regions):
819        // an edge through the shared vertex that strictly separates the two
820        // triangles certifies the contact is exactly that vertex.
821        if others.len() == 2 && others[0].1 == Sign::Zero && others[1].1 == Sign::Zero {
822            let axis = dominant_axis_f64(t1);
823            let p2 = |v: Vec3| project_f64(v, axis);
824            let v0 = shared_f[0];
825            let mut own = [Vec3::default(); 3];
826            let mut n_own = 0usize;
827            for &v in &t1 {
828                if !t2.contains(&v) {
829                    own[n_own] = v;
830                    n_own += 1;
831                }
832            }
833            let own = &own[..n_own];
834            let other = [others[0].0, others[1].0];
835            // Candidate separators: each triangle's two edges through v0,
836            // tested against its own third corner vs the other triangle's
837            // two corners.
838            let separated = |ea: Vec3, third: Vec3, far: [Vec3; 2]| -> bool {
839                let s_t = super::exact::filtered::orient2d(p2(v0), p2(ea), p2(third));
840                if s_t == Sign::Zero {
841                    return false;
842                }
843                far.iter().all(|&f| {
844                    let s = super::exact::filtered::orient2d(p2(v0), p2(ea), p2(f));
845                    s != Sign::Zero && s != s_t
846                })
847            };
848            if own.len() == 2
849                && (separated(own[0], own[1], other)
850                    || separated(own[1], own[0], other)
851                    || separated(other[0], other[1], [own[0], own[1]])
852                    || separated(other[1], other[0], [own[0], own[1]]))
853            {
854                stats.vert_benign += 1;
855                return None;
856            }
857        }
858    }
859
860    stats.full += 1;
861    let t_full = crate::timing::Stopwatch::start();
862    let isect = tri_tri_intersect(t1, t2);
863    stats.full_secs += t_full.elapsed_secs();
864    match isect {
865        TriTriIsect::None => {
866            stats.full_none += 1;
867            None
868        }
869        // Isolated point contacts (vertex-on-face, edge-through-edge) have
870        // zero area on both sides: they never change which sheet a region
871        // is on, so they need no cut.
872        TriTriIsect::Point(_) => {
873            stats.full_point += 1;
874            None
875        }
876        TriTriIsect::Segment(x, y) => {
877            let benign = shared_f.len() >= 2 && {
878                let s0 = R3::from_vec3(shared_f[0]);
879                let s1 = R3::from_vec3(shared_f[1]);
880                point_on_segment(&x, &s0, &s1) && point_on_segment(&y, &s0, &s1)
881            };
882            if benign {
883                stats.full_seg_benign += 1;
884            }
885            (!benign).then(|| vec![(x, y)])
886        }
887        // Positive-area coplanar overlap (a fold or doubled patch): cut both
888        // triangles along the overlap region's boundary.
889        TriTriIsect::Coplanar { polygon, .. } => Some(
890            (0..polygon.len())
891                .map(|i| {
892                    (
893                        polygon[i].clone(),
894                        polygon[(i + 1) % polygon.len()].clone(),
895                    )
896                })
897                .collect(),
898        ),
899    }
900}
901
902/// Exact point-in-convex-polygon test for a point on the polygon's plane.
903fn point_in_polygon_coplanar(p: &R3, poly: &[R3]) -> bool {
904    use super::exact::predicates::{orient2d_r, tri_normal_r};
905    use super::exact::Sign;
906    use super::tri_tri::dominant_axis;
907
908    let n = tri_normal_r(&poly[0], &poly[1], &poly[2]);
909    let axis = dominant_axis(&n);
910    let mut pts2: Vec<_> = poly.iter().map(|q| q.project_drop(axis)).collect();
911    if orient2d_r(&pts2[0], &pts2[1], &pts2[2]) == Sign::Neg {
912        pts2.reverse();
913    }
914    let p2 = p.project_drop(axis);
915    for i in 0..pts2.len() {
916        if orient2d_r(&pts2[i], &pts2[(i + 1) % pts2.len()], &p2) == Sign::Neg {
917            return false;
918        }
919    }
920    true
921}