Skip to main content

exact_poly/
ring.rs

1//! Ring (closed polygon) utility functions.
2//!
3//! A ring is a Vec<[i64; 2]> of vertices in order (first != last, closing edge implied).
4//! All operations use exact integer arithmetic — no epsilon, no floating point.
5//!
6//! Input contract for decompose():
7//! - Rings must be CCW (counter-clockwise)
8//! - Rings must be simple (no self-intersections)
9//! - Collinear vertices should be removed for cleaner decomposition
10
11use crate::primitives::{cross2d, segments_properly_intersect};
12
13/// Twice the signed area of the ring (positive = CCW, negative = CW).
14/// Uses the shoelace formula with i128 intermediate arithmetic.
15/// NOT the same as twice_area_fp2 in area.rs — this preserves sign for orientation detection.
16pub fn signed_area_2x(ring: &[[i64; 2]]) -> i128 {
17    let n = ring.len();
18    if n < 3 {
19        return 0;
20    }
21    let mut sum: i128 = 0;
22    for i in 0..n {
23        let j = (i + 1) % n;
24        let xi = ring[i][0] as i128;
25        let yi = ring[i][1] as i128;
26        let xj = ring[j][0] as i128;
27        let yj = ring[j][1] as i128;
28        sum += xi * yj - xj * yi;
29    }
30    sum
31}
32
33/// True if the ring is counter-clockwise (positive signed area).
34pub fn is_ccw(ring: &[[i64; 2]]) -> bool {
35    signed_area_2x(ring) > 0
36}
37
38/// Ensure the ring is CCW. Reverses in place if CW.
39pub fn ensure_ccw(ring: &mut Vec<[i64; 2]>) {
40    if !is_ccw(ring) {
41        ring.reverse();
42    }
43}
44
45/// Remove collinear vertices from a ring.
46/// A vertex is collinear if cross(prev, curr, next) == 0 (exact integer test).
47/// Returns a new ring with collinear vertices removed.
48/// The result may have fewer vertices, but preserves the polygon shape exactly.
49pub fn remove_collinear(ring: &[[i64; 2]]) -> Vec<[i64; 2]> {
50    let n = ring.len();
51    if n < 3 {
52        return ring.to_vec();
53    }
54
55    let mut result: Vec<[i64; 2]> = Vec::with_capacity(n);
56
57    for i in 0..n {
58        let prev = ring[(i + n - 1) % n];
59        let curr = ring[i];
60        let next = ring[(i + 1) % n];
61
62        let cross = cross2d(prev[0], prev[1], curr[0], curr[1], next[0], next[1]);
63
64        if cross != 0 {
65            result.push(curr);
66        }
67    }
68
69    result
70}
71
72/// True if the ring has no self-intersections (is simple).
73/// O(n²) check — all non-adjacent edge pairs.
74/// A ring with n < 3 vertices is not simple.
75pub fn is_simple(ring: &[[i64; 2]]) -> bool {
76    let n = ring.len();
77    if n < 3 {
78        return false;
79    }
80
81    for i in 0..n {
82        let i1 = i;
83        let i2 = (i + 1) % n;
84        let a1 = ring[i1];
85        let a2 = ring[i2];
86
87        for j in (i + 2)..n {
88            // skip adjacent edges: edge 0 and edge n-1 share a vertex
89            if i == 0 && j == n - 1 {
90                continue;
91            }
92
93            let j1 = j;
94            let j2 = (j + 1) % n;
95            let b1 = ring[j1];
96            let b2 = ring[j2];
97
98            if segments_properly_intersect(a1[0], a1[1], a2[0], a2[1], b1[0], b1[1], b2[0], b2[1]) {
99                return false;
100            }
101        }
102    }
103
104    true
105}
106
107/// Normalize a ring: remove collinear vertices, ensure CCW winding.
108/// Does NOT check for simplicity (caller must ensure input is simple).
109/// Returns None if the result has fewer than 3 vertices (degenerate).
110pub fn normalize_ring(ring: &[[i64; 2]]) -> Option<Vec<[i64; 2]>> {
111    let mut result = remove_collinear(ring);
112    if result.len() < 3 {
113        return None;
114    }
115    ensure_ccw(&mut result);
116    Some(result)
117}
118
119/// Rotate ring so that index `start` becomes index 0. Preserves winding.
120pub fn rotate_ring(ring: &[[i64; 2]], start: usize) -> Vec<[i64; 2]> {
121    let n = ring.len();
122    if n == 0 || start == 0 {
123        return ring.to_vec();
124    }
125    let start = start % n;
126    let mut result = Vec::with_capacity(n);
127    result.extend_from_slice(&ring[start..]);
128    result.extend_from_slice(&ring[..start]);
129    result
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    const M: i64 = 1_000_000; // 1 meter
137
138    fn square_ccw() -> Vec<[i64; 2]> {
139        vec![[0, 0], [M, 0], [M, M], [0, M]]
140    }
141
142    fn square_cw() -> Vec<[i64; 2]> {
143        vec![[0, 0], [0, M], [M, M], [M, 0]]
144    }
145
146    #[test]
147    fn signed_area_ccw_is_positive() {
148        let ring = square_ccw();
149        assert!(signed_area_2x(&ring) > 0);
150    }
151
152    #[test]
153    fn signed_area_cw_is_negative() {
154        let ring = square_cw();
155        assert!(signed_area_2x(&ring) < 0);
156    }
157
158    #[test]
159    fn signed_area_magnitude_is_twice_area() {
160        let ring = square_ccw();
161        let area_2x = signed_area_2x(&ring).unsigned_abs();
162        // Square 1M x 1M = 1e12, twice = 2e12
163        assert_eq!(area_2x, 2 * (M as u128) * (M as u128));
164    }
165
166    #[test]
167    fn is_ccw_detects_winding() {
168        assert!(is_ccw(&square_ccw()));
169        assert!(!is_ccw(&square_cw()));
170    }
171
172    #[test]
173    fn ensure_ccw_reverses_cw_ring() {
174        let mut ring = square_cw();
175        ensure_ccw(&mut ring);
176        assert!(is_ccw(&ring));
177    }
178
179    #[test]
180    fn ensure_ccw_leaves_ccw_ring_unchanged() {
181        let ccw = square_ccw();
182        let mut ring = ccw.clone();
183        ensure_ccw(&mut ring);
184        assert_eq!(ring, ccw);
185    }
186
187    #[test]
188    fn remove_collinear_removes_midpoint_on_edge() {
189        // Square with extra vertex at midpoint of bottom edge
190        let ring = vec![[0, 0], [M / 2, 0], [M, 0], [M, M], [0, M]];
191        let result = remove_collinear(&ring);
192        // Midpoint at (M/2, 0) should be removed — collinear with (0,0) and (M,0)
193        assert_eq!(result.len(), 4);
194        assert!(!result.contains(&[M / 2, 0]));
195    }
196
197    #[test]
198    fn remove_collinear_preserves_non_collinear() {
199        let ring = square_ccw();
200        let result = remove_collinear(&ring);
201        assert_eq!(result, ring); // all 4 corners are non-collinear
202    }
203
204    #[test]
205    fn remove_collinear_exact_zero_check() {
206        // Nearly-collinear (but not exactly) should NOT be removed
207        let ring = vec![[0, 0], [M, 1], [2 * M, 0], [2 * M, M], [0, M]];
208        let result = remove_collinear(&ring);
209        // (M, 1) is not exactly collinear with (0,0) and (2M,0) — should be kept
210        assert_eq!(result.len(), 5);
211    }
212
213    #[test]
214    fn is_simple_convex_ring() {
215        assert!(is_simple(&square_ccw()));
216    }
217
218    #[test]
219    fn is_simple_self_intersecting() {
220        // Figure-8: self-intersecting
221        let ring = vec![[0, 0], [2 * M, 2 * M], [2 * M, 0], [0, 2 * M]];
222        assert!(!is_simple(&ring));
223    }
224
225    #[test]
226    fn normalize_ring_removes_collinear_and_ensures_ccw() {
227        // CW ring with collinear vertex
228        let ring = vec![[0, 0], [0, M], [0, 2 * M], [M, 2 * M], [M, 0]]; // CW, has collinear
229        let result = normalize_ring(&ring);
230        assert!(result.is_some());
231        let normalized = result.unwrap();
232        assert!(is_ccw(&normalized));
233        // Collinear vertex [0, M] should be removed (between [0,0] and [0,2M])
234        assert_eq!(normalized.len(), 4);
235    }
236
237    #[test]
238    fn rotate_ring_shifts_start() {
239        let ring = square_ccw();
240        let rotated = rotate_ring(&ring, 2);
241        assert_eq!(rotated[0], [M, M]);
242        assert_eq!(rotated.len(), 4);
243    }
244
245    #[test]
246    fn rotate_ring_zero_start_unchanged() {
247        let ring = square_ccw();
248        let rotated = rotate_ring(&ring, 0);
249        assert_eq!(rotated, ring);
250    }
251}