Skip to main content

straight_skeleton/
predicates.rs

1//! Exact geometric predicates, in `i32`.
2//!
3//! # Why the coordinate cap exists
4//!
5//! [`orient2d`] multiplies two coordinate *differences* and subtracts:
6//!
7//! ```text
8//!     (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)
9//! ```
10//!
11//! so the width it needs is set by the largest difference `d`, as `2 * d^2`.
12//! That single expression is what fixes the crate's coordinate range:
13//!
14//! | coordinates | largest `d` | `2 * d^2` | in `i32`? |
15//! |---|---|---|---|
16//! | full `i16`, `-32768..=32767` | 65_535 | 8_589_672_450 | **overflows**, and wraps to the *wrong side* |
17//! | capped, `-16384..=16383` | 32_767 | 2_147_352_578 | fits, with 131_069 to spare |
18//!
19//! Giving up one bit of range therefore buys **exact** predicates in `i32`: no
20//! epsilon, no rounding, no overflow, for every input a [`Polygon`] will
21//! accept. The tests in this module pin down both halves of that table —
22//! including real triples where `i32` at the full range, and `f32` at any
23//! range, each get the answer wrong.
24//!
25//! `f32` is not an option here at any range: its mantissa holds 24 bits, and
26//! these products need up to 31.
27//!
28//! [`Polygon`]: crate::Polygon
29
30use crate::Point;
31
32/// Which side of a directed line a point falls on.
33///
34/// Returned by [`orient2d`].
35#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
36pub enum Orientation {
37    /// `c` lies strictly left of the directed line `a -> b`
38    /// (a counter-clockwise turn).
39    CounterClockwise,
40    /// `c` lies exactly on the line through `a` and `b`.
41    Collinear,
42    /// `c` lies strictly right of the directed line `a -> b`
43    /// (a clockwise turn).
44    Clockwise,
45}
46
47/// Twice the signed area of triangle `(a, b, c)`, computed exactly in `i32`.
48///
49/// Positive when `a -> b -> c` turns counter-clockwise, negative when it turns
50/// clockwise, and zero exactly when the three points are collinear.
51///
52/// Exact — no error term, no epsilon — for every point within the
53/// [coordinate cap](crate::Point::MAX_COORD), which is precisely the range
54/// where it cannot overflow. See the [module docs](self) for the arithmetic.
55///
56/// # Panics
57///
58/// In debug builds, if a coordinate is outside the cap, where the result would
59/// silently wrap. [`Polygon`] rejects such points, so this cannot be reached
60/// through the normal API.
61///
62/// [`Polygon`]: crate::Polygon
63///
64/// # Examples
65///
66/// ```
67/// use straight_skeleton::predicates::signed_area2;
68/// use straight_skeleton::Point;
69///
70/// let a = Point::new(0, 0);
71/// let b = Point::new(4, 0);
72/// // The unit triangle has area 1/2, so twice its area is 1.
73/// assert_eq!(signed_area2(Point::new(0, 0), Point::new(1, 0), Point::new(0, 1)), 1);
74/// // Counter-clockwise is positive, clockwise negative, collinear zero.
75/// assert!(signed_area2(a, b, Point::new(0, 3)) > 0);
76/// assert!(signed_area2(a, b, Point::new(0, -3)) < 0);
77/// assert_eq!(signed_area2(a, b, Point::new(9, 0)), 0);
78/// ```
79#[inline]
80pub fn signed_area2(a: Point, b: Point, c: Point) -> i32 {
81    debug_assert!(
82        a.in_range() && b.in_range() && c.in_range(),
83        "signed_area2 is only exact within the coordinate cap"
84    );
85    let abx = b.x as i32 - a.x as i32;
86    let aby = b.y as i32 - a.y as i32;
87    let acx = c.x as i32 - a.x as i32;
88    let acy = c.y as i32 - a.y as i32;
89    abx * acy - aby * acx
90}
91
92/// Exact orientation of the point triple `(a, b, c)`.
93///
94/// This is the sign of [`signed_area2`], named for readability at call sites.
95///
96/// # Examples
97///
98/// ```
99/// use straight_skeleton::predicates::{orient2d, Orientation};
100/// use straight_skeleton::Point;
101///
102/// let a = Point::new(0, 0);
103/// let b = Point::new(4, 0);
104/// assert_eq!(orient2d(a, b, Point::new(2, 1)), Orientation::CounterClockwise);
105/// assert_eq!(orient2d(a, b, Point::new(2, -1)), Orientation::Clockwise);
106/// assert_eq!(orient2d(a, b, Point::new(2, 0)), Orientation::Collinear);
107/// ```
108#[inline]
109pub fn orient2d(a: Point, b: Point, c: Point) -> Orientation {
110    match signed_area2(a, b, c) {
111        d if d > 0 => Orientation::CounterClockwise,
112        d if d < 0 => Orientation::Clockwise,
113        _ => Orientation::Collinear,
114    }
115}
116
117/// Twice the signed area enclosed by a closed ring, computed exactly.
118///
119/// Positive for a counter-clockwise ring, negative for a clockwise one, and
120/// zero for a ring enclosing no area (all vertices collinear, or a ring that
121/// doubles back on itself exactly).
122///
123/// # Why this one is `i64`
124///
125/// Unlike [`orient2d`], this *sums* triangles, and a ring that doubles back can
126/// wind around a region more than once, so the running total is bounded by the
127/// vertex count rather than by the coordinate box. One bit of range cannot fix
128/// that; only a wider accumulator can.
129///
130/// It is a fair exception to make, because this is validation — it runs once,
131/// on the host, when a [`Polygon`] is built, and never during the simulation.
132/// The skeleton itself uses only `i32` and `f32`.
133///
134/// [`Polygon`]: crate::Polygon
135///
136/// # Examples
137///
138/// ```
139/// use straight_skeleton::predicates::ring_area2;
140/// use straight_skeleton::Point;
141///
142/// let ccw = [Point::new(0, 0), Point::new(10, 0), Point::new(10, 10), Point::new(0, 10)];
143/// assert_eq!(ring_area2(&ccw), 200); // twice the 10x10 area
144///
145/// let mut cw = ccw;
146/// cw.reverse();
147/// assert_eq!(ring_area2(&cw), -200);
148/// ```
149pub fn ring_area2(ring: &[Point]) -> i64 {
150    if ring.len() < 3 {
151        return 0;
152    }
153    // The shoelace formula about the ring's first vertex. Anchoring at a real
154    // vertex, rather than at the origin, keeps every individual term inside the
155    // exact i32 range that `signed_area2` needs.
156    let origin = ring[0];
157    let mut acc: i64 = 0;
158    for w in ring.windows(2) {
159        acc += signed_area2(origin, w[0], w[1]) as i64;
160    }
161    acc
162}
163
164/// Whether a ring winds counter-clockwise (encloses positive area).
165///
166/// # Examples
167///
168/// ```
169/// use straight_skeleton::predicates::is_ccw;
170/// use straight_skeleton::Point;
171///
172/// let ring = [Point::new(0, 0), Point::new(4, 0), Point::new(4, 4), Point::new(0, 4)];
173/// assert!(is_ccw(&ring));
174/// ```
175#[inline]
176pub fn is_ccw(ring: &[Point]) -> bool {
177    ring_area2(ring) > 0
178}
179
180/// Whether closed segments `a1-a2` and `b1-b2` properly cross.
181///
182/// "Properly" means they intersect at a single point interior to both segments.
183/// Segments that merely touch at an endpoint, or that overlap collinearly, are
184/// **not** proper crossings and return `false`. This is exactly the test needed
185/// for detecting self-intersecting rings, where shared endpoints between
186/// consecutive edges are expected and legal.
187#[inline]
188pub fn segments_properly_cross(a1: Point, a2: Point, b1: Point, b2: Point) -> bool {
189    let d1 = orient2d(a1, a2, b1);
190    let d2 = orient2d(a1, a2, b2);
191    let d3 = orient2d(b1, b2, a1);
192    let d4 = orient2d(b1, b2, a2);
193
194    // A strict crossing requires each segment to straddle the other's line.
195    // Any Collinear result means a touch or an overlap, not a proper crossing.
196    d1 != d2
197        && d3 != d4
198        && d1 != Orientation::Collinear
199        && d2 != Orientation::Collinear
200        && d3 != Orientation::Collinear
201        && d4 != Orientation::Collinear
202}
203
204/// Whether point `p` lies on the closed segment `a-b`.
205#[inline]
206pub fn point_on_segment(p: Point, a: Point, b: Point) -> bool {
207    if orient2d(a, b, p) != Orientation::Collinear {
208        return false;
209    }
210    // Collinear: p is on the segment iff it is inside the bounding box.
211    p.x >= a.x.min(b.x) && p.x <= a.x.max(b.x) && p.y >= a.y.min(b.y) && p.y <= a.y.max(b.y)
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn orientation_basics() {
220        let a = Point::new(0, 0);
221        let b = Point::new(1, 0);
222        assert_eq!(
223            orient2d(a, b, Point::new(0, 1)),
224            Orientation::CounterClockwise
225        );
226        assert_eq!(orient2d(a, b, Point::new(0, -1)), Orientation::Clockwise);
227        assert_eq!(orient2d(a, b, Point::new(2, 0)), Orientation::Collinear);
228    }
229
230    #[test]
231    fn orientation_is_antisymmetric_under_swap() {
232        let a = Point::new(-5, 3);
233        let b = Point::new(7, -2);
234        let c = Point::new(1, 9);
235        assert_eq!(signed_area2(a, b, c), -signed_area2(a, c, b));
236        assert_eq!(signed_area2(a, b, c), signed_area2(b, c, a));
237        assert_eq!(signed_area2(a, b, c), signed_area2(c, a, b));
238    }
239
240    /// The cap is exactly as wide as `i32` allows, and no wider: the largest
241    /// triangle it admits lands just inside the type.
242    #[test]
243    fn orientation_is_exact_at_the_coordinate_cap() {
244        let a = Point::new(Point::MIN_COORD, Point::MIN_COORD);
245        let b = Point::new(Point::MAX_COORD, Point::MIN_COORD);
246        let c = Point::new(Point::MIN_COORD, Point::MAX_COORD);
247
248        // Twice the area of the largest right triangle that fits: 32767^2.
249        assert_eq!(signed_area2(a, b, c), 32_767 * 32_767);
250        assert_eq!(orient2d(a, b, c), Orientation::CounterClockwise);
251
252        // The worst case is twice that, and it still fits -- which is what
253        // makes the cap exactly one bit, rather than two.
254        assert!(2i64 * 32_767 * 32_767 <= i32::MAX as i64);
255        assert!(
256            2i64 * 65_535 * 65_535 > i32::MAX as i64,
257            "the uncapped range would not fit, which is why the cap exists"
258        );
259    }
260
261    #[test]
262    fn orientation_detects_one_unit_of_non_collinearity_at_full_scale() {
263        let a = Point::new(Point::MIN_COORD, Point::MIN_COORD);
264        let b = Point::new(Point::MAX_COORD, Point::MAX_COORD);
265        // Exactly on the line a->b.
266        assert_eq!(orient2d(a, b, Point::new(0, 0)), Orientation::Collinear);
267        // One unit off the line — must still be detected, not rounded away.
268        assert_eq!(
269            orient2d(a, b, Point::new(0, 1)),
270            Orientation::CounterClockwise
271        );
272        assert_eq!(orient2d(a, b, Point::new(0, -1)), Orientation::Clockwise);
273    }
274
275    /// A real triple, found by search, **inside the coordinate cap**, where the
276    /// naive `f32` determinant collapses a genuine turn into a false
277    /// "collinear".
278    ///
279    /// This is why capping the range does not rescue `f32`, and so why the
280    /// predicate is `i32` instead. `f32` rounds each product by up to `2^6`
281    /// here, so any true determinant below that can vanish entirely — and
282    /// near-collinear triples are exactly what degenerate input looks like.
283    #[test]
284    fn f32_would_report_a_real_turn_as_collinear_even_inside_the_cap() {
285        let (a, b, c) = (
286            Point::new(14176, -12146),
287            Point::new(-9937, 5341),
288            Point::new(4434, -5081),
289        );
290        assert!(
291            a.in_range() && b.in_range() && c.in_range(),
292            "the point is that capping the range does not save f32"
293        );
294
295        // The truth: c is (just barely) left of a->b.
296        assert_eq!(signed_area2(a, b, c), 9);
297        assert_eq!(orient2d(a, b, c), Orientation::CounterClockwise);
298
299        let naive_f32 = {
300            let (ax, ay) = (a.x as f32, a.y as f32);
301            let (bx, by) = (b.x as f32, b.y as f32);
302            let (cx, cy) = (c.x as f32, c.y as f32);
303            (bx - ax) * (cy - ay) - (by - ay) * (cx - ax)
304        };
305        assert_eq!(naive_f32, 0.0, "f32 loses the turn entirely");
306    }
307
308    /// A real triple, found by search, showing why the cap is not optional:
309    /// **beyond** it the `i32` determinant overflows and flips sign, reporting
310    /// a left turn where the truth is a right turn. Silent, and catastrophic
311    /// for any algorithm that branches on it.
312    ///
313    /// These points are outside [`Point::MAX_COORD`], so a [`Polygon`] rejects
314    /// them and [`signed_area2`] is never asked about them. This test is what
315    /// justifies that rejection.
316    ///
317    /// [`Polygon`]: crate::Polygon
318    #[test]
319    fn beyond_the_cap_i32_would_report_the_wrong_side() {
320        let (a, b, c) = (
321            Point::new(21203, -24650),
322            Point::new(-22519, 1049),
323            Point::new(26449, 26335),
324        );
325        assert!(
326            !a.in_range() && !b.in_range() && !c.in_range(),
327            "the whole point is that these are out of range"
328        );
329
330        // The truth, computed in i64.
331        let exact = |p: Point, q: Point, r: Point| -> i64 {
332            (q.x as i64 - p.x as i64) * (r.y as i64 - p.y as i64)
333                - (q.y as i64 - p.y as i64) * (r.x as i64 - p.x as i64)
334        };
335        assert_eq!(exact(a, b, c), -2_363_983_124);
336
337        // The same expression in i32 wraps clean around to positive.
338        let naive_i32 = {
339            let (ax, ay) = (a.x as i32, a.y as i32);
340            let (bx, by) = (b.x as i32, b.y as i32);
341            let (cx, cy) = (c.x as i32, c.y as i32);
342            (bx - ax)
343                .wrapping_mul(cy - ay)
344                .wrapping_sub((by - ay).wrapping_mul(cx - ax))
345        };
346        assert_eq!(naive_i32, 1_930_984_172);
347        assert!(
348            naive_i32 > 0 && exact(a, b, c) < 0,
349            "i32 reports the opposite side once the cap is exceeded"
350        );
351    }
352
353    #[test]
354    fn ring_area_signs_and_magnitude() {
355        let square = [
356            Point::new(0, 0),
357            Point::new(10, 0),
358            Point::new(10, 10),
359            Point::new(0, 10),
360        ];
361        assert_eq!(ring_area2(&square), 200);
362        assert!(is_ccw(&square));
363
364        let mut rev = square;
365        rev.reverse();
366        assert_eq!(ring_area2(&rev), -200);
367        assert!(!is_ccw(&rev));
368    }
369
370    #[test]
371    fn ring_area_is_translation_invariant() {
372        let tri = [Point::new(0, 0), Point::new(30, 0), Point::new(0, 40)];
373        let shifted: [Point; 3] =
374            core::array::from_fn(|i| Point::new(tri[i].x - 1000, tri[i].y + 500));
375        assert_eq!(ring_area2(&tri), ring_area2(&shifted));
376        assert_eq!(ring_area2(&tri), 1200);
377    }
378
379    #[test]
380    fn ring_area_of_degenerate_rings_is_zero() {
381        assert_eq!(ring_area2(&[]), 0);
382        assert_eq!(ring_area2(&[Point::new(1, 1)]), 0);
383        assert_eq!(ring_area2(&[Point::new(1, 1), Point::new(2, 2)]), 0);
384        // Collinear "ring" encloses nothing.
385        assert_eq!(
386            ring_area2(&[Point::new(0, 0), Point::new(5, 0), Point::new(9, 0)]),
387            0
388        );
389    }
390
391    #[test]
392    fn ring_area_does_not_overflow_at_full_extent() {
393        let big = [
394            Point::new(Point::MIN_COORD, Point::MIN_COORD),
395            Point::new(Point::MAX_COORD, Point::MIN_COORD),
396            Point::new(Point::MAX_COORD, Point::MAX_COORD),
397            Point::new(Point::MIN_COORD, Point::MAX_COORD),
398        ];
399        assert_eq!(ring_area2(&big), 2 * 32_767i64 * 32_767i64);
400    }
401
402    #[test]
403    fn proper_crossing_detection() {
404        // A clean X.
405        assert!(segments_properly_cross(
406            Point::new(0, 0),
407            Point::new(10, 10),
408            Point::new(0, 10),
409            Point::new(10, 0),
410        ));
411        // Disjoint.
412        assert!(!segments_properly_cross(
413            Point::new(0, 0),
414            Point::new(1, 1),
415            Point::new(5, 5),
416            Point::new(6, 6),
417        ));
418    }
419
420    #[test]
421    fn touching_endpoints_are_not_proper_crossings() {
422        // Consecutive polygon edges share a vertex; that must stay legal.
423        assert!(!segments_properly_cross(
424            Point::new(0, 0),
425            Point::new(5, 0),
426            Point::new(5, 0),
427            Point::new(5, 5),
428        ));
429        // T-junction: endpoint lands in the other segment's interior.
430        assert!(!segments_properly_cross(
431            Point::new(0, 0),
432            Point::new(10, 0),
433            Point::new(5, 0),
434            Point::new(5, 5),
435        ));
436    }
437
438    #[test]
439    fn collinear_overlap_is_not_a_proper_crossing() {
440        assert!(!segments_properly_cross(
441            Point::new(0, 0),
442            Point::new(10, 0),
443            Point::new(5, 0),
444            Point::new(15, 0),
445        ));
446    }
447
448    #[test]
449    fn point_on_segment_cases() {
450        let a = Point::new(0, 0);
451        let b = Point::new(10, 5);
452        assert!(point_on_segment(Point::new(4, 2), a, b));
453        assert!(point_on_segment(a, a, b), "endpoints count as on-segment");
454        assert!(point_on_segment(b, a, b));
455        // Collinear with the line but beyond the segment.
456        assert!(!point_on_segment(Point::new(20, 10), a, b));
457        assert!(!point_on_segment(Point::new(-2, -1), a, b));
458        // Off the line entirely.
459        assert!(!point_on_segment(Point::new(4, 3), a, b));
460    }
461}