Skip to main content

ifc_lite_geometry/kernel/
tritri.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Triangle–triangle intersection machinery — exact, predicate-driven.
6//!
7//! Classifies a triangle against another's plane (via exact `orient3d`) and
8//! constructs the edge∩plane intersection points as LPI implicit points. The
9//! full intersection segment (interval overlap along the planes' crossing
10//! line) and the in-plane re-triangulation build on this.
11//!
12//! Every intersection point is an LPI carried symbolically over the original
13//! input coordinates — never materialised — so downstream predicates stay exact
14//! and platform-deterministic.
15
16use super::predicates::orient3d;
17use super::{ImplicitPoint, Lpi, Sign};
18
19#[inline]
20fn e(p: [f64; 3]) -> ImplicitPoint {
21    ImplicitPoint::Explicit(p)
22}
23
24/// The implicit point where edge `a→b` crosses the plane through `plane`.
25#[inline]
26pub fn edge_plane_lpi(a: [f64; 3], b: [f64; 3], plane: &[[f64; 3]; 3]) -> Lpi {
27    Lpi { p: a, q: b, r: plane[0], s: plane[1], t: plane[2] }
28}
29
30/// For a `Crosses { apex }` triangle, the two LPI points where the apex's two
31/// edges cross `plane` — the triangle's interval endpoints on the crossing line.
32pub fn crossing_lpis(tri: &[[f64; 3]; 3], apex: usize, plane: &[[f64; 3]; 3]) -> [Lpi; 2] {
33    let o1 = (apex + 1) % 3;
34    let o2 = (apex + 2) % 3;
35    [
36        edge_plane_lpi(tri[apex], tri[o1], plane),
37        edge_plane_lpi(tri[apex], tri[o2], plane),
38    ]
39}
40
41#[inline]
42fn sub_f64(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
43    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
44}
45#[inline]
46fn cross_f64(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
47    [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
48}
49#[inline]
50fn plane_normal(t: &[[f64; 3]; 3]) -> [f64; 3] {
51    cross_f64(sub_f64(t[1], t[0]), sub_f64(t[2], t[0]))
52}
53/// Approximate direction of the crossing line L = t1.plane ∩ t2.plane (n1 × n2),
54/// rounded to an INTEGER-valued direction. The raw cross product lands on a ~2^64
55/// grid (cross of 2^32 vectors) — off the 2^16 snap grid — so `gi(u)` fails and
56/// EVERY `cmp_along` falls into slow BigRational. Only the SIGN of `(a−b)·u`
57/// matters and `u` need only be approximately along L, so we normalise + round to
58/// integers: `gi` then scales it on-grid and the exact fixed-width tier resolves
59/// the 1-D ordering. (~600µs/intersection → microseconds.)
60fn line_direction(t1: &[[f64; 3]; 3], t2: &[[f64; 3]; 3]) -> [f64; 3] {
61    let n = cross_f64(plane_normal(t1), plane_normal(t2));
62    let m = n[0].abs().max(n[1].abs()).max(n[2].abs());
63    if m == 0.0 || !m.is_finite() {
64        return n;
65    }
66    let s = 1_048_576.0 / m; // normalise the max component to ~2^20
67    [(n[0] * s).round(), (n[1] * s).round(), (n[2] * s).round()]
68}
69
70/// Result of an exact triangle–triangle intersection test.
71#[derive(Clone, Debug)]
72pub enum TriTri {
73    /// No intersection.
74    None,
75    /// The triangles are coplanar (a 2D-overlap case — handled in `coplanar.rs`).
76    Coplanar,
77    /// Contact at a single point (a vertex touch) — no cutting segment.
78    Point(ImplicitPoint),
79    /// The intersection segment; endpoints lie on line L = plane(T1) ∩ plane(T2).
80    /// An endpoint is `Explicit` (an on-plane vertex) or `Lpi` (an edge crossing).
81    Segment([ImplicitPoint; 2]),
82}
83
84#[inline]
85fn cmp_along(a: &ImplicitPoint, b: &ImplicitPoint, u: [f64; 3]) -> Sign {
86    // f64 interval filter FIRST (pure f64), then the exact I512 tier, then
87    // BigRational. The interval resolves the non-degenerate majority off the
88    // wasm-emulated wide-integer path; a definite interval sign equals the exact
89    // sign (outward rounding, no FMA) ⇒ identical ordering, byte-identical.
90    super::interval::cmp_along(a, b, u)
91        .or_else(|| super::fixed::cmp_along(a, b, u))
92        .unwrap_or_else(|| super::rational::cmp_along(a, b, u))
93}
94
95/// A triangle's intersection with another triangle's supporting plane — the
96/// generalisation that admits on-plane vertices (Touches), not just clean
97/// edge crossings. Every endpoint lies on BOTH planes ⇒ on line L.
98enum PlaneInterval {
99    /// Triangle strictly on one side — no plane intersection.
100    None,
101    /// Triangle lies in the plane.
102    Coplanar,
103    /// Touches the plane at a single vertex only (no chord).
104    Point(ImplicitPoint),
105    /// A chord (2 on-plane endpoints): 2 edge crossings, vertex + edge crossing,
106    /// or an on-plane edge.
107    Chord([ImplicitPoint; 2]),
108}
109
110fn plane_interval(tri: &[[f64; 3]; 3], plane: &[[f64; 3]; 3]) -> PlaneInterval {
111    let s = [
112        orient3d(&e(plane[0]), &e(plane[1]), &e(plane[2]), &e(tri[0])),
113        orient3d(&e(plane[0]), &e(plane[1]), &e(plane[2]), &e(tri[1])),
114        orient3d(&e(plane[0]), &e(plane[1]), &e(plane[2]), &e(tri[2])),
115    ];
116    let zeros: Vec<usize> = (0..3).filter(|&i| s[i] == Sign::Zero).collect();
117    match zeros.len() {
118        3 => PlaneInterval::Coplanar,
119        2 => PlaneInterval::Chord([e(tri[zeros[0]]), e(tri[zeros[1]])]), // an on-plane edge
120        1 => {
121            let vz = zeros[0];
122            let (o1, o2) = ((vz + 1) % 3, (vz + 2) % 3);
123            if s[o1] == s[o2] {
124                PlaneInterval::Point(e(tri[vz])) // both others same side: single vertex touch
125            } else {
126                // the far edge crosses: chord [on-plane vertex, edge∩plane]
127                PlaneInterval::Chord([e(tri[vz]), ImplicitPoint::Lpi(edge_plane_lpi(tri[o1], tri[o2], plane))])
128            }
129        }
130        _ => {
131            let pos = s.iter().filter(|&&x| x == Sign::Positive).count();
132            if pos == 0 || pos == 3 {
133                PlaneInterval::None
134            } else {
135                let apex = if s[0] != s[1] && s[0] != s[2] {
136                    0
137                } else if s[1] != s[0] && s[1] != s[2] {
138                    1
139                } else {
140                    2
141                };
142                let [a, b] = crossing_lpis(tri, apex, plane);
143                PlaneInterval::Chord([ImplicitPoint::Lpi(a), ImplicitPoint::Lpi(b)])
144            }
145        }
146    }
147}
148
149/// Near-coplanar band formula, canonical in `mesh_bridge` (sized to the
150/// snap-scatter envelope `mesh_bridge::mesh_to_tris` produces).
151use super::mesh_bridge::near_band_from_extent;
152
153#[inline]
154fn ti_sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
155    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
156}
157#[inline]
158fn ti_cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
159    [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
160}
161#[inline]
162fn ti_dot(a: [f64; 3], b: [f64; 3]) -> f64 {
163    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
164}
165#[inline]
166fn ti_normal(t: &[[f64; 3]; 3]) -> [f64; 3] {
167    ti_cross(ti_sub(t[1], t[0]), ti_sub(t[2], t[0]))
168}
169
170/// Are `t1` and `t2` an INTENDED-FLUSH coplanar pair that per-axis snapping
171/// pushed just off exact coplanarity? — the flush-cap detector.
172///
173/// `mesh_bridge` snaps every operand coordinate to [`SNAP_GRID`] INDEPENDENTLY
174/// per axis. That keeps an AXIS-ALIGNED flush face exactly coplanar but pushes a
175/// *tilted* flush face up to `SNAP_GRID·√3` off its plane PER OPERAND — so a roof-
176/// slope opening cap authored EXACTLY flush with the slanted roof surface lands a
177/// few µm off after import (#1007 host #1112 openings #2150/#2154). The exact
178/// `orient3d`-only test then sees it as a razor-thin CROSSING (or Disjoint), never
179/// `Coplanar`, so the footprint is never carved and a sliver bridges the hole.
180///
181/// The test is ONE deterministic FMA-free f64 condition (byte-identical
182/// native==wasm — NO coordinate is moved, this is purely a CLASSIFICATION):
183/// **the noise-slab test** — ALL THREE vertices of one triangle sit within
184/// `band` of the other triangle's plane (either direction qualifies). A facet
185/// entirely inside the other face's snap-scatter slab is geometrically
186/// indistinguishable from lying ON that face, so it must be routed to the exact
187/// coplanar handler. A genuine transversal cut (box−box, every real crossing)
188/// has vertices FAR off the other plane ⇒ fails the slab test.
189///
190/// WHY vertex-slab and not the earlier fixed angle gate: the old formulation
191/// ALSO required the two plane normals to agree to ~2^-20 (≈1.4 mrad) — but
192/// the tilt that f32 import noise induces on an intended-flush facet scales as
193/// `scatter / edge_length`. At 300–400 m from origin (f32 ULP 30.5 µm —
194/// tunnel-alignment walls) a SMALL flush facet (0.03–0.05 m edges, 3-segment
195/// recess cutters) tilts 1.4–1.9 mrad: past the fixed gate while sitting
196/// 3–24 µm INSIDE the slab. The missed pair then enters the razor-thin-
197/// crossing path whose degenerate sub-triangle keep/drop is a noise lottery →
198/// open edges + volumes off by −85%…+19 763% (a 749-element divergence family,
199/// ~84% adjudicated PURE-WRONG against IfcOpenShell 0.8.2). The slab test is
200/// scale-correct: small facets get exactly the angular allowance their size
201/// implies, large facets proportionally less (a large tilted partner's far
202/// vertices leave the slab, so it still fails).
203///
204/// `band` is an absolute power-of-two multiple of `SNAP_GRID` (≈ the 2-operand
205/// scatter envelope, ~0.12 mm) widened only for far-from-origin operands where
206/// f32 import is coarser — always THREE orders below the smallest real feature
207/// edge (~0.2 m). A poke-through cap fails the slab test (its far vertices sit
208/// midway through the host, far from the surface) so it can never qualify; a
209/// sub-band-sized transversal micro-sliver CAN now qualify, but its entire
210/// geometric effect is below the import-noise floor by construction, and the
211/// coplanar overlay's degenerate-projection guards (`w0 == Zero`) handle it.
212fn near_coplanar(t1: &[[f64; 3]; 3], t2: &[[f64; 3]; 3]) -> bool {
213    let (n1, n2) = (ti_normal(t1), ti_normal(t2));
214    let (nn1, nn2) = (ti_dot(n1, n1), ti_dot(n2, n2));
215    if nn1 <= 0.0 || nn2 <= 0.0 || !nn1.is_finite() || !nn2.is_finite() {
216        return false; // a degenerate triangle is never a flush coplanar partner
217    }
218    let mut extent = 1.0f64;
219    for p in t1.iter().chain(t2.iter()) {
220        for &c in p {
221            extent = extent.max(c.abs());
222        }
223    }
224    let band = near_band_from_extent(extent); // 2^-22
225    let band2 = band * band;
226    // All three vertices of `t` within `band` of `plane`'s supporting plane?
227    let in_slab = |t: &[[f64; 3]; 3], plane: &[[f64; 3]; 3], n: [f64; 3], nn: f64| {
228        t.iter().all(|&v| {
229            let d = ti_dot(ti_sub(v, plane[0]), n); // perp_dist · |n|
230            (d * d) / nn <= band2
231        })
232    };
233    in_slab(t2, t1, n1, nn1) || in_slab(t1, t2, n2, nn2)
234}
235
236/// Exact triangle–triangle intersection: the overlap of each triangle's
237/// plane-interval along line L = `[max(lo1,lo2), min(hi1,hi2)]`. Handles clean
238/// crossings AND Touches (on-plane vertices/edges). Coplanar is deferred to
239/// `coplanar.rs`; a single shared point returns `Point` (no cut).
240pub fn tri_tri_intersection(t1: &[[f64; 3]; 3], t2: &[[f64; 3]; 3]) -> TriTri {
241    use PlaneInterval as PI;
242    // Near-coplanar guard (the flush-cap fix): an intended-flush coplanar
243    // interface that per-axis snapping pushed just off exact coplanarity is
244    // routed to the exact coplanar handler so the footprint is carved (otherwise
245    // a sliver bridges the opening). See `near_coplanar`.
246    if near_coplanar(t1, t2) {
247        return TriTri::Coplanar;
248    }
249    let (i1, i2) = (plane_interval(t1, t2), plane_interval(t2, t1));
250    let ends = |pi: &PI| -> Option<[ImplicitPoint; 2]> {
251        match pi {
252            PI::Point(p) => Some([p.clone(), p.clone()]),
253            PI::Chord([a, b]) => Some([a.clone(), b.clone()]),
254            _ => None,
255        }
256    };
257    if matches!(i1, PI::Coplanar) || matches!(i2, PI::Coplanar) {
258        return TriTri::Coplanar;
259    }
260    let ([a1, b1], [a2, b2]) = match (ends(&i1), ends(&i2)) {
261        (Some(s1), Some(s2)) => (s1, s2),
262        _ => return TriTri::None,
263    };
264    let u = line_direction(t1, t2);
265    let order = |a: ImplicitPoint, b: ImplicitPoint| {
266        if cmp_along(&a, &b, u) == Sign::Positive {
267            (b, a)
268        } else {
269            (a, b)
270        }
271    };
272    let (lo1, hi1) = order(a1, b1);
273    let (lo2, hi2) = order(a2, b2);
274    let lo = if cmp_along(&lo1, &lo2, u) == Sign::Positive { lo1 } else { lo2 };
275    let hi = if cmp_along(&hi1, &hi2, u) == Sign::Negative { hi1 } else { hi2 };
276    match cmp_along(&lo, &hi, u) {
277        Sign::Positive => TriTri::None,           // intervals disjoint
278        Sign::Zero => TriTri::Point(lo),          // a single shared point
279        Sign::Negative => TriTri::Segment([lo, hi]),
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    const ZPLANE: [[f64; 3]; 3] = [[0., 0., 0.], [2., 0., 0.], [0., 2., 0.]]; // z = 0
288
289    #[test]
290    fn edge_crossing_lpi_lies_exactly_on_the_plane() {
291        // The defining property: orient3d(LPI, plane[0], plane[1], plane[2]) == 0
292        // (the edge∩plane point is coplanar with the plane). This ties the LPI
293        // construction to the exact LPI-orient3d predicate.
294        let lpi = edge_plane_lpi([0.5, 0.5, -1.], [0.5, 0.5, 3.], &ZPLANE);
295        assert_eq!(
296            orient3d(&ImplicitPoint::Lpi(lpi), &e(ZPLANE[0]), &e(ZPLANE[1]), &e(ZPLANE[2])),
297            Sign::Zero,
298            "edge∩plane LPI is not exactly on the plane"
299        );
300        // tilted plane + tilted edge
301        let tilted = [[0., 0., 1.], [3., 0., 2.], [0., 3., 2.]];
302        let lpi2 = edge_plane_lpi([1., 1., 0.], [1.5, 0.5, 5.], &tilted);
303        assert_eq!(
304            orient3d(&ImplicitPoint::Lpi(lpi2), &e(tilted[0]), &e(tilted[1]), &e(tilted[2])),
305            Sign::Zero,
306            "tilted edge∩plane LPI is not exactly on the plane"
307        );
308    }
309
310    #[test]
311    fn proper_crossing_yields_segment_on_both_planes() {
312        let t1 = [[-2., 0., -1.], [2., 0., -1.], [0., 0., 2.]]; // plane y=0
313        let t2 = [[1., -2., 1.], [1., 2., 1.], [1., 0.5, -3.]]; // plane x=1
314        match tri_tri_intersection(&t1, &t2) {
315            TriTri::Segment([a, b]) => {
316                // The two endpoints are distinct (a non-degenerate segment).
317                assert_ne!(
318                    super::cmp_along(&a, &b, super::line_direction(&t1, &t2)),
319                    Sign::Zero,
320                    "segment collapsed to a point"
321                );
322                // Every segment endpoint lies on BOTH triangles' planes (on L).
323                for ep in [&a, &b] {
324                    assert_eq!(
325                        orient3d(ep, &e(t1[0]), &e(t1[1]), &e(t1[2])),
326                        Sign::Zero,
327                        "segment endpoint off t1's plane"
328                    );
329                    assert_eq!(
330                        orient3d(ep, &e(t2[0]), &e(t2[1]), &e(t2[2])),
331                        Sign::Zero,
332                        "segment endpoint off t2's plane"
333                    );
334                }
335            }
336            other => panic!("expected a segment, got {other:?}"),
337        }
338    }
339
340    #[test]
341    fn touches_vertex_on_plane_yields_segment_with_explicit_endpoint() {
342        // t2 crosses t1's plane (y=0) but with ONE vertex EXACTLY on it.
343        let t1 = [[-2., 0., -1.], [2., 0., -1.], [0., 0., 2.]]; // plane y=0
344        let t2 = [[0., 0., 0.5], [0.5, -1., 0.5], [0.5, 1., 0.5]]; // v0 at y=0, in plane z=0.5
345        match tri_tri_intersection(&t1, &t2) {
346            TriTri::Segment([a, b]) => {
347                // exactly one endpoint is the Explicit on-plane vertex (0,0,0.5)
348                let explicits = [&a, &b]
349                    .iter()
350                    .filter(|p| matches!(p, ImplicitPoint::Explicit(_)))
351                    .count();
352                assert_eq!(explicits, 1, "expected one Explicit (on-plane vertex) endpoint");
353                // both endpoints lie on BOTH planes (on L)
354                for ep in [&a, &b] {
355                    assert_eq!(orient3d(ep, &e(t1[0]), &e(t1[1]), &e(t1[2])), Sign::Zero);
356                    assert_eq!(orient3d(ep, &e(t2[0]), &e(t2[1]), &e(t2[2])), Sign::Zero);
357                }
358            }
359            other => panic!("Touches case should yield a Segment, got {other:?}"),
360        }
361    }
362
363    #[test]
364    fn planes_cross_but_intervals_disjoint_is_none() {
365        let t1 = [[-2., 0., -1.], [2., 0., -1.], [0., 0., 2.]]; // y=0, crosses x=1 at z∈[-1,0.5]
366        let t2 = [[1., -2., 5.], [1., 2., 5.], [1., 0.5, 9.]]; // x=1, crosses y=0 at z∈[5,8.2]
367        // both planes DO cross (checked via tri_tri_intersection's own plane_interval
368        // path below); the disjoint-intervals-along-L outcome is the real assertion.
369        assert!(
370            matches!(tri_tri_intersection(&t1, &t2), TriTri::None),
371            "disjoint intervals along L should give no intersection"
372        );
373    }
374}