Skip to main content

exact_poly/
containment.rs

1//! Polygon containment checking.
2//!
3//! Determines if one polygon (set of convex parts) is fully inside another.
4//! Uses vertex checking plus edge midpoint sampling (t=1/3, t=2/3) for
5//! multi-part outer polygons where the composite shape may be concave.
6//!
7//! Ported from polygon.move::contains_polygon / part_is_inside_parts.
8
9use crate::aabb::Aabb;
10use crate::spatial::point_inside_or_on_boundary;
11
12/// True if point (x, y) is inside any of the given convex parts.
13pub fn point_inside_any_part(parts: &[Vec<[i64; 2]>], x: i64, y: i64) -> bool {
14    for part in parts {
15        if point_inside_or_on_boundary(x, y, part) {
16            return true;
17        }
18    }
19    false
20}
21
22/// True if the inner polygon (set of parts) is fully contained within the outer polygon.
23///
24/// Algorithm (matching polygon.move::contains_polygon):
25/// 1. AABB pre-filter: inner AABB must fit within outer AABB
26/// 2. All inner vertices must be inside some outer part
27/// 3. For multi-part outers only: sample edge points at t=1/3 and t=2/3 of each
28///    inner edge (handles concave composite shapes where an inner edge might bridge
29///    two outer parts through a gap)
30///
31/// Integer division for sample points: `(2*a + b) / 3` and `(a + 2*b) / 3`.
32/// Single-part outers skip edge sampling (convex ⊂ convex is proven by vertices).
33pub fn contains_polygon(outer_parts: &[Vec<[i64; 2]>], inner_parts: &[Vec<[i64; 2]>]) -> bool {
34    if outer_parts.is_empty() || inner_parts.is_empty() {
35        return false;
36    }
37
38    // AABB pre-filter
39    let outer_aabb = compute_multi_part_aabb(outer_parts);
40    let inner_aabb = compute_multi_part_aabb(inner_parts);
41
42    if inner_aabb.min_x < outer_aabb.min_x
43        || inner_aabb.max_x > outer_aabb.max_x
44        || inner_aabb.min_y < outer_aabb.min_y
45        || inner_aabb.max_y > outer_aabb.max_y
46    {
47        return false;
48    }
49
50    // Check each inner part is fully inside the outer polygon
51    for part in inner_parts {
52        if !part_is_inside(outer_parts, part) {
53            return false;
54        }
55    }
56
57    true
58}
59
60/// Check that a single inner part is fully inside the outer parts.
61/// Matches polygon.move::part_is_inside_parts.
62fn part_is_inside(outer_parts: &[Vec<[i64; 2]>], inner_part: &[[i64; 2]]) -> bool {
63    // All vertices must be inside some outer part
64    for &v in inner_part {
65        if !point_inside_any_part(outer_parts, v[0], v[1]) {
66            return false;
67        }
68    }
69
70    // Edge sampling only for multi-part outers (concave composite shapes).
71    // Single-part convex outers: vertex check is sufficient.
72    if outer_parts.len() > 1 {
73        let n = inner_part.len();
74        for i in 0..n {
75            let j = (i + 1) % n;
76            let x1 = inner_part[i][0];
77            let y1 = inner_part[i][1];
78            let x2 = inner_part[j][0];
79            let y2 = inner_part[j][1];
80
81            // t=1/3: (2*x1 + x2) / 3
82            let sx1 = (2 * x1 + x2) / 3;
83            let sy1 = (2 * y1 + y2) / 3;
84            if !point_inside_any_part(outer_parts, sx1, sy1) {
85                return false;
86            }
87
88            // t=2/3: (x1 + 2*x2) / 3
89            let sx2 = (x1 + 2 * x2) / 3;
90            let sy2 = (y1 + 2 * y2) / 3;
91            if !point_inside_any_part(outer_parts, sx2, sy2) {
92                return false;
93            }
94        }
95    }
96
97    true
98}
99
100/// Compute the bounding AABB of all parts combined.
101fn compute_multi_part_aabb(parts: &[Vec<[i64; 2]>]) -> Aabb {
102    let ring: Vec<[i64; 2]> = parts.iter().flat_map(|p| p.iter().copied()).collect();
103    Aabb::from_ring(&ring)
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    const M: i64 = 1_000_000;
111
112    fn square(ox: i64, oy: i64, size: i64) -> Vec<[i64; 2]> {
113        vec![
114            [ox, oy],
115            [ox + size, oy],
116            [ox + size, oy + size],
117            [ox, oy + size],
118        ]
119    }
120
121    #[test]
122    fn small_square_inside_large_square() {
123        let outer = vec![square(0, 0, 10 * M)];
124        let inner = vec![square(2 * M, 2 * M, 3 * M)];
125        assert!(contains_polygon(&outer, &inner));
126    }
127
128    #[test]
129    fn touching_outer_boundary_is_not_contained() {
130        let outer = vec![square(0, 0, 5 * M), square(7 * M, 0, 5 * M)];
131        let inner = vec![square(5 * M, 2 * M, 4 * M)];
132
133        assert!(!contains_polygon(&outer, &inner));
134    }
135
136    #[test]
137    fn partially_outside_returns_false() {
138        let outer = vec![square(0, 0, 10 * M)];
139        let inner = vec![square(8 * M, 8 * M, 5 * M)]; // extends beyond outer
140        assert!(!contains_polygon(&outer, &inner));
141    }
142
143    #[test]
144    fn completely_outside_returns_false() {
145        let outer = vec![square(0, 0, M)];
146        let inner = vec![square(3 * M, 3 * M, M)];
147        assert!(!contains_polygon(&outer, &inner));
148    }
149
150    #[test]
151    fn same_polygon_contains_itself() {
152        let polygon = vec![square(0, 0, 10 * M)];
153        assert!(contains_polygon(&polygon, &polygon));
154    }
155
156    #[test]
157    fn multipart_outer_contains_inner() {
158        // Two-part outer (two adjacent rectangles)
159        let outer = vec![square(0, 0, 10 * M), square(10 * M, 0, 10 * M)];
160        // Inner well inside the right part
161        let inner = vec![square(12 * M, 2 * M, 5 * M)];
162        assert!(contains_polygon(&outer, &inner));
163    }
164
165    #[test]
166    fn multipart_outer_inner_bridges_gap_returns_false() {
167        // Two outer parts with a gap between them
168        // Left part: (0,0)-(5M, 10M), Right part: (7M,0)-(12M, 10M)
169        // Gap from x=5M to x=7M
170        let outer = vec![square(0, 0, 5 * M), square(7 * M, 0, 5 * M)];
171        // Inner spans the gap: x=4M to x=8M
172        let inner = vec![square(4 * M, 2 * M, 4 * M)];
173        assert!(!contains_polygon(&outer, &inner));
174    }
175
176    #[test]
177    fn edge_sampling_catches_concave_bridge() {
178        // Two outer parts that share an edge but form an L-shape.
179        // Bottom: (0,0)-(10M,5M), Right: (5M,5M)-(10M,10M)
180        // Inner triangle whose vertices are all inside but edge crosses the void.
181        let outer = vec![
182            vec![[0, 0], [10 * M, 0], [10 * M, 5 * M], [0, 5 * M]],
183            vec![
184                [5 * M, 5 * M],
185                [10 * M, 5 * M],
186                [10 * M, 10 * M],
187                [5 * M, 10 * M],
188            ],
189        ];
190        // All 3 vertices inside parts, but edge (2M,4M)→(6M,6M) crosses void.
191        // t=2/3 sample lands at (4_666_666, 5_333_333) — outside both parts.
192        let inner = vec![vec![[2 * M, 4 * M], [6 * M, 6 * M], [6 * M, 4 * M]]];
193        assert!(!contains_polygon(&outer, &inner));
194    }
195
196    #[test]
197    fn empty_outer_returns_false() {
198        let inner = vec![square(0, 0, M)];
199        assert!(!contains_polygon(&[], &inner));
200    }
201
202    #[test]
203    fn empty_inner_returns_false() {
204        let outer = vec![square(0, 0, M)];
205        assert!(!contains_polygon(&outer, &[]));
206    }
207
208    #[test]
209    fn point_inside_any_part_works() {
210        let parts = vec![
211            square(0, 0, 10 * M),
212            square(12 * M, 0, 10 * M),
213            square(24 * M, 0, 10 * M),
214        ];
215        assert!(point_inside_any_part(&parts, 5 * M, 5 * M));
216        assert!(point_inside_any_part(&parts, 17 * M, 5 * M));
217        assert!(point_inside_any_part(&parts, 12 * M, 5 * M));
218        assert!(!point_inside_any_part(&parts, 35 * M, 5 * M));
219    }
220
221    #[test]
222    fn multipart_inner_fully_contained() {
223        let outer = vec![square(0, 0, 20 * M)];
224        let inner = vec![square(1 * M, 1 * M, 3 * M), square(5 * M, 5 * M, 3 * M)];
225        assert!(contains_polygon(&outer, &inner));
226    }
227
228    #[test]
229    fn multipart_inner_one_part_outside() {
230        let outer = vec![square(0, 0, 10 * M)];
231        let inner = vec![square(1 * M, 1 * M, 3 * M), square(15 * M, 15 * M, 3 * M)];
232        assert!(!contains_polygon(&outer, &inner));
233    }
234}