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}