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}