#[derive(Debug, Clone, Copy, PartialEq)]
pub enum HitShape {
Capsule { a: [f32; 2], b: [f32; 2], r: f32 },
Circle { c: [f32; 2], r: f32 },
}
impl HitShape {
pub fn radius(&self) -> f32 {
match self {
HitShape::Capsule { r, .. } | HitShape::Circle { r, .. } => *r,
}
}
pub fn spine_distance(&self, p: [f32; 2]) -> f32 {
match self {
HitShape::Capsule { a, b, .. } => point_segment_distance(p, *a, *b),
HitShape::Circle { c, .. } => dist2(p, *c),
}
}
pub fn contains(&self, p: [f32; 2]) -> bool {
self.spine_distance(p) <= self.radius()
}
}
const FRONT_EPS: f64 = 1e-6;
pub trait RegionCamera {
fn is_orthographic(&self) -> bool;
fn depth(&self, p: [f64; 3]) -> f64;
fn project_px(&self, p: [f64; 3]) -> Option<[f32; 2]>;
}
pub fn segment_region(cam: &impl RegionCamera, a: [f64; 3], b: [f64; 3], r: f32) -> Option<HitShape> {
if cam.is_orthographic() {
return Some(HitShape::Capsule {
a: cam.project_px(a)?,
b: cam.project_px(b)?,
r,
});
}
let da = cam.depth(a);
let db = cam.depth(b);
match (da > FRONT_EPS, db > FRONT_EPS) {
(true, true) => Some(HitShape::Capsule {
a: cam.project_px(a)?,
b: cam.project_px(b)?,
r,
}),
(true, false) => Some(HitShape::Capsule {
a: cam.project_px(a)?,
b: cam.project_px(clip_to_front(a, b, da, db))?,
r,
}),
(false, true) => Some(HitShape::Capsule {
a: cam.project_px(b)?,
b: cam.project_px(clip_to_front(b, a, db, da))?,
r,
}),
(false, false) => None, }
}
pub fn point_region(cam: &impl RegionCamera, p: [f64; 3], r: f32) -> Option<HitShape> {
if !cam.is_orthographic() && cam.depth(p) <= FRONT_EPS {
return None;
}
Some(HitShape::Circle {
c: cam.project_px(p)?,
r,
})
}
fn clip_to_front(front: [f64; 3], back: [f64; 3], d_front: f64, d_back: f64) -> [f64; 3] {
let denom = d_front - d_back;
let t = if denom.abs() < 1e-12 {
0.0
} else {
(d_front / denom).clamp(0.0, 1.0) * 0.995
};
[
front[0] + (back[0] - front[0]) * t,
front[1] + (back[1] - front[1]) * t,
front[2] + (back[2] - front[2]) * t,
]
}
fn dist2(p: [f32; 2], q: [f32; 2]) -> f32 {
((p[0] - q[0]).powi(2) + (p[1] - q[1]).powi(2)).sqrt()
}
fn point_segment_distance(p: [f32; 2], a: [f32; 2], b: [f32; 2]) -> f32 {
let abx = b[0] - a[0];
let aby = b[1] - a[1];
let len2 = abx * abx + aby * aby;
if len2 < 1e-12 {
return dist2(p, a);
}
let t = (((p[0] - a[0]) * abx + (p[1] - a[1]) * aby) / len2).clamp(0.0, 1.0);
let foot = [a[0] + abx * t, a[1] + aby * t];
dist2(p, foot)
}
#[cfg(test)]
mod tests {
use super::*;
struct FlatOrtho;
impl RegionCamera for FlatOrtho {
fn is_orthographic(&self) -> bool {
true
}
fn depth(&self, p: [f64; 3]) -> f64 {
p[2]
}
fn project_px(&self, p: [f64; 3]) -> Option<[f32; 2]> {
Some([p[0] as f32, p[1] as f32])
}
}
#[test]
fn capsule_contains_along_spine_and_within_radius() {
let s = segment_region(&FlatOrtho, [0.0, 0.0, 0.0], [10.0, 0.0, 0.0], 3.0).unwrap();
assert!(s.contains([5.0, 0.0])); assert!(s.contains([5.0, 2.9])); assert!(!s.contains([5.0, 3.1])); assert!(s.contains([-2.9, 0.0])); assert!(!s.contains([-3.1, 0.0])); }
#[test]
fn circle_contains_within_radius() {
let c = point_region(&FlatOrtho, [4.0, 4.0, 0.0], 5.0).unwrap();
assert!(c.contains([4.0, 4.0]));
assert!(c.contains([4.0, 8.9]));
assert!(!c.contains([4.0, 9.1]));
}
#[test]
fn ortho_projects_behind_eye_geometry_in_full() {
let s = segment_region(&FlatOrtho, [0.0, 0.0, 5.0], [10.0, 0.0, -5.0], 2.0).unwrap();
match s {
HitShape::Capsule { a, b, .. } => {
assert_eq!(a, [0.0, 0.0]);
assert_eq!(b, [10.0, 0.0]); }
_ => panic!("expected a capsule"),
}
}
}