Skip to main content

exact_poly/
shared_edge.rs

1//! Shared edge detection and collinear contact classification.
2//! Ported from polygon.move::has_exact_shared_edge, edges_match_exactly,
3//! segments_contact, and shared_edge_relation.
4//!
5//! Used for topology validation (parts must connect via shared edges, not just
6//! vertices, and partial collinear contact is rejected as a T-junction).
7
8use crate::primitives::cross2d;
9
10/// Normalize an edge as an unordered pair with (min, max) ordering.
11/// Uses lexicographic comparison: (x1,y1) < (x2,y2) iff x1<x2 or (x1==x2 and y1<y2).
12/// Ensures edge A→B and B→A are treated as the same edge.
13///
14/// Matches polygon.move::normalize_edge + point_precedes.
15pub(crate) fn normalize_edge(a: [i64; 2], b: [i64; 2]) -> ([i64; 2], [i64; 2]) {
16    if a <= b {
17        (a, b)
18    } else {
19        (b, a)
20    }
21}
22
23/// True if polygon A and polygon B share at least one exact edge.
24/// An "exact shared edge" is an edge (v_i, v_{i+1}) that appears in both polygons,
25/// possibly in reverse order.
26///
27/// Matches polygon.move::has_exact_shared_edge() + edges_match_exactly().
28pub fn has_exact_shared_edge(a: &[[i64; 2]], b: &[[i64; 2]]) -> bool {
29    let na = a.len();
30    let nb = b.len();
31
32    for i in 0..na {
33        let j = (i + 1) % na;
34        let edge_a = normalize_edge(a[i], a[j]);
35
36        for k in 0..nb {
37            let l = (k + 1) % nb;
38            let edge_b = normalize_edge(b[k], b[l]);
39
40            if edge_a == edge_b {
41                return true;
42            }
43        }
44    }
45
46    false
47}
48
49/// True if two line segments are collinear and overlap along a non-degenerate interval.
50/// Contact means: parallel, collinear, and 1D intervals strictly overlap.
51/// Touching at a single endpoint is NOT contact (strict overlap required).
52///
53/// This detects partial edge contact (T-junctions etc.) not covered by exact edge matching.
54/// Used in classify_contact to detect collinear overlaps between polygon edges.
55pub fn segments_contact(
56    ax1: i64,
57    ay1: i64,
58    ax2: i64,
59    ay2: i64,
60    bx1: i64,
61    by1: i64,
62    bx2: i64,
63    by2: i64,
64) -> bool {
65    // Parallel check: cross of direction vectors must be 0
66    let dax = (ax2 as i128) - (ax1 as i128);
67    let day = (ay2 as i128) - (ay1 as i128);
68    let dbx = (bx2 as i128) - (bx1 as i128);
69    let dby = (by2 as i128) - (by1 as i128);
70
71    if dax * dby != day * dbx {
72        return false; // Not parallel
73    }
74
75    // Collinear check: b1 must lie on the line through a1→a2
76    let collinear = cross2d(ax1, ay1, ax2, ay2, bx1, by1);
77    if collinear != 0 {
78        return false;
79    }
80
81    // 1D overlap check along dominant axis (strict: lo < hi, not <=)
82    if dax.abs() > day.abs() {
83        // Project onto X axis
84        let (a_lo, a_hi) = (ax1.min(ax2), ax1.max(ax2));
85        let (b_lo, b_hi) = (bx1.min(bx2), bx1.max(bx2));
86        a_lo.max(b_lo) < a_hi.min(b_hi)
87    } else {
88        // Project onto Y axis
89        let (a_lo, a_hi) = (ay1.min(ay2), ay1.max(ay2));
90        let (b_lo, b_hi) = (by1.min(by2), by1.max(by2));
91        a_lo.max(b_lo) < a_hi.min(b_hi)
92    }
93}
94
95/// Classification of how two polygon parts touch along their boundaries.
96///
97/// Mirrors the three-way outcome of polygon.move::shared_edge_relation:
98/// exact shared edge (valid adjacency), no contact (independent), or partial
99/// collinear overlap (on-chain aborts `EInvalidMultipartContact`).
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum ContactKind {
102    /// No collinear overlap between any edge pair. Vertex-only contact is
103    /// classified here as well — callers that care must check vertices
104    /// separately.
105    None,
106    /// At least one edge appears in both parts (possibly reversed). This is
107    /// the only form of adjacency on-chain accepts.
108    SharedEdge,
109    /// Edges overlap on a non-degenerate interval without matching exactly
110    /// (T-junction). On-chain aborts `EInvalidMultipartContact`; in Rust the
111    /// caller must surface this as `TopologyError::UnsupportedContact`.
112    PartialContact,
113}
114
115/// Classify the contact between two polygon parts.
116///
117/// Matches polygon.move::shared_edge_relation's decision tree:
118///
119/// 1. If any edge pair matches exactly → `SharedEdge`.
120/// 2. Else if any edge pair has a non-degenerate collinear overlap →
121///    `PartialContact` (the on-chain abort case).
122/// 3. Otherwise → `None`.
123///
124/// Note: Move also performs `aabbs_may_contact` + SAT overlap checks before
125/// this point, but those are orthogonal (overlap → `EPartOverlap`). This
126/// function only classifies boundary contact.
127pub fn classify_contact(a: &[[i64; 2]], b: &[[i64; 2]]) -> ContactKind {
128    if has_exact_shared_edge(a, b) {
129        return ContactKind::SharedEdge;
130    }
131
132    let na = a.len();
133    let nb = b.len();
134    for i in 0..na {
135        let j = (i + 1) % na;
136        for k in 0..nb {
137            let l = (k + 1) % nb;
138            if segments_contact(
139                a[i][0], a[i][1], a[j][0], a[j][1], b[k][0], b[k][1], b[l][0], b[l][1],
140            ) {
141                return ContactKind::PartialContact;
142            }
143        }
144    }
145
146    ContactKind::None
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    const M: i64 = 1_000_000;
154
155    // ── normalize_edge ──────────────────────────────────────────────
156
157    #[test]
158    fn shared_edge_normalize_orders_lexicographically() {
159        // (0,0)→(M,M) already sorted
160        assert_eq!(normalize_edge([0, 0], [M, M]), ([0, 0], [M, M]));
161        // Reversed input should give same result
162        assert_eq!(normalize_edge([M, M], [0, 0]), ([0, 0], [M, M]));
163    }
164
165    #[test]
166    fn shared_edge_normalize_same_x_sorts_by_y() {
167        assert_eq!(normalize_edge([M, 2 * M], [M, 0]), ([M, 0], [M, 2 * M]));
168        assert_eq!(normalize_edge([M, 0], [M, 2 * M]), ([M, 0], [M, 2 * M]));
169    }
170
171    #[test]
172    fn shared_edge_normalize_degenerate_point() {
173        // Same point: both orderings give same result
174        assert_eq!(normalize_edge([5, 5], [5, 5]), ([5, 5], [5, 5]));
175    }
176
177    // ── has_exact_shared_edge ───────────────────────────────────────
178
179    #[test]
180    fn shared_edge_exact_detected() {
181        // A: left square, B: right square, sharing edge x=M
182        let a = vec![[0, 0], [M, 0], [M, M], [0, M]];
183        let b = vec![[M, 0], [2 * M, 0], [2 * M, M], [M, M]];
184        assert!(has_exact_shared_edge(&a, &b));
185    }
186
187    #[test]
188    fn shared_edge_exact_direction_independent() {
189        // Same edge but B has reversed winding
190        let a = vec![[0, 0], [M, 0], [M, M], [0, M]];
191        // B winding: (M,M)→(2M,M)→(2M,0)→(M,0)
192        // Edge (M,M)→(M,0) is reverse of A's (M,0)→(M,M)
193        let b = vec![[M, M], [2 * M, M], [2 * M, 0], [M, 0]];
194        assert!(has_exact_shared_edge(&a, &b));
195    }
196
197    #[test]
198    fn shared_edge_no_match_separated_squares() {
199        let a = vec![[0, 0], [M, 0], [M, M], [0, M]];
200        let b = vec![[3 * M, 0], [4 * M, 0], [4 * M, M], [3 * M, M]];
201        assert!(!has_exact_shared_edge(&a, &b));
202    }
203
204    #[test]
205    fn shared_edge_no_match_vertex_only_contact() {
206        // Diagonal neighbors share only corner (M,M)
207        let a = vec![[0, 0], [M, 0], [M, M], [0, M]];
208        let b = vec![[M, M], [2 * M, M], [2 * M, 2 * M], [M, 2 * M]];
209        assert!(!has_exact_shared_edge(&a, &b));
210    }
211
212    #[test]
213    fn shared_edge_triangles_sharing_hypotenuse() {
214        // Triangle A: (0,0), (M,0), (0,M) — hypotenuse (M,0)→(0,M)
215        // Triangle B: (M,M), (0,M), (M,0) — hypotenuse (0,M)→(M,0) reversed
216        let a = vec![[0, 0], [M, 0], [0, M]];
217        let b = vec![[M, M], [0, M], [M, 0]];
218        assert!(has_exact_shared_edge(&a, &b));
219    }
220
221    // ── segments_contact ────────────────────────────────────────────
222
223    #[test]
224    fn shared_edge_segments_collinear_overlap() {
225        // Horizontal on y=0: [0,2M] and [M,3M] overlap on [M,2M]
226        assert!(segments_contact(0, 0, 2 * M, 0, M, 0, 3 * M, 0));
227    }
228
229    #[test]
230    fn shared_edge_segments_collinear_overlap_vertical() {
231        // Vertical on x=0: [0,2M] and [M,3M] overlap on [M,2M]
232        assert!(segments_contact(0, 0, 0, 2 * M, 0, M, 0, 3 * M));
233    }
234
235    #[test]
236    fn shared_edge_segments_collinear_overlap_diagonal() {
237        // Diagonal: (0,0)→(4M,4M) and (M,M)→(3M,3M) — contained overlap
238        assert!(segments_contact(0, 0, 4 * M, 4 * M, M, M, 3 * M, 3 * M));
239    }
240
241    #[test]
242    fn shared_edge_segments_no_overlap_gap() {
243        // [0,M] and [2M,3M] on y=0 — collinear but gap
244        assert!(!segments_contact(0, 0, M, 0, 2 * M, 0, 3 * M, 0));
245    }
246
247    #[test]
248    fn shared_edge_segments_touching_endpoint_no_overlap() {
249        // [0,M] and [M,2M] on y=0 — touching at single point, NOT contact
250        assert!(!segments_contact(0, 0, M, 0, M, 0, 2 * M, 0));
251    }
252
253    #[test]
254    fn shared_edge_segments_not_collinear() {
255        // Perpendicular segments
256        assert!(!segments_contact(0, 0, M, 0, 0, 0, 0, M));
257    }
258
259    #[test]
260    fn shared_edge_segments_parallel_not_collinear() {
261        // Parallel but offset — not collinear
262        assert!(!segments_contact(0, 0, M, 0, 0, M, M, M));
263    }
264
265    #[test]
266    fn shared_edge_segments_reversed_direction() {
267        // Same overlap but segment B is reversed
268        assert!(segments_contact(0, 0, 2 * M, 0, 3 * M, 0, M, 0));
269    }
270
271    #[test]
272    fn shared_edge_segments_degenerate_point() {
273        // Zero-length segment can't have strict overlap
274        assert!(!segments_contact(M, 0, M, 0, 0, 0, 2 * M, 0));
275    }
276
277    // ── classify_contact ────────────────────────────────────────────
278
279    #[test]
280    fn classify_contact_via_exact_edge() {
281        let a = vec![[0, 0], [M, 0], [M, M], [0, M]];
282        let b = vec![[M, 0], [2 * M, 0], [2 * M, M], [M, M]];
283        assert_eq!(classify_contact(&a, &b), ContactKind::SharedEdge);
284    }
285
286    #[test]
287    fn shared_edge_no_match_partial_overlap_subsegment() {
288        let a = vec![[M, 0], [2 * M, 0], [2 * M, M], [M, M]];
289        let b = vec![[0, 0], [4 * M, 0], [4 * M, M], [0, M]];
290        assert!(!has_exact_shared_edge(&a, &b));
291    }
292
293    #[test]
294    fn shared_edge_no_match_collinear_but_disjoint_edges() {
295        let a = vec![[0, 0], [M, 0], [M, M], [0, M]];
296        let b = vec![[2 * M, 0], [3 * M, 0], [3 * M, M], [2 * M, M]];
297        assert!(!has_exact_shared_edge(&a, &b));
298    }
299
300    #[test]
301    fn classify_contact_none_for_separated_squares() {
302        let a = vec![[0, 0], [M, 0], [M, M], [0, M]];
303        let b = vec![[3 * M, 0], [4 * M, 0], [4 * M, M], [3 * M, M]];
304        assert_eq!(classify_contact(&a, &b), ContactKind::None);
305    }
306
307    #[test]
308    fn classify_contact_symmetric_shared_edge() {
309        let a = vec![[0, 0], [M, 0], [M, M], [0, M]];
310        let b = vec![[M, 0], [2 * M, 0], [2 * M, M], [M, M]];
311        assert_eq!(classify_contact(&a, &b), classify_contact(&b, &a));
312    }
313
314    /// Rectangle base [(0,0)-(4M,0)-(4M,M)-(0,M)] and triangle roof
315    /// [(M,M),(3M,M),(2M,2M)]. Triangle's bottom edge (M,M)→(3M,M) is
316    /// collinear with rectangle's top edge (4M,M)→(0,M) but not an exact
317    /// match — classic T-junction.
318    #[test]
319    fn classify_contact_partial_edge_is_t_junction() {
320        let a = vec![[0, 0], [4 * M, 0], [4 * M, M], [0, M]];
321        let b = vec![[M, M], [3 * M, M], [2 * M, 2 * M]];
322        assert_eq!(classify_contact(&a, &b), ContactKind::PartialContact);
323    }
324
325    #[test]
326    fn classify_contact_t_junction_symmetric() {
327        let a = vec![[0, 0], [4 * M, 0], [4 * M, M], [0, M]];
328        let b = vec![[M, M], [3 * M, M], [2 * M, 2 * M]];
329        assert_eq!(classify_contact(&a, &b), classify_contact(&b, &a));
330    }
331
332    /// Vertex-only touch is not a "shared edge" and not a "partial contact"
333    /// (no 1D overlap). Callers that want to reject vertex touching must do
334    /// so via a separate vertex check.
335    #[test]
336    fn classify_contact_vertex_only_is_none() {
337        let a = vec![[0, 0], [M, 0], [M, M], [0, M]];
338        let b = vec![[M, M], [2 * M, M], [2 * M, 2 * M], [M, 2 * M]];
339        assert_eq!(classify_contact(&a, &b), ContactKind::None);
340    }
341
342    #[test]
343    fn classify_contact_vertex_only_sharing_one_corner_is_none() {
344        let a = vec![[0, 0], [2 * M, 0], [2 * M, 2 * M], [0, 2 * M]];
345        let b = vec![
346            [2 * M, 2 * M],
347            [4 * M, 2 * M],
348            [4 * M, 4 * M],
349            [2 * M, 4 * M],
350        ];
351        assert_eq!(classify_contact(&a, &b), ContactKind::None);
352    }
353
354    #[test]
355    fn classify_contact_t_junction_returns_partial_contact() {
356        let a = vec![[0, 0], [4 * M, 0], [4 * M, M], [0, M]];
357        let b = vec![[M, M], [3 * M, M], [3 * M, 2 * M], [M, 2 * M]];
358        assert_eq!(classify_contact(&a, &b), ContactKind::PartialContact);
359    }
360}