Skip to main content

symbios_tensor/
geometry.rs

1//! Low-level 2D geometry primitives used by the tracer and spatial hash.
2
3use glam::Vec2;
4
5/// Epsilon for treating a segment as degenerate (length² below this).
6const DEGENERATE_LEN_SQ: f32 = 1e-6;
7
8/// Relative epsilon for treating two lines as parallel. The raw cross
9/// product scales with segment lengths, so we compare against
10/// `PARALLEL_REL_EPS * |s1| * |s2|` to remain robust at any world scale.
11const PARALLEL_REL_EPS: f32 = 1e-6;
12
13/// Returns the closest point on segment `a`–`b` to point `p`.
14pub fn closest_point_on_segment(p: Vec2, a: Vec2, b: Vec2) -> Vec2 {
15    let ab = b - a;
16    let len_sq = ab.length_squared();
17    if len_sq < DEGENERATE_LEN_SQ {
18        return a;
19    }
20    let t = ((p - a).dot(ab) / len_sq).clamp(0.0, 1.0);
21    a + t * ab
22}
23
24/// Returns the intersection point of segments `a1`–`a2` and `b1`–`b2`, if any.
25pub fn segment_intersection(a1: Vec2, a2: Vec2, b1: Vec2, b2: Vec2) -> Option<Vec2> {
26    let s1 = a2 - a1;
27    let s2 = b2 - b1;
28
29    let denom = -s2.x * s1.y + s1.x * s2.y;
30    // Use a relative threshold so the parallel check works at any scale.
31    let len_product = s1.length() * s2.length();
32    if denom.abs() <= PARALLEL_REL_EPS * len_product {
33        return None;
34    }
35
36    let d = a1 - b1;
37    let s = (-s1.y * d.x + s1.x * d.y) / denom;
38    let t = (s2.x * d.y - s2.y * d.x) / denom;
39
40    if (0.0..=1.0).contains(&s) && (0.0..=1.0).contains(&t) {
41        Some(a1 + t * s1)
42    } else {
43        None
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    #[test]
52    fn closest_point_midpoint() {
53        let p = Vec2::new(1.0, 1.0);
54        let a = Vec2::new(0.0, 0.0);
55        let b = Vec2::new(2.0, 0.0);
56        let c = closest_point_on_segment(p, a, b);
57        assert!((c - Vec2::new(1.0, 0.0)).length() < 1e-5);
58    }
59
60    #[test]
61    fn closest_point_clamped_start() {
62        let p = Vec2::new(-5.0, 0.0);
63        let a = Vec2::new(0.0, 0.0);
64        let b = Vec2::new(2.0, 0.0);
65        let c = closest_point_on_segment(p, a, b);
66        assert!((c - a).length() < 1e-5);
67    }
68
69    #[test]
70    fn closest_point_degenerate() {
71        let p = Vec2::new(3.0, 4.0);
72        let a = Vec2::new(1.0, 1.0);
73        let c = closest_point_on_segment(p, a, a);
74        assert!((c - a).length() < 1e-5);
75    }
76
77    #[test]
78    fn segments_cross() {
79        let hit = segment_intersection(
80            Vec2::new(0.0, 0.0),
81            Vec2::new(2.0, 2.0),
82            Vec2::new(0.0, 2.0),
83            Vec2::new(2.0, 0.0),
84        );
85        assert!(hit.is_some());
86        let h = hit.unwrap();
87        assert!((h - Vec2::new(1.0, 1.0)).length() < 1e-5);
88    }
89
90    #[test]
91    fn parallel_segments_no_intersection() {
92        let hit = segment_intersection(
93            Vec2::new(0.0, 0.0),
94            Vec2::new(2.0, 0.0),
95            Vec2::new(0.0, 1.0),
96            Vec2::new(2.0, 1.0),
97        );
98        assert!(hit.is_none());
99    }
100
101    #[test]
102    fn non_overlapping_segments() {
103        let hit = segment_intersection(
104            Vec2::new(0.0, 0.0),
105            Vec2::new(1.0, 0.0),
106            Vec2::new(2.0, -1.0),
107            Vec2::new(2.0, 1.0),
108        );
109        assert!(hit.is_none());
110    }
111}