Skip to main content

u_geometry/
polygon.rs

1//! Polygon operations: area, centroid, convex hull, winding.
2//!
3//! All functions operate on slices of `(f64, f64)` tuples for
4//! maximum compatibility. Use `Point2::to_tuple()` for interop.
5//!
6//! # References
7//!
8//! - O'Rourke (1998), "Computational Geometry in C", Ch.1 (polygon area)
9//! - Graham (1972), "An efficient algorithm for determining the convex hull"
10//! - de Berg et al. (2008), "Computational Geometry", Ch.1
11
12use crate::robust::orient2d;
13
14/// Computes the signed area of a simple polygon using the Shoelace formula.
15///
16/// Positive for counter-clockwise winding, negative for clockwise.
17/// Uses Kahan summation for numerical stability.
18///
19/// # Complexity
20/// O(n)
21///
22/// # Reference
23/// Meister (1769), Shoelace formula
24pub fn signed_area(polygon: &[(f64, f64)]) -> f64 {
25    let n = polygon.len();
26    if n < 3 {
27        return 0.0;
28    }
29
30    // Kahan compensated summation
31    let mut sum = 0.0;
32    let mut c = 0.0;
33
34    for i in 0..n {
35        let j = (i + 1) % n;
36        let term = polygon[i].0 * polygon[j].1 - polygon[j].0 * polygon[i].1;
37
38        let y = term - c;
39        let t = sum + y;
40        c = (t - sum) - y;
41        sum = t;
42    }
43
44    sum * 0.5
45}
46
47/// Computes the unsigned area of a simple polygon.
48///
49/// # Complexity
50/// O(n)
51pub fn area(polygon: &[(f64, f64)]) -> f64 {
52    signed_area(polygon).abs()
53}
54
55/// Computes the centroid (center of mass) of a simple polygon.
56///
57/// Assumes the polygon is non-degenerate (area > 0).
58/// Returns `None` if the polygon has fewer than 3 vertices or zero area.
59///
60/// # Complexity
61/// O(n)
62///
63/// # Reference
64/// O'Rourke (1998), Eq. 1.6
65pub fn centroid(polygon: &[(f64, f64)]) -> Option<(f64, f64)> {
66    let n = polygon.len();
67    if n < 3 {
68        return None;
69    }
70
71    let a = signed_area(polygon);
72    if a.abs() < 1e-15 {
73        return None;
74    }
75
76    let mut cx = 0.0;
77    let mut cy = 0.0;
78
79    for i in 0..n {
80        let j = (i + 1) % n;
81        let cross = polygon[i].0 * polygon[j].1 - polygon[j].0 * polygon[i].1;
82        cx += (polygon[i].0 + polygon[j].0) * cross;
83        cy += (polygon[i].1 + polygon[j].1) * cross;
84    }
85
86    let inv = 1.0 / (6.0 * a);
87    Some((cx * inv, cy * inv))
88}
89
90/// Computes the unsigned area of a polygon with holes.
91///
92/// The total area is the exterior area minus the sum of hole areas.
93///
94/// # Complexity
95/// O(n) where n is the total number of vertices across all rings
96pub fn area_with_holes(exterior: &[(f64, f64)], holes: &[&[(f64, f64)]]) -> f64 {
97    let mut total = area(exterior);
98    for hole in holes {
99        total -= area(hole);
100    }
101    total.max(0.0)
102}
103
104/// Computes the centroid of a polygon with holes.
105///
106/// Uses area-weighted centroid: each ring's centroid is weighted by its
107/// signed area, then combined. Returns `None` if the net area is zero.
108///
109/// # Complexity
110/// O(n) where n is the total number of vertices across all rings
111///
112/// # Reference
113/// Extension of O'Rourke (1998), Eq. 1.6 to multiply-connected polygons
114pub fn centroid_with_holes(exterior: &[(f64, f64)], holes: &[&[(f64, f64)]]) -> Option<(f64, f64)> {
115    let ext_area = signed_area(exterior);
116    let ext_centroid = centroid(exterior)?;
117
118    if holes.is_empty() {
119        return Some(ext_centroid);
120    }
121
122    let mut weighted_cx = ext_area * ext_centroid.0;
123    let mut weighted_cy = ext_area * ext_centroid.1;
124    let mut total_area = ext_area;
125
126    for hole in holes {
127        let h_area = signed_area(hole);
128        if let Some((hcx, hcy)) = centroid(hole) {
129            weighted_cx -= h_area.abs() * hcx;
130            weighted_cy -= h_area.abs() * hcy;
131            total_area -= h_area.abs();
132        }
133    }
134
135    if total_area.abs() < 1e-15 {
136        return None;
137    }
138
139    Some((weighted_cx / total_area, weighted_cy / total_area))
140}
141
142/// Computes the perimeter of a polygon.
143///
144/// # Complexity
145/// O(n)
146pub fn perimeter(polygon: &[(f64, f64)]) -> f64 {
147    let n = polygon.len();
148    if n < 2 {
149        return 0.0;
150    }
151
152    let mut p = 0.0;
153    for i in 0..n {
154        let j = (i + 1) % n;
155        let dx = polygon[j].0 - polygon[i].0;
156        let dy = polygon[j].1 - polygon[i].1;
157        p += (dx * dx + dy * dy).sqrt();
158    }
159    p
160}
161
162/// Computes the convex hull of a set of points using Graham scan.
163///
164/// Returns the hull vertices in counter-clockwise order.
165/// Uses robust orientation tests for correctness.
166///
167/// # Complexity
168/// O(n log n) time, O(n) space
169///
170/// # Reference
171/// Graham (1972), "An efficient algorithm for determining the convex hull
172/// of a finite planar set"
173pub fn convex_hull(points: &[(f64, f64)]) -> Vec<(f64, f64)> {
174    let n = points.len();
175    if n < 3 {
176        return points.to_vec();
177    }
178
179    // Find the lowest-leftmost point (pivot)
180    let mut pts: Vec<(f64, f64)> = points.to_vec();
181    let mut pivot_idx = 0;
182    for (i, &(x, y)) in pts.iter().enumerate() {
183        let (px, py) = pts[pivot_idx];
184        if y < py || (y == py && x < px) {
185            pivot_idx = i;
186        }
187    }
188    pts.swap(0, pivot_idx);
189    let pivot = pts[0];
190
191    // Sort by polar angle from pivot
192    pts[1..].sort_by(|a, b| {
193        let o = orient2d(pivot, *a, *b);
194        match o {
195            crate::robust::Orientation::CounterClockwise => std::cmp::Ordering::Less,
196            crate::robust::Orientation::Clockwise => std::cmp::Ordering::Greater,
197            crate::robust::Orientation::Collinear => {
198                // Closer point first
199                let da = (a.0 - pivot.0).powi(2) + (a.1 - pivot.1).powi(2);
200                let db = (b.0 - pivot.0).powi(2) + (b.1 - pivot.1).powi(2);
201                da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
202            }
203        }
204    });
205
206    // Graham scan
207    let mut hull: Vec<(f64, f64)> = Vec::with_capacity(n);
208    for &p in &pts {
209        while hull.len() >= 2 {
210            let top = hull[hull.len() - 1];
211            let second = hull[hull.len() - 2];
212            if !orient2d(second, top, p).is_ccw() {
213                hull.pop();
214            } else {
215                break;
216            }
217        }
218        hull.push(p);
219    }
220
221    hull
222}
223
224/// Checks if a simple polygon is convex.
225///
226/// Uses robust orientation predicates for numerical stability.
227/// A polygon is convex if all consecutive edge turns have the same
228/// orientation (all CCW or all CW). Collinear edges are skipped.
229/// Returns `false` for polygons with fewer than 3 vertices.
230///
231/// # Complexity
232/// O(n)
233pub fn is_convex(polygon: &[(f64, f64)]) -> bool {
234    let n = polygon.len();
235    if n < 3 {
236        return false;
237    }
238
239    let mut expected: Option<crate::robust::Orientation> = None;
240
241    for i in 0..n {
242        let p0 = polygon[i];
243        let p1 = polygon[(i + 1) % n];
244        let p2 = polygon[(i + 2) % n];
245
246        let o = orient2d(p0, p1, p2);
247
248        if o.is_collinear() {
249            continue;
250        }
251
252        match expected {
253            None => expected = Some(o),
254            Some(ref exp) if *exp != o => return false,
255            _ => {}
256        }
257    }
258
259    true
260}
261
262/// Ensures a polygon has counter-clockwise winding order.
263///
264/// If the polygon is already CCW, returns a clone. Otherwise reverses it.
265///
266/// # Complexity
267/// O(n)
268pub fn ensure_ccw(polygon: &[(f64, f64)]) -> Vec<(f64, f64)> {
269    if polygon.len() < 3 {
270        return polygon.to_vec();
271    }
272    if crate::robust::is_ccw(polygon) {
273        polygon.to_vec()
274    } else {
275        let mut reversed = polygon.to_vec();
276        reversed.reverse();
277        reversed
278    }
279}
280
281/// Checks if a point lies inside a simple polygon using the winding number method.
282///
283/// Uses robust orientation tests for correctness on edges and vertices.
284///
285/// # Complexity
286/// O(n) where n is the number of polygon vertices
287///
288/// # Reference
289/// O'Rourke (1998), Ch. 7.4 — Winding number algorithm
290pub fn contains_point(polygon: &[(f64, f64)], point: (f64, f64)) -> bool {
291    let n = polygon.len();
292    if n < 3 {
293        return false;
294    }
295
296    let mut winding = 0i32;
297
298    for i in 0..n {
299        let j = (i + 1) % n;
300        let (ax, ay) = polygon[i];
301        let (bx, by) = polygon[j];
302
303        if ay <= point.1 {
304            if by > point.1 {
305                // Upward crossing
306                if orient2d((ax, ay), (bx, by), point).is_ccw() {
307                    winding += 1;
308                }
309            }
310        } else if by <= point.1 {
311            // Downward crossing
312            if orient2d((ax, ay), (bx, by), point).is_cw() {
313                winding -= 1;
314            }
315        }
316    }
317
318    winding != 0
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    // ---- Area tests ----
326
327    // ---- Shoelace formula: reference values ----
328    //
329    // A = |Σ(x_i·y_{i+1} − x_{i+1}·y_i)| / 2
330    // Reference: Meister (1769), also known as the Gauss area formula.
331    //
332    // Exact integer-coordinate cases provide lossless f64 results.
333
334    #[test]
335    fn test_shoelace_unit_square_exact() {
336        // Unit square [(0,0),(1,0),(1,1),(0,1)] → A = 1.0 exactly
337        let unit_square = [(0.0_f64, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
338        let a = area(&unit_square);
339        assert!(
340            (a - 1.0).abs() < 1e-15,
341            "unit square area must be exactly 1.0, got {a}"
342        );
343    }
344
345    #[test]
346    fn test_shoelace_unit_triangle_exact() {
347        // Right triangle [(0,0),(1,0),(0,1)] → A = 0.5 exactly
348        let tri = [(0.0_f64, 0.0), (1.0, 0.0), (0.0, 1.0)];
349        let a = area(&tri);
350        assert!(
351            (a - 0.5).abs() < 1e-15,
352            "unit triangle area must be exactly 0.5, got {a}"
353        );
354    }
355
356    #[test]
357    fn test_shoelace_rectangle_exact() {
358        // Rectangle [(0,0),(3,0),(3,2),(0,2)] → A = 6.0 exactly
359        let rect = [(0.0_f64, 0.0), (3.0, 0.0), (3.0, 2.0), (0.0, 2.0)];
360        let a = area(&rect);
361        assert!(
362            (a - 6.0).abs() < 1e-15,
363            "3×2 rectangle area must be exactly 6.0, got {a}"
364        );
365    }
366
367    #[test]
368    fn test_signed_area_ccw_square() {
369        let square = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
370        let a = signed_area(&square);
371        assert!((a - 100.0).abs() < 1e-10);
372    }
373
374    #[test]
375    fn test_signed_area_cw_square() {
376        let square = [(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
377        let a = signed_area(&square);
378        assert!((a - (-100.0)).abs() < 1e-10);
379    }
380
381    #[test]
382    fn test_area_triangle() {
383        let tri = [(0.0, 0.0), (10.0, 0.0), (5.0, 10.0)];
384        assert!((area(&tri) - 50.0).abs() < 1e-10);
385    }
386
387    #[test]
388    fn test_area_degenerate() {
389        assert!((area(&[(0.0, 0.0), (1.0, 0.0)]) - 0.0).abs() < 1e-15);
390        assert!((area(&[]) - 0.0).abs() < 1e-15);
391    }
392
393    // ---- Centroid tests ----
394
395    #[test]
396    fn test_centroid_square() {
397        let square = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
398        let (cx, cy) = centroid(&square).unwrap();
399        assert!((cx - 5.0).abs() < 1e-10);
400        assert!((cy - 5.0).abs() < 1e-10);
401    }
402
403    #[test]
404    fn test_centroid_triangle() {
405        let tri = [(0.0, 0.0), (6.0, 0.0), (3.0, 6.0)];
406        let (cx, cy) = centroid(&tri).unwrap();
407        assert!((cx - 3.0).abs() < 1e-10);
408        assert!((cy - 2.0).abs() < 1e-10);
409    }
410
411    #[test]
412    fn test_centroid_degenerate() {
413        assert!(centroid(&[(0.0, 0.0), (1.0, 0.0)]).is_none());
414        // Collinear points (zero area)
415        assert!(centroid(&[(0.0, 0.0), (1.0, 0.0), (2.0, 0.0)]).is_none());
416    }
417
418    // ---- Area with holes tests ----
419
420    #[test]
421    fn test_area_with_holes_no_holes() {
422        let ext = [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)];
423        assert!((area_with_holes(&ext, &[]) - 10000.0).abs() < 1e-10);
424    }
425
426    #[test]
427    fn test_area_with_holes_one_hole() {
428        let ext = [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)];
429        let hole: &[(f64, f64)] = &[(25.0, 25.0), (75.0, 25.0), (75.0, 75.0), (25.0, 75.0)];
430        // 10000 - 2500 = 7500
431        assert!((area_with_holes(&ext, &[hole]) - 7500.0).abs() < 1e-10);
432    }
433
434    #[test]
435    fn test_area_with_holes_two_holes() {
436        let ext = [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)];
437        let h1: &[(f64, f64)] = &[(10.0, 10.0), (30.0, 10.0), (30.0, 30.0), (10.0, 30.0)];
438        let h2: &[(f64, f64)] = &[(50.0, 50.0), (70.0, 50.0), (70.0, 70.0), (50.0, 70.0)];
439        // 10000 - 400 - 400 = 9200
440        assert!((area_with_holes(&ext, &[h1, h2]) - 9200.0).abs() < 1e-10);
441    }
442
443    // ---- Centroid with holes tests ----
444
445    #[test]
446    fn test_centroid_with_holes_no_holes() {
447        let ext = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
448        let (cx, cy) = centroid_with_holes(&ext, &[]).unwrap();
449        assert!((cx - 5.0).abs() < 1e-10);
450        assert!((cy - 5.0).abs() < 1e-10);
451    }
452
453    #[test]
454    fn test_centroid_with_holes_symmetric_hole() {
455        // Square with centered square hole — centroid stays at center
456        let ext = [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)];
457        let hole: &[(f64, f64)] = &[(25.0, 25.0), (75.0, 25.0), (75.0, 75.0), (25.0, 75.0)];
458        let (cx, cy) = centroid_with_holes(&ext, &[hole]).unwrap();
459        assert!((cx - 50.0).abs() < 1e-6);
460        assert!((cy - 50.0).abs() < 1e-6);
461    }
462
463    #[test]
464    fn test_centroid_with_holes_asymmetric_hole() {
465        // Square with hole in upper-right — centroid shifts toward lower-left
466        let ext = [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)];
467        let hole: &[(f64, f64)] = &[(50.0, 50.0), (100.0, 50.0), (100.0, 100.0), (50.0, 100.0)];
468        let (cx, cy) = centroid_with_holes(&ext, &[hole]).unwrap();
469        // Centroid should shift toward lower-left
470        assert!(cx < 50.0);
471        assert!(cy < 50.0);
472    }
473
474    // ---- Perimeter tests ----
475
476    #[test]
477    fn test_perimeter_square() {
478        let square = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
479        assert!((perimeter(&square) - 40.0).abs() < 1e-10);
480    }
481
482    #[test]
483    fn test_perimeter_empty() {
484        assert!((perimeter(&[]) - 0.0).abs() < 1e-15);
485    }
486
487    // ---- Convex Hull tests ----
488
489    #[test]
490    fn test_convex_hull_square() {
491        let points = [
492            (0.0, 0.0),
493            (10.0, 0.0),
494            (10.0, 10.0),
495            (0.0, 10.0),
496            (5.0, 5.0), // interior point
497        ];
498        let hull = convex_hull(&points);
499        assert_eq!(hull.len(), 4);
500        // All hull points should be corners (not the interior point)
501        assert!((area(&hull) - 100.0).abs() < 1e-10);
502    }
503
504    #[test]
505    fn test_convex_hull_triangle() {
506        let points = [(0.0, 0.0), (10.0, 0.0), (5.0, 10.0)];
507        let hull = convex_hull(&points);
508        assert_eq!(hull.len(), 3);
509    }
510
511    #[test]
512    fn test_convex_hull_collinear() {
513        let points = [(0.0, 0.0), (1.0, 0.0), (2.0, 0.0)];
514        let hull = convex_hull(&points);
515        // Collinear points produce a degenerate hull
516        assert!(hull.len() <= 3);
517    }
518
519    #[test]
520    fn test_convex_hull_l_shape() {
521        let l_shape = [
522            (0.0, 0.0),
523            (10.0, 0.0),
524            (10.0, 5.0),
525            (5.0, 5.0),
526            (5.0, 10.0),
527            (0.0, 10.0),
528        ];
529        let hull = convex_hull(&l_shape);
530        // L-shape hull: (0,0), (10,0), (10,5), (5,10), (0,10) — 5 vertices
531        assert_eq!(hull.len(), 5);
532    }
533
534    // ---- is_convex tests ----
535
536    #[test]
537    fn test_is_convex_square() {
538        let square = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
539        assert!(is_convex(&square));
540    }
541
542    #[test]
543    fn test_is_convex_triangle() {
544        let tri = [(0.0, 0.0), (10.0, 0.0), (5.0, 10.0)];
545        assert!(is_convex(&tri));
546    }
547
548    #[test]
549    fn test_is_convex_l_shape() {
550        let l = [
551            (0.0, 0.0),
552            (20.0, 0.0),
553            (20.0, 10.0),
554            (10.0, 10.0),
555            (10.0, 20.0),
556            (0.0, 20.0),
557        ];
558        assert!(!is_convex(&l));
559    }
560
561    #[test]
562    fn test_is_convex_degenerate() {
563        assert!(!is_convex(&[(0.0, 0.0), (1.0, 0.0)]));
564        assert!(!is_convex(&[]));
565    }
566
567    // ---- ensure_ccw tests ----
568
569    #[test]
570    fn test_ensure_ccw_already_ccw() {
571        let ccw = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
572        let result = ensure_ccw(&ccw);
573        assert_eq!(result, ccw.to_vec());
574    }
575
576    #[test]
577    fn test_ensure_ccw_from_cw() {
578        let cw = [(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
579        let result = ensure_ccw(&cw);
580        assert!(crate::robust::is_ccw(&result));
581    }
582
583    // ---- contains_point tests ----
584
585    #[test]
586    fn test_contains_point_inside_square() {
587        let square = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
588        assert!(contains_point(&square, (5.0, 5.0)));
589    }
590
591    #[test]
592    fn test_contains_point_outside_square() {
593        let square = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
594        assert!(!contains_point(&square, (15.0, 5.0)));
595    }
596
597    #[test]
598    fn test_contains_point_concave_polygon() {
599        // L-shape polygon
600        let l_shape = [
601            (0.0, 0.0),
602            (10.0, 0.0),
603            (10.0, 5.0),
604            (5.0, 5.0),
605            (5.0, 10.0),
606            (0.0, 10.0),
607        ];
608        // Inside the L
609        assert!(contains_point(&l_shape, (2.0, 2.0)));
610        assert!(contains_point(&l_shape, (2.0, 8.0)));
611        // In the concave "notch" — outside
612        assert!(!contains_point(&l_shape, (8.0, 8.0)));
613    }
614
615    #[test]
616    fn test_contains_point_triangle() {
617        let tri = [(0.0, 0.0), (10.0, 0.0), (5.0, 10.0)];
618        assert!(contains_point(&tri, (5.0, 3.0)));
619        assert!(!contains_point(&tri, (20.0, 5.0)));
620    }
621}