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