use glam::Vec2;
const DEGENERATE_LEN_SQ: f32 = 1e-6;
const PARALLEL_REL_EPS: f32 = 1e-6;
pub fn closest_point_on_segment(p: Vec2, a: Vec2, b: Vec2) -> Vec2 {
let ab = b - a;
let len_sq = ab.length_squared();
if len_sq < DEGENERATE_LEN_SQ {
return a;
}
let t = ((p - a).dot(ab) / len_sq).clamp(0.0, 1.0);
a + t * ab
}
pub fn segment_intersection(a1: Vec2, a2: Vec2, b1: Vec2, b2: Vec2) -> Option<Vec2> {
let s1 = a2 - a1;
let s2 = b2 - b1;
let denom = -s2.x * s1.y + s1.x * s2.y;
let len_product = s1.length() * s2.length();
if denom.abs() <= PARALLEL_REL_EPS * len_product {
return None;
}
let d = a1 - b1;
let s = (-s1.y * d.x + s1.x * d.y) / denom;
let t = (s2.x * d.y - s2.y * d.x) / denom;
if (0.0..=1.0).contains(&s) && (0.0..=1.0).contains(&t) {
Some(a1 + t * s1)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn closest_point_midpoint() {
let p = Vec2::new(1.0, 1.0);
let a = Vec2::new(0.0, 0.0);
let b = Vec2::new(2.0, 0.0);
let c = closest_point_on_segment(p, a, b);
assert!((c - Vec2::new(1.0, 0.0)).length() < 1e-5);
}
#[test]
fn closest_point_clamped_start() {
let p = Vec2::new(-5.0, 0.0);
let a = Vec2::new(0.0, 0.0);
let b = Vec2::new(2.0, 0.0);
let c = closest_point_on_segment(p, a, b);
assert!((c - a).length() < 1e-5);
}
#[test]
fn closest_point_degenerate() {
let p = Vec2::new(3.0, 4.0);
let a = Vec2::new(1.0, 1.0);
let c = closest_point_on_segment(p, a, a);
assert!((c - a).length() < 1e-5);
}
#[test]
fn segments_cross() {
let hit = segment_intersection(
Vec2::new(0.0, 0.0),
Vec2::new(2.0, 2.0),
Vec2::new(0.0, 2.0),
Vec2::new(2.0, 0.0),
);
assert!(hit.is_some());
let h = hit.unwrap();
assert!((h - Vec2::new(1.0, 1.0)).length() < 1e-5);
}
#[test]
fn parallel_segments_no_intersection() {
let hit = segment_intersection(
Vec2::new(0.0, 0.0),
Vec2::new(2.0, 0.0),
Vec2::new(0.0, 1.0),
Vec2::new(2.0, 1.0),
);
assert!(hit.is_none());
}
#[test]
fn non_overlapping_segments() {
let hit = segment_intersection(
Vec2::new(0.0, 0.0),
Vec2::new(1.0, 0.0),
Vec2::new(2.0, -1.0),
Vec2::new(2.0, 1.0),
);
assert!(hit.is_none());
}
}