u_nesting_core/robust.rs
1//! Robust geometric predicates for numerical stability.
2//!
3//! This module provides numerically robust implementations of geometric predicates
4//! using Shewchuk's adaptive precision floating-point arithmetic.
5//!
6//! ## Background
7//!
8//! Standard floating-point arithmetic can produce incorrect results for geometric
9//! predicates when points are nearly collinear or cocircular. This module provides
10//! robust alternatives that guarantee correct results.
11//!
12//! ## References
13//!
14//! - Shewchuk, J.R. (1997). "Adaptive Precision Floating-Point Arithmetic and
15//! Fast Robust Predicates for Computational Geometry"
16//! - <https://www.cs.cmu.edu/~quake/robust.html>
17//!
18//! ## Example
19//!
20//! ```rust
21//! use u_nesting_core::robust::{orient2d, Orientation};
22//!
23//! // Check orientation of three points
24//! let a = (0.0, 0.0);
25//! let b = (1.0, 0.0);
26//! let c = (0.5, 1.0);
27//!
28//! match orient2d(a, b, c) {
29//! Orientation::CounterClockwise => println!("Left turn"),
30//! Orientation::Clockwise => println!("Right turn"),
31//! Orientation::Collinear => println!("Straight"),
32//! }
33//! ```
34
35use robust::{orient2d as robust_orient2d, Coord};
36
37/// Result of an orientation test.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Orientation {
40 /// Points are arranged counter-clockwise (left turn).
41 CounterClockwise,
42 /// Points are arranged clockwise (right turn).
43 Clockwise,
44 /// Points are collinear (on the same line).
45 Collinear,
46}
47
48impl Orientation {
49 /// Returns true if the orientation is counter-clockwise.
50 #[inline]
51 pub fn is_ccw(self) -> bool {
52 matches!(self, Orientation::CounterClockwise)
53 }
54
55 /// Returns true if the orientation is clockwise.
56 #[inline]
57 pub fn is_cw(self) -> bool {
58 matches!(self, Orientation::Clockwise)
59 }
60
61 /// Returns true if the points are collinear.
62 #[inline]
63 pub fn is_collinear(self) -> bool {
64 matches!(self, Orientation::Collinear)
65 }
66}
67
68// ============================================================================
69// Core Predicates (using robust crate)
70// ============================================================================
71
72/// Determines the orientation of three 2D points.
73///
74/// This is a numerically robust implementation using Shewchuk's adaptive
75/// precision arithmetic. It correctly handles near-degenerate cases where
76/// standard floating-point arithmetic would fail.
77///
78/// # Arguments
79///
80/// * `pa` - First point
81/// * `pb` - Second point
82/// * `pc` - Third point (the point being tested)
83///
84/// # Returns
85///
86/// - `Orientation::CounterClockwise` if `pc` lies to the left of the directed line from `pa` to `pb`
87/// - `Orientation::Clockwise` if `pc` lies to the right
88/// - `Orientation::Collinear` if the three points are collinear
89///
90/// # Example
91///
92/// ```rust
93/// use u_nesting_core::robust::{orient2d, Orientation};
94///
95/// let a = (0.0, 0.0);
96/// let b = (1.0, 0.0);
97/// let c = (0.5, 1.0);
98///
99/// assert_eq!(orient2d(a, b, c), Orientation::CounterClockwise);
100/// ```
101#[inline]
102pub fn orient2d(pa: (f64, f64), pb: (f64, f64), pc: (f64, f64)) -> Orientation {
103 let result = robust_orient2d(
104 Coord { x: pa.0, y: pa.1 },
105 Coord { x: pb.0, y: pb.1 },
106 Coord { x: pc.0, y: pc.1 },
107 );
108
109 if result > 0.0 {
110 Orientation::CounterClockwise
111 } else if result < 0.0 {
112 Orientation::Clockwise
113 } else {
114 Orientation::Collinear
115 }
116}
117
118/// Returns the raw orientation determinant value.
119///
120/// This is useful when you need the actual signed area value, not just the sign.
121/// The magnitude is proportional to twice the signed area of the triangle formed
122/// by the three points.
123///
124/// # Returns
125///
126/// - Positive if counter-clockwise
127/// - Negative if clockwise
128/// - Zero if collinear
129#[inline]
130pub fn orient2d_raw(pa: (f64, f64), pb: (f64, f64), pc: (f64, f64)) -> f64 {
131 robust_orient2d(
132 Coord { x: pa.0, y: pa.1 },
133 Coord { x: pb.0, y: pb.1 },
134 Coord { x: pc.0, y: pc.1 },
135 )
136}
137
138// ============================================================================
139// Floating-Point Filter (Fast Path + Exact Fallback)
140// ============================================================================
141
142/// Epsilon for fast floating-point filter.
143///
144/// If the result magnitude is larger than this threshold times the input magnitude,
145/// we can trust the fast path result. Otherwise, fall back to exact arithmetic.
146const FILTER_EPSILON: f64 = 1e-12;
147
148/// Fast orientation test with exact fallback.
149///
150/// This implements a floating-point filter that:
151/// 1. First attempts a fast approximate calculation
152/// 2. Falls back to exact arithmetic only when necessary
153///
154/// In practice, ~95% of cases are resolved by the fast path, providing
155/// near-native performance while guaranteeing correctness.
156///
157/// # Arguments
158///
159/// * `pa` - First point
160/// * `pb` - Second point
161/// * `pc` - Third point
162///
163/// # Returns
164///
165/// The orientation of the three points.
166#[inline]
167pub fn orient2d_filtered(pa: (f64, f64), pb: (f64, f64), pc: (f64, f64)) -> Orientation {
168 // Fast path: simple cross product
169 let acx = pa.0 - pc.0;
170 let bcx = pb.0 - pc.0;
171 let acy = pa.1 - pc.1;
172 let bcy = pb.1 - pc.1;
173
174 let det = acx * bcy - acy * bcx;
175
176 // Compute error bound
177 let det_sum = (acx * bcy).abs() + (acy * bcx).abs();
178
179 // If the determinant is clearly non-zero, use fast path
180 if det.abs() > FILTER_EPSILON * det_sum {
181 return if det > 0.0 {
182 Orientation::CounterClockwise
183 } else {
184 Orientation::Clockwise
185 };
186 }
187
188 // Fall back to exact arithmetic
189 orient2d(pa, pb, pc)
190}
191
192// ============================================================================
193// Higher-Level Geometric Predicates
194// ============================================================================
195
196/// Checks if a point lies strictly inside a triangle.
197///
198/// Uses robust orientation tests to correctly handle edge cases.
199///
200/// # Arguments
201///
202/// * `p` - The point to test
203/// * `a`, `b`, `c` - Triangle vertices (in any order)
204///
205/// # Returns
206///
207/// `true` if the point is strictly inside the triangle (not on the boundary)
208pub fn point_in_triangle_robust(
209 p: (f64, f64),
210 a: (f64, f64),
211 b: (f64, f64),
212 c: (f64, f64),
213) -> bool {
214 let o1 = orient2d(a, b, p);
215 let o2 = orient2d(b, c, p);
216 let o3 = orient2d(c, a, p);
217
218 // Point is inside if all orientations are the same (all CCW or all CW)
219 // and none are collinear (strictly inside)
220 (o1 == Orientation::CounterClockwise
221 && o2 == Orientation::CounterClockwise
222 && o3 == Orientation::CounterClockwise)
223 || (o1 == Orientation::Clockwise
224 && o2 == Orientation::Clockwise
225 && o3 == Orientation::Clockwise)
226}
227
228/// Checks if a point lies inside or on the boundary of a triangle.
229///
230/// Uses robust orientation tests to correctly handle edge cases.
231///
232/// # Arguments
233///
234/// * `p` - The point to test
235/// * `a`, `b`, `c` - Triangle vertices (in any order)
236///
237/// # Returns
238///
239/// `true` if the point is inside or on the boundary of the triangle
240pub fn point_in_triangle_inclusive_robust(
241 p: (f64, f64),
242 a: (f64, f64),
243 b: (f64, f64),
244 c: (f64, f64),
245) -> bool {
246 let o1 = orient2d(a, b, p);
247 let o2 = orient2d(b, c, p);
248 let o3 = orient2d(c, a, p);
249
250 // Point is inside or on boundary if:
251 // - No orientations are opposite to each other
252 // - At least one orientation must match the others (or be collinear)
253
254 let has_ccw = o1.is_ccw() || o2.is_ccw() || o3.is_ccw();
255 let has_cw = o1.is_cw() || o2.is_cw() || o3.is_cw();
256
257 // If we have both CCW and CW, point is outside
258 !(has_ccw && has_cw)
259}
260
261/// Checks if a polygon is convex using robust orientation tests.
262///
263/// # Arguments
264///
265/// * `polygon` - Slice of polygon vertices in order
266///
267/// # Returns
268///
269/// `true` if the polygon is convex
270pub fn is_convex_robust(polygon: &[(f64, f64)]) -> bool {
271 let n = polygon.len();
272 if n < 3 {
273 return false;
274 }
275
276 let mut expected_orientation: Option<Orientation> = None;
277
278 for i in 0..n {
279 let p0 = polygon[i];
280 let p1 = polygon[(i + 1) % n];
281 let p2 = polygon[(i + 2) % n];
282
283 let o = orient2d(p0, p1, p2);
284
285 // Skip collinear edges
286 if o.is_collinear() {
287 continue;
288 }
289
290 match expected_orientation {
291 None => expected_orientation = Some(o),
292 Some(expected) if expected != o => return false,
293 _ => {}
294 }
295 }
296
297 true
298}
299
300/// Checks if a polygon has counter-clockwise winding order.
301///
302/// # Arguments
303///
304/// * `polygon` - Slice of polygon vertices
305///
306/// # Returns
307///
308/// `true` if the polygon is wound counter-clockwise
309pub fn is_ccw_robust(polygon: &[(f64, f64)]) -> bool {
310 if polygon.len() < 3 {
311 return false;
312 }
313
314 // Find the lowest-leftmost vertex (guaranteed to be convex)
315 let mut min_idx = 0;
316 for (i, &(x, y)) in polygon.iter().enumerate() {
317 let (min_x, min_y) = polygon[min_idx];
318 if y < min_y || (y == min_y && x < min_x) {
319 min_idx = i;
320 }
321 }
322
323 let n = polygon.len();
324 let prev = polygon[(min_idx + n - 1) % n];
325 let curr = polygon[min_idx];
326 let next = polygon[(min_idx + 1) % n];
327
328 orient2d(prev, curr, next).is_ccw()
329}
330
331/// Computes the signed area of a polygon using robust arithmetic.
332///
333/// The shoelace formula is used, with careful handling of numerical precision.
334///
335/// # Arguments
336///
337/// * `polygon` - Slice of polygon vertices in order
338///
339/// # Returns
340///
341/// Positive area if counter-clockwise, negative if clockwise
342pub fn signed_area_robust(polygon: &[(f64, f64)]) -> f64 {
343 let n = polygon.len();
344 if n < 3 {
345 return 0.0;
346 }
347
348 // Use Kahan summation for better numerical stability
349 let mut sum = 0.0;
350 let mut c = 0.0; // Compensation for lost low-order bits
351
352 for i in 0..n {
353 let j = (i + 1) % n;
354 let term = polygon[i].0 * polygon[j].1 - polygon[j].0 * polygon[i].1;
355
356 let y = term - c;
357 let t = sum + y;
358 c = (t - sum) - y;
359 sum = t;
360 }
361
362 sum / 2.0
363}
364
365// ============================================================================
366// Integer Coordinate Scaling
367// ============================================================================
368
369/// Configuration for coordinate scaling.
370#[derive(Debug, Clone, Copy)]
371pub struct ScalingConfig {
372 /// The scale factor (coordinates are multiplied by this value).
373 pub scale: f64,
374 /// The inverse scale factor (for converting back).
375 pub inv_scale: f64,
376}
377
378impl ScalingConfig {
379 /// Creates a new scaling configuration.
380 ///
381 /// # Arguments
382 ///
383 /// * `precision` - Number of decimal places to preserve
384 ///
385 /// # Example
386 ///
387 /// ```rust
388 /// use u_nesting_core::robust::ScalingConfig;
389 ///
390 /// // Preserve 3 decimal places
391 /// let config = ScalingConfig::new(3);
392 /// assert_eq!(config.scale, 1000.0);
393 /// ```
394 pub fn new(precision: u32) -> Self {
395 let scale = 10.0_f64.powi(precision as i32);
396 Self {
397 scale,
398 inv_scale: 1.0 / scale,
399 }
400 }
401
402 /// Scales a coordinate to integer range.
403 #[inline]
404 pub fn scale_coord(&self, x: f64) -> f64 {
405 (x * self.scale).round()
406 }
407
408 /// Scales a point to integer range.
409 #[inline]
410 pub fn scale_point(&self, p: (f64, f64)) -> (f64, f64) {
411 (self.scale_coord(p.0), self.scale_coord(p.1))
412 }
413
414 /// Unscales a coordinate back to original range.
415 #[inline]
416 pub fn unscale_coord(&self, x: f64) -> f64 {
417 x * self.inv_scale
418 }
419
420 /// Unscales a point back to original range.
421 #[inline]
422 pub fn unscale_point(&self, p: (f64, f64)) -> (f64, f64) {
423 (self.unscale_coord(p.0), self.unscale_coord(p.1))
424 }
425
426 /// Scales an entire polygon.
427 pub fn scale_polygon(&self, polygon: &[(f64, f64)]) -> Vec<(f64, f64)> {
428 polygon.iter().map(|&p| self.scale_point(p)).collect()
429 }
430
431 /// Unscales an entire polygon.
432 pub fn unscale_polygon(&self, polygon: &[(f64, f64)]) -> Vec<(f64, f64)> {
433 polygon.iter().map(|&p| self.unscale_point(p)).collect()
434 }
435}
436
437impl Default for ScalingConfig {
438 /// Default scaling preserves 6 decimal places.
439 fn default() -> Self {
440 Self::new(6)
441 }
442}
443
444/// Snaps coordinates to a grid of the given resolution.
445///
446/// # Arguments
447///
448/// * `point` - The point to snap
449/// * `resolution` - Grid cell size
450///
451/// # Returns
452///
453/// The point snapped to the nearest grid intersection
454#[inline]
455pub fn snap_to_grid(point: (f64, f64), resolution: f64) -> (f64, f64) {
456 (
457 (point.0 / resolution).round() * resolution,
458 (point.1 / resolution).round() * resolution,
459 )
460}
461
462/// Snaps an entire polygon to a grid.
463pub fn snap_polygon_to_grid(polygon: &[(f64, f64)], resolution: f64) -> Vec<(f64, f64)> {
464 polygon
465 .iter()
466 .map(|&p| snap_to_grid(p, resolution))
467 .collect()
468}
469
470// ============================================================================
471// Tests
472// ============================================================================
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477
478 #[test]
479 fn test_orient2d_basic() {
480 // Counter-clockwise triangle
481 let a = (0.0, 0.0);
482 let b = (1.0, 0.0);
483 let c = (0.5, 1.0);
484
485 assert_eq!(orient2d(a, b, c), Orientation::CounterClockwise);
486 assert_eq!(orient2d(a, c, b), Orientation::Clockwise);
487 }
488
489 #[test]
490 fn test_orient2d_collinear() {
491 let a = (0.0, 0.0);
492 let b = (1.0, 1.0);
493 let c = (2.0, 2.0);
494
495 assert_eq!(orient2d(a, b, c), Orientation::Collinear);
496 }
497
498 #[test]
499 fn test_orient2d_near_collinear() {
500 // Near-collinear points that would fail with naive floating-point
501 let a = (0.0, 0.0);
502 let b = (1.0, 1.0);
503 let c = (2.0, 2.0 + 1e-15);
504
505 // Should detect the slight deviation from collinear
506 let result = orient2d(a, b, c);
507 // Due to floating-point representation, this might still be collinear
508 // or slightly CCW - the important thing is it doesn't crash or give wrong sign
509 assert!(
510 result == Orientation::Collinear || result == Orientation::CounterClockwise,
511 "Expected collinear or CCW, got {:?}",
512 result
513 );
514 }
515
516 #[test]
517 fn test_orient2d_filtered_fast_path() {
518 // Clear non-collinear case - should use fast path
519 let a = (0.0, 0.0);
520 let b = (10.0, 0.0);
521 let c = (5.0, 10.0);
522
523 assert_eq!(orient2d_filtered(a, b, c), Orientation::CounterClockwise);
524 }
525
526 #[test]
527 fn test_point_in_triangle_robust() {
528 let a = (0.0, 0.0);
529 let b = (10.0, 0.0);
530 let c = (5.0, 10.0);
531
532 // Point inside
533 assert!(point_in_triangle_robust((5.0, 3.0), a, b, c));
534
535 // Point outside
536 assert!(!point_in_triangle_robust((20.0, 5.0), a, b, c));
537
538 // Point on edge (not strictly inside)
539 assert!(!point_in_triangle_robust((5.0, 0.0), a, b, c));
540 }
541
542 #[test]
543 fn test_point_in_triangle_inclusive() {
544 let a = (0.0, 0.0);
545 let b = (10.0, 0.0);
546 let c = (5.0, 10.0);
547
548 // Point inside
549 assert!(point_in_triangle_inclusive_robust((5.0, 3.0), a, b, c));
550
551 // Point on edge (should be included)
552 assert!(point_in_triangle_inclusive_robust((5.0, 0.0), a, b, c));
553
554 // Point at vertex (should be included)
555 assert!(point_in_triangle_inclusive_robust((0.0, 0.0), a, b, c));
556
557 // Point outside
558 assert!(!point_in_triangle_inclusive_robust((20.0, 5.0), a, b, c));
559 }
560
561 #[test]
562 fn test_is_convex_robust() {
563 // Convex square
564 let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
565 assert!(is_convex_robust(&square));
566
567 // Convex triangle
568 let triangle = vec![(0.0, 0.0), (10.0, 0.0), (5.0, 10.0)];
569 assert!(is_convex_robust(&triangle));
570
571 // Non-convex L-shape
572 let l_shape = vec![
573 (0.0, 0.0),
574 (10.0, 0.0),
575 (10.0, 5.0),
576 (5.0, 5.0),
577 (5.0, 10.0),
578 (0.0, 10.0),
579 ];
580 assert!(!is_convex_robust(&l_shape));
581 }
582
583 #[test]
584 fn test_is_ccw_robust() {
585 // CCW square
586 let ccw_square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
587 assert!(is_ccw_robust(&ccw_square));
588
589 // CW square (reversed)
590 let cw_square = vec![(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
591 assert!(!is_ccw_robust(&cw_square));
592 }
593
594 #[test]
595 fn test_signed_area_robust() {
596 // CCW square with area 100
597 let ccw_square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
598 let area = signed_area_robust(&ccw_square);
599 assert!((area - 100.0).abs() < 1e-10);
600
601 // CW square with negative area
602 let cw_square = vec![(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
603 let area = signed_area_robust(&cw_square);
604 assert!((area + 100.0).abs() < 1e-10);
605 }
606
607 #[test]
608 fn test_scaling_config() {
609 let config = ScalingConfig::new(3);
610
611 // Scale a point
612 let p = (1.234, 5.678);
613 let scaled = config.scale_point(p);
614 assert_eq!(scaled, (1234.0, 5678.0));
615
616 // Unscale back
617 let unscaled = config.unscale_point(scaled);
618 assert!((unscaled.0 - p.0).abs() < 1e-10);
619 assert!((unscaled.1 - p.1).abs() < 1e-10);
620 }
621
622 #[test]
623 fn test_snap_to_grid() {
624 let p = (1.23, 4.56);
625
626 // Snap to grid of 0.5
627 let snapped = snap_to_grid(p, 0.5);
628 assert_eq!(snapped, (1.0, 4.5));
629
630 // Snap to grid of 1.0
631 let snapped = snap_to_grid(p, 1.0);
632 assert_eq!(snapped, (1.0, 5.0));
633 }
634
635 #[test]
636 fn test_orientation_methods() {
637 assert!(Orientation::CounterClockwise.is_ccw());
638 assert!(!Orientation::CounterClockwise.is_cw());
639 assert!(!Orientation::CounterClockwise.is_collinear());
640
641 assert!(!Orientation::Clockwise.is_ccw());
642 assert!(Orientation::Clockwise.is_cw());
643 assert!(!Orientation::Clockwise.is_collinear());
644
645 assert!(!Orientation::Collinear.is_ccw());
646 assert!(!Orientation::Collinear.is_cw());
647 assert!(Orientation::Collinear.is_collinear());
648 }
649
650 #[test]
651 fn test_degenerate_triangle() {
652 // Degenerate triangle (all points collinear)
653 let a = (0.0, 0.0);
654 let b = (5.0, 0.0);
655 let c = (10.0, 0.0);
656
657 // Point should not be "inside" a degenerate triangle
658 assert!(!point_in_triangle_robust((5.0, 0.0), a, b, c));
659 }
660
661 #[test]
662 fn test_extreme_coordinates() {
663 // Very large coordinates
664 let a = (1e10, 1e10);
665 let b = (1e10 + 1.0, 1e10);
666 let c = (1e10 + 0.5, 1e10 + 1.0);
667
668 assert_eq!(orient2d(a, b, c), Orientation::CounterClockwise);
669
670 // Very small coordinates
671 let a = (1e-10, 1e-10);
672 let b = (1e-10 + 1e-12, 1e-10);
673 let c = (1e-10 + 5e-13, 1e-10 + 1e-12);
674
675 // Should still produce correct orientation
676 let result = orient2d(a, b, c);
677 assert!(
678 result == Orientation::CounterClockwise || result == Orientation::Collinear,
679 "Unexpected orientation: {:?}",
680 result
681 );
682 }
683}