Skip to main content

ifc_lite_geometry/kernel/
rational.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//! Exact (BigRational) predicate tier — the correctness ground truth and the
6//! cascade's last-resort exact fallback. f64 coordinates are exactly
7//! representable as `BigRational`, so every sign here is mathematically exact.
8
9use super::{DropAxis, ImplicitPoint, Lpi, Sign, Tpi};
10use num_rational::BigRational;
11use num_traits::{Signed, Zero};
12
13#[inline]
14fn r(x: f64) -> BigRational {
15    BigRational::from_float(x).expect("kernel: non-finite coordinate reached the exact predicate")
16}
17
18#[inline]
19fn sign_of(x: &BigRational) -> Sign {
20    if x.is_negative() {
21        Sign::Negative
22    } else if x.is_positive() {
23        Sign::Positive
24    } else {
25        Sign::Zero
26    }
27}
28
29type V3 = [BigRational; 3];
30
31#[inline]
32fn vec(p: [f64; 3]) -> V3 {
33    [r(p[0]), r(p[1]), r(p[2])]
34}
35
36/// Exact average of explicit points. Finite fallback for a degenerate (d==0)
37/// implicit point whose λ/d is undefined (avoids a worker-aborting div-by-zero).
38fn average(pts: &[[f64; 3]]) -> V3 {
39    let n = BigRational::from_float(pts.len() as f64).unwrap();
40    let mut acc = [r(0.0), r(0.0), r(0.0)];
41    for p in pts {
42        let v = vec(*p);
43        acc = [&acc[0] + &v[0], &acc[1] + &v[1], &acc[2] + &v[2]];
44    }
45    [&acc[0] / &n, &acc[1] / &n, &acc[2] / &n]
46}
47
48#[inline]
49fn sub3(a: &V3, b: &V3) -> V3 {
50    [&a[0] - &b[0], &a[1] - &b[1], &a[2] - &b[2]]
51}
52
53/// det of the 3×3 matrix with rows u, v, w  (= u · (v × w)).
54fn det3(u: &V3, v: &V3, w: &V3) -> BigRational {
55    &u[0] * (&v[1] * &w[2] - &v[2] * &w[1])
56        + &u[1] * (&v[2] * &w[0] - &v[0] * &w[2])
57        + &u[2] * (&v[0] * &w[1] - &v[1] * &w[0])
58}
59
60#[inline]
61fn cross(u: &V3, v: &V3) -> V3 {
62    [
63        &u[1] * &v[2] - &u[2] * &v[1],
64        &u[2] * &v[0] - &u[0] * &v[2],
65        &u[0] * &v[1] - &u[1] * &v[0],
66    ]
67}
68
69/// Exact explicit orient3d — Shewchuk's sign convention (matches
70/// `geometry_predicates::orient3d`).
71pub fn orient3d_exact(a: [f64; 3], b: [f64; 3], c: [f64; 3], d: [f64; 3]) -> Sign {
72    let (a, b, c, d) = (vec(a), vec(b), vec(c), vec(d));
73    let ad = sub3(&a, &d);
74    let bd = sub3(&b, &d);
75    let cd = sub3(&c, &d);
76    sign_of(&det3(&ad, &bd, &cd))
77}
78
79/// Exact orient2d on the two axes remaining after dropping `axis`.
80pub fn orient2d_exact(a: [f64; 3], b: [f64; 3], c: [f64; 3], axis: DropAxis) -> Sign {
81    let (i, j) = match axis {
82        DropAxis::X => (1, 2),
83        DropAxis::Y => (0, 2),
84        DropAxis::Z => (0, 1),
85    };
86    let det = (r(a[i]) - r(c[i])) * (r(b[j]) - r(c[j])) - (r(a[j]) - r(c[j])) * (r(b[i]) - r(c[i]));
87    sign_of(&det)
88}
89
90/// LPI λ-construction (exact): the implicit point is `(λx/d, λy/d, λz/d)`.
91/// Line `PQ` ∩ plane `RST`: parametrise `X = P + τ·(Q−P)`; on-plane gives
92/// `(P−R)·(SR×TR) + τ·(Q−P)·(SR×TR) = 0`, i.e. `n + τ·d = 0`, so `τ = −n/d`
93/// and `λ = d·P − n·(Q−P)` (the MINUS is load-bearing — `+` lands off the
94/// plane; verified by `tritri::edge_crossing_lpi_lies_exactly_on_the_plane`).
95/// `qp=Q−P; sr=S−R; tr=T−R; pr=P−R; d=det3(qp,sr,tr); n=det3(pr,sr,tr)`.
96pub fn lpi_lambda(l: &Lpi) -> (V3, BigRational) {
97    let p = vec(l.p);
98    let q = vec(l.q);
99    let rr = vec(l.r);
100    let s = vec(l.s);
101    let t = vec(l.t);
102    let qp = sub3(&q, &p);
103    let sr = sub3(&s, &rr);
104    let tr = sub3(&t, &rr);
105    let pr = sub3(&p, &rr);
106    let d = det3(&qp, &sr, &tr);
107    let n = det3(&pr, &sr, &tr);
108    let lx = &d * &p[0] - &n * &qp[0];
109    let ly = &d * &p[1] - &n * &qp[1];
110    let lz = &d * &p[2] - &n * &qp[2];
111    ([lx, ly, lz], d)
112}
113
114/// The materialised LPI point `λ/d` (exact). Used by the oracle test that
115/// independently checks the homogenised form in [`lpi_orient3d`].
116pub fn lpi_point(l: &Lpi) -> V3 {
117    let (lambda, d) = lpi_lambda(l);
118    if d.is_zero() {
119        // Degenerate LPI (line parallel to plane): λ/d undefined. Return the
120        // segment midpoint instead of dividing by zero (which aborts the worker;
121        // reachable via classify::to_f64_pt / boolean::point_via_interner).
122        return average(&[l.p, l.q]);
123    }
124    [&lambda[0] / &d, &lambda[1] / &d, &lambda[2] / &d]
125}
126
127/// Homogenised indirect orient3d for ONE implicit first-argument point `(λ/d)`
128/// against three explicit points. `orient3d = (1/d)·Λ′`, where
129/// `Λ′ = det3( (λ − d·p4), (p2−p4), (p3−p4) )`, so the geometric sign is
130/// `assemble_sign(sign(Λ′), &[sign(d)])`. Shared by LPI and TPI — the
131/// homogenisation depends only on the implicit-row count, not the point's
132/// origin. The `sign(d)` flip (odd-multiplicity denominator) is mandatory.
133fn indirect_orient3d(lambda: &V3, d: &BigRational, p2: [f64; 3], p3: [f64; 3], p4: [f64; 3]) -> Sign {
134    let p4r = vec(p4);
135    let row1 = [
136        &lambda[0] - d * &p4r[0],
137        &lambda[1] - d * &p4r[1],
138        &lambda[2] - d * &p4r[2],
139    ];
140    let row2 = sub3(&vec(p2), &p4r);
141    let row3 = sub3(&vec(p3), &p4r);
142    super::assemble_sign(sign_of(&det3(&row1, &row2, &row3)), &[sign_of(d)])
143}
144
145/// Exact `orient3d(p1=LPI, p2, p3, p4)` with `p2,p3,p4` explicit.
146pub fn lpi_orient3d(l: &Lpi, p2: [f64; 3], p3: [f64; 3], p4: [f64; 3]) -> Sign {
147    let (lambda, d) = lpi_lambda(l);
148    indirect_orient3d(&lambda, &d, p2, p3, p4)
149}
150
151/// TPI λ-construction (exact) via Cramer on the three plane equations
152/// `nᵢ·x = cᵢ`, with `nᵢ=(Bᵢ−Aᵢ)×(Cᵢ−Aᵢ)`, `cᵢ=nᵢ·Aᵢ` (un-normalised → all
153/// polynomials, no sqrt). Cite: Attene 2020 §4. `d=det3(n1,n2,n3)`, `λ` =
154/// the Cramer numerators (column k replaced by `(c1,c2,c3)`).
155pub fn tpi_lambda(t: &Tpi) -> (V3, BigRational) {
156    let plane = |pl: &[[f64; 3]; 3]| -> (V3, BigRational) {
157        let a = vec(pl[0]);
158        let ba = sub3(&vec(pl[1]), &a);
159        let ca = sub3(&vec(pl[2]), &a);
160        let n = cross(&ba, &ca);
161        let off = &n[0] * &a[0] + &n[1] * &a[1] + &n[2] * &a[2];
162        (n, off)
163    };
164    let (n1, c1) = plane(&t.planes[0]);
165    let (n2, c2) = plane(&t.planes[1]);
166    let (n3, c3) = plane(&t.planes[2]);
167    let d = det3(&n1, &n2, &n3);
168    let ns = [&n1, &n2, &n3];
169    let cs = [&c1, &c2, &c3];
170    let cramer = |k: usize| -> BigRational {
171        let mut rows: [V3; 3] = [ns[0].clone(), ns[1].clone(), ns[2].clone()];
172        for (row, ci) in rows.iter_mut().zip(cs.iter()) {
173            row[k] = (*ci).clone();
174        }
175        det3(&rows[0], &rows[1], &rows[2])
176    };
177    ([cramer(0), cramer(1), cramer(2)], d)
178}
179
180/// Exact `orient3d(p1=TPI, p2, p3, p4)` with `p2,p3,p4` explicit.
181pub fn tpi_orient3d(t: &Tpi, p2: [f64; 3], p3: [f64; 3], p4: [f64; 3]) -> Sign {
182    let (lambda, d) = tpi_lambda(t);
183    indirect_orient3d(&lambda, &d, p2, p3, p4)
184}
185
186/// Materialised TPI point `λ/d` (exact) — for the oracle cross-check.
187pub fn tpi_point(t: &Tpi) -> V3 {
188    let (lambda, d) = tpi_lambda(t);
189    if d.is_zero() {
190        // Degenerate TPI (planes not concurrent at a point): λ/d undefined. Fall
191        // back to the first plane's centroid instead of dividing by zero.
192        return average(&t.planes[0]);
193    }
194    [&lambda[0] / &d, &lambda[1] / &d, &lambda[2] / &d]
195}
196
197/// Oracle cross-check: orient3d with the first argument already materialised
198/// (the exact LPI point). Independent of the homogenisation above — the two
199/// MUST agree, which is what proves the `Λ′`/flip construction is correct.
200pub fn orient3d_exact_pt(a: &V3, b: [f64; 3], c: [f64; 3], d: [f64; 3]) -> Sign {
201    let (b, c, d) = (vec(b), vec(c), vec(d));
202    let ad = sub3(a, &d);
203    let bd = sub3(&b, &d);
204    let cd = sub3(&c, &d);
205    sign_of(&det3(&ad, &bd, &cd))
206}
207
208#[inline]
209fn axis_idx(axis: DropAxis) -> (usize, usize) {
210    match axis {
211        DropAxis::X => (1, 2),
212        DropAxis::Y => (0, 2),
213        DropAxis::Z => (0, 1),
214    }
215}
216
217/// Homogenised indirect orient2d for one implicit point `(λ/d)` against two
218/// explicit points, projected on the two axes remaining after dropping `axis`.
219/// `orient2d = (1/d)·Λ′₂` (the predicate is linear in the single implicit
220/// point), so `sign = assemble_sign(sign(Λ′₂), &[sign(d)])` — the same odd
221/// `sign(d)` flip as the 1-implicit orient3d.
222fn indirect_orient2d(lambda: &V3, d: &BigRational, b: [f64; 3], c: [f64; 3], axis: DropAxis) -> Sign {
223    let (i, j) = axis_idx(axis);
224    let br = vec(b);
225    let cr = vec(c);
226    let li = &lambda[i] - d * &cr[i];
227    let lj = &lambda[j] - d * &cr[j];
228    let lambda_det2 = &li * (&br[j] - &cr[j]) - &lj * (&br[i] - &cr[i]);
229    super::assemble_sign(sign_of(&lambda_det2), &[sign_of(d)])
230}
231
232/// Exact `orient2d(p1=LPI, b, c)` (b,c explicit), projected after `axis`.
233pub fn lpi_orient2d(l: &Lpi, b: [f64; 3], c: [f64; 3], axis: DropAxis) -> Sign {
234    let (lambda, d) = lpi_lambda(l);
235    indirect_orient2d(&lambda, &d, b, c, axis)
236}
237
238/// Exact `orient2d(p1=TPI, b, c)` (b,c explicit), projected after `axis`.
239pub fn tpi_orient2d(t: &Tpi, b: [f64; 3], c: [f64; 3], axis: DropAxis) -> Sign {
240    let (lambda, d) = tpi_lambda(t);
241    indirect_orient2d(&lambda, &d, b, c, axis)
242}
243
244/// Oracle cross-check: orient2d with the first arg already materialised.
245pub fn orient2d_exact_pt(a: &V3, b: [f64; 3], c: [f64; 3], axis: DropAxis) -> Sign {
246    let (i, j) = axis_idx(axis);
247    let (br, cr) = (vec(b), vec(c));
248    let det = (&a[i] - &cr[i]) * (&br[j] - &cr[j]) - (&a[j] - &cr[j]) * (&br[i] - &cr[i]);
249    sign_of(&det)
250}
251
252/// Exact sign of `(proj_u(l1) − proj_u(l2))` where `proj_u(X)=X·u` — i.e. order
253/// two LPI points along direction `u`. `proj = (λ·u)/d`, so the sign is
254/// `assemble_sign(sign((λ1·u)·d2 − (λ2·u)·d1), &[sign(d1), sign(d2)])`.
255///
256/// `u` need only be APPROXIMATELY along the points' shared line L: for points on
257/// L, `proj_u(p1)−proj_u(p2) = (s1−s2)(L_dir·u)`, so as long as `L_dir·u > 0` the
258/// exact sign equals the true 1D order along L regardless of `u`'s rounding.
259pub fn lpi_compare_along(l1: &Lpi, l2: &Lpi, u: [f64; 3]) -> Sign {
260    let (lam1, d1) = lpi_lambda(l1);
261    let (lam2, d2) = lpi_lambda(l2);
262    let ur = vec(u);
263    let dot1 = &lam1[0] * &ur[0] + &lam1[1] * &ur[1] + &lam1[2] * &ur[2];
264    let dot2 = &lam2[0] * &ur[0] + &lam2[1] * &ur[1] + &lam2[2] * &ur[2];
265    let num = &dot1 * &d2 - &dot2 * &d1;
266    super::assemble_sign(sign_of(&num), &[sign_of(&d1), sign_of(&d2)])
267}
268
269/// λ/d of an implicit point (Lpi or Tpi). Callers dispatch — never `Explicit`.
270pub(crate) fn lambda_of(p: &ImplicitPoint) -> (V3, BigRational) {
271    match p {
272        ImplicitPoint::Lpi(l) => lpi_lambda(l),
273        ImplicitPoint::Tpi(t) => tpi_lambda(t),
274        ImplicitPoint::Explicit(_) => unreachable!("lambda_of: Explicit point"),
275    }
276}
277
278/// λ/d of ANY point: an `Explicit` coordinate is `(λ=coord, d=1)`. Inline here so
279/// `cmp_along` never routes an `Explicit` through `lambda_of`'s `unreachable!`.
280fn lambda_or_explicit(p: &ImplicitPoint) -> (V3, BigRational) {
281    match p {
282        ImplicitPoint::Explicit(e) => (vec(*e), r(1.0)),
283        _ => lambda_of(p),
284    }
285}
286
287/// Exact sign of `(a − b)·u` — order two points along direction `u`, over ANY
288/// Explicit/Lpi/Tpi mix (generalises [`lpi_compare_along`]). With `a=λa/da`,
289/// `b=λb/db`: `(a−b)·u = ((λa·u)·db − (λb·u)·da)/(da·db)`.
290pub fn cmp_along(a: &ImplicitPoint, b: &ImplicitPoint, u: [f64; 3]) -> Sign {
291    let (la, da) = lambda_or_explicit(a);
292    let (lb, db) = lambda_or_explicit(b);
293    let ur = vec(u);
294    let dot_a = &la[0] * &ur[0] + &la[1] * &ur[1] + &la[2] * &ur[2];
295    let dot_b = &lb[0] * &ur[0] + &lb[1] * &ur[1] + &lb[2] * &ur[2];
296    let num = &dot_a * &db - &dot_b * &da;
297    super::assemble_sign(sign_of(&num), &[sign_of(&da), sign_of(&db)])
298}
299
300/// Exact materialised coordinates of any point (for the oracle).
301pub(crate) fn point_of(p: &ImplicitPoint) -> V3 {
302    match p {
303        ImplicitPoint::Lpi(l) => lpi_point(l),
304        ImplicitPoint::Tpi(t) => tpi_point(t),
305        ImplicitPoint::Explicit(e) => vec(*e),
306    }
307}
308
309/// orient2d on three already-materialised points (oracle), projected by `axis`.
310// Used only by the exact-arithmetic oracle in unit tests.
311#[cfg_attr(not(test), allow(dead_code))]
312pub(crate) fn orient2d_pts(a: &V3, b: &V3, c: &V3, axis: DropAxis) -> Sign {
313    sign_of(&tri_area2(a, b, c, axis))
314}
315
316/// Exact twice-signed-area of a projected triangle (for coverage checks).
317// Used only by the exact-arithmetic oracle in unit tests.
318#[cfg_attr(not(test), allow(dead_code))]
319pub(crate) fn tri_area2(a: &V3, b: &V3, c: &V3, axis: DropAxis) -> BigRational {
320    let (i, j) = axis_idx(axis);
321    (&a[i] - &c[i]) * (&b[j] - &c[j]) - (&a[j] - &c[j]) * (&b[i] - &c[i])
322}
323
324/// orient2d with TWO implicit points (a,b) and one explicit (c), projected after
325/// `axis`. `orient2d = Λ′/(d1·d2)` with
326/// `Λ′ = (λ1_i−d1·c_i)(λ2_j−d2·c_j) − (λ1_j−d1·c_j)(λ2_i−d2·c_i)`; both
327/// denominators are odd → `den_signs = [sign(d1), sign(d2)]`.
328pub fn orient2d_2i(a: &ImplicitPoint, b: &ImplicitPoint, c: [f64; 3], axis: DropAxis) -> Sign {
329    let (i, j) = axis_idx(axis);
330    let (lam1, d1) = lambda_of(a);
331    let (lam2, d2) = lambda_of(b);
332    let cr = vec(c);
333    let a_i = &lam1[i] - &d1 * &cr[i];
334    let a_j = &lam1[j] - &d1 * &cr[j];
335    let b_i = &lam2[i] - &d2 * &cr[i];
336    let b_j = &lam2[j] - &d2 * &cr[j];
337    let det = &a_i * &b_j - &a_j * &b_i;
338    super::assemble_sign(sign_of(&det), &[sign_of(&d1), sign_of(&d2)])
339}
340
341/// orient2d with THREE implicit points (a,b,c), projected after `axis`, based on
342/// `a`: `Λ′ = (d1·λ2_i−d2·λ1_i)(d1·λ3_j−d3·λ1_j) − (d1·λ2_j−d2·λ1_j)(d1·λ3_i−d3·λ1_i)`.
343/// `D′ = d1²·d2·d3`, so the squared `d1` is dropped → `den_signs = [sign(d2), sign(d3)]`.
344pub fn orient2d_3i(a: &ImplicitPoint, b: &ImplicitPoint, c: &ImplicitPoint, axis: DropAxis) -> Sign {
345    let (i, j) = axis_idx(axis);
346    let (lam1, d1) = lambda_of(a);
347    let (lam2, d2) = lambda_of(b);
348    let (lam3, d3) = lambda_of(c);
349    let u_i = &d1 * &lam2[i] - &d2 * &lam1[i];
350    let u_j = &d1 * &lam2[j] - &d2 * &lam1[j];
351    let v_i = &d1 * &lam3[i] - &d3 * &lam1[i];
352    let v_j = &d1 * &lam3[j] - &d3 * &lam1[j];
353    let det = &u_i * &v_j - &u_j * &v_i;
354    super::assemble_sign(sign_of(&det), &[sign_of(&d2), sign_of(&d3)])
355}
356
357/// Exact sign of `a[k] − b[k]` (coordinate `k`) over any explicit/implicit mix.
358fn cmp_axis(a: &ImplicitPoint, b: &ImplicitPoint, k: usize) -> Sign {
359    use ImplicitPoint::Explicit;
360    match (a, b) {
361        (Explicit(ae), Explicit(be)) => sign_of(&(r(ae[k]) - r(be[k]))),
362        (_, Explicit(be)) => {
363            // a implicit: (λa_k − da·b_k)/da
364            let (lam, d) = lambda_of(a);
365            let bk = r(be[k]);
366            super::assemble_sign(sign_of(&(&lam[k] - &d * &bk)), &[sign_of(&d)])
367        }
368        (Explicit(ae), _) => {
369            // b implicit: (a_k·db − λb_k)/db
370            let (lam, d) = lambda_of(b);
371            let ak = r(ae[k]);
372            super::assemble_sign(sign_of(&(&ak * &d - &lam[k])), &[sign_of(&d)])
373        }
374        (_, _) => {
375            // both implicit: (λa_k·db − λb_k·da)/(da·db)
376            let (la, da) = lambda_of(a);
377            let (lb, db) = lambda_of(b);
378            super::assemble_sign(sign_of(&(&la[k] * &db - &lb[k] * &da)), &[sign_of(&da), sign_of(&db)])
379        }
380    }
381}
382
383/// Exact lexicographic total order on points (x, then y, then z), over any mix
384/// of `Explicit`/`Lpi`/`Tpi`. `Zero` ⇔ the two points are EXACTLY coincident —
385/// this is the interner's symbolic vertex-identity test (no float weld). A
386/// strict total order: antisymmetric, transitive (proven by property test).
387pub fn cmp_lex(a: &ImplicitPoint, b: &ImplicitPoint) -> Sign {
388    for k in 0..3 {
389        let s = cmp_axis(a, b, k);
390        if s != Sign::Zero {
391            return s;
392        }
393    }
394    Sign::Zero
395}
396
397#[cfg(test)]
398#[path = "rational_tests.rs"]
399mod rational_tests;