Skip to main content

ifc_lite_geometry/kernel/
predicates.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//! Public predicate dispatch over `ImplicitPoint` configurations.
6//!
7//! Implements every explicit/implicit `orient3d`/`orient2d` configuration the
8//! arrangement pipeline produces, each first through the fast interval and
9//! fixed-width tiers and escalating to the exact (BigRational) tier on a
10//! straddling filter — every fast tier verified `≡` the exact tier here.
11
12use super::{fixed, interval, rational};
13use super::{DropAxis, ImplicitPoint, Sign};
14
15/// Exact `orient3d` over a mix of explicit + implicit points.
16///
17/// Cascade: explicit args go through the Shewchuk adaptive predicate (its own
18/// semi-static→exact ladder). Indirect args try the interval tier first and
19/// escalate to the exact (BigRational) tier only on a straddle. Every tier
20/// returns the SAME sign — verified against the oracle in tests.
21pub fn orient3d(a: &ImplicitPoint, b: &ImplicitPoint, c: &ImplicitPoint, d: &ImplicitPoint) -> Sign {
22    use ImplicitPoint::{Explicit, Lpi, Tpi};
23    match (a, b, c, d) {
24        (Explicit(a), Explicit(b), Explicit(c), Explicit(d)) => {
25            Sign::from_f64(geometry_predicates::orient3d(*a, *b, *c, *d))
26        }
27        (Lpi(l), Explicit(b), Explicit(c), Explicit(d)) => interval::lpi_orient3d(l, *b, *c, *d)
28            .or_else(|| {
29                crate::kernel::budget::note_escalation();
30                fixed::indirect_orient3d(a, *b, *c, *d)
31            })
32            .unwrap_or_else(|| rational::lpi_orient3d(l, *b, *c, *d)),
33        (Tpi(t), Explicit(b), Explicit(c), Explicit(d)) => interval::tpi_orient3d(t, *b, *c, *d)
34            .or_else(|| {
35                crate::kernel::budget::note_escalation();
36                fixed::indirect_orient3d(a, *b, *c, *d)
37            })
38            .unwrap_or_else(|| rational::tpi_orient3d(t, *b, *c, *d)),
39        // By-construction unreachable: kernel callers only ever build the configurations above.
40        _ => unimplemented!(
41            "kernel::orient3d: implicit-point configuration never produced by the arrangement pipeline"
42        ),
43    }
44}
45
46/// Exact `orient2d(a, b, c)` projected on the two axes remaining after dropping
47/// `axis` (the in-plane predicate for re-triangulation). Same cascade as
48/// `orient3d`; the indirect 1-implicit case shares the `sign(d)` flip.
49pub fn orient2d(a: &ImplicitPoint, b: &ImplicitPoint, c: &ImplicitPoint, axis: DropAxis) -> Sign {
50    use ImplicitPoint::{Explicit, Lpi, Tpi};
51    let (i, j) = match axis {
52        DropAxis::X => (1, 2),
53        DropAxis::Y => (0, 2),
54        DropAxis::Z => (0, 1),
55    };
56    match (a, b, c) {
57        (Explicit(a), Explicit(b), Explicit(c)) => {
58            Sign::from_f64(geometry_predicates::orient2d([a[i], a[j]], [b[i], b[j]], [c[i], c[j]]))
59        }
60        (Lpi(l), Explicit(b), Explicit(c)) => interval::lpi_orient2d(l, *b, *c, axis)
61            .or_else(|| {
62                crate::kernel::budget::note_escalation();
63                fixed::indirect_orient2d(a, *b, *c, axis)
64            })
65            .unwrap_or_else(|| rational::lpi_orient2d(l, *b, *c, axis)),
66        (Tpi(t), Explicit(b), Explicit(c)) => interval::tpi_orient2d(t, *b, *c, axis)
67            .or_else(|| {
68                crate::kernel::budget::note_escalation();
69                fixed::indirect_orient2d(a, *b, *c, axis)
70            })
71            .unwrap_or_else(|| rational::tpi_orient2d(t, *b, *c, axis)),
72        // By-construction unreachable: kernel callers only ever build the configurations above.
73        _ => unimplemented!(
74            "kernel::orient2d: implicit-point configuration never produced by the arrangement pipeline"
75        ),
76    }
77}
78
79/// orient2d with two implicit points (a,b) + one explicit (c) — cascade.
80pub fn orient2d_2i(a: &ImplicitPoint, b: &ImplicitPoint, c: [f64; 3], axis: DropAxis) -> Sign {
81    // cascade: interval filter → fixed-width exact (fast) → BigRational (off-grid / overflow)
82    interval::orient2d_2i(a, b, c, axis)
83        .or_else(|| {
84            crate::kernel::budget::note_escalation();
85            fixed::orient2d_2i(a, b, c, axis)
86        })
87        .unwrap_or_else(|| rational::orient2d_2i(a, b, c, axis))
88}
89
90/// orient2d with three implicit points (a,b,c) — cascade.
91pub fn orient2d_3i(a: &ImplicitPoint, b: &ImplicitPoint, c: &ImplicitPoint, axis: DropAxis) -> Sign {
92    interval::orient2d_3i(a, b, c, axis)
93        .or_else(|| {
94            crate::kernel::budget::note_escalation();
95            fixed::orient2d_3i(a, b, c, axis)
96        })
97        .unwrap_or_else(|| rational::orient2d_3i(a, b, c, axis))
98}
99
100/// Exact lexicographic total order on points — the interner's comparison (cascade).
101pub fn cmp_lex(a: &ImplicitPoint, b: &ImplicitPoint) -> Sign {
102    interval::cmp_lex(a, b)
103        .or_else(|| {
104            crate::kernel::budget::note_escalation();
105            fixed::cmp_lex(a, b)
106        })
107        .unwrap_or_else(|| rational::cmp_lex(a, b))
108}
109
110#[inline]
111fn explicit_coord(p: &ImplicitPoint) -> [f64; 3] {
112    match p {
113        ImplicitPoint::Explicit(c) => *c,
114        _ => unreachable!("explicit_coord on an implicit point"),
115    }
116}
117
118/// `orient2d` over ANY mix of explicit/implicit points in ANY argument position
119/// (the predicate the re-triangulation's point location needs). `orient2d` is
120/// antisymmetric, so we canonicalise the args to implicit-first (stable, to keep
121/// it a pure function), dispatch to the 0I/1I/2I/3I config, and flip the result
122/// once per transposition (the permutation parity).
123
124pub fn orient2d_any(a: &ImplicitPoint, b: &ImplicitPoint, c: &ImplicitPoint, axis: DropAxis) -> Sign {
125    let pts = [a, b, c];
126    let key = |p: &ImplicitPoint| u8::from(matches!(p, ImplicitPoint::Explicit(_))); // implicit=0
127    let keys = [key(a), key(b), key(c)];
128    let mut perm = [0usize, 1, 2];
129    perm.sort_by_key(|&i| keys[i]); // stable → implicit first, original order kept
130    let inversions = u8::from(perm[0] > perm[1]) + u8::from(perm[0] > perm[2]) + u8::from(perm[1] > perm[2]);
131    let rp = [pts[perm[0]], pts[perm[1]], pts[perm[2]]];
132    let n_implicit = 3 - (keys[0] + keys[1] + keys[2]) as usize;
133    let canonical = match n_implicit {
134        // (E,E,E) and (I,E,E) are handled by the position-specific dispatch.
135        0 | 1 => orient2d(rp[0], rp[1], rp[2], axis),
136        2 => orient2d_2i(rp[0], rp[1], explicit_coord(rp[2]), axis),
137        _ => orient2d_3i(rp[0], rp[1], rp[2], axis),
138    };
139    if inversions % 2 == 1 {
140        canonical.flip()
141    } else {
142        canonical
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::super::{rational, DropAxis, Lpi, Tpi};
149    use super::{
150        cmp_lex, orient2d, orient2d_2i, orient2d_3i, orient2d_any, orient3d, ImplicitPoint, Sign,
151    };
152
153    fn e(p: [f64; 3]) -> ImplicitPoint {
154        ImplicitPoint::Explicit(p)
155    }
156
157    /// Adversarial explicit-orient3d configurations (coplanar, building-scale
158    /// off-plane, near-coincident large coords, sub-mm + mirrored tetra).
159    fn battery() -> Vec<[[f64; 3]; 4]> {
160        vec![
161            [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.], [1., 1., 0.]], // coplanar -> 0
162            [[0., 0., 12.3456789], [10., 0., 12.3456789], [0., 7., 12.3456789], [3.3, 2.1, 12.3456789 + 1e-9]],
163            [[0., 0., 12.3456789], [10., 0., 12.3456789], [0., 7., 12.3456789], [3.3, 2.1, 12.3456789 - 1e-9]],
164            [[1e7, 1e7, 0.], [1e7 + 1., 1e7, 0.], [1e7, 1e7 + 1., 0.], [1e7 + 0.5, 1e7 + 0.5, 1e-7]],
165            [[0., 0., 0.], [1., 2., 3.], [-2., 1., 0.5], [0.5, 0.5, 0.5]],
166            [[0., 0., 0.], [1., 1., 1.], [2., 2., 2.], [5., 1., 9.]], // collinear base -> 0
167            [[0., 0., 0.], [1e-4, 0., 0.], [0., 1e-4, 0.], [0., 0., 1e-4]],
168            [[0., 0., 0.], [0., 1e-4, 0.], [1e-4, 0., 0.], [0., 0., 1e-4]],
169            [[-3., 2., 5.], [7., -1., 2.], [4., 4., -6.], [1.5, 0.0, 0.25]],
170        ]
171    }
172
173    /// LPI cases: (line PQ ∩ plane RST), plus a query triangle (p2,p3,p4).
174    fn lpi_cases() -> Vec<(Lpi, [f64; 3], [f64; 3], [f64; 3])> {
175        vec![
176            // vertical line ∩ z=0 plane -> (0.3,0.3,0); query triangle at z=1 (LPI below)
177            (
178                Lpi { p: [0.3, 0.3, -1.], q: [0.3, 0.3, 1.], r: [0., 0., 0.], s: [2., 0., 0.], t: [0., 2., 0.] },
179                [0., 0., 1.], [1., 0., 1.], [0., 1., 1.],
180            ),
181            // same LPI, query triangle at z=-1 (LPI above)
182            (
183                Lpi { p: [0.3, 0.3, -1.], q: [0.3, 0.3, 1.], r: [0., 0., 0.], s: [2., 0., 0.], t: [0., 2., 0.] },
184                [0., 0., -1.], [1., 0., -1.], [0., 1., -1.],
185            ),
186            // tilted line ∩ tilted plane
187            (
188                Lpi { p: [1., 1., 0.], q: [2., 3., 4.], r: [0., 0., 1.], s: [3., 0., 2.], t: [0., 3., 2.] },
189                [5., -2., 0.], [-1., 4., 3.], [2., 2., -3.],
190            ),
191            // building-scale
192            (
193                Lpi { p: [12.3, 4.5, -2.], q: [12.3, 4.5, 6.], r: [0., 0., 3.1], s: [20., 0., 3.1], t: [0., 9., 3.1] },
194                [10., 10., 10.], [-5., 0., 0.], [0., -5., 8.],
195            ),
196        ]
197    }
198
199    #[test]
200    fn explicit_orient3d_matches_rational_oracle() {
201        for cfg in battery() {
202            let [a, b, c, d] = cfg;
203            let fast = orient3d(&e(a), &e(b), &e(c), &e(d));
204            let oracle = rational::orient3d_exact(a, b, c, d);
205            assert_eq!(fast, oracle, "explicit orient3d != rational oracle on {cfg:?}");
206        }
207    }
208
209    #[test]
210    fn lpi_orient3d_matches_materialised_point() {
211        // The homogenised LPI-orient3d must equal the direct orient3d on the
212        // exact materialised λ/d point — proving the Λ′ + sign(d)-flip.
213        for (l, p2, p3, p4) in lpi_cases() {
214            let homog = rational::lpi_orient3d(&l, p2, p3, p4);
215            let direct = rational::orient3d_exact_pt(&rational::lpi_point(&l), p2, p3, p4);
216            assert_eq!(homog, direct, "LPI homogenisation/flip wrong for {l:?}");
217            // sanity: these are non-degenerate, so the sign is definite
218            assert_ne!(homog, Sign::Zero, "test LPI case should be off-plane: {l:?}");
219        }
220    }
221
222    #[test]
223    fn lpi_orient3d_sign_invariant_to_plane_winding() {
224        // Re-wind the plane (swap S,T): flips sign(d) but the point + geometry
225        // are identical, so the per-config flip must yield the SAME sign. This
226        // is the test that catches a missing/extra `sign(d)` flip.
227        for (l, p2, p3, p4) in lpi_cases() {
228            let l_rewound = Lpi { s: l.t, t: l.s, ..l };
229            assert_eq!(
230                rational::lpi_orient3d(&l, p2, p3, p4),
231                rational::lpi_orient3d(&l_rewound, p2, p3, p4),
232                "LPI-orient3d sign changed under plane re-winding — the sign(d) flip is wrong/missing"
233            );
234        }
235    }
236
237    #[test]
238    fn assemble_sign_per_config_flip() {
239        use super::super::assemble_sign;
240        // odd #negatives -> flip; even -> no flip; any zero -> Zero.
241        assert_eq!(assemble_sign(Sign::Positive, &[Sign::Negative]), Sign::Negative);
242        assert_eq!(assemble_sign(Sign::Positive, &[Sign::Negative, Sign::Negative]), Sign::Positive);
243        assert_eq!(assemble_sign(Sign::Negative, &[Sign::Positive]), Sign::Negative);
244        assert_eq!(assemble_sign(Sign::Positive, &[Sign::Zero]), Sign::Zero);
245        assert_eq!(assemble_sign(Sign::Positive, &[]), Sign::Positive);
246    }
247
248    #[test]
249    fn next_up_down_are_adjacent() {
250        use super::super::interval::{next_down, next_up};
251        for &x in &[1.0, -1.0, 0.0, 1e7, -1e-9, 12.3456789, f64::MIN_POSITIVE] {
252            assert!(next_up(x) > x, "next_up({x}) not strictly greater");
253            assert!(next_down(x) < x, "next_down({x}) not strictly less");
254            // Round-trip = adjacency: nothing representable strictly between.
255            assert_eq!(next_down(next_up(x)), x, "next_up/next_down not adjacent at {x}");
256        }
257    }
258
259    /// Deterministic LCG for the soundness fuzz (no Math::random; fixed seed).
260    struct Lcg(u64);
261    impl Lcg {
262        fn u(&mut self) -> u64 {
263            self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
264            self.0
265        }
266        fn f(&mut self, lo: f64, hi: f64) -> f64 {
267            let unit = (self.u() >> 11) as f64 / (1u64 << 53) as f64; // [0,1)
268            lo + (hi - lo) * unit
269        }
270        fn p(&mut self) -> [f64; 3] {
271            [self.f(-10., 10.), self.f(-10., 10.), self.f(-10., 10.)]
272        }
273    }
274
275    #[test]
276    fn interval_tier_is_sound_and_the_cascade_equals_exact() {
277        use super::super::{interval, rational, Lpi};
278        let mut rng = Lcg(0x1234_5678_9abc_def0);
279        let (mut definite, mut escalated) = (0u32, 0u32);
280        for _ in 0..3000 {
281            let l = Lpi { p: rng.p(), q: rng.p(), r: rng.p(), s: rng.p(), t: rng.p() };
282            let (p2, p3, p4) = (rng.p(), rng.p(), rng.p());
283            let exact = rational::lpi_orient3d(&l, p2, p3, p4);
284            // Soundness: a definite interval sign must equal the exact sign.
285            match interval::lpi_orient3d(&l, p2, p3, p4) {
286                Some(s) => {
287                    assert_eq!(s, exact, "interval returned a WRONG definite sign for {l:?}");
288                    definite += 1;
289                }
290                None => escalated += 1,
291            }
292            // The public cascade (interval → escalate) must always equal exact.
293            let cascade = orient3d(&ImplicitPoint::Lpi(l), &e(p2), &e(p3), &e(p4));
294            assert_eq!(cascade, exact, "cascade != exact for {l:?}");
295        }
296        // The interval fast path must carry the overwhelming majority (perf gate).
297        assert!(
298            definite as f64 / (definite + escalated) as f64 > 0.95,
299            "interval resolved only {definite}/{} — fast path too cold",
300            definite + escalated
301        );
302        eprintln!("interval tier: {definite} definite, {escalated} escalated to exact");
303    }
304
305
306    /// TPI cases: three planes (each a triangle) + a query triangle.
307    fn tpi_cases() -> Vec<(Tpi, [f64; 3], [f64; 3], [f64; 3])> {
308        // planes x=0.3, y=0.4, z=0 -> point (0.3,0.4,0)
309        let axis_aligned = Tpi {
310            planes: [
311                [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.]],     // z=0
312                [[0.3, 0., 0.], [0.3, 1., 0.], [0.3, 0., 1.]],  // x=0.3
313                [[0., 0.4, 0.], [1., 0.4, 0.], [0., 0.4, 1.]],  // y=0.4
314            ],
315        };
316        // three tilted planes meeting at a general point
317        let tilted = Tpi {
318            planes: [
319                [[0., 0., 1.], [3., 0., 2.], [0., 3., 2.]],
320                [[1., 0., 0.], [1., 2., 1.], [2., 0., 3.]],
321                [[-1., -1., 0.], [2., -1., 1.], [-1., 2., 2.]],
322            ],
323        };
324        vec![
325            (axis_aligned, [0., 0., 1.], [1., 0., 1.], [0., 1., 1.]),   // query above
326            (axis_aligned, [0., 0., -1.], [1., 0., -1.], [0., 1., -1.]), // query below
327            (tilted, [5., -2., 0.], [-1., 4., 3.], [2., 2., -3.]),
328            (tilted, [10., 10., 10.], [-5., 0., 0.], [0., -5., 8.]),
329        ]
330    }
331
332    #[test]
333    fn tpi_orient3d_matches_materialised_point() {
334        // The homogenised TPI-orient3d must equal the direct orient3d on the
335        // exact materialised λ/d point — proving the TPI Cramer λ + the flip.
336        for (t, p2, p3, p4) in tpi_cases() {
337            let homog = rational::tpi_orient3d(&t, p2, p3, p4);
338            let direct = rational::orient3d_exact_pt(&rational::tpi_point(&t), p2, p3, p4);
339            assert_eq!(homog, direct, "TPI homogenisation/flip wrong for {t:?}");
340            assert_ne!(homog, Sign::Zero, "test TPI case should be off-plane: {t:?}");
341        }
342    }
343
344    #[test]
345    fn tpi_orient3d_sign_invariant_to_plane_winding() {
346        // Re-wind plane 0 (swap its 2nd/3rd points): flips that plane's normal
347        // and hence sign(d), but the meeting point is identical → the sign(d)
348        // flip must yield the SAME geometric sign.
349        for (t, p2, p3, p4) in tpi_cases() {
350            let mut rewound = t;
351            rewound.planes[0].swap(1, 2);
352            assert_eq!(
353                rational::tpi_orient3d(&t, p2, p3, p4),
354                rational::tpi_orient3d(&rewound, p2, p3, p4),
355                "TPI-orient3d sign changed under plane re-winding — the sign(d) flip is wrong/missing"
356            );
357        }
358    }
359
360    #[test]
361    fn tpi_interval_is_sound_and_the_cascade_equals_exact() {
362        use super::super::interval;
363        let mut rng = Lcg(0xfeed_face_cafe_d00d);
364        let (mut definite, mut escalated) = (0u32, 0u32);
365        for _ in 0..2000 {
366            // a random TPI = three random planes (generically meet at a point)
367            let plane = |rng: &mut Lcg| [rng.p(), rng.p(), rng.p()];
368            let t = Tpi { planes: [plane(&mut rng), plane(&mut rng), plane(&mut rng)] };
369            let (p2, p3, p4) = (rng.p(), rng.p(), rng.p());
370            let exact = rational::tpi_orient3d(&t, p2, p3, p4);
371            match interval::tpi_orient3d(&t, p2, p3, p4) {
372                Some(s) => {
373                    assert_eq!(s, exact, "TPI interval returned a WRONG definite sign for {t:?}");
374                    definite += 1;
375                }
376                None => escalated += 1,
377            }
378            let cascade = orient3d(&ImplicitPoint::Tpi(t), &e(p2), &e(p3), &e(p4));
379            assert_eq!(cascade, exact, "TPI cascade != exact for {t:?}");
380        }
381        // TPI Λ′ is degree-3 in the planes (heavier than LPI) so the interval is
382        // wider — still expect a healthy majority to resolve in f64.
383        assert!(
384            definite as f64 / (definite + escalated) as f64 > 0.80,
385            "TPI interval resolved only {definite}/{}",
386            definite + escalated
387        );
388        eprintln!("TPI interval tier: {definite} definite, {escalated} escalated");
389    }
390
391    const AXES: [DropAxis; 3] = [DropAxis::X, DropAxis::Y, DropAxis::Z];
392
393    #[test]
394    fn explicit_orient2d_matches_oracle() {
395        for axis in AXES {
396            for cfg in battery() {
397                let [a, b, c, _d] = cfg;
398                let fast = orient2d(&e(a), &e(b), &e(c), axis);
399                let oracle = rational::orient2d_exact(a, b, c, axis);
400                assert_eq!(fast, oracle, "explicit orient2d != oracle on {cfg:?} axis {axis:?}");
401            }
402        }
403    }
404
405    #[test]
406    fn indirect_orient2d_matches_materialised_point() {
407        // Homogenised LPI/TPI orient2d == the direct orient2d on the exact
408        // materialised λ/d point, for every projection axis; cascade == exact.
409        for axis in AXES {
410            for (l, p2, p3, _p4) in lpi_cases() {
411                let homog = rational::lpi_orient2d(&l, p2, p3, axis);
412                let direct = rational::orient2d_exact_pt(&rational::lpi_point(&l), p2, p3, axis);
413                assert_eq!(homog, direct, "LPI orient2d homog/flip wrong, axis {axis:?}");
414                let cascade = orient2d(&ImplicitPoint::Lpi(l), &e(p2), &e(p3), axis);
415                assert_eq!(cascade, direct, "LPI orient2d cascade != exact, axis {axis:?}");
416            }
417            for (t, p2, p3, _p4) in tpi_cases() {
418                let homog = rational::tpi_orient2d(&t, p2, p3, axis);
419                let direct = rational::orient2d_exact_pt(&rational::tpi_point(&t), p2, p3, axis);
420                assert_eq!(homog, direct, "TPI orient2d homog/flip wrong, axis {axis:?}");
421                let cascade = orient2d(&ImplicitPoint::Tpi(t), &e(p2), &e(p3), axis);
422                assert_eq!(cascade, direct, "TPI orient2d cascade != exact, axis {axis:?}");
423            }
424        }
425    }
426
427    #[test]
428    fn orient2d_interval_is_sound_vs_oracle() {
429        use super::super::interval;
430        let mut rng = Lcg(0xabcd_1234_5678_9999);
431        for _ in 0..2000 {
432            let l = Lpi { p: rng.p(), q: rng.p(), r: rng.p(), s: rng.p(), t: rng.p() };
433            let (b, c) = (rng.p(), rng.p());
434            let axis = AXES[(rng.u() % 3) as usize];
435            let exact = rational::lpi_orient2d(&l, b, c, axis);
436            if let Some(s) = interval::lpi_orient2d(&l, b, c, axis) {
437                assert_eq!(s, exact, "orient2d interval returned a WRONG definite sign for {l:?}");
438            }
439            // cascade always equals exact
440            assert_eq!(orient2d(&ImplicitPoint::Lpi(l), &e(b), &e(c), axis), exact);
441        }
442    }
443
444    #[test]
445    fn lpi_point_lies_on_its_defining_plane() {
446        // GEOMETRIC correctness (not just self-consistency): orient3d(LPI, R,S,T)
447        // must be 0 — the LPI point is on plane RST by definition. This guard is
448        // what the consistency tests miss; it caught the λ = d·P ± n·qp sign bug.
449        for (l, _, _, _) in lpi_cases() {
450            assert_eq!(
451                orient3d(&ImplicitPoint::Lpi(l), &e(l.r), &e(l.s), &e(l.t)),
452                Sign::Zero,
453                "LPI point is not on its defining plane R,S,T: {l:?}"
454            );
455        }
456    }
457
458    #[test]
459    fn tpi_point_lies_on_all_three_defining_planes() {
460        for (t, _, _, _) in tpi_cases() {
461            for plane in &t.planes {
462                assert_eq!(
463                    orient3d(&ImplicitPoint::Tpi(t), &e(plane[0]), &e(plane[1]), &e(plane[2])),
464                    Sign::Zero,
465                    "TPI point is not on one of its defining planes: {t:?}"
466                );
467            }
468        }
469    }
470
471    #[test]
472    fn orient2d_1i_sign_invariant_to_plane_winding() {
473        // Guards Risk #1 (the doc-vs-code sign-table conflict): rewinding the
474        // implicit point's defining plane flips sign(d) while leaving the point +
475        // 2D query geometrically identical, so the TRUE orient2d sign is
476        // unchanged. The shipped d¹ flip preserves it; the doc's old d²/no-flip
477        // would invert it. The orient3d analogue is tested at line ~137 — this
478        // closes the matching gap for orient2d (the gap the LPI λ-sign bug used).
479        for axis in AXES {
480            for (l, p2, p3, _p4) in lpi_cases() {
481                let rewound = Lpi { s: l.t, t: l.s, ..l };
482                assert_eq!(
483                    rational::lpi_orient2d(&l, p2, p3, axis),
484                    rational::lpi_orient2d(&rewound, p2, p3, axis),
485                    "orient2d 1I sign changed under plane re-winding (axis {axis:?})"
486                );
487            }
488        }
489    }
490
491    #[test]
492    fn multi_implicit_orient2d_matches_materialised_oracle() {
493        // orient2d_2i / orient2d_3i over all {Lpi,Tpi} mixtures must equal
494        // the direct orient2d on the materialised λ/d points, for every drop axis.
495        let mut pts: Vec<ImplicitPoint> =
496            lpi_cases().into_iter().map(|(l, ..)| ImplicitPoint::Lpi(l)).collect();
497        pts.extend(tpi_cases().into_iter().map(|(t, ..)| ImplicitPoint::Tpi(t)));
498        let c = [1.3, -0.7, 2.1];
499        let cpt = rational::point_of(&ImplicitPoint::Explicit(c));
500        for axis in AXES {
501            for a in &pts {
502                for b in &pts {
503                    let oracle = rational::orient2d_pts(
504                        &rational::point_of(a), &rational::point_of(b), &cpt, axis);
505                    assert_eq!(rational::orient2d_2i(a, b, c, axis), oracle, "orient2d_2i (axis {axis:?})");
506                    for cc in &pts {
507                        let oracle3 = rational::orient2d_pts(
508                            &rational::point_of(a), &rational::point_of(b), &rational::point_of(cc), axis);
509                        assert_eq!(rational::orient2d_3i(a, b, cc, axis), oracle3, "orient2d_3i (axis {axis:?})");
510                    }
511                }
512            }
513        }
514    }
515
516    #[test]
517    fn multi_implicit_orient2d_winding_invariant() {
518        // Rewinding a's defining plane flips sign(d); 2I/3I must keep the sign.
519        let l = lpi_cases()[2].0;
520        let a = ImplicitPoint::Lpi(l);
521        let a_rw = ImplicitPoint::Lpi(Lpi { s: l.t, t: l.s, ..l });
522        let b = ImplicitPoint::Tpi(tpi_cases()[0].0);
523        let cc = ImplicitPoint::Lpi(lpi_cases()[0].0);
524        let c = [0.4, 1.1, -0.3];
525        for axis in AXES {
526            assert_eq!(
527                rational::orient2d_2i(&a, &b, c, axis),
528                rational::orient2d_2i(&a_rw, &b, c, axis),
529                "2I sign changed under plane re-winding"
530            );
531            assert_eq!(
532                rational::orient2d_3i(&a, &b, &cc, axis),
533                rational::orient2d_3i(&a_rw, &b, &cc, axis),
534                "3I sign changed under plane re-winding"
535            );
536        }
537    }
538
539    #[test]
540    fn orient2d_any_matches_oracle_for_every_permutation_and_mix() {
541        // The general dispatch must equal the direct orient2d on the materialised
542        // points for EVERY ordered triple (covers all positions + permutation
543        // parities) and every drop axis, across explicit/LPI/TPI mixes.
544        let mut pts: Vec<ImplicitPoint> = vec![e([0.0, 0.0, 0.0]), e([3.0, 1.0, -2.0])];
545        pts.extend(lpi_cases().into_iter().take(2).map(|(l, ..)| ImplicitPoint::Lpi(l)));
546        pts.extend(tpi_cases().into_iter().take(2).map(|(t, ..)| ImplicitPoint::Tpi(t)));
547        for axis in AXES {
548            for a in &pts {
549                for b in &pts {
550                    for c in &pts {
551                        let got = orient2d_any(a, b, c, axis);
552                        let want = rational::orient2d_pts(
553                            &rational::point_of(a),
554                            &rational::point_of(b),
555                            &rational::point_of(c),
556                            axis,
557                        );
558                        assert_eq!(got, want, "orient2d_any mismatch (axis {axis:?})");
559                    }
560                }
561            }
562        }
563    }
564
565    #[test]
566    fn cmp_lex_matches_materialised_order_and_is_a_total_order() {
567        use std::cmp::Ordering;
568        let mut pts: Vec<ImplicitPoint> =
569            lpi_cases().into_iter().map(|(l, ..)| ImplicitPoint::Lpi(l)).collect();
570        pts.extend(tpi_cases().into_iter().map(|(t, ..)| ImplicitPoint::Tpi(t)));
571        pts.push(e([1.5, -2.0, 0.25]));
572        pts.push(e([0.0, 0.0, 0.0]));
573        let oracle = |a: &ImplicitPoint, b: &ImplicitPoint| -> Sign {
574            let (pa, pb) = (rational::point_of(a), rational::point_of(b));
575            for k in 0..3 {
576                match pa[k].cmp(&pb[k]) {
577                    Ordering::Less => return Sign::Negative,
578                    Ordering::Greater => return Sign::Positive,
579                    Ordering::Equal => {}
580                }
581            }
582            Sign::Zero
583        };
584        for a in &pts {
585            assert_eq!(rational::cmp_lex(a, a), Sign::Zero, "cmp_lex not reflexive-zero");
586            for b in &pts {
587                assert_eq!(rational::cmp_lex(a, b), oracle(a, b), "cmp_lex != materialised lex");
588                assert_eq!(
589                    rational::cmp_lex(a, b),
590                    rational::cmp_lex(b, a).flip(),
591                    "cmp_lex not antisymmetric"
592                );
593            }
594        }
595        // transitivity: a<b<c ⇒ a<c (no ordering cycles).
596        for a in &pts {
597            for b in &pts {
598                for c in &pts {
599                    if rational::cmp_lex(a, b) == Sign::Negative
600                        && rational::cmp_lex(b, c) == Sign::Negative
601                    {
602                        assert_eq!(rational::cmp_lex(a, c), Sign::Negative, "cmp_lex not transitive");
603                    }
604                }
605            }
606        }
607    }
608
609    #[test]
610    fn new_tier_interval_is_sound_and_cascade_equals_exact() {
611        // The interval fast tiers for 2I/3I orient2d + cmp_lex never
612        // return a wrong definite sign, and the public cascade always == exact.
613        use super::super::interval;
614        let mut rng = Lcg(0x0bad_c0de_1234_5678);
615        for _ in 0..500 {
616            let mk = |rng: &mut Lcg| Lpi { p: rng.p(), q: rng.p(), r: rng.p(), s: rng.p(), t: rng.p() };
617            let a = ImplicitPoint::Lpi(mk(&mut rng));
618            let b = ImplicitPoint::Lpi(mk(&mut rng));
619            let cc = ImplicitPoint::Lpi(mk(&mut rng));
620            let c = rng.p();
621            for axis in AXES {
622                let ex2 = rational::orient2d_2i(&a, &b, c, axis);
623                if let Some(s) = interval::orient2d_2i(&a, &b, c, axis) {
624                    assert_eq!(s, ex2, "orient2d_2i interval wrong sign");
625                }
626                assert_eq!(orient2d_2i(&a, &b, c, axis), ex2, "2i cascade != exact");
627                let ex3 = rational::orient2d_3i(&a, &b, &cc, axis);
628                if let Some(s) = interval::orient2d_3i(&a, &b, &cc, axis) {
629                    assert_eq!(s, ex3, "orient2d_3i interval wrong sign");
630                }
631                assert_eq!(orient2d_3i(&a, &b, &cc, axis), ex3, "3i cascade != exact");
632            }
633            let exl = rational::cmp_lex(&a, &b);
634            if let Some(s) = interval::cmp_lex(&a, &b) {
635                assert_eq!(s, exl, "cmp_lex interval wrong sign");
636            }
637            assert_eq!(cmp_lex(&a, &b), exl, "cmp_lex cascade != exact");
638        }
639    }
640
641    #[test]
642    fn cmp_lex_welds_coincident_lpi_and_tpi() {
643        // An LPI and a TPI built from DIFFERENT constructions at the SAME physical
644        // point must compare equal (Zero) so the interner welds them to one VID.
645        let lpi = ImplicitPoint::Lpi(Lpi {
646            p: [0.3, 0.4, -1.],
647            q: [0.3, 0.4, 1.],
648            r: [0., 0., 0.],
649            s: [1., 0., 0.],
650            t: [0., 1., 0.],
651        });
652        let tpi = ImplicitPoint::Tpi(Tpi {
653            planes: [
654                [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.]],    // z=0
655                [[0.3, 0., 0.], [0.3, 1., 0.], [0.3, 0., 1.]], // x=0.3
656                [[0., 0.4, 0.], [1., 0.4, 0.], [0., 0.4, 1.]], // y=0.4
657            ],
658        });
659        assert_eq!(rational::point_of(&lpi), rational::point_of(&tpi), "test points not coincident");
660        assert_eq!(
661            rational::cmp_lex(&lpi, &tpi),
662            Sign::Zero,
663            "coincident LPI/TPI not welded by cmp_lex"
664        );
665    }
666
667    #[test]
668    fn cmp_along_matches_materialised_oracle() {
669        use num_rational::BigRational;
670        use num_traits::Signed;
671        let mut pts: Vec<ImplicitPoint> =
672            vec![e([0., 0., 0.]), e([3., 1., -2.]), e([1.5, -2., 0.25])];
673        pts.extend(lpi_cases().into_iter().take(2).map(|(l, ..)| ImplicitPoint::Lpi(l)));
674        pts.extend(tpi_cases().into_iter().take(2).map(|(t, ..)| ImplicitPoint::Tpi(t)));
675        let dirs = [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.], [1., 2., -1.], [-0.3, 1.7, 0.5]];
676        for u in dirs {
677            let ur = [
678                BigRational::from_float(u[0]).unwrap(),
679                BigRational::from_float(u[1]).unwrap(),
680                BigRational::from_float(u[2]).unwrap(),
681            ];
682            for a in &pts {
683                for b in &pts {
684                    let pa = rational::point_of(a);
685                    let pb = rational::point_of(b);
686                    let dot = (&pa[0] - &pb[0]) * &ur[0]
687                        + (&pa[1] - &pb[1]) * &ur[1]
688                        + (&pa[2] - &pb[2]) * &ur[2];
689                    let want = if dot.is_negative() {
690                        Sign::Negative
691                    } else if dot.is_positive() {
692                        Sign::Positive
693                    } else {
694                        Sign::Zero
695                    };
696                    assert_eq!(rational::cmp_along(a, b, u), want, "cmp_along != oracle (u={u:?})");
697                }
698            }
699        }
700    }
701}