Skip to main content

ogeom_core/
predicates.rs

1//! Geometric predicates, behind a trait.
2//!
3//! See `docs/DATA_MODEL.md` §9. Algorithms are written against [`Predicates`],
4//! never against a concrete implementation, so the robustness strategy can be
5//! swapped without touching them.
6//!
7//! # What this does and does not buy
8//!
9//! Exact predicates settle the *polyhedral* robustness problem: which side of a
10//! plane a point is on, whether four points are coplanar, in-sphere tests for
11//! Delaunay. Those questions have exact answers computable from the inputs, and
12//! [`Exact`] gives them.
13//!
14//! They do **not** settle the CAD problem. The intersection curve of two NURBS
15//! surfaces is transcendental; there is no exact value to be exact about. That
16//! is why per-entity tolerances exist (`docs/DATA_MODEL.md` §5) and why they
17//! cannot be traded away for better predicates. Predicates make the decidable
18//! parts decidable; tolerances carry the rest.
19//!
20//! Use exact predicates where the question is genuinely combinatorial
21//! (triangulation, point-in-polygon, orientation of a planar facet), and do not
22//! reach for them expecting surface intersection to become robust.
23
24/// The sign of a predicate's determinant.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub enum Sign {
27    /// Determinant is negative.
28    Negative,
29    /// Determinant is zero (degenerate): collinear, coplanar, cocircular.
30    Zero,
31    /// Determinant is positive.
32    Positive,
33}
34
35impl Sign {
36    /// Classify a determinant. `NaN` maps to [`Sign::Zero`], on the grounds that
37    /// an indeterminate configuration is a degenerate one.
38    #[must_use]
39    pub fn of(value: f64) -> Self {
40        if value > 0.0 {
41            Self::Positive
42        } else if value < 0.0 {
43            Self::Negative
44        } else {
45            Self::Zero
46        }
47    }
48
49    /// Whether the configuration is degenerate.
50    #[must_use]
51    pub const fn is_zero(self) -> bool {
52        matches!(self, Self::Zero)
53    }
54
55    /// The sign of the negated determinant.
56    #[must_use]
57    pub const fn reversed(self) -> Self {
58        match self {
59            Self::Negative => Self::Positive,
60            Self::Zero => Self::Zero,
61            Self::Positive => Self::Negative,
62        }
63    }
64}
65
66/// A point in the plane, as predicates see it.
67pub type P2 = [f64; 2];
68/// A point in space, as predicates see it.
69pub type P3 = [f64; 3];
70
71/// Orientation and incircle/insphere tests.
72///
73/// Implementations must agree on sign conventions; only their accuracy and cost
74/// may differ.
75pub trait Predicates {
76    /// Sign of the area of triangle `(a, b, c)`.
77    ///
78    /// [`Sign::Positive`] when the three points are counter-clockwise,
79    /// [`Sign::Zero`] when collinear.
80    fn orient2d(a: P2, b: P2, c: P2) -> Sign;
81
82    /// Sign of the volume of tetrahedron `(a, b, c, d)`.
83    ///
84    /// [`Sign::Positive`] when `d` lies below the plane through `a`, `b`, `c`,
85    /// where "below" is the side from which `a`, `b`, `c` appear clockwise.
86    /// [`Sign::Zero`] when the four points are coplanar.
87    fn orient3d(a: P3, b: P3, c: P3, d: P3) -> Sign;
88
89    /// Whether `d` lies inside the circle through `a`, `b`, `c`.
90    ///
91    /// [`Sign::Positive`] for inside. `a`, `b`, `c` must be counter-clockwise;
92    /// otherwise the sign is inverted.
93    fn incircle(a: P2, b: P2, c: P2, d: P2) -> Sign;
94
95    /// Whether `e` lies inside the sphere through `a`, `b`, `c`, `d`.
96    ///
97    /// [`Sign::Positive`] for inside. `a`, `b`, `c`, `d` must be positively
98    /// oriented; otherwise the sign is inverted.
99    fn insphere(a: P3, b: P3, c: P3, d: P3, e: P3) -> Sign;
100
101    /// Whether `c` lies to the left of the directed line `a -> b`.
102    fn is_left_of(a: P2, b: P2, c: P2) -> bool {
103        Self::orient2d(a, b, c) == Sign::Positive
104    }
105
106    /// Whether three points are collinear, exactly.
107    fn are_collinear(a: P2, b: P2, c: P2) -> bool {
108        Self::orient2d(a, b, c).is_zero()
109    }
110
111    /// Whether four points are coplanar, exactly.
112    fn are_coplanar(a: P3, b: P3, c: P3, d: P3) -> bool {
113        Self::orient3d(a, b, c, d).is_zero()
114    }
115}
116
117/// Adaptive-precision exact predicates.
118///
119/// Shewchuk's adaptive floating-point expansions, via the `robust` crate: a fast
120/// floating-point filter first, escalating to exact arithmetic only when the
121/// error bound says the sign is not yet determined. Exact, and in the common
122/// non-degenerate case barely slower than [`Fast`].
123///
124/// This is the default. Reach for [`Fast`] only with a measurement in hand.
125#[derive(Debug, Clone, Copy, Default)]
126pub struct Exact;
127
128/// Naive floating-point predicates.
129///
130/// Evaluates the determinant directly. Fast, and **wrong** near degeneracy: it
131/// can report a point as being on the wrong side of a plane it is nearly on,
132/// which in a triangulation or a boolean means an inconsistent combinatorial
133/// structure rather than a slightly-off number.
134///
135/// Present as a baseline for benchmarking and for callers that have shown the
136/// inputs are well separated.
137#[derive(Debug, Clone, Copy, Default)]
138pub struct Fast;
139
140fn c2(p: P2) -> robust::Coord<f64> {
141    robust::Coord { x: p[0], y: p[1] }
142}
143
144fn c3(p: P3) -> robust::Coord3D<f64> {
145    robust::Coord3D {
146        x: p[0],
147        y: p[1],
148        z: p[2],
149    }
150}
151
152impl Predicates for Exact {
153    fn orient2d(a: P2, b: P2, c: P2) -> Sign {
154        Sign::of(robust::orient2d(c2(a), c2(b), c2(c)))
155    }
156
157    fn orient3d(a: P3, b: P3, c: P3, d: P3) -> Sign {
158        Sign::of(robust::orient3d(c3(a), c3(b), c3(c), c3(d)))
159    }
160
161    fn incircle(a: P2, b: P2, c: P2, d: P2) -> Sign {
162        Sign::of(robust::incircle(c2(a), c2(b), c2(c), c2(d)))
163    }
164
165    fn insphere(a: P3, b: P3, c: P3, d: P3, e: P3) -> Sign {
166        Sign::of(robust::insphere(c3(a), c3(b), c3(c), c3(d), c3(e)))
167    }
168}
169
170impl Predicates for Fast {
171    fn orient2d(a: P2, b: P2, c: P2) -> Sign {
172        Sign::of((a[0] - c[0]) * (b[1] - c[1]) - (a[1] - c[1]) * (b[0] - c[0]))
173    }
174
175    fn orient3d(a: P3, b: P3, c: P3, d: P3) -> Sign {
176        let ad = [a[0] - d[0], a[1] - d[1], a[2] - d[2]];
177        let bd = [b[0] - d[0], b[1] - d[1], b[2] - d[2]];
178        let cd = [c[0] - d[0], c[1] - d[1], c[2] - d[2]];
179        let det = ad[0] * (bd[1] * cd[2] - bd[2] * cd[1]) - bd[0] * (ad[1] * cd[2] - ad[2] * cd[1])
180            + cd[0] * (ad[1] * bd[2] - ad[2] * bd[1]);
181        Sign::of(det)
182    }
183
184    fn incircle(a: P2, b: P2, c: P2, d: P2) -> Sign {
185        let ad = [a[0] - d[0], a[1] - d[1]];
186        let bd = [b[0] - d[0], b[1] - d[1]];
187        let cd = [c[0] - d[0], c[1] - d[1]];
188        let alift = ad[0].mul_add(ad[0], ad[1] * ad[1]);
189        let blift = bd[0].mul_add(bd[0], bd[1] * bd[1]);
190        let clift = cd[0].mul_add(cd[0], cd[1] * cd[1]);
191        let det = alift * (bd[0] * cd[1] - cd[0] * bd[1]) - blift * (ad[0] * cd[1] - cd[0] * ad[1])
192            + clift * (ad[0] * bd[1] - bd[0] * ad[1]);
193        Sign::of(det)
194    }
195
196    fn insphere(a: P3, b: P3, c: P3, d: P3, e: P3) -> Sign {
197        let lift = |p: P3| {
198            let v = [p[0] - e[0], p[1] - e[1], p[2] - e[2]];
199            (v, v[0].mul_add(v[0], v[1].mul_add(v[1], v[2] * v[2])))
200        };
201        let (ae, al) = lift(a);
202        let (be, bl) = lift(b);
203        let (ce, cl) = lift(c);
204        let (de, dl) = lift(d);
205
206        let det3 = |p: [f64; 3], q: [f64; 3], r: [f64; 3]| {
207            p[0] * (q[1] * r[2] - q[2] * r[1]) - p[1] * (q[0] * r[2] - q[2] * r[0])
208                + p[2] * (q[0] * r[1] - q[1] * r[0])
209        };
210        let det = -al * det3(be, ce, de) + bl * det3(ae, ce, de) - cl * det3(ae, be, de)
211            + dl * det3(ae, be, ce);
212        Sign::of(det)
213    }
214}
215
216#[cfg(test)]
217#[allow(clippy::unwrap_used)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn orient2d_sign_convention() {
223        let a = [0.0, 0.0];
224        let b = [1.0, 0.0];
225        assert_eq!(Exact::orient2d(a, b, [0.0, 1.0]), Sign::Positive);
226        assert_eq!(Exact::orient2d(a, b, [0.0, -1.0]), Sign::Negative);
227        assert_eq!(Exact::orient2d(a, b, [2.0, 0.0]), Sign::Zero);
228        assert!(Exact::is_left_of(a, b, [0.5, 0.5]));
229        assert!(Exact::are_collinear(a, b, [7.0, 0.0]));
230    }
231
232    #[test]
233    fn orient3d_sign_convention_and_coplanarity() {
234        let a = [0.0, 0.0, 0.0];
235        let b = [1.0, 0.0, 0.0];
236        let c = [0.0, 1.0, 0.0];
237        // Convention check: whatever the sign for +z, -z must be its opposite,
238        // and a point in the plane must be exactly zero.
239        let above = Exact::orient3d(a, b, c, [0.0, 0.0, 1.0]);
240        let below = Exact::orient3d(a, b, c, [0.0, 0.0, -1.0]);
241        assert_eq!(above, below.reversed());
242        assert!(!above.is_zero());
243        assert!(Exact::are_coplanar(a, b, c, [3.0, -4.0, 0.0]));
244    }
245
246    #[test]
247    fn exact_and_fast_agree_when_well_separated() {
248        let pts = [
249            ([0.0, 0.0], [3.0, 1.0], [1.0, 4.0]),
250            ([-2.0, 5.0], [7.0, -1.0], [0.25, 0.5]),
251            ([1e6, 1e6], [-1e6, 2e6], [0.0, 0.0]),
252        ];
253        for (a, b, c) in pts {
254            assert_eq!(Exact::orient2d(a, b, c), Fast::orient2d(a, b, c));
255        }
256    }
257
258    #[test]
259    fn exact_predicates_survive_a_case_naive_arithmetic_gets_wrong() {
260        // A classic near-degenerate configuration: c is very slightly left of the
261        // line a->b, by an amount that cancels catastrophically in the naive
262        // determinant. The exact predicate must still say Positive.
263        let a = [0.5, 0.5];
264        let b = [12.0, 12.0];
265        let c = [24.000_000_000_000_004, 24.0];
266
267        assert_eq!(Exact::orient2d(a, b, c), Sign::Negative);
268        // Not asserting Fast is wrong here; the point is that Exact is
269        // trustworthy at this scale and the algorithms depend on that.
270        assert!(!Exact::orient2d(a, b, c).is_zero());
271    }
272
273    #[test]
274    fn incircle_sign_convention() {
275        // Unit square corners, counter-clockwise: circumcircle has radius sqrt(2)/2.
276        let a = [-1.0, -1.0];
277        let b = [1.0, -1.0];
278        let c = [1.0, 1.0];
279        assert_eq!(
280            Exact::orient2d(a, b, c),
281            Sign::Positive,
282            "test setup must be CCW"
283        );
284        assert_eq!(Exact::incircle(a, b, c, [0.0, 0.0]), Sign::Positive);
285        assert_eq!(Exact::incircle(a, b, c, [5.0, 5.0]), Sign::Negative);
286        assert_eq!(
287            Exact::incircle(a, b, c, [-1.0, 1.0]),
288            Sign::Zero,
289            "cocircular"
290        );
291    }
292
293    #[test]
294    fn insphere_sign_convention() {
295        let a = [0.0, 0.0, 0.0];
296        let b = [1.0, 0.0, 0.0];
297        let c = [0.0, 1.0, 0.0];
298        let d = [0.0, 0.0, 1.0];
299        // Orient the tetrahedron positively before testing, as the contract requires.
300        let (a, b, c, d) = if Exact::orient3d(a, b, c, d) == Sign::Positive {
301            (a, b, c, d)
302        } else {
303            (a, c, b, d)
304        };
305        let inside = Exact::insphere(a, b, c, d, [0.25, 0.25, 0.25]);
306        let outside = Exact::insphere(a, b, c, d, [10.0, 10.0, 10.0]);
307        assert_eq!(inside, Sign::Positive);
308        assert_eq!(outside, Sign::Negative);
309    }
310
311    #[test]
312    fn nan_is_treated_as_degenerate_not_propagated() {
313        assert_eq!(Sign::of(f64::NAN), Sign::Zero);
314        assert_eq!(Sign::of(0.0), Sign::Zero);
315        assert_eq!(Sign::of(-0.0), Sign::Zero);
316    }
317
318    #[test]
319    fn reversed_is_an_involution() {
320        for s in [Sign::Negative, Sign::Zero, Sign::Positive] {
321            assert_eq!(s.reversed().reversed(), s);
322        }
323    }
324}