ifc_lite_geometry/kernel/
coplanar.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//! Coplanar triangle overlap — the constraints two coplanar overlapping
6//! faces impose on each other.
7//!
8//! When triangle `tb` is coplanar with `ta`, each of `tb`'s edges is clipped to
9//! `ta` and the portion inside becomes a constraint for `ta` (and symmetrically).
10//! A clipped endpoint is either an `Explicit` vertex (inside `ta`) or a coplanar
11//! edge×edge crossing — represented EXACTLY as an auxiliary-plane LPI: the line
12//! `q0q1` ∩ the plane through ta-edge `(e0,e1)` extruded perpendicular to the
13//! shared plane (`e0 + n`). Within the shared plane that plane reads as the line
14//! `e0e1`, so the LPI is exactly the 2D crossing — and it lies on the shared
15//! plane, keeping everything exact and conforming.
16
17use super::predicates::orient2d_any;
18use super::retriangulate::projection_axis;
19use super::{DropAxis, ImplicitPoint, Lpi, Sign};
20
21type Tri = [[f64; 3]; 3];
22
23#[inline]
24fn e(p: [f64; 3]) -> ImplicitPoint {
25    ImplicitPoint::Explicit(p)
26}
27
28fn plane_normal(t: &Tri) -> [f64; 3] {
29    let u = [t[1][0] - t[0][0], t[1][1] - t[0][1], t[1][2] - t[0][2]];
30    let v = [t[2][0] - t[0][0], t[2][1] - t[0][1], t[2][2] - t[0][2]];
31    [
32        u[1] * v[2] - u[2] * v[1],
33        u[2] * v[0] - u[0] * v[2],
34        u[0] * v[1] - u[1] * v[0],
35    ]
36}
37
38/// Is explicit point `q` inside the closed triangle `tri` (oriented `w0`)?
39fn point_in_tri(q: [f64; 3], tri: &Tri, axis: DropAxis, w0: Sign) -> bool {
40    let opp = w0.flip();
41    let on_inner = |a: [f64; 3], b: [f64; 3]| orient2d_any(&e(a), &e(b), &e(q), axis) != opp;
42    on_inner(tri[0], tri[1]) && on_inner(tri[1], tri[2]) && on_inner(tri[2], tri[0])
43}
44
45/// Do segments `(q0,q1)` and `(e0,e1)` properly cross in the `axis` projection?
46fn seg_seg_cross(q0: [f64; 3], q1: [f64; 3], e0: [f64; 3], e1: [f64; 3], axis: DropAxis) -> bool {
47    let s1 = orient2d_any(&e(q0), &e(q1), &e(e0), axis);
48    let s2 = orient2d_any(&e(q0), &e(q1), &e(e1), axis);
49    let s3 = orient2d_any(&e(e0), &e(e1), &e(q0), axis);
50    let s4 = orient2d_any(&e(e0), &e(e1), &e(q1), axis);
51    s1 != Sign::Zero && s2 != Sign::Zero && s1 != s2 && s3 != Sign::Zero && s4 != Sign::Zero && s3 != s4
52}
53
54/// Auxiliary-plane LPI for the coplanar crossing of `q0q1` with ta-edge `(e0,e1)`:
55/// `line(q0,q1) ∩ plane(e0, e1, e0+n)`, `n` = shared-plane normal.
56///
57/// PERF (off-grid aux fix): the raw `n` is the UN-normalised cross product of two
58/// on-grid (`k/2^16`) edge vectors, so its components land on the `1/2^32` grid —
59/// OFF the `1/2^16` snap grid. `aux = e0 + n` is then off-grid, every predicate on
60/// the resulting LPI fails the fixed-width tier's on-grid `gi` and falls to the
61/// ~3 ms BigRational path (the dominant flush-cap/near-coplanar cut cost: 841 had
62/// ~2000 BigRational λ-rebuilds per slow opening). The aux point only needs to make
63/// the plane `(e0,e1,aux)` read as the LINE `e0e1` in-plane and stay perpendicular
64/// to the shared plane — its exact offset along `n` is irrelevant. So we ROUND `n`
65/// to a grid-aligned integer direction (scale the max component to ~2^10, round),
66/// like `tritri::line_direction` does: `aux` then lands on-grid and the fixed
67/// tier resolves the LPI. The SIGN of every predicate is unchanged — `n` still
68/// points off the shared plane along the same perpendicular, and the in-plane
69/// reading of the plane is still the exact line `e0e1` (which only uses `e0,e1`).
70/// Deterministic (FMA-free f64 round) ⇒ byte-identical native==wasm.
71///
72/// SCALE 2^10, not the original 2^20 (crack-family fix): when `e0` is a WELDED
73/// seam vertex on the fine `k/2^36` grid (|coord| < 2^13 ⇒ ≤49 bits), adding an
74/// integer component up to 2^20 needs up to 56 bits — `e0 + ng` then ROUNDS and
75/// the aux point lands off every grid, kicking every predicate on the LPI to the
76/// BigRational tier (~200k residual exact-rational calls on a tunnel corpus).
77/// At 2^10 the sum stays ≤50 bits ⇒ exact f64 ⇒ on-grid. The LPI's VALUE is
78/// aux-invariant (any off-plane aux reads as the line `e0e1` in-plane), so
79/// signs, topology and the pinned manifests are unchanged.
80fn crossing_lpi(q0: [f64; 3], q1: [f64; 3], e0: [f64; 3], e1: [f64; 3], n: [f64; 3]) -> ImplicitPoint {
81    let m = n[0].abs().max(n[1].abs()).max(n[2].abs());
82    let ng = if m > 0.0 && m.is_finite() {
83        let s = 1024.0 / m; // normalise the max component to ~2^10, then round
84        [(n[0] * s).round(), (n[1] * s).round(), (n[2] * s).round()]
85    } else {
86        n
87    };
88    let aux = [e0[0] + ng[0], e0[1] + ng[1], e0[2] + ng[2]];
89    ImplicitPoint::Lpi(Lpi { p: q0, q: q1, r: e0, s: e1, t: aux })
90}
91
92/// Clip segment `(q0,q1)` to triangle `ta` (2D). Returns the in-`ta` portion as a
93/// constraint, or `None` if the segment misses `ta`.
94fn clip_seg(
95    q0: [f64; 3],
96    q1: [f64; 3],
97    ta: &Tri,
98    axis: DropAxis,
99    w0: Sign,
100    n: [f64; 3],
101) -> Option<(ImplicitPoint, ImplicitPoint)> {
102    let in0 = point_in_tri(q0, ta, axis, w0);
103    let in1 = point_in_tri(q1, ta, axis, w0);
104    let edges = [(ta[0], ta[1]), (ta[1], ta[2]), (ta[2], ta[0])];
105    let crossings: Vec<ImplicitPoint> = edges
106        .iter()
107        .filter(|&&(e0, e1)| seg_seg_cross(q0, q1, e0, e1, axis))
108        .map(|&(e0, e1)| crossing_lpi(q0, q1, e0, e1, n))
109        .collect();
110    match (in0, in1) {
111        (true, true) => Some((e(q0), e(q1))),
112        (true, false) => crossings.into_iter().next().map(|c| (e(q0), c)),
113        (false, true) => crossings.into_iter().next().map(|c| (c, e(q1))),
114        (false, false) => {
115            if crossings.len() >= 2 {
116                Some((crossings[0].clone(), crossings[1].clone()))
117            } else {
118                None
119            }
120        }
121    }
122}
123
124/// Constraints `tb` (coplanar with `ta`) imposes on `ta`: each `tb` edge clipped
125/// to `ta`. Empty if `ta` is degenerate or the triangles don't overlap.
126pub fn coplanar_clip(ta: &Tri, tb: &Tri) -> Vec<(ImplicitPoint, ImplicitPoint)> {
127    let (axis, w0) = match projection_axis(ta) {
128        Some(x) => x,
129        None => return Vec::new(),
130    };
131    let n = plane_normal(ta);
132    let tb_edges = [(tb[0], tb[1]), (tb[1], tb[2]), (tb[2], tb[0])];
133    tb_edges
134        .iter()
135        .filter_map(|&(q0, q1)| clip_seg(q0, q1, ta, axis, w0, n))
136        .collect()
137}
138
139#[cfg(test)]
140mod tests {
141    use super::super::predicates::orient3d;
142    use super::*;
143
144    #[test]
145    fn coplanar_clip_of_contained_face_is_its_edges_on_plane() {
146        // ta = big triangle in z=0; tb = small triangle fully inside it (z=0).
147        let ta: Tri = [[0., 0., 0.], [6., 0., 0.], [0., 6., 0.]];
148        let tb: Tri = [[1., 1., 0.], [3., 1., 0.], [1., 3., 0.]];
149        let cons = coplanar_clip(&ta, &tb);
150        assert_eq!(cons.len(), 3, "contained face should give all 3 edges");
151        for (a, b) in &cons {
152            // both endpoints explicit (inside) and on ta's plane (z=0)
153            assert!(matches!(a, ImplicitPoint::Explicit(_)));
154            assert!(matches!(b, ImplicitPoint::Explicit(_)));
155            assert_eq!(orient3d(a, &e(ta[0]), &e(ta[1]), &e(ta[2])), Sign::Zero);
156        }
157    }
158
159    #[test]
160    fn coplanar_clip_of_overhanging_face_clips_to_aux_lpi_on_plane() {
161        // tb sticks out past ta's hypotenuse → clipped endpoints include an
162        // auxiliary-plane LPI crossing, which must lie on ta's plane (z=0).
163        let ta: Tri = [[0., 0., 0.], [4., 0., 0.], [0., 4., 0.]]; // hypotenuse x+y=4
164        let tb: Tri = [[1., 1., 0.], [5., 1., 0.], [1., 5., 0.]]; // overhangs
165        let cons = coplanar_clip(&ta, &tb);
166        assert!(!cons.is_empty(), "overlapping faces must produce constraints");
167        let mut saw_lpi = false;
168        for (a, b) in &cons {
169            for ep in [a, b] {
170                if matches!(ep, ImplicitPoint::Lpi(_)) {
171                    saw_lpi = true;
172                }
173                // every endpoint lies exactly on ta's plane
174                assert_eq!(
175                    orient3d(ep, &e(ta[0]), &e(ta[1]), &e(ta[2])),
176                    Sign::Zero,
177                    "coplanar-clip endpoint off the shared plane"
178                );
179            }
180        }
181        assert!(saw_lpi, "an overhanging edge must yield an auxiliary-plane LPI crossing");
182    }
183}