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