Skip to main content

u_geometry/
robust.rs

1//! Numerically robust geometric predicates.
2//!
3//! Uses Shewchuk's adaptive precision floating-point arithmetic
4//! via the `robust` crate to guarantee correct results even for
5//! near-degenerate geometric configurations.
6//!
7//! # Background
8//!
9//! Standard floating-point arithmetic can produce incorrect results
10//! for geometric predicates when points are nearly collinear or cocircular.
11//! This module provides robust alternatives.
12//!
13//! # References
14//!
15//! - Shewchuk (1997), "Adaptive Precision Floating-Point Arithmetic
16//!   and Fast Robust Predicates for Computational Geometry"
17//!   <https://www.cs.cmu.edu/~quake/robust.html>
18
19use robust::{orient2d as shewchuk_orient2d, Coord};
20
21/// Result of an orientation test.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub enum Orientation {
25    /// Points form a counter-clockwise (left) turn.
26    CounterClockwise,
27    /// Points form a clockwise (right) turn.
28    Clockwise,
29    /// Points are collinear.
30    Collinear,
31}
32
33impl Orientation {
34    /// Returns true if counter-clockwise.
35    #[inline]
36    pub fn is_ccw(self) -> bool {
37        matches!(self, Self::CounterClockwise)
38    }
39
40    /// Returns true if clockwise.
41    #[inline]
42    pub fn is_cw(self) -> bool {
43        matches!(self, Self::Clockwise)
44    }
45
46    /// Returns true if collinear.
47    #[inline]
48    pub fn is_collinear(self) -> bool {
49        matches!(self, Self::Collinear)
50    }
51}
52
53/// Determines the orientation of three points using exact arithmetic.
54///
55/// Uses Shewchuk's adaptive precision algorithm. The result is
56/// mathematically exact regardless of floating-point rounding.
57///
58/// # Returns
59///
60/// - `CounterClockwise` if `pc` is to the left of the directed line `pa → pb`
61/// - `Clockwise` if to the right
62/// - `Collinear` if on the line
63///
64/// # Complexity
65/// O(1) amortized (fast path resolves ~95% of cases)
66///
67/// # Example
68///
69/// ```
70/// use u_geometry::robust::{orient2d, Orientation};
71///
72/// let a = (0.0, 0.0);
73/// let b = (1.0, 0.0);
74/// let c = (0.5, 1.0);
75/// assert_eq!(orient2d(a, b, c), Orientation::CounterClockwise);
76/// ```
77#[inline]
78pub fn orient2d(pa: (f64, f64), pb: (f64, f64), pc: (f64, f64)) -> Orientation {
79    let result = shewchuk_orient2d(
80        Coord { x: pa.0, y: pa.1 },
81        Coord { x: pb.0, y: pb.1 },
82        Coord { x: pc.0, y: pc.1 },
83    );
84
85    if result > 0.0 {
86        Orientation::CounterClockwise
87    } else if result < 0.0 {
88        Orientation::Clockwise
89    } else {
90        Orientation::Collinear
91    }
92}
93
94/// Returns the raw orientation determinant (twice the signed area).
95///
96/// Positive if CCW, negative if CW, zero if collinear.
97/// The magnitude equals twice the signed area of the triangle.
98#[inline]
99pub fn orient2d_raw(pa: (f64, f64), pb: (f64, f64), pc: (f64, f64)) -> f64 {
100    shewchuk_orient2d(
101        Coord { x: pa.0, y: pa.1 },
102        Coord { x: pb.0, y: pb.1 },
103        Coord { x: pc.0, y: pc.1 },
104    )
105}
106
107/// Epsilon for floating-point filter.
108const FILTER_EPSILON: f64 = 1e-12;
109
110/// Fast orientation test with exact fallback.
111///
112/// Uses a floating-point filter: attempts fast approximate calculation
113/// first, falls back to exact arithmetic only when the result is
114/// ambiguous (~5% of cases).
115#[inline]
116pub fn orient2d_filtered(pa: (f64, f64), pb: (f64, f64), pc: (f64, f64)) -> Orientation {
117    let acx = pa.0 - pc.0;
118    let bcx = pb.0 - pc.0;
119    let acy = pa.1 - pc.1;
120    let bcy = pb.1 - pc.1;
121
122    let det = acx * bcy - acy * bcx;
123    let det_sum = (acx * bcy).abs() + (acy * bcx).abs();
124
125    if det.abs() > FILTER_EPSILON * det_sum {
126        return if det > 0.0 {
127            Orientation::CounterClockwise
128        } else {
129            Orientation::Clockwise
130        };
131    }
132
133    orient2d(pa, pb, pc)
134}
135
136/// Checks if a point lies strictly inside a triangle.
137///
138/// Uses robust orientation tests. Returns false for points
139/// on the boundary (edges or vertices).
140///
141/// # Complexity
142/// O(1)
143pub fn point_in_triangle(p: (f64, f64), a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> bool {
144    let o1 = orient2d(a, b, p);
145    let o2 = orient2d(b, c, p);
146    let o3 = orient2d(c, a, p);
147
148    (o1 == Orientation::CounterClockwise
149        && o2 == Orientation::CounterClockwise
150        && o3 == Orientation::CounterClockwise)
151        || (o1 == Orientation::Clockwise
152            && o2 == Orientation::Clockwise
153            && o3 == Orientation::Clockwise)
154}
155
156/// Checks if a point lies inside or on the boundary of a triangle.
157///
158/// # Complexity
159/// O(1)
160pub fn point_in_triangle_inclusive(
161    p: (f64, f64),
162    a: (f64, f64),
163    b: (f64, f64),
164    c: (f64, f64),
165) -> bool {
166    let o1 = orient2d(a, b, p);
167    let o2 = orient2d(b, c, p);
168    let o3 = orient2d(c, a, p);
169
170    let has_ccw = o1.is_ccw() || o2.is_ccw() || o3.is_ccw();
171    let has_cw = o1.is_cw() || o2.is_cw() || o3.is_cw();
172
173    !(has_ccw && has_cw)
174}
175
176/// Checks if a polygon is convex using robust orientation tests.
177///
178/// # Complexity
179/// O(n) where n is the number of vertices
180pub fn is_convex(polygon: &[(f64, f64)]) -> bool {
181    let n = polygon.len();
182    if n < 3 {
183        return false;
184    }
185
186    let mut expected: Option<Orientation> = None;
187
188    for i in 0..n {
189        let p0 = polygon[i];
190        let p1 = polygon[(i + 1) % n];
191        let p2 = polygon[(i + 2) % n];
192
193        let o = orient2d(p0, p1, p2);
194        if o.is_collinear() {
195            continue;
196        }
197
198        match expected {
199            None => expected = Some(o),
200            Some(e) if e != o => return false,
201            _ => {}
202        }
203    }
204
205    true
206}
207
208/// Checks if a polygon has counter-clockwise winding order.
209///
210/// Uses the lowest-leftmost vertex (guaranteed to be convex)
211/// to determine winding.
212///
213/// # Complexity
214/// O(n)
215pub fn is_ccw(polygon: &[(f64, f64)]) -> bool {
216    if polygon.len() < 3 {
217        return false;
218    }
219
220    let mut min_idx = 0;
221    for (i, &(x, y)) in polygon.iter().enumerate() {
222        let (mx, my) = polygon[min_idx];
223        if y < my || (y == my && x < mx) {
224            min_idx = i;
225        }
226    }
227
228    let n = polygon.len();
229    let prev = polygon[(min_idx + n - 1) % n];
230    let curr = polygon[min_idx];
231    let next = polygon[(min_idx + 1) % n];
232
233    orient2d(prev, curr, next).is_ccw()
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn test_orient2d_ccw() {
242        assert_eq!(
243            orient2d((0.0, 0.0), (1.0, 0.0), (0.5, 1.0)),
244            Orientation::CounterClockwise
245        );
246    }
247
248    #[test]
249    fn test_orient2d_cw() {
250        assert_eq!(
251            orient2d((0.0, 0.0), (0.5, 1.0), (1.0, 0.0)),
252            Orientation::Clockwise
253        );
254    }
255
256    #[test]
257    fn test_orient2d_collinear() {
258        assert_eq!(
259            orient2d((0.0, 0.0), (1.0, 1.0), (2.0, 2.0)),
260            Orientation::Collinear
261        );
262    }
263
264    #[test]
265    fn test_orient2d_filtered_matches_exact() {
266        let cases = [
267            ((0.0, 0.0), (10.0, 0.0), (5.0, 10.0)),
268            ((0.0, 0.0), (1.0, 1.0), (2.0, 2.0)),
269            ((0.0, 0.0), (1.0, 0.0), (0.5, -1.0)),
270        ];
271        for (a, b, c) in &cases {
272            assert_eq!(orient2d(*a, *b, *c), orient2d_filtered(*a, *b, *c));
273        }
274    }
275
276    #[test]
277    fn test_point_in_triangle_inside() {
278        let a = (0.0, 0.0);
279        let b = (10.0, 0.0);
280        let c = (5.0, 10.0);
281        assert!(point_in_triangle((5.0, 3.0), a, b, c));
282    }
283
284    #[test]
285    fn test_point_in_triangle_outside() {
286        let a = (0.0, 0.0);
287        let b = (10.0, 0.0);
288        let c = (5.0, 10.0);
289        assert!(!point_in_triangle((20.0, 5.0), a, b, c));
290    }
291
292    #[test]
293    fn test_point_in_triangle_on_edge() {
294        let a = (0.0, 0.0);
295        let b = (10.0, 0.0);
296        let c = (5.0, 10.0);
297        // Strict: on edge is NOT inside
298        assert!(!point_in_triangle((5.0, 0.0), a, b, c));
299    }
300
301    #[test]
302    fn test_point_in_triangle_inclusive() {
303        let a = (0.0, 0.0);
304        let b = (10.0, 0.0);
305        let c = (5.0, 10.0);
306        assert!(point_in_triangle_inclusive((5.0, 3.0), a, b, c));
307        assert!(point_in_triangle_inclusive((5.0, 0.0), a, b, c)); // on edge
308        assert!(point_in_triangle_inclusive((0.0, 0.0), a, b, c)); // on vertex
309        assert!(!point_in_triangle_inclusive((20.0, 5.0), a, b, c));
310    }
311
312    #[test]
313    fn test_is_convex_square() {
314        let square = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
315        assert!(is_convex(&square));
316    }
317
318    #[test]
319    fn test_is_convex_l_shape() {
320        let l_shape = [
321            (0.0, 0.0),
322            (10.0, 0.0),
323            (10.0, 5.0),
324            (5.0, 5.0),
325            (5.0, 10.0),
326            (0.0, 10.0),
327        ];
328        assert!(!is_convex(&l_shape));
329    }
330
331    #[test]
332    fn test_is_convex_triangle() {
333        assert!(is_convex(&[(0.0, 0.0), (10.0, 0.0), (5.0, 10.0)]));
334    }
335
336    #[test]
337    fn test_is_convex_degenerate() {
338        assert!(!is_convex(&[(0.0, 0.0), (1.0, 0.0)])); // too few
339    }
340
341    #[test]
342    fn test_is_ccw_square() {
343        assert!(is_ccw(&[
344            (0.0, 0.0),
345            (10.0, 0.0),
346            (10.0, 10.0),
347            (0.0, 10.0)
348        ]));
349        assert!(!is_ccw(&[
350            (0.0, 0.0),
351            (0.0, 10.0),
352            (10.0, 10.0),
353            (10.0, 0.0)
354        ]));
355    }
356
357    #[test]
358    fn test_orientation_methods() {
359        assert!(Orientation::CounterClockwise.is_ccw());
360        assert!(!Orientation::CounterClockwise.is_cw());
361        assert!(Orientation::Clockwise.is_cw());
362        assert!(Orientation::Collinear.is_collinear());
363    }
364
365    #[test]
366    fn test_orient2d_extreme_coords() {
367        // Large coordinates
368        assert_eq!(
369            orient2d((1e10, 1e10), (1e10 + 1.0, 1e10), (1e10 + 0.5, 1e10 + 1.0)),
370            Orientation::CounterClockwise
371        );
372    }
373
374    #[test]
375    fn test_degenerate_triangle() {
376        // Collinear points: not inside
377        assert!(!point_in_triangle(
378            (5.0, 0.0),
379            (0.0, 0.0),
380            (5.0, 0.0),
381            (10.0, 0.0)
382        ));
383    }
384}