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, canonical in `near_band` (sized to the snap-scatter
150/// envelope `mesh_bridge::mesh_to_tris` produces, then projected onto the
151/// plane being tested).
152use super::near_band::NearBand;
153
154#[inline]
155fn ti_sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
156    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
157}
158#[inline]
159fn ti_cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
160    [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
161}
162#[inline]
163fn ti_dot(a: [f64; 3], b: [f64; 3]) -> f64 {
164    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
165}
166#[inline]
167fn ti_normal(t: &[[f64; 3]; 3]) -> [f64; 3] {
168    ti_cross(ti_sub(t[1], t[0]), ti_sub(t[2], t[0]))
169}
170
171/// Are `t1` and `t2` an INTENDED-FLUSH coplanar pair that per-axis snapping
172/// pushed just off exact coplanarity? — the flush-cap detector.
173///
174/// `mesh_bridge` snaps every operand coordinate to [`SNAP_GRID`] INDEPENDENTLY
175/// per axis. That keeps an AXIS-ALIGNED flush face exactly coplanar but pushes a
176/// *tilted* flush face up to `SNAP_GRID·√3` off its plane PER OPERAND — so a roof-
177/// slope opening cap authored EXACTLY flush with the slanted roof surface lands a
178/// few µm off after import (#1007 host #1112 openings #2150/#2154). The exact
179/// `orient3d`-only test then sees it as a razor-thin CROSSING (or Disjoint), never
180/// `Coplanar`, so the footprint is never carved and a sliver bridges the hole.
181///
182/// The test is ONE deterministic FMA-free f64 condition (byte-identical
183/// native==wasm — NO coordinate is moved, this is purely a CLASSIFICATION):
184/// **the noise-slab test** — ALL THREE vertices of one triangle sit within
185/// `band` of the other triangle's plane (either direction qualifies). A facet
186/// entirely inside the other face's snap-scatter slab is geometrically
187/// indistinguishable from lying ON that face, so it must be routed to the exact
188/// coplanar handler. A genuine transversal cut (box−box, every real crossing)
189/// has vertices FAR off the other plane ⇒ fails the slab test.
190///
191/// WHY vertex-slab and not the earlier fixed angle gate: the old formulation
192/// ALSO required the two plane normals to agree to ~2^-20 (≈1.4 mrad) — but
193/// the tilt that f32 import noise induces on an intended-flush facet scales as
194/// `scatter / edge_length`. At 300–400 m from origin (f32 ULP 30.5 µm —
195/// tunnel-alignment walls) a SMALL flush facet (0.03–0.05 m edges, 3-segment
196/// recess cutters) tilts 1.4–1.9 mrad: past the fixed gate while sitting
197/// 3–24 µm INSIDE the slab. The missed pair then enters the razor-thin-
198/// crossing path whose degenerate sub-triangle keep/drop is a noise lottery →
199/// open edges + volumes off by −85%…+19 763% (a 749-element divergence family,
200/// ~84% adjudicated PURE-WRONG against IfcOpenShell 0.8.2). The slab test is
201/// scale-correct: small facets get exactly the angular allowance their size
202/// implies, large facets proportionally less (a large tilted partner's far
203/// vertices leave the slab, so it still fails).
204///
205/// `band` is an absolute power-of-two multiple of `SNAP_GRID` (≈ the 2-operand
206/// scatter envelope, ~0.12 mm) widened only for far-from-origin operands where
207/// f32 import is coarser — and only by the part of that offset the tested
208/// plane can actually see, since [`NearBand`] projects the per-axis extents
209/// onto that plane's normal. A model 10 km out in X therefore leaves a
210/// Z-normal slab at the floor rather than opening it to ~2.4 mm and welding
211/// genuinely separate faces. The band stays THREE orders below the smallest real feature
212/// edge (~0.2 m). A poke-through cap fails the slab test (its far vertices sit
213/// midway through the host, far from the surface) so it can never qualify; a
214/// sub-band-sized transversal micro-sliver CAN now qualify, but its entire
215/// geometric effect is below the import-noise floor by construction, and the
216/// coplanar overlay's degenerate-projection guards (`w0 == Zero`) handle it.
217fn near_coplanar(t1: &[[f64; 3]; 3], t2: &[[f64; 3]; 3]) -> bool {
218    let (n1, n2) = (ti_normal(t1), ti_normal(t2));
219    let (nn1, nn2) = (ti_dot(n1, n1), ti_dot(n2, n2));
220    if nn1 <= 0.0 || nn2 <= 0.0 || !nn1.is_finite() || !nn2.is_finite() {
221        return false; // a degenerate triangle is never a flush coplanar partner
222    }
223    let mut band = NearBand::default();
224    for p in t1.iter().chain(t2.iter()) {
225        band.observe_point(p);
226    }
227    // All three vertices of `t` within the band of `plane`'s supporting plane?
228    // The band is resolved against THAT plane's own normal, so an operand far
229    // from the origin along an axis the plane does not face does not widen it.
230    let in_slab = |t: &[[f64; 3]; 3], plane: &[[f64; 3]; 3], n: [f64; 3], nn: f64| {
231        let band2 = band.scaled_band2(n, nn); // scaled by |n|², like `d` below
232        t.iter().all(|&v| {
233            let d = ti_dot(ti_sub(v, plane[0]), n); // perp_dist · |n|
234            d * d <= band2
235        })
236    };
237    in_slab(t2, t1, n1, nn1) || in_slab(t1, t2, n2, nn2)
238}
239
240/// Exact triangle–triangle intersection: the overlap of each triangle's
241/// plane-interval along line L = `[max(lo1,lo2), min(hi1,hi2)]`. Handles clean
242/// crossings AND Touches (on-plane vertices/edges). Coplanar is deferred to
243/// `coplanar.rs`; a single shared point returns `Point` (no cut).
244pub fn tri_tri_intersection(t1: &[[f64; 3]; 3], t2: &[[f64; 3]; 3]) -> TriTri {
245    use PlaneInterval as PI;
246    // Near-coplanar guard (the flush-cap fix): an intended-flush coplanar
247    // interface that per-axis snapping pushed just off exact coplanarity is
248    // routed to the exact coplanar handler so the footprint is carved (otherwise
249    // a sliver bridges the opening). See `near_coplanar`.
250    if near_coplanar(t1, t2) {
251        return TriTri::Coplanar;
252    }
253    let (i1, i2) = (plane_interval(t1, t2), plane_interval(t2, t1));
254    let ends = |pi: &PI| -> Option<[ImplicitPoint; 2]> {
255        match pi {
256            PI::Point(p) => Some([p.clone(), p.clone()]),
257            PI::Chord([a, b]) => Some([a.clone(), b.clone()]),
258            _ => None,
259        }
260    };
261    if matches!(i1, PI::Coplanar) || matches!(i2, PI::Coplanar) {
262        return TriTri::Coplanar;
263    }
264    let ([a1, b1], [a2, b2]) = match (ends(&i1), ends(&i2)) {
265        (Some(s1), Some(s2)) => (s1, s2),
266        _ => return TriTri::None,
267    };
268    let u = line_direction(t1, t2);
269    let order = |a: ImplicitPoint, b: ImplicitPoint| {
270        if cmp_along(&a, &b, u) == Sign::Positive {
271            (b, a)
272        } else {
273            (a, b)
274        }
275    };
276    let (lo1, hi1) = order(a1, b1);
277    let (lo2, hi2) = order(a2, b2);
278    let lo = if cmp_along(&lo1, &lo2, u) == Sign::Positive { lo1 } else { lo2 };
279    let hi = if cmp_along(&hi1, &hi2, u) == Sign::Negative { hi1 } else { hi2 };
280    match cmp_along(&lo, &hi, u) {
281        Sign::Positive => TriTri::None,           // intervals disjoint
282        Sign::Zero => TriTri::Point(lo),          // a single shared point
283        Sign::Negative => TriTri::Segment([lo, hi]),
284    }
285}
286
287#[cfg(test)]
288#[path = "tritri_tests.rs"]
289mod tests;