Skip to main content

u_geometry/
collision.rs

1//! Collision detection for 2D polygons.
2//!
3//! # Algorithms
4//!
5//! - **SAT (Separating Axis Theorem)**: Exact overlap test for convex polygons.
6//!   For concave polygons, it tests the convex hull approximation.
7//! - **AABB broad phase**: Fast rejection using bounding boxes.
8//!
9//! # References
10//!
11//! - Ericson (2005), "Real-Time Collision Detection", Ch. 4.4
12//! - Gottschalk, Lin, Manocha (1996), "OBB-Tree: A Hierarchical Structure
13//!   for Rapid Interference Detection"
14
15use crate::primitives::{AABB2, AABB3};
16
17/// Checks if two convex polygons overlap using the Separating Axis Theorem.
18///
19/// Returns `true` if the polygons overlap (not just touching).
20/// Uses a tolerance to allow touching without reporting overlap.
21///
22/// For concave polygons, this tests the convex hull — it may produce
23/// false positives but never false negatives.
24///
25/// # Complexity
26/// O(n + m) where n, m are vertex counts
27///
28/// # Reference
29/// Ericson (2005), Real-Time Collision Detection, Ch. 4.4
30pub fn polygons_overlap(poly_a: &[(f64, f64)], poly_b: &[(f64, f64)]) -> bool {
31    polygons_overlap_with_tolerance(poly_a, poly_b, 1e-6)
32}
33
34/// SAT overlap test with configurable tolerance.
35///
36/// Returns `true` if the polygons overlap by more than `tolerance`.
37pub fn polygons_overlap_with_tolerance(
38    poly_a: &[(f64, f64)],
39    poly_b: &[(f64, f64)],
40    tolerance: f64,
41) -> bool {
42    if poly_a.len() < 3 || poly_b.len() < 3 {
43        return false;
44    }
45
46    // Broad phase: AABB check
47    if let (Some(aabb_a), Some(aabb_b)) = (aabb_from_tuples(poly_a), aabb_from_tuples(poly_b)) {
48        let expanded_a = aabb_a.expand(tolerance);
49        if !expanded_a.intersects(&aabb_b) {
50            return false;
51        }
52    }
53
54    // SAT: test edges from both polygons
55    for polygon in [poly_a, poly_b] {
56        let n = polygon.len();
57        for i in 0..n {
58            let j = (i + 1) % n;
59            let edge_x = polygon[j].0 - polygon[i].0;
60            let edge_y = polygon[j].1 - polygon[i].1;
61
62            // Axis = normal to edge (perpendicular)
63            let len = (edge_x * edge_x + edge_y * edge_y).sqrt();
64            if len < 1e-15 {
65                continue;
66            }
67            let axis = (-edge_y / len, edge_x / len);
68
69            // Project both polygons onto axis
70            let (min_a, max_a) = project_on_axis(poly_a, axis);
71            let (min_b, max_b) = project_on_axis(poly_b, axis);
72
73            // Check for separation gap.
74            // Overlap on this axis = min(max_a, max_b) - max(min_a, min_b)
75            // If overlap <= tolerance, treat as separated (allows touching).
76            let overlap = max_a.min(max_b) - min_a.max(min_b);
77            if overlap < tolerance {
78                return false; // Separating axis found → no overlap
79            }
80        }
81    }
82
83    true // No separating axis found → overlap
84}
85
86/// Computes the overlap depth (penetration) between two convex polygons.
87///
88/// Returns the minimum translation distance to separate the polygons,
89/// or `None` if they don't overlap.
90///
91/// # Complexity
92/// O(n + m)
93pub fn overlap_depth(poly_a: &[(f64, f64)], poly_b: &[(f64, f64)]) -> Option<f64> {
94    if poly_a.len() < 3 || poly_b.len() < 3 {
95        return None;
96    }
97
98    let mut min_depth = f64::INFINITY;
99
100    for polygon in [poly_a, poly_b] {
101        let n = polygon.len();
102        for i in 0..n {
103            let j = (i + 1) % n;
104            let edge_x = polygon[j].0 - polygon[i].0;
105            let edge_y = polygon[j].1 - polygon[i].1;
106
107            let len = (edge_x * edge_x + edge_y * edge_y).sqrt();
108            if len < 1e-15 {
109                continue;
110            }
111            let axis = (-edge_y / len, edge_x / len);
112
113            let (min_a, max_a) = project_on_axis(poly_a, axis);
114            let (min_b, max_b) = project_on_axis(poly_b, axis);
115
116            let overlap = (max_a.min(max_b) - min_a.max(min_b)).max(0.0);
117            if overlap <= 0.0 {
118                return None; // Separated
119            }
120            min_depth = min_depth.min(overlap);
121        }
122    }
123
124    if min_depth < f64::INFINITY {
125        Some(min_depth)
126    } else {
127        None
128    }
129}
130
131/// Checks if two AABBs overlap (broad-phase test).
132///
133/// # Complexity
134/// O(1)
135#[inline]
136pub fn aabb_overlap(a: &AABB2, b: &AABB2) -> bool {
137    a.intersects(b)
138}
139
140/// Projects a polygon onto an axis and returns (min, max) extent.
141#[inline]
142fn project_on_axis(polygon: &[(f64, f64)], axis: (f64, f64)) -> (f64, f64) {
143    let mut min_proj = f64::INFINITY;
144    let mut max_proj = f64::NEG_INFINITY;
145
146    for &(x, y) in polygon {
147        let proj = x * axis.0 + y * axis.1;
148        min_proj = min_proj.min(proj);
149        max_proj = max_proj.max(proj);
150    }
151
152    (min_proj, max_proj)
153}
154
155// ======================== 3D Collision ========================
156
157/// Checks if two 3D AABBs overlap.
158///
159/// # Complexity
160/// O(1)
161#[inline]
162pub fn aabb3_overlap(a: &AABB3, b: &AABB3) -> bool {
163    a.intersects(b)
164}
165
166/// Checks if two 3D AABBs overlap with a tolerance margin.
167///
168/// Returns `true` if the boxes overlap by more than `tolerance` on all axes.
169/// This allows touching (overlap ≤ tolerance) without reporting collision.
170///
171/// # Complexity
172/// O(1)
173pub fn aabb3_overlap_with_tolerance(a: &AABB3, b: &AABB3, tolerance: f64) -> bool {
174    let overlap_x = a.max.x.min(b.max.x) - a.min.x.max(b.min.x);
175    let overlap_y = a.max.y.min(b.max.y) - a.min.y.max(b.min.y);
176    let overlap_z = a.max.z.min(b.max.z) - a.min.z.max(b.min.z);
177    overlap_x > tolerance && overlap_y > tolerance && overlap_z > tolerance
178}
179
180/// Checks if a 3D AABB is fully contained within a boundary AABB.
181///
182/// Returns `true` if `inner` fits completely inside `boundary`.
183///
184/// # Complexity
185/// O(1)
186#[inline]
187pub fn aabb3_within(inner: &AABB3, boundary: &AABB3) -> bool {
188    boundary.contains(inner)
189}
190
191/// Checks if a 3D AABB fits within a boundary with a margin.
192///
193/// Returns `true` if `inner` fits inside `boundary` shrunk by `margin`.
194///
195/// # Complexity
196/// O(1)
197pub fn aabb3_within_with_margin(inner: &AABB3, boundary: &AABB3, margin: f64) -> bool {
198    inner.min.x >= boundary.min.x + margin
199        && inner.min.y >= boundary.min.y + margin
200        && inner.min.z >= boundary.min.z + margin
201        && inner.max.x <= boundary.max.x - margin
202        && inner.max.y <= boundary.max.y - margin
203        && inner.max.z <= boundary.max.z - margin
204}
205
206/// Computes AABB from tuple points.
207fn aabb_from_tuples(points: &[(f64, f64)]) -> Option<AABB2> {
208    let first = points.first()?;
209    let mut min_x = first.0;
210    let mut min_y = first.1;
211    let mut max_x = first.0;
212    let mut max_y = first.1;
213
214    for &(x, y) in points.iter().skip(1) {
215        min_x = min_x.min(x);
216        min_y = min_y.min(y);
217        max_x = max_x.max(x);
218        max_y = max_y.max(y);
219    }
220
221    Some(AABB2::new(min_x, min_y, max_x, max_y))
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    fn square(x: f64, y: f64, size: f64) -> Vec<(f64, f64)> {
229        vec![(x, y), (x + size, y), (x + size, y + size), (x, y + size)]
230    }
231
232    fn triangle(x: f64, y: f64, size: f64) -> Vec<(f64, f64)> {
233        vec![(x, y), (x + size, y), (x + size / 2.0, y + size)]
234    }
235
236    #[test]
237    fn test_overlapping_squares() {
238        let a = square(0.0, 0.0, 10.0);
239        let b = square(5.0, 5.0, 10.0);
240        assert!(polygons_overlap(&a, &b));
241    }
242
243    #[test]
244    fn test_separated_squares() {
245        let a = square(0.0, 0.0, 10.0);
246        let b = square(20.0, 0.0, 10.0);
247        assert!(!polygons_overlap(&a, &b));
248    }
249
250    #[test]
251    fn test_touching_squares() {
252        // Touching = not overlapping (within tolerance)
253        let a = square(0.0, 0.0, 10.0);
254        let b = square(10.0, 0.0, 10.0);
255        assert!(!polygons_overlap(&a, &b));
256    }
257
258    #[test]
259    fn test_contained_square() {
260        let a = square(0.0, 0.0, 10.0);
261        let b = square(2.0, 2.0, 3.0);
262        assert!(polygons_overlap(&a, &b));
263    }
264
265    #[test]
266    fn test_triangle_overlap() {
267        let a = triangle(0.0, 0.0, 10.0);
268        let b = triangle(5.0, 0.0, 10.0);
269        assert!(polygons_overlap(&a, &b));
270    }
271
272    #[test]
273    fn test_triangle_no_overlap() {
274        let a = triangle(0.0, 0.0, 10.0);
275        let b = triangle(20.0, 0.0, 10.0);
276        assert!(!polygons_overlap(&a, &b));
277    }
278
279    #[test]
280    fn test_degenerate_polygons() {
281        let a = vec![(0.0, 0.0), (1.0, 0.0)]; // Not a polygon
282        let b = square(0.0, 0.0, 10.0);
283        assert!(!polygons_overlap(&a, &b));
284    }
285
286    #[test]
287    fn test_tolerance_effect() {
288        let a = square(0.0, 0.0, 10.0);
289        // Slightly overlapping by 0.5 units
290        let b = square(9.5, 0.0, 10.0);
291        // With default tolerance 1e-6, should overlap
292        assert!(polygons_overlap(&a, &b));
293        // With large tolerance, should NOT overlap
294        assert!(!polygons_overlap_with_tolerance(&a, &b, 1.0));
295    }
296
297    #[test]
298    fn test_overlap_depth_overlapping() {
299        let a = square(0.0, 0.0, 10.0);
300        let b = square(7.0, 0.0, 10.0);
301        let depth = overlap_depth(&a, &b);
302        assert!(depth.is_some());
303        assert!((depth.unwrap() - 3.0).abs() < 1e-10);
304    }
305
306    #[test]
307    fn test_overlap_depth_separated() {
308        let a = square(0.0, 0.0, 10.0);
309        let b = square(20.0, 0.0, 10.0);
310        assert!(overlap_depth(&a, &b).is_none());
311    }
312
313    #[test]
314    fn test_aabb_overlap() {
315        let a = AABB2::new(0.0, 0.0, 10.0, 10.0);
316        let b = AABB2::new(5.0, 5.0, 15.0, 15.0);
317        assert!(aabb_overlap(&a, &b));
318
319        let c = AABB2::new(20.0, 20.0, 30.0, 30.0);
320        assert!(!aabb_overlap(&a, &c));
321    }
322
323    // ======================== 3D Collision Tests ========================
324
325    fn box3d(x: f64, y: f64, z: f64, w: f64, d: f64, h: f64) -> AABB3 {
326        AABB3::new(x, y, z, x + w, y + d, z + h)
327    }
328
329    #[test]
330    fn test_aabb3_overlap_intersecting() {
331        let a = box3d(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
332        let b = box3d(5.0, 5.0, 5.0, 10.0, 10.0, 10.0);
333        assert!(aabb3_overlap(&a, &b));
334    }
335
336    #[test]
337    fn test_aabb3_overlap_separated() {
338        let a = box3d(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
339        let b = box3d(20.0, 0.0, 0.0, 10.0, 10.0, 10.0);
340        assert!(!aabb3_overlap(&a, &b));
341    }
342
343    #[test]
344    fn test_aabb3_overlap_touching() {
345        // Touching on one face — intersects returns true (boundary overlap)
346        let a = box3d(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
347        let b = box3d(10.0, 0.0, 0.0, 10.0, 10.0, 10.0);
348        assert!(aabb3_overlap(&a, &b));
349    }
350
351    #[test]
352    fn test_aabb3_overlap_with_tolerance() {
353        let a = box3d(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
354        let b = box3d(10.0, 0.0, 0.0, 10.0, 10.0, 10.0);
355        // Touching: overlap = 0 on x-axis, not > tolerance
356        assert!(!aabb3_overlap_with_tolerance(&a, &b, 1e-6));
357
358        // Slightly overlapping
359        let c = box3d(9.5, 0.0, 0.0, 10.0, 10.0, 10.0);
360        assert!(aabb3_overlap_with_tolerance(&a, &c, 1e-6));
361        // With large tolerance, not enough overlap
362        assert!(!aabb3_overlap_with_tolerance(&a, &c, 1.0));
363    }
364
365    #[test]
366    fn test_aabb3_within() {
367        let outer = box3d(0.0, 0.0, 0.0, 20.0, 20.0, 20.0);
368        let inner = box3d(5.0, 5.0, 5.0, 5.0, 5.0, 5.0);
369        assert!(aabb3_within(&inner, &outer));
370        assert!(!aabb3_within(&outer, &inner));
371    }
372
373    #[test]
374    fn test_aabb3_within_with_margin() {
375        let boundary = box3d(0.0, 0.0, 0.0, 20.0, 20.0, 20.0);
376        let inner = box3d(2.0, 2.0, 2.0, 5.0, 5.0, 5.0);
377        assert!(aabb3_within_with_margin(&inner, &boundary, 1.0));
378        // With larger margin, inner is too close to boundary
379        assert!(!aabb3_within_with_margin(&inner, &boundary, 3.0));
380    }
381
382    #[test]
383    fn test_aabb3_overlap_z_separated() {
384        // Overlap in x,y but separated in z
385        let a = box3d(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
386        let b = box3d(5.0, 5.0, 20.0, 10.0, 10.0, 10.0);
387        assert!(!aabb3_overlap(&a, &b));
388    }
389
390    // ---- SAT collision: invariants ----
391    //
392    // The Separating Axis Theorem guarantees:
393    //   - Separated polygons  → no collision
394    //   - Overlapping polygons → collision
395    //   - Fully contained     → collision
396    //   - Boundary touching   → no collision (within tolerance 1e-6)
397    //
398    // Reference: Ericson (2005), Real-Time Collision Detection, Ch. 4.4
399
400    #[test]
401    fn test_sat_separated_no_collision() {
402        // Two squares with a clear gap → never overlapping
403        let a = square(0.0, 0.0, 5.0);
404        let b = square(10.0, 0.0, 5.0); // gap of 5 units on x
405        assert!(
406            !polygons_overlap(&a, &b),
407            "clearly separated polygons must not overlap"
408        );
409    }
410
411    #[test]
412    fn test_sat_overlapping_collision() {
413        // Squares that share a 2×2 area
414        let a = square(0.0, 0.0, 6.0);
415        let b = square(4.0, 0.0, 6.0); // 2 units overlap in x
416        assert!(
417            polygons_overlap(&a, &b),
418            "overlapping polygons must report collision"
419        );
420    }
421
422    #[test]
423    fn test_sat_fully_contained_collision() {
424        // Small square fully inside large square
425        let outer = square(0.0, 0.0, 10.0);
426        let inner = square(3.0, 3.0, 3.0);
427        assert!(
428            polygons_overlap(&outer, &inner),
429            "fully contained polygon must report collision"
430        );
431        // Symmetric
432        assert!(
433            polygons_overlap(&inner, &outer),
434            "collision must be symmetric"
435        );
436    }
437
438    #[test]
439    fn test_sat_touching_boundary_no_collision() {
440        // Squares that share only an edge (no area overlap)
441        let a = square(0.0, 0.0, 5.0);
442        let b = square(5.0, 0.0, 5.0); // touching at x=5
443        assert!(
444            !polygons_overlap(&a, &b),
445            "boundary-touching polygons must not report collision (tolerance=1e-6)"
446        );
447    }
448
449    #[test]
450    fn test_sat_symmetry() {
451        // polygons_overlap(A, B) == polygons_overlap(B, A)
452        let a = square(0.0, 0.0, 7.0);
453        let b = square(5.0, 3.0, 7.0);
454        assert_eq!(
455            polygons_overlap(&a, &b),
456            polygons_overlap(&b, &a),
457            "collision detection must be symmetric"
458        );
459    }
460
461    #[test]
462    fn test_sat_self_overlap() {
463        // A polygon always overlaps with itself
464        let a = square(0.0, 0.0, 5.0);
465        assert!(polygons_overlap(&a, &a), "polygon must overlap with itself");
466    }
467}