Skip to main content

ifc_lite_geometry/kernel/
interval.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//! Directed-rounding f64 interval tier — the predicate cascade's fast path.
6//!
7//! Every op widens the interval OUTWARD (round-to-nearest then ±1 ULP), so the
8//! true real value is always bracketed. A predicate's sign is returned only
9//! when the result interval is strictly one side of zero; a straddling interval
10//! is a genuine near-degeneracy and escalates to the exact (BigRational) tier.
11//! Because the interval can never claim a definite sign it doesn't have, it can
12//! never return a WRONG sign — proven by the soundness test against the oracle.
13//!
14//! `next_up`/`next_down` are integer bit-twiddles (not `f64::next_up`), so the
15//! widening is bit-identical across x86_64/aarch64/wasm — the determinism bar.
16//! No `mul_add`/FMA anywhere (contraction would break the directed rounding).
17
18use super::{DropAxis, ImplicitPoint, Lpi, Sign, Tpi};
19
20#[derive(Clone, Copy, Debug)]
21pub struct RnInterval {
22    pub lo: f64,
23    pub hi: f64,
24}
25
26/// Smallest representable f64 strictly greater than `x` (toward +∞).
27#[inline]
28pub fn next_up(x: f64) -> f64 {
29    if x.is_nan() || x == f64::INFINITY {
30        return x;
31    }
32    if x == 0.0 {
33        return f64::from_bits(1); // +smallest subnormal
34    }
35    let b = x.to_bits();
36    f64::from_bits(if x > 0.0 { b + 1 } else { b - 1 })
37}
38
39/// Largest representable f64 strictly less than `x` (toward −∞).
40#[inline]
41pub fn next_down(x: f64) -> f64 {
42    if x.is_nan() || x == f64::NEG_INFINITY {
43        return x;
44    }
45    if x == 0.0 {
46        return -f64::from_bits(1); // −smallest subnormal
47    }
48    let b = x.to_bits();
49    f64::from_bits(if x > 0.0 { b - 1 } else { b + 1 })
50}
51
52impl RnInterval {
53    #[inline]
54    pub fn point(x: f64) -> Self {
55        Self { lo: x, hi: x }
56    }
57    #[inline]
58    pub fn add(self, o: Self) -> Self {
59        Self { lo: next_down(self.lo + o.lo), hi: next_up(self.hi + o.hi) }
60    }
61    #[inline]
62    pub fn sub(self, o: Self) -> Self {
63        Self { lo: next_down(self.lo - o.hi), hi: next_up(self.hi - o.lo) }
64    }
65    #[inline]
66    pub fn mul(self, o: Self) -> Self {
67        let c = [self.lo * o.lo, self.lo * o.hi, self.hi * o.lo, self.hi * o.hi];
68        let mut mn = c[0];
69        let mut mx = c[0];
70        for &v in &c[1..] {
71            if v < mn {
72                mn = v;
73            }
74            if v > mx {
75                mx = v;
76            }
77        }
78        Self { lo: next_down(mn), hi: next_up(mx) }
79    }
80    /// Definite sign, or `None` if the interval straddles zero (escalate).
81    #[inline]
82    pub fn sign(self) -> Option<Sign> {
83        if self.lo > 0.0 {
84            Some(Sign::Positive)
85        } else if self.hi < 0.0 {
86            Some(Sign::Negative)
87        } else if self.lo == 0.0 && self.hi == 0.0 {
88            Some(Sign::Zero)
89        } else {
90            None
91        }
92    }
93}
94
95type Iv3 = [RnInterval; 3];
96
97#[inline]
98fn ivec(p: [f64; 3]) -> Iv3 {
99    [RnInterval::point(p[0]), RnInterval::point(p[1]), RnInterval::point(p[2])]
100}
101#[inline]
102fn isub(a: &Iv3, b: &Iv3) -> Iv3 {
103    [a[0].sub(b[0]), a[1].sub(b[1]), a[2].sub(b[2])]
104}
105fn idet3(u: &Iv3, v: &Iv3, w: &Iv3) -> RnInterval {
106    u[0]
107        .mul(v[1].mul(w[2]).sub(v[2].mul(w[1])))
108        .add(u[1].mul(v[2].mul(w[0]).sub(v[0].mul(w[2]))))
109        .add(u[2].mul(v[0].mul(w[1]).sub(v[1].mul(w[0]))))
110}
111
112#[inline]
113fn icross(u: &Iv3, v: &Iv3) -> Iv3 {
114    [
115        u[1].mul(v[2]).sub(u[2].mul(v[1])),
116        u[2].mul(v[0]).sub(u[0].mul(v[2])),
117        u[0].mul(v[1]).sub(u[1].mul(v[0])),
118    ]
119}
120
121fn lpi_lambda(l: &Lpi) -> (Iv3, RnInterval) {
122    let p = ivec(l.p);
123    let q = ivec(l.q);
124    let rr = ivec(l.r);
125    let s = ivec(l.s);
126    let t = ivec(l.t);
127    let qp = isub(&q, &p);
128    let sr = isub(&s, &rr);
129    let tr = isub(&t, &rr);
130    let pr = isub(&p, &rr);
131    let d = idet3(&qp, &sr, &tr);
132    let n = idet3(&pr, &sr, &tr);
133    // λ = d·P − n·(Q−P)  (see rational::lpi_lambda — the minus is load-bearing).
134    let lx = d.mul(p[0]).sub(n.mul(qp[0]));
135    let ly = d.mul(p[1]).sub(n.mul(qp[1]));
136    let lz = d.mul(p[2]).sub(n.mul(qp[2]));
137    ([lx, ly, lz], d)
138}
139
140fn tpi_lambda(t: &Tpi) -> (Iv3, RnInterval) {
141    let plane = |pl: &[[f64; 3]; 3]| -> (Iv3, RnInterval) {
142        let a = ivec(pl[0]);
143        let ba = isub(&ivec(pl[1]), &a);
144        let ca = isub(&ivec(pl[2]), &a);
145        let n = icross(&ba, &ca);
146        let off = n[0].mul(a[0]).add(n[1].mul(a[1])).add(n[2].mul(a[2]));
147        (n, off)
148    };
149    let (n1, c1) = plane(&t.planes[0]);
150    let (n2, c2) = plane(&t.planes[1]);
151    let (n3, c3) = plane(&t.planes[2]);
152    let d = idet3(&n1, &n2, &n3);
153    let ns = [n1, n2, n3];
154    let cs = [c1, c2, c3];
155    let cramer = |k: usize| -> RnInterval {
156        let mut rows = [ns[0], ns[1], ns[2]];
157        for (row, ci) in rows.iter_mut().zip(cs.iter()) {
158            row[k] = *ci;
159        }
160        idet3(&rows[0], &rows[1], &rows[2])
161    };
162    ([cramer(0), cramer(1), cramer(2)], d)
163}
164
165/// Interval-tier indirect orient3d for one implicit point `(λ/d)`. `None` ⇒ the
166/// `Λ′` or `d` interval straddles zero ⇒ escalate to the exact tier.
167fn indirect_orient3d(
168    lambda: &Iv3,
169    d: RnInterval,
170    p2: [f64; 3],
171    p3: [f64; 3],
172    p4: [f64; 3],
173) -> Option<Sign> {
174    let p4i = ivec(p4);
175    let row1 = [
176        lambda[0].sub(d.mul(p4i[0])),
177        lambda[1].sub(d.mul(p4i[1])),
178        lambda[2].sub(d.mul(p4i[2])),
179    ];
180    let row2 = isub(&ivec(p2), &p4i);
181    let row3 = isub(&ivec(p3), &p4i);
182    let lambda_det = idet3(&row1, &row2, &row3);
183    let sd = d.sign()?;
184    let sld = lambda_det.sign()?;
185    Some(super::assemble_sign(sld, &[sd]))
186}
187
188pub fn lpi_orient3d(l: &Lpi, p2: [f64; 3], p3: [f64; 3], p4: [f64; 3]) -> Option<Sign> {
189    let (lambda, d) = lpi_lambda(l);
190    indirect_orient3d(&lambda, d, p2, p3, p4)
191}
192
193pub fn tpi_orient3d(t: &Tpi, p2: [f64; 3], p3: [f64; 3], p4: [f64; 3]) -> Option<Sign> {
194    let (lambda, d) = tpi_lambda(t);
195    indirect_orient3d(&lambda, d, p2, p3, p4)
196}
197
198#[inline]
199fn axis_idx(axis: DropAxis) -> (usize, usize) {
200    match axis {
201        DropAxis::X => (1, 2),
202        DropAxis::Y => (0, 2),
203        DropAxis::Z => (0, 1),
204    }
205}
206
207/// Interval-tier indirect orient2d for one implicit point `(λ/d)`. `None` ⇒ the
208/// `Λ′₂` or `d` interval straddles zero ⇒ escalate to the exact tier.
209fn indirect_orient2d(
210    lambda: &Iv3,
211    d: RnInterval,
212    b: [f64; 3],
213    c: [f64; 3],
214    axis: DropAxis,
215) -> Option<Sign> {
216    let (i, j) = axis_idx(axis);
217    let br = ivec(b);
218    let cr = ivec(c);
219    let li = lambda[i].sub(d.mul(cr[i]));
220    let lj = lambda[j].sub(d.mul(cr[j]));
221    let lambda_det2 = li.mul(br[j].sub(cr[j])).sub(lj.mul(br[i].sub(cr[i])));
222    let sd = d.sign()?;
223    let sld = lambda_det2.sign()?;
224    Some(super::assemble_sign(sld, &[sd]))
225}
226
227pub fn lpi_orient2d(l: &Lpi, b: [f64; 3], c: [f64; 3], axis: DropAxis) -> Option<Sign> {
228    let (lambda, d) = lpi_lambda(l);
229    indirect_orient2d(&lambda, d, b, c, axis)
230}
231
232pub fn tpi_orient2d(t: &Tpi, b: [f64; 3], c: [f64; 3], axis: DropAxis) -> Option<Sign> {
233    let (lambda, d) = tpi_lambda(t);
234    indirect_orient2d(&lambda, d, b, c, axis)
235}
236
237/// λ/d (interval) of an implicit point. Callers dispatch — never `Explicit`.
238fn ilambda_of(p: &ImplicitPoint) -> (Iv3, RnInterval) {
239    match p {
240        ImplicitPoint::Lpi(l) => lpi_lambda(l),
241        ImplicitPoint::Tpi(t) => tpi_lambda(t),
242        ImplicitPoint::Explicit(_) => unreachable!("ilambda_of: Explicit"),
243    }
244}
245
246/// Cached homogeneous interval lambda `(λ, d)` for ANY point — the f64-interval
247/// analogue of `fixed::Lam`. Computed ONCE per interned point (the interner caches
248/// it), so the hot re-triangulation predicates can run a directed-rounding f64
249/// determinant straight from it — NO per-call LPI/TPI recompute, and crucially NO
250/// wide-integer (I512) arithmetic, which wasm emulates ~hundreds× slower than
251/// native. An `Explicit` point is `(coords, d=1)`.
252pub(super) type IvLam = ([RnInterval; 3], RnInterval);
253
254#[inline]
255pub(super) fn ilambda_cached(p: &ImplicitPoint) -> IvLam {
256    match p {
257        ImplicitPoint::Lpi(l) => lpi_lambda(l),
258        ImplicitPoint::Tpi(t) => tpi_lambda(t),
259        ImplicitPoint::Explicit(e) => (ivec(*e), RnInterval::point(1.0)),
260    }
261}
262
263/// 2-D orientation of three points from their CACHED interval lambdas — the exact
264/// mirror of `fixed::orient2d_from_lam`'s determinant, in directed-rounding f64.
265/// Returns `Some(sign)` ONLY when the interval determinant is strictly one side of
266/// zero ⇒ that sign equals the exact sign (outward rounding, no FMA) and is
267/// bit-identical native==wasm; a straddle returns `None` so the caller runs the
268/// exact I512/BigRational tiers, unchanged.
269pub(super) fn orient2d_from_lam_iv(a: &IvLam, b: &IvLam, c: &IvLam, axis: DropAxis) -> Option<Sign> {
270    let (i, j) = axis_idx(axis);
271    let (l1, d1) = (&a.0, a.1);
272    let (l2, d2) = (&b.0, b.1);
273    let (l3, d3) = (&c.0, c.1);
274    let u_i = d1.mul(l2[i]).sub(d2.mul(l1[i]));
275    let u_j = d1.mul(l2[j]).sub(d2.mul(l1[j]));
276    let v_i = d1.mul(l3[i]).sub(d3.mul(l1[i]));
277    let v_j = d1.mul(l3[j]).sub(d3.mul(l1[j]));
278    let det = u_i.mul(v_j).sub(u_j.mul(v_i));
279    Some(super::assemble_sign(det.sign()?, &[d2.sign()?, d3.sign()?]))
280}
281
282/// Interval tier of `cmp_along` — the f64 mirror of `fixed::cmp_along`: the sign
283/// of `(a−b)·u` over the homogeneous lambdas. `None` on a zero-straddle ⇒ the
284/// caller runs the exact I512/BigRational tiers. Closes the latent slow path where
285/// EVERY tri-tri `cmp_along` skipped the float filter and went straight to bignum.
286pub fn cmp_along(a: &ImplicitPoint, b: &ImplicitPoint, u: [f64; 3]) -> Option<Sign> {
287    let (la, da) = ilambda_cached(a);
288    let (lb, db) = ilambda_cached(b);
289    let ur = ivec(u);
290    let dot_a = la[0].mul(ur[0]).add(la[1].mul(ur[1])).add(la[2].mul(ur[2]));
291    let dot_b = lb[0].mul(ur[0]).add(lb[1].mul(ur[1])).add(lb[2].mul(ur[2]));
292    let num = dot_a.mul(db).sub(dot_b.mul(da));
293    Some(super::assemble_sign(num.sign()?, &[da.sign()?, db.sign()?]))
294}
295
296/// Lexicographic compare of two points from their CACHED interval lambdas — the
297/// f64 mirror of `fixed::cmp_lex_from_lam`. `None` as soon as any axis straddles.
298pub(super) fn cmp_lex_from_lam_iv(a: &IvLam, b: &IvLam) -> Option<Sign> {
299    let (la, da) = (&a.0, a.1);
300    let (lb, db) = (&b.0, b.1);
301    for k in 0..3 {
302        let s = la[k].mul(db).sub(lb[k].mul(da));
303        let sg = super::assemble_sign(s.sign()?, &[da.sign()?, db.sign()?]);
304        if sg != Sign::Zero {
305            return Some(sg);
306        }
307    }
308    Some(Sign::Zero)
309}
310
311/// Interval tier of `rational::orient2d_2i` — `None` on a zero-straddle.
312pub fn orient2d_2i(a: &ImplicitPoint, b: &ImplicitPoint, c: [f64; 3], axis: DropAxis) -> Option<Sign> {
313    let (i, j) = axis_idx(axis);
314    let (lam1, d1) = ilambda_of(a);
315    let (lam2, d2) = ilambda_of(b);
316    let cr = ivec(c);
317    let a_i = lam1[i].sub(d1.mul(cr[i]));
318    let a_j = lam1[j].sub(d1.mul(cr[j]));
319    let b_i = lam2[i].sub(d2.mul(cr[i]));
320    let b_j = lam2[j].sub(d2.mul(cr[j]));
321    let det = a_i.mul(b_j).sub(a_j.mul(b_i));
322    let sd1 = d1.sign()?;
323    let sd2 = d2.sign()?;
324    Some(super::assemble_sign(det.sign()?, &[sd1, sd2]))
325}
326
327/// Interval tier of `rational::orient2d_3i` — `None` on a zero-straddle.
328pub fn orient2d_3i(a: &ImplicitPoint, b: &ImplicitPoint, c: &ImplicitPoint, axis: DropAxis) -> Option<Sign> {
329    let (i, j) = axis_idx(axis);
330    let (lam1, d1) = ilambda_of(a);
331    let (lam2, d2) = ilambda_of(b);
332    let (lam3, d3) = ilambda_of(c);
333    let u_i = d1.mul(lam2[i]).sub(d2.mul(lam1[i]));
334    let u_j = d1.mul(lam2[j]).sub(d2.mul(lam1[j]));
335    let v_i = d1.mul(lam3[i]).sub(d3.mul(lam1[i]));
336    let v_j = d1.mul(lam3[j]).sub(d3.mul(lam1[j]));
337    let det = u_i.mul(v_j).sub(u_j.mul(v_i));
338    let sd2 = d2.sign()?;
339    let sd3 = d3.sign()?;
340    Some(super::assemble_sign(det.sign()?, &[sd2, sd3]))
341}
342
343/// Interval tier of one `cmp_lex` axis comparison — `None` on a zero-straddle.
344fn icmp_axis(a: &ImplicitPoint, b: &ImplicitPoint, k: usize) -> Option<Sign> {
345    use ImplicitPoint::Explicit;
346    match (a, b) {
347        (Explicit(ae), Explicit(be)) => Some(if ae[k] < be[k] {
348            Sign::Negative
349        } else if ae[k] > be[k] {
350            Sign::Positive
351        } else {
352            Sign::Zero
353        }),
354        (_, Explicit(be)) => {
355            let (lam, d) = ilambda_of(a);
356            let num = lam[k].sub(d.mul(RnInterval::point(be[k])));
357            Some(super::assemble_sign(num.sign()?, &[d.sign()?]))
358        }
359        (Explicit(ae), _) => {
360            let (lam, d) = ilambda_of(b);
361            let num = RnInterval::point(ae[k]).mul(d).sub(lam[k]);
362            Some(super::assemble_sign(num.sign()?, &[d.sign()?]))
363        }
364        (_, _) => {
365            let (la, da) = ilambda_of(a);
366            let (lb, db) = ilambda_of(b);
367            let num = la[k].mul(db).sub(lb[k].mul(da));
368            Some(super::assemble_sign(num.sign()?, &[da.sign()?, db.sign()?]))
369        }
370    }
371}
372
373/// Interval tier of `rational::cmp_lex`. `None` if the deciding axis straddles
374/// zero (escalate to exact). A definite-Zero axis advances to the next.
375pub fn cmp_lex(a: &ImplicitPoint, b: &ImplicitPoint) -> Option<Sign> {
376    for k in 0..3 {
377        match icmp_axis(a, b, k)? {
378            Sign::Zero => continue,
379            s => return Some(s),
380        }
381    }
382    Some(Sign::Zero)
383}