Skip to main content

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//! ## Core Predicates
7//!
8//! The core predicates (`orient2d`, `orient2d_filtered`, `point_in_triangle`,
9//! `is_convex`, `is_ccw`, `Orientation`) are provided by `u-geometry` and
10//! re-exported here for API compatibility.
11//!
12//! ## Nesting-Specific Utilities
13//!
14//! - [`signed_area_robust`]: Signed polygon area with Kahan summation for numerical stability
15//! - [`ScalingConfig`]: Coordinate scaling for integer-range robust arithmetic
16//! - [`snap_to_grid`] / [`snap_polygon_to_grid`]: Grid-snapping utilities
17//!
18//! ## References
19//!
20//! - Shewchuk, J.R. (1997). "Adaptive Precision Floating-Point Arithmetic and
21//!   Fast Robust Predicates for Computational Geometry"
22//! - <https://www.cs.cmu.edu/~quake/robust.html>
23//!
24//! ## Example
25//!
26//! ```rust
27//! use u_nesting_core::robust::{orient2d, Orientation};
28//!
29//! // Check orientation of three points
30//! let a = (0.0, 0.0);
31//! let b = (1.0, 0.0);
32//! let c = (0.5, 1.0);
33//!
34//! match orient2d(a, b, c) {
35//!     Orientation::CounterClockwise => println!("Left turn"),
36//!     Orientation::Clockwise => println!("Right turn"),
37//!     Orientation::Collinear => println!("Straight"),
38//! }
39//! ```
40
41// ============================================================================
42// Re-exports from u-geometry (canonical implementations)
43// ============================================================================
44
45pub use u_geometry::robust::{
46    is_ccw, is_convex, orient2d, orient2d_filtered, orient2d_raw, point_in_triangle,
47    point_in_triangle_inclusive, Orientation,
48};
49
50// ============================================================================
51// Nesting-specific aliases (backward compatibility)
52// ============================================================================
53
54/// Checks if a point lies strictly inside a triangle.
55///
56/// This is an alias for [`point_in_triangle`] preserved for backward compatibility
57/// with existing u-nesting code that uses the `_robust` suffix convention.
58#[inline]
59pub fn point_in_triangle_robust(
60    p: (f64, f64),
61    a: (f64, f64),
62    b: (f64, f64),
63    c: (f64, f64),
64) -> bool {
65    point_in_triangle(p, a, b, c)
66}
67
68/// Checks if a point lies inside or on the boundary of a triangle.
69///
70/// This is an alias for [`point_in_triangle_inclusive`] preserved for backward
71/// compatibility with existing u-nesting code.
72#[inline]
73pub fn point_in_triangle_inclusive_robust(
74    p: (f64, f64),
75    a: (f64, f64),
76    b: (f64, f64),
77    c: (f64, f64),
78) -> bool {
79    point_in_triangle_inclusive(p, a, b, c)
80}
81
82/// Checks if a polygon is convex using robust orientation tests.
83///
84/// This is an alias for [`is_convex`] preserved for backward compatibility.
85#[inline]
86pub fn is_convex_robust(polygon: &[(f64, f64)]) -> bool {
87    is_convex(polygon)
88}
89
90/// Checks if a polygon has counter-clockwise winding order.
91///
92/// This is an alias for [`is_ccw`] preserved for backward compatibility.
93#[inline]
94pub fn is_ccw_robust(polygon: &[(f64, f64)]) -> bool {
95    is_ccw(polygon)
96}
97
98// ============================================================================
99// Nesting-Specific Utilities (not in u-geometry)
100// ============================================================================
101
102/// Computes the signed area of a polygon using Kahan summation.
103///
104/// The shoelace formula is used with Kahan compensated summation for
105/// better numerical stability on large polygons with diverse vertex scales.
106///
107/// # Arguments
108///
109/// * `polygon` - Slice of polygon vertices in order
110///
111/// # Returns
112///
113/// Positive area if counter-clockwise, negative if clockwise
114///
115/// # Reference
116/// Kahan (1965), "Pracniques: Further Remarks on Reducing Truncation Errors"
117pub fn signed_area_robust(polygon: &[(f64, f64)]) -> f64 {
118    let n = polygon.len();
119    if n < 3 {
120        return 0.0;
121    }
122
123    // Kahan summation for better numerical stability
124    let mut sum = 0.0;
125    let mut c = 0.0; // Compensation for lost low-order bits
126
127    for i in 0..n {
128        let j = (i + 1) % n;
129        let term = polygon[i].0 * polygon[j].1 - polygon[j].0 * polygon[i].1;
130
131        let y = term - c;
132        let t = sum + y;
133        c = (t - sum) - y;
134        sum = t;
135    }
136
137    sum / 2.0
138}
139
140// ============================================================================
141// Integer Coordinate Scaling
142// ============================================================================
143
144/// Configuration for coordinate scaling.
145#[derive(Debug, Clone, Copy)]
146pub struct ScalingConfig {
147    /// The scale factor (coordinates are multiplied by this value).
148    pub scale: f64,
149    /// The inverse scale factor (for converting back).
150    pub inv_scale: f64,
151}
152
153impl ScalingConfig {
154    /// Creates a new scaling configuration.
155    ///
156    /// # Arguments
157    ///
158    /// * `precision` - Number of decimal places to preserve
159    ///
160    /// # Example
161    ///
162    /// ```rust
163    /// use u_nesting_core::robust::ScalingConfig;
164    ///
165    /// // Preserve 3 decimal places
166    /// let config = ScalingConfig::new(3);
167    /// assert_eq!(config.scale, 1000.0);
168    /// ```
169    pub fn new(precision: u32) -> Self {
170        let scale = 10.0_f64.powi(precision as i32);
171        Self {
172            scale,
173            inv_scale: 1.0 / scale,
174        }
175    }
176
177    /// Scales a coordinate to integer range.
178    #[inline]
179    pub fn scale_coord(&self, x: f64) -> f64 {
180        (x * self.scale).round()
181    }
182
183    /// Scales a point to integer range.
184    #[inline]
185    pub fn scale_point(&self, p: (f64, f64)) -> (f64, f64) {
186        (self.scale_coord(p.0), self.scale_coord(p.1))
187    }
188
189    /// Unscales a coordinate back to original range.
190    #[inline]
191    pub fn unscale_coord(&self, x: f64) -> f64 {
192        x * self.inv_scale
193    }
194
195    /// Unscales a point back to original range.
196    #[inline]
197    pub fn unscale_point(&self, p: (f64, f64)) -> (f64, f64) {
198        (self.unscale_coord(p.0), self.unscale_coord(p.1))
199    }
200
201    /// Scales an entire polygon.
202    pub fn scale_polygon(&self, polygon: &[(f64, f64)]) -> Vec<(f64, f64)> {
203        polygon.iter().map(|&p| self.scale_point(p)).collect()
204    }
205
206    /// Unscales an entire polygon.
207    pub fn unscale_polygon(&self, polygon: &[(f64, f64)]) -> Vec<(f64, f64)> {
208        polygon.iter().map(|&p| self.unscale_point(p)).collect()
209    }
210}
211
212impl Default for ScalingConfig {
213    /// Default scaling preserves 6 decimal places.
214    fn default() -> Self {
215        Self::new(6)
216    }
217}
218
219/// Snaps coordinates to a grid of the given resolution.
220///
221/// # Arguments
222///
223/// * `point` - The point to snap
224/// * `resolution` - Grid cell size
225///
226/// # Returns
227///
228/// The point snapped to the nearest grid intersection
229#[inline]
230pub fn snap_to_grid(point: (f64, f64), resolution: f64) -> (f64, f64) {
231    (
232        (point.0 / resolution).round() * resolution,
233        (point.1 / resolution).round() * resolution,
234    )
235}
236
237/// Snaps an entire polygon to a grid.
238pub fn snap_polygon_to_grid(polygon: &[(f64, f64)], resolution: f64) -> Vec<(f64, f64)> {
239    polygon
240        .iter()
241        .map(|&p| snap_to_grid(p, resolution))
242        .collect()
243}
244
245// ============================================================================
246// Tests
247// ============================================================================
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn test_orient2d_basic() {
255        // Counter-clockwise triangle
256        let a = (0.0, 0.0);
257        let b = (1.0, 0.0);
258        let c = (0.5, 1.0);
259
260        assert_eq!(orient2d(a, b, c), Orientation::CounterClockwise);
261        assert_eq!(orient2d(a, c, b), Orientation::Clockwise);
262    }
263
264    #[test]
265    fn test_orient2d_collinear() {
266        let a = (0.0, 0.0);
267        let b = (1.0, 1.0);
268        let c = (2.0, 2.0);
269
270        assert_eq!(orient2d(a, b, c), Orientation::Collinear);
271    }
272
273    #[test]
274    fn test_orient2d_near_collinear() {
275        let a = (0.0, 0.0);
276        let b = (1.0, 1.0);
277        let c = (2.0, 2.0 + 1e-15);
278
279        let result = orient2d(a, b, c);
280        assert!(
281            result == Orientation::Collinear || result == Orientation::CounterClockwise,
282            "Expected collinear or CCW, got {:?}",
283            result
284        );
285    }
286
287    #[test]
288    fn test_orient2d_filtered_fast_path() {
289        let a = (0.0, 0.0);
290        let b = (10.0, 0.0);
291        let c = (5.0, 10.0);
292
293        assert_eq!(orient2d_filtered(a, b, c), Orientation::CounterClockwise);
294    }
295
296    #[test]
297    fn test_point_in_triangle_robust() {
298        let a = (0.0, 0.0);
299        let b = (10.0, 0.0);
300        let c = (5.0, 10.0);
301
302        assert!(point_in_triangle_robust((5.0, 3.0), a, b, c));
303        assert!(!point_in_triangle_robust((20.0, 5.0), a, b, c));
304        assert!(!point_in_triangle_robust((5.0, 0.0), a, b, c));
305    }
306
307    #[test]
308    fn test_point_in_triangle_inclusive() {
309        let a = (0.0, 0.0);
310        let b = (10.0, 0.0);
311        let c = (5.0, 10.0);
312
313        assert!(point_in_triangle_inclusive_robust((5.0, 3.0), a, b, c));
314        assert!(point_in_triangle_inclusive_robust((5.0, 0.0), a, b, c));
315        assert!(point_in_triangle_inclusive_robust((0.0, 0.0), a, b, c));
316        assert!(!point_in_triangle_inclusive_robust((20.0, 5.0), a, b, c));
317    }
318
319    #[test]
320    fn test_is_convex_robust() {
321        let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
322        assert!(is_convex_robust(&square));
323
324        let triangle = vec![(0.0, 0.0), (10.0, 0.0), (5.0, 10.0)];
325        assert!(is_convex_robust(&triangle));
326
327        let l_shape = vec![
328            (0.0, 0.0),
329            (10.0, 0.0),
330            (10.0, 5.0),
331            (5.0, 5.0),
332            (5.0, 10.0),
333            (0.0, 10.0),
334        ];
335        assert!(!is_convex_robust(&l_shape));
336    }
337
338    #[test]
339    fn test_is_ccw_robust() {
340        let ccw_square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
341        assert!(is_ccw_robust(&ccw_square));
342
343        let cw_square = vec![(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
344        assert!(!is_ccw_robust(&cw_square));
345    }
346
347    #[test]
348    fn test_signed_area_robust() {
349        let ccw_square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
350        let area = signed_area_robust(&ccw_square);
351        assert!((area - 100.0).abs() < 1e-10);
352
353        let cw_square = vec![(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
354        let area = signed_area_robust(&cw_square);
355        assert!((area + 100.0).abs() < 1e-10);
356    }
357
358    #[test]
359    fn test_scaling_config() {
360        let config = ScalingConfig::new(3);
361
362        let p = (1.234, 5.678);
363        let scaled = config.scale_point(p);
364        assert_eq!(scaled, (1234.0, 5678.0));
365
366        let unscaled = config.unscale_point(scaled);
367        assert!((unscaled.0 - p.0).abs() < 1e-10);
368        assert!((unscaled.1 - p.1).abs() < 1e-10);
369    }
370
371    #[test]
372    fn test_snap_to_grid() {
373        let p = (1.23, 4.56);
374
375        let snapped = snap_to_grid(p, 0.5);
376        assert_eq!(snapped, (1.0, 4.5));
377
378        let snapped = snap_to_grid(p, 1.0);
379        assert_eq!(snapped, (1.0, 5.0));
380    }
381
382    #[test]
383    fn test_orientation_methods() {
384        assert!(Orientation::CounterClockwise.is_ccw());
385        assert!(!Orientation::CounterClockwise.is_cw());
386        assert!(!Orientation::CounterClockwise.is_collinear());
387
388        assert!(!Orientation::Clockwise.is_ccw());
389        assert!(Orientation::Clockwise.is_cw());
390        assert!(!Orientation::Clockwise.is_collinear());
391
392        assert!(!Orientation::Collinear.is_ccw());
393        assert!(!Orientation::Collinear.is_cw());
394        assert!(Orientation::Collinear.is_collinear());
395    }
396
397    #[test]
398    fn test_degenerate_triangle() {
399        let a = (0.0, 0.0);
400        let b = (5.0, 0.0);
401        let c = (10.0, 0.0);
402
403        assert!(!point_in_triangle_robust((5.0, 0.0), a, b, c));
404    }
405
406    #[test]
407    fn test_extreme_coordinates() {
408        let a = (1e10, 1e10);
409        let b = (1e10 + 1.0, 1e10);
410        let c = (1e10 + 0.5, 1e10 + 1.0);
411
412        assert_eq!(orient2d(a, b, c), Orientation::CounterClockwise);
413
414        let a = (1e-10, 1e-10);
415        let b = (1e-10 + 1e-12, 1e-10);
416        let c = (1e-10 + 5e-13, 1e-10 + 1e-12);
417
418        let result = orient2d(a, b, c);
419        assert!(
420            result == Orientation::CounterClockwise || result == Orientation::Collinear,
421            "Unexpected orientation: {:?}",
422            result
423        );
424    }
425}