Skip to main content

exact_poly/
spatial.rs

1//! Point-in-polygon and segment intersection for convex polygons.
2
3use crate::primitives::{cross2d, point_on_segment};
4
5/// True if point (px, py) is strictly inside the convex polygon.
6/// Uses cross-product sign consistency: for CCW polygon, point is inside iff
7/// it's on the left side of ALL edges (all cross products positive).
8///
9/// Matches polygon.move::point_inside_convex_part_or_on_boundary behavior
10/// but this is strict interior only (returns false for boundary).
11/// Reference: overlap.ts::pointStrictlyInsideConvex (lines 7-19)
12pub fn point_strictly_inside_convex(px: i64, py: i64, ring: &[[i64; 2]]) -> bool {
13    let n = ring.len();
14    if n < 3 {
15        return false;
16    }
17    let mut all_pos = true;
18    let mut all_neg = true;
19    for i in 0..n {
20        let j = (i + 1) % n;
21        let cp = cross2d(ring[i][0], ring[i][1], ring[j][0], ring[j][1], px, py);
22        if cp <= 0 {
23            all_pos = false;
24        }
25        if cp >= 0 {
26            all_neg = false;
27        }
28    }
29    all_pos || all_neg
30}
31
32/// True if point (px, py) is on the boundary of the polygon (on any edge).
33pub fn point_on_polygon_boundary(px: i64, py: i64, ring: &[[i64; 2]]) -> bool {
34    let n = ring.len();
35    for i in 0..n {
36        let j = (i + 1) % n;
37        if point_on_segment(px, py, ring[i][0], ring[i][1], ring[j][0], ring[j][1]) {
38            return true;
39        }
40    }
41    false
42}
43
44/// Ray-casting point-in-polygon for general (including non-convex) polygons.
45/// Counts how many times a rightward ray from (px, py) crosses the polygon edges.
46/// Odd count = inside. Uses exact integer arithmetic — no division, no float.
47fn point_inside_polygon_ray_cast(px: i64, py: i64, ring: &[[i64; 2]]) -> bool {
48    let n = ring.len();
49    if n < 3 {
50        return false;
51    }
52    let mut crossings = 0i32;
53    for i in 0..n {
54        let j = (i + 1) % n;
55        let ax = ring[i][0];
56        let ay = ring[i][1];
57        let bx = ring[j][0];
58        let by = ring[j][1];
59        // Edge crosses the ray's horizontal level (asymmetric to handle vertices correctly)
60        if (ay > py) != (by > py) {
61            // x_intersect = ax + (py - ay) * (bx - ax) / (by - ay)
62            // x_intersect > px  without division:
63            //   (py - ay) * (bx - ax) [vs] (px - ax) * (by - ay)
64            //   flip inequality when (by - ay) < 0
65            let lhs = (py as i128 - ay as i128) * (bx as i128 - ax as i128);
66            let rhs = (px as i128 - ax as i128) * (by as i128 - ay as i128);
67            let to_right = if by > ay { lhs > rhs } else { lhs < rhs };
68            if to_right {
69                crossings += 1;
70            }
71        }
72    }
73    crossings % 2 == 1
74}
75
76/// True if point is inside OR on the boundary of a polygon (convex or non-convex).
77pub fn point_inside_or_on_boundary(px: i64, py: i64, ring: &[[i64; 2]]) -> bool {
78    point_on_polygon_boundary(px, py, ring) || point_inside_polygon_ray_cast(px, py, ring)
79}
80
81/// Collinear segment overlap: true if two collinear segments share more than a point.
82///
83/// Steps:
84/// 1. Segments must be parallel (same direction or opposite)
85/// 2. Must be collinear (b1 lies on line through a1→a2)
86/// 3. 1D projections must strictly overlap (not just touch)
87/// 4. Interiors must be on the same side of the shared line (area overlap, not adjacency)
88///
89/// Reference: overlap.ts::collinearEdgesOverlapArea (lines 38-78)
90pub fn collinear_segments_overlap_area(
91    a1x: i64,
92    a1y: i64,
93    a2x: i64,
94    a2y: i64,
95    b1x: i64,
96    b1y: i64,
97    b2x: i64,
98    b2y: i64,
99    a_ring: &[[i64; 2]], // full polygon A for interior side check
100    b_ring: &[[i64; 2]], // full polygon B
101) -> bool {
102    let dax = (a2x as i128) - (a1x as i128);
103    let day = (a2y as i128) - (a1y as i128);
104    let dbx = (b2x as i128) - (b1x as i128);
105    let dby = (b2y as i128) - (b1y as i128);
106
107    // Must be parallel: cross of directions == 0
108    if dax * dby != day * dbx {
109        return false;
110    }
111
112    // Must be collinear: b1 lies on the line through a1→a2
113    let collinear_check = cross2d(a1x, a1y, a2x, a2y, b1x, b1y);
114    if collinear_check != 0 {
115        return false;
116    }
117
118    // Strict 1D interval overlap along dominant axis
119    let has_overlap = if dax != 0 || dbx != 0 {
120        // Horizontal-ish: project onto X
121        let (a_lo, a_hi) = (a1x.min(a2x), a1x.max(a2x));
122        let (b_lo, b_hi) = (b1x.min(b2x), b1x.max(b2x));
123        a_lo.max(b_lo) < a_hi.min(b_hi)
124    } else {
125        // Vertical: project onto Y
126        let (a_lo, a_hi) = (a1y.min(a2y), a1y.max(a2y));
127        let (b_lo, b_hi) = (b1y.min(b2y), b1y.max(b2y));
128        a_lo.max(b_lo) < a_hi.min(b_hi)
129    };
130
131    if !has_overlap {
132        return false;
133    }
134
135    // Interior side check: find first off-line vertex of each polygon
136    let mut side_a: i128 = 0;
137    for point in a_ring {
138        let cp = cross2d(a1x, a1y, a2x, a2y, point[0], point[1]);
139        if cp != 0 {
140            side_a = cp;
141            break;
142        }
143    }
144
145    let mut side_b: i128 = 0;
146    for point in b_ring {
147        let cp = cross2d(a1x, a1y, a2x, a2y, point[0], point[1]);
148        if cp != 0 {
149            side_b = cp;
150            break;
151        }
152    }
153
154    if side_a == 0 || side_b == 0 {
155        return false;
156    } // degenerate
157
158    // Same side = interiors interpenetrate = area overlap
159    // Opposite side = adjacent parcels sharing edge = no area overlap
160    (side_a > 0) == (side_b > 0)
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    const M: i64 = 1_000_000;
168
169    fn square() -> Vec<[i64; 2]> {
170        vec![[0, 0], [M, 0], [M, M], [0, M]]
171    }
172
173    fn rhombus() -> Vec<[i64; 2]> {
174        vec![[0, 4 * M], [2 * M, 0], [0, -4 * M], [-2 * M, 0]]
175    }
176
177    #[test]
178    fn point_strictly_inside() {
179        assert!(point_strictly_inside_convex(M / 2, M / 2, &square()));
180    }
181
182    #[test]
183    fn point_strictly_inside_convex_rhombus_centroid_and_edge_neighbors() {
184        let ring = rhombus();
185
186        assert!(point_strictly_inside_convex(0, 0, &ring));
187
188        let edge_mid_x = M;
189        let edge_mid_y = 2 * M;
190        assert!(point_strictly_inside_convex(
191            edge_mid_x - 2,
192            edge_mid_y - 1,
193            &ring
194        ));
195        assert!(!point_strictly_inside_convex(
196            edge_mid_x + 2,
197            edge_mid_y + 1,
198            &ring
199        ));
200    }
201
202    #[test]
203    fn point_on_boundary_not_strictly_inside() {
204        // Point on edge: (0.5M, 0)
205        assert!(!point_strictly_inside_convex(M / 2, 0, &square()));
206    }
207
208    #[test]
209    fn point_at_vertex_not_strictly_inside() {
210        assert!(!point_strictly_inside_convex(0, 0, &square()));
211    }
212
213    #[test]
214    fn point_outside() {
215        assert!(!point_strictly_inside_convex(2 * M, 2 * M, &square()));
216    }
217
218    #[test]
219    fn point_inside_or_on_boundary_interior() {
220        assert!(point_inside_or_on_boundary(M / 2, M / 2, &square()));
221    }
222
223    #[test]
224    fn point_inside_or_on_boundary_edge() {
225        assert!(point_inside_or_on_boundary(M / 2, 0, &square()));
226    }
227
228    #[test]
229    fn point_inside_or_on_boundary_vertex() {
230        assert!(point_inside_or_on_boundary(0, 0, &square()));
231    }
232
233    #[test]
234    fn point_inside_or_on_boundary_outside() {
235        assert!(!point_inside_or_on_boundary(2 * M, 0, &square()));
236    }
237
238    #[test]
239    fn point_on_polygon_boundary_edge_midpoint_and_off_edge() {
240        let ring = rhombus();
241
242        assert!(point_on_polygon_boundary(M, 2 * M, &ring));
243        assert!(!point_on_polygon_boundary(M + 2, 2 * M + 1, &ring));
244    }
245
246    #[test]
247    fn point_inside_or_on_boundary_inclusive_cases() {
248        let ring = rhombus();
249
250        assert!(point_inside_or_on_boundary(0, 4 * M, &ring));
251        assert!(point_inside_or_on_boundary(M, 2 * M, &ring));
252        assert!(point_inside_or_on_boundary(0, 0, &ring));
253        assert!(!point_inside_or_on_boundary(3 * M, 3 * M, &ring));
254    }
255
256    // L-shape: non-convex polygon — the original convex algorithm failed here
257    fn l_shape() -> Vec<[i64; 2]> {
258        vec![
259            [0, 0], [60 * M, 0], [60 * M, 40 * M], [30 * M, 40 * M],
260            [30 * M, 80 * M], [0, 80 * M],
261        ]
262    }
263
264    #[test]
265    fn point_inside_or_on_boundary_non_convex_l_shape_interior() {
266        let ring = l_shape();
267        // Bottom-left interior
268        assert!(point_inside_or_on_boundary(15 * M, 20 * M, &ring));
269        // Upper-left interior (above the step)
270        assert!(point_inside_or_on_boundary(15 * M, 60 * M, &ring));
271        // Outside: inside the "missing" rectangle top-right
272        assert!(!point_inside_or_on_boundary(45 * M, 60 * M, &ring));
273    }
274
275    #[test]
276    fn ray_cast_non_convex_star_interior() {
277        // Simple concave shape: arrow-like [[0,30],[60,30],[60,0],[100,50],[60,100],[60,70],[0,70]]
278        let arrow: Vec<[i64; 2]> = vec![
279            [0, 30 * M], [60 * M, 30 * M], [60 * M, 0],
280            [100 * M, 50 * M], [60 * M, 100 * M], [60 * M, 70 * M], [0, 70 * M],
281        ];
282        // Interior of the shaft
283        assert!(point_inside_or_on_boundary(30 * M, 50 * M, &arrow));
284        // Outside (above shaft, left of arrowhead)
285        assert!(!point_inside_or_on_boundary(30 * M, 90 * M, &arrow));
286    }
287
288    #[test]
289    fn collinear_overlap_same_side_returns_true() {
290        // Two squares overlapping via collinear edges
291        // Polygon A: bottom row (0,0)→(2M,0)→(2M,M)→(0,M)
292        // Polygon B: same bottom edge but offset right: (M,0)→(3M,0)→(3M,M)→(M,M)
293        // Edge A: (0,0)→(2M,0), Edge B: (M,0)→(3M,0) — same horizontal line, overlap at M..2M
294        // Both polygons are ABOVE the shared edge line (same side)
295        let a_ring = vec![[0, 0], [2 * M, 0], [2 * M, M], [0, M]];
296        let b_ring = vec![[M, 0], [3 * M, 0], [3 * M, M], [M, M]];
297
298        let result = collinear_segments_overlap_area(
299            0,
300            0,
301            2 * M,
302            0, // edge from A
303            M,
304            0,
305            3 * M,
306            0, // edge from B
307            &a_ring,
308            &b_ring,
309        );
310        // Both rectangles are above y=0 — same side — area overlap → true
311        assert!(result);
312    }
313
314    #[test]
315    fn adjacent_polygons_opposite_sides_no_overlap() {
316        // Two rectangles sharing the x-axis edge: A above, B below
317        let a_ring = vec![[0, 0], [2 * M, 0], [2 * M, M], [0, M]]; // above y=0
318        let b_ring = vec![[0, 0], [2 * M, 0], [2 * M, -M], [0, -M]]; // below y=0
319
320        let result =
321            collinear_segments_overlap_area(0, 0, 2 * M, 0, 0, 0, 2 * M, 0, &a_ring, &b_ring);
322        // Opposite sides → adjacent parcels → no area overlap → false
323        assert!(!result);
324    }
325
326    #[test]
327    fn non_collinear_segments_return_false() {
328        // Perpendicular edges — not collinear
329        let a_ring = vec![[0, 0], [M, 0], [M, M], [0, M]]; // square A
330        let b_ring = vec![[2 * M, 0], [3 * M, 0], [3 * M, M], [2 * M, M]]; // square B far away
331
332        let result = collinear_segments_overlap_area(
333            0,
334            0,
335            M,
336            0, // horizontal edge from A
337            2 * M,
338            0,
339            2 * M,
340            M, // vertical edge from B — NOT collinear
341            &a_ring,
342            &b_ring,
343        );
344        assert!(!result);
345    }
346}