Skip to main content

manifold_rust/robust/exact/
predicates.rs

1// robust/exact/predicates.rs — Fully exact predicates and geometric
2// constructions on rational points. Ground truth for the robust boolean
3// engine: robust/exact/filtered.rs escalates here whenever its float
4// filters cannot certify a sign, and the intersection code builds all new
5// vertices through the constructions at the bottom of this file.
6//
7// Orientation conventions (documented once, used everywhere in src/robust):
8//   orient2d(a,b,c)   = sign of cross(b-a, c-a); Pos = a,b,c counterclockwise.
9//   orient3d(a,b,c,d) = sign of dot(cross(b-a, c-a), d-a); Pos = d on the
10//                       side of plane(a,b,c) that its CCW normal points to.
11//   incircle(a,b,c,d) with a,b,c CCW: Pos = d strictly inside the circle
12//                       through a,b,c. (For CW a,b,c the sign flips.)
13
14use num_bigint::BigInt;
15use num_rational::BigRational;
16use num_traits::{Signed, Zero};
17
18use super::rational::{R2, R3};
19use super::Sign;
20
21// ─── Exact predicates ────────────────────────────────────────────────────────
22//
23// Predicate signs are computed in pure BigInt arithmetic: each point is
24// homogenized once ((x, y) = (X/W, Y/W) with W > 0 — num-rational keeps
25// denominators positive), and the determinant is scaled through by positive
26// denominator products, which preserves its sign. This avoids BigRational's
27// gcd normalization on every intermediate operation — the dominant cost of
28// the original rational formulation (the CDT's incircle calls on constructed
29// intersection points made it ~80% of robust-boolean wall time).
30
31/// (X, Y, W): x = X/W, y = Y/W with W > 0.
32#[inline]
33fn homog2(p: &R2) -> (BigInt, BigInt, BigInt) {
34    let (xn, xd) = (p.x.numer(), p.x.denom());
35    let (yn, yd) = (p.y.numer(), p.y.denom());
36    (xn * yd, yn * xd, xd * yd)
37}
38
39/// (X, Y, Z, W): coordinates over one positive common denominator.
40#[inline]
41fn homog3(p: &R3) -> (BigInt, BigInt, BigInt, BigInt) {
42    let (xn, xd) = (p.x.numer(), p.x.denom());
43    let (yn, yd) = (p.y.numer(), p.y.denom());
44    let (zn, zd) = (p.z.numer(), p.z.denom());
45    let yz = yd * zd;
46    (xn * &yz, yn * (xd * zd), zn * (xd * yd), xd * yz)
47}
48
49/// Cached homogenization of a 2D point: (X, Y, W), x = X/W, W > 0. Hot
50/// loops that test one point against many (the arrangement's segment sweep)
51/// homogenize each point once and reuse it across every predicate call.
52#[derive(Clone, Debug)]
53pub struct Homog2(pub BigInt, pub BigInt, pub BigInt);
54
55pub fn homog2_of(p: &R2) -> Homog2 {
56    let (x, y, w) = homog2(p);
57    Homog2(x, y, w)
58}
59
60/// `orient2d_r` over pre-homogenized points — identical sign, no repeated
61/// denominator work.
62pub fn orient2d_h(a: &Homog2, b: &Homog2, c: &Homog2) -> Sign {
63    let ux = &b.0 * &a.2 - &a.0 * &b.2;
64    let uy = &b.1 * &a.2 - &a.1 * &b.2;
65    let vx = &c.0 * &a.2 - &a.0 * &c.2;
66    let vy = &c.1 * &a.2 - &a.1 * &c.2;
67    sign_of_int(&(ux * vy - uy * vx))
68}
69
70/// `incircle_r` over pre-homogenized points — identical sign (same row
71/// scaling argument), computed without re-clearing any denominators.
72pub fn incircle_h(a: &Homog2, b: &Homog2, c: &Homog2, d: &Homog2) -> Sign {
73    let row = |p: &Homog2| -> (BigInt, BigInt, BigInt) {
74        let nx = &p.0 * &d.2 - &d.0 * &p.2;
75        let ny = &p.1 * &d.2 - &d.1 * &p.2;
76        let s = &p.2 * &d.2;
77        let lift = &nx * &nx + &ny * &ny;
78        (nx * &s, ny * &s, lift)
79    };
80    let (ux, uy, ul) = row(a);
81    let (vx, vy, vl) = row(b);
82    let (wx, wy, wl) = row(c);
83    let det = ul * (&vx * &wy - &vy * &wx)
84        + vl * (&wx * &uy - &wy * &ux)
85        + wl * (&ux * &vy - &uy * &vx);
86    sign_of_int(&det)
87}
88
89/// `point_in_tri_2d` over pre-homogenized points.
90pub fn point_in_tri_2d_h(p: &Homog2, a: &Homog2, b: &Homog2, c: &Homog2) -> TriLoc {
91    let orient = orient2d_h(a, b, c);
92    if orient == Sign::Zero {
93        return TriLoc::Outside;
94    }
95    let normalize = |s: Sign| if orient == Sign::Pos { s } else { s.flip() };
96    let s0 = normalize(orient2d_h(a, b, p));
97    let s1 = normalize(orient2d_h(b, c, p));
98    let s2 = normalize(orient2d_h(c, a, p));
99    if s0 == Sign::Neg || s1 == Sign::Neg || s2 == Sign::Neg {
100        return TriLoc::Outside;
101    }
102    match (s0 == Sign::Zero, s1 == Sign::Zero, s2 == Sign::Zero) {
103        (false, false, false) => TriLoc::Inside,
104        (true, false, false) => TriLoc::OnEdge(0),
105        (false, true, false) => TriLoc::OnEdge(1),
106        (false, false, true) => TriLoc::OnEdge(2),
107        (true, false, true) => TriLoc::OnVertex(0),
108        (true, true, false) => TriLoc::OnVertex(1),
109        (false, true, true) => TriLoc::OnVertex(2),
110        (true, true, true) => TriLoc::Outside,
111    }
112}
113
114#[inline]
115fn sign_of_int(v: &BigInt) -> Sign {
116    if v.is_positive() {
117        Sign::Pos
118    } else if v.is_negative() {
119        Sign::Neg
120    } else {
121        Sign::Zero
122    }
123}
124
125/// Sign of cross(b-a, c-a). Pos ⇔ a,b,c wind counterclockwise.
126pub fn orient2d_r(a: &R2, b: &R2, c: &R2) -> Sign {
127    let (ax, ay, aw) = homog2(a);
128    let (bx, by, bw) = homog2(b);
129    let (cx, cy, cw) = homog2(c);
130    // det(b-a, c-a) · Wa²WbWc = (BxWa−AxWb)(CyWa−AyWc) − (ByWa−AyWb)(CxWa−AxWc)
131    let ux = &bx * &aw - &ax * &bw;
132    let uy = &by * &aw - &ay * &bw;
133    let vx = &cx * &aw - &ax * &cw;
134    let vy = &cy * &aw - &ay * &cw;
135    sign_of_int(&(ux * vy - uy * vx))
136}
137
138/// Sign of dot(cross(b-a, c-a), d-a). Pos ⇔ d lies on the CCW-normal side
139/// of the plane through a, b, c; Zero ⇔ the four points are coplanar.
140pub fn orient3d_r(a: &R3, b: &R3, c: &R3, d: &R3) -> Sign {
141    let (ax, ay, az, aw) = homog3(a);
142    let (bx, by, bz, bw) = homog3(b);
143    let (cx, cy, cz, cw) = homog3(c);
144    let (dx, dy, dz, dw) = homog3(d);
145    // Each difference row is scaled by the positive factor Wa·W_row; the
146    // triple product then carries a positive overall scale.
147    let ux = &bx * &aw - &ax * &bw;
148    let uy = &by * &aw - &ay * &bw;
149    let uz = &bz * &aw - &az * &bw;
150    let vx = &cx * &aw - &ax * &cw;
151    let vy = &cy * &aw - &ay * &cw;
152    let vz = &cz * &aw - &az * &cw;
153    let wx = &dx * &aw - &ax * &dw;
154    let wy = &dy * &aw - &ay * &dw;
155    let wz = &dz * &aw - &az * &dw;
156    let det = (&uy * &vz - &uz * &vy) * wx
157        + (&uz * &vx - &ux * &vz) * wy
158        + (&ux * &vy - &uy * &vx) * wz;
159    sign_of_int(&det)
160}
161
162/// Incircle test. With a,b,c counterclockwise: Pos ⇔ d strictly inside the
163/// circumcircle of (a,b,c). Computed as the standard 3×3 determinant of
164/// coordinates lifted onto the paraboloid, with rows differenced against d.
165pub fn incircle_r(a: &R2, b: &R2, c: &R2, d: &R2) -> Sign {
166    let (dx, dy, dw) = homog2(d);
167    // Row i (i = a,b,c): (xi−xd, yi−yd) over the positive denominator WiWd.
168    // Scaling row i by (WiWd)² keeps the lift column polynomial:
169    //   Ui = (XiWd−XdWi)·WiWd,  Vi = (YiWd−YdWi)·WiWd,
170    //   Li = (XiWd−XdWi)² + (YiWd−YdWi)².
171    let row = |p: &R2| -> (BigInt, BigInt, BigInt) {
172        let (px, py, pw) = homog2(p);
173        let nx = &px * &dw - &dx * &pw;
174        let ny = &py * &dw - &dy * &pw;
175        let s = pw * &dw;
176        let lift = &nx * &nx + &ny * &ny;
177        (nx * &s, ny * &s, lift)
178    };
179    let (ux, uy, ul) = row(a);
180    let (vx, vy, vl) = row(b);
181    let (wx, wy, wl) = row(c);
182    let det = ul * (&vx * &wy - &vy * &wx)
183        + vl * (&wx * &uy - &wy * &ux)
184        + wl * (&ux * &vy - &uy * &vx);
185    sign_of_int(&det)
186}
187
188/// Exact: p collinear with (a,b) and within the closed segment [a,b].
189/// Same integer-only strategy as the sign predicates above — the registry
190/// sweeps in robust/intersection_graph.rs call this in a tight loop.
191pub fn point_on_segment_r(p: &R3, a: &R3, b: &R3) -> bool {
192    let (px, py, pz, pw) = homog3(p);
193    let (ax, ay, az, aw) = homog3(a);
194    let (bx, by, bz, bw) = homog3(b);
195    // ap scaled by the positive PwAw; d = b−a scaled by the positive AwBw.
196    let apx = &px * &aw - &ax * &pw;
197    let apy = &py * &aw - &ay * &pw;
198    let apz = &pz * &aw - &az * &pw;
199    let dx = &bx * &aw - &ax * &bw;
200    let dy = &by * &aw - &ay * &bw;
201    let dz = &bz * &aw - &az * &bw;
202    // Collinearity: cross(ap, d) = 0 (positive scales cannot zero a component).
203    if !(&apy * &dz - &apz * &dy).is_zero()
204        || !(&apz * &dx - &apx * &dz).is_zero()
205        || !(&apx * &dy - &apy * &dx).is_zero()
206    {
207        return false;
208    }
209    // 0 ≤ ap·d and ap·d ≤ d·d, cleared of their (positive) denominators:
210    //   ap·d = S1 / (PwAw·AwBw),  d·d = S2 / (AwBw)²
211    //   S1 ≥ 0   and   S1·AwBw ≤ S2·PwAw.
212    let s1 = &apx * &dx + &apy * &dy + &apz * &dz;
213    if s1.is_negative() {
214        return false;
215    }
216    let s2 = &dx * &dx + &dy * &dy + &dz * &dz;
217    s1 * (&aw * &bw) <= s2 * (pw * aw)
218}
219
220/// (a−o)·u as an unreduced fraction (numerator, positive denominator) —
221/// integer-only, no gcd normalization. For sign tests and cross-multiplied
222/// comparisons (e.g. the radial ring sort in robust/classify.rs) the
223/// unreduced form is exactly as good as the canonical one and much cheaper
224/// to produce.
225pub fn dot_diff_raw(a: &R3, o: &R3, u: &R3) -> (BigInt, BigInt) {
226    let (ax, ay, az, aw) = homog3(a);
227    let (ox, oy, oz, ow) = homog3(o);
228    let (ux, uy, uz, uw) = homog3(u);
229    // (a−o) scaled by the positive AwOw; dot with u adds a Uw denominator.
230    let num = (&ax * &ow - &ox * &aw) * ux
231        + (&ay * &ow - &oy * &aw) * uy
232        + (&az * &ow - &oz * &aw) * uz;
233    (num, aw * ow * uw)
234}
235
236/// Triangle normal as a denominator-cleared integer vector: cross(b−a, c−a)
237/// scaled by the positive Aw²BwCw. Direction (and zero-ness) match
238/// `tri_normal_r`; use where only the normal's direction matters.
239pub fn tri_normal_int(a: &R3, b: &R3, c: &R3) -> [BigInt; 3] {
240    let (ax, ay, az, aw) = homog3(a);
241    let (bx, by, bz, bw) = homog3(b);
242    let (cx, cy, cz, cw) = homog3(c);
243    let ux = &bx * &aw - &ax * &bw;
244    let uy = &by * &aw - &ay * &bw;
245    let uz = &bz * &aw - &az * &bw;
246    let vx = &cx * &aw - &ax * &cw;
247    let vy = &cy * &aw - &ay * &cw;
248    let vz = &cz * &aw - &az * &cw;
249    [
250        &uy * &vz - &uz * &vy,
251        &uz * &vx - &ux * &vz,
252        &ux * &vy - &uy * &vx,
253    ]
254}
255
256/// d·p as an unreduced fraction (numerator, positive denominator), for an
257/// integer direction `d` and rational point `p`. Comparable across points by
258/// cross-multiplication — the segment-interval overlap in robust/tri_tri.rs
259/// orders plane-crossing points along the intersection line with this.
260pub fn dot_point_raw(d: &[BigInt; 3], p: &R3) -> (BigInt, BigInt) {
261    let (px, py, pz, pw) = homog3(p);
262    (&d[0] * &px + &d[1] * &py + &d[2] * &pz, pw)
263}
264
265/// Unnormalized CCW normal of triangle (a,b,c): cross(b-a, c-a). Zero vector
266/// ⇔ the triangle is degenerate.
267pub fn tri_normal_r(a: &R3, b: &R3, c: &R3) -> R3 {
268    b.sub(a).cross(&c.sub(a))
269}
270
271/// Where a point lies relative to a non-degenerate triangle, in 2D.
272#[derive(Clone, Copy, Debug, PartialEq, Eq)]
273pub enum TriLoc {
274    Inside,
275    /// On the open edge i, where edge i runs from vertex i to vertex i+1 (mod 3).
276    OnEdge(u8),
277    /// Coincident with vertex i.
278    OnVertex(u8),
279    Outside,
280}
281
282/// Locate point p relative to triangle (a,b,c). Works for either winding;
283/// a degenerate (zero-area) triangle reports every point as Outside, which
284/// is consistent with the pipeline dropping degenerate triangles up front.
285pub fn point_in_tri_2d(p: &R2, a: &R2, b: &R2, c: &R2) -> TriLoc {
286    let orient = orient2d_r(a, b, c);
287    if orient == Sign::Zero {
288        return TriLoc::Outside;
289    }
290    // Normalize so the triangle reads as CCW.
291    let normalize = |s: Sign| if orient == Sign::Pos { s } else { s.flip() };
292    let s0 = normalize(orient2d_r(a, b, p)); // edge 0: a→b
293    let s1 = normalize(orient2d_r(b, c, p)); // edge 1: b→c
294    let s2 = normalize(orient2d_r(c, a, p)); // edge 2: c→a
295    if s0 == Sign::Neg || s1 == Sign::Neg || s2 == Sign::Neg {
296        return TriLoc::Outside;
297    }
298    match (s0 == Sign::Zero, s1 == Sign::Zero, s2 == Sign::Zero) {
299        (false, false, false) => TriLoc::Inside,
300        (true, false, false) => TriLoc::OnEdge(0),
301        (false, true, false) => TriLoc::OnEdge(1),
302        (false, false, true) => TriLoc::OnEdge(2),
303        (true, false, true) => TriLoc::OnVertex(0),  // a: on edges c→a and a→b
304        (true, true, false) => TriLoc::OnVertex(1),  // b
305        (false, true, true) => TriLoc::OnVertex(2),  // c
306        (true, true, true) => TriLoc::Outside,       // impossible for orient != 0
307    }
308}
309
310// ─── Exact constructions ─────────────────────────────────────────────────────
311
312/// Intersection of the line through p,q with the plane of triangle (a,b,c).
313///
314/// Returns None when the segment direction is parallel to the plane (no
315/// unique intersection point — the coplanar-overlap machinery handles that
316/// case separately). The caller decides whether the parameter t lies inside
317/// [0,1]; use the orient3d signs of p and q for that, not float comparisons.
318pub fn line_plane_intersect(p: &R3, q: &R3, a: &R3, b: &R3, c: &R3) -> Option<R3> {
319    // Integer-only formulation (same exact point as the rational one): the
320    // plane normal enters both numerator and denominator of t, so any
321    // positive common scale on it cancels — compute it as an integer cross
322    // of denominator-cleared edge vectors and never normalize intermediates.
323    // Each output coordinate reduces exactly once in BigRational::new.
324    let (px, py, pz, pw) = homog3(p);
325    let (qx, qy, qz, qw) = homog3(q);
326    let (ax, ay, az, aw) = homog3(a);
327    let (bx, by, bz, bw) = homog3(b);
328    let (cx, cy, cz, cw) = homog3(c);
329
330    // (b−a)·AwBw and (c−a)·AwCw; their cross is the normal up to Aw²BwCw > 0.
331    let ux = &bx * &aw - &ax * &bw;
332    let uy = &by * &aw - &ay * &bw;
333    let uz = &bz * &aw - &az * &bw;
334    let vx = &cx * &aw - &ax * &cw;
335    let vy = &cy * &aw - &ay * &cw;
336    let vz = &cz * &aw - &az * &cw;
337    let nx = &uy * &vz - &uz * &vy;
338    let ny = &uz * &vx - &ux * &vz;
339    let nz = &ux * &vy - &uy * &vx;
340
341    // dir = q−p scaled by PwQw; e = a−p scaled by PwAw.
342    let dx = &qx * &pw - &px * &qw;
343    let dy = &qy * &pw - &py * &qw;
344    let dz = &qz * &pw - &pz * &qw;
345    let n_dot_d = &nx * &dx + &ny * &dy + &nz * &dz;
346    if n_dot_d.is_zero() {
347        return None;
348    }
349    let ex = &ax * &pw - &px * &aw;
350    let ey = &ay * &pw - &py * &aw;
351    let ez = &az * &pw - &pz * &aw;
352    let n_dot_e = &nx * &ex + &ny * &ey + &nz * &ez;
353
354    // t = (n·e)·Qw / (Aw·(n·d));  x_i = (P_i·Qw·T_d + D_i·T_n) / (Pw·Qw·T_d)
355    let t_n = &n_dot_e * &qw;
356    let t_d = &aw * &n_dot_d;
357    let den = &pw * &qw * &t_d;
358    let coord = |pi: &BigInt, di: &BigInt| -> BigRational {
359        BigRational::new(pi * &qw * &t_d + di * &t_n, den.clone())
360    };
361    Some(R3::new(coord(&px, &dx), coord(&py, &dy), coord(&pz, &dz)))
362}
363
364/// Intersection point of the 2D lines through (a,b) and (c,d). None when the
365/// lines are parallel (including collinear — overlap is handled by the
366/// caller's collinear branch).
367pub fn line_line_intersect_2d(a: &R2, b: &R2, c: &R2, d: &R2) -> Option<R2> {
368    // Integer-only (same exact point as the rational formulation): with
369    // homogenized points, x = a + t·(b−a) where
370    //   t = N·Bw / (Dn·Cw),
371    //   N  = cross(c−a, d−c)·AwCw·CwDw,   Dn = cross(b−a, d−c)·AwBw·CwDw,
372    // and each output coordinate reduces exactly once in BigRational::new.
373    let (ax, ay, aw) = homog2(a);
374    let (bx, by, bw) = homog2(b);
375    let (cx, cy, cw) = homog2(c);
376    let (dx, dy, dw) = homog2(d);
377
378    let abx = &bx * &aw - &ax * &bw;
379    let aby = &by * &aw - &ay * &bw;
380    let cdx = &dx * &cw - &cx * &dw;
381    let cdy = &dy * &cw - &cy * &dw;
382    let dn = &abx * &cdy - &aby * &cdx;
383    if dn.is_zero() {
384        return None;
385    }
386    let cax = &cx * &aw - &ax * &cw;
387    let cay = &cy * &aw - &ay * &cw;
388    let n = &cax * &cdy - &cay * &cdx;
389
390    // x_i = (A_i·Dn·Cw + N·ab_i) / (Aw·Cw·Dn)
391    let den = &aw * &cw * &dn;
392    let dn_cw = &dn * &cw;
393    let x = BigRational::new(&ax * &dn_cw + &n * &abx, den.clone());
394    let y = BigRational::new(&ay * &dn_cw + &n * &aby, den);
395    Some(R2::new(x, y))
396}
397
398/// Inverse of `R3::project_drop`: rebuild the dropped coordinate from the
399/// plane through `a` with (unnormalized, rational) normal `n`, whose `axis`
400/// component must be nonzero. Integer-only: the reconstructed coordinate is
401/// one `BigRational::new` (a single gcd); the carried coordinates are clones
402/// of the projection's already-canonical rationals.
403pub fn lift_to_plane(p: &R2, axis: usize, a: &R3, n: &R3) -> R3 {
404    let (nx, ny, nz, nw) = homog3(n);
405    let (ax, ay, az, aw) = homog3(a);
406    let (px, py, pw) = homog2(p);
407    // S = (n·a)·NwAw; dropped = (n·a − n_i·p_i − n_j·p_j) / n_k
408    //   = (S·Pw − Aw·(N_i·P_i + N_j·P_j)) / (Aw·Pw·N_k).
409    let s = &nx * &ax + &ny * &ay + &nz * &az;
410    let rebuild = |ni: &BigInt, nj: &BigInt, nk: &BigInt| -> BigRational {
411        BigRational::new(
412            &s * &pw - &aw * (ni * &px + nj * &py),
413            &aw * &pw * nk,
414        )
415    };
416    let _ = nw; // cancels: both S and the subtracted terms carry 1/Nw
417    match axis {
418        0 => {
419            let x = rebuild(&ny, &nz, &nx);
420            R3::new(x, p.x.clone(), p.y.clone())
421        }
422        1 => {
423            let y = rebuild(&nz, &nx, &ny);
424            R3::new(p.y.clone(), y, p.x.clone())
425        }
426        2 => {
427            let z = rebuild(&nx, &ny, &nz);
428            R3::new(p.x.clone(), p.y.clone(), z)
429        }
430        _ => unreachable!("axis must be 0, 1, or 2"),
431    }
432}
433
434/// Parameter of point x on segment (p,q) along the dominant axis of the
435/// segment direction — exact, in [0,1] iff x lies within the segment. The
436/// caller guarantees x is on the line through p and q and p != q.
437pub fn segment_param(p: &R3, q: &R3, x: &R3) -> BigRational {
438    let d = q.sub(p);
439    let (num, den) = if !d.x.is_zero() {
440        (&x.x - &p.x, d.x)
441    } else if !d.y.is_zero() {
442        (&x.y - &p.y, d.y)
443    } else {
444        (&x.z - &p.z, d.z)
445    };
446    debug_assert!(!den.is_zero(), "segment_param requires p != q");
447    num / den
448}