use collide::{Collider, CollisionInfo};
use collide_ray::Ray;
use inner_space::InnerSpace;
use scalars::{One, Sqrt, Zero};
use crate::Capsule;
impl<V: Copy + InnerSpace> Collider<Ray<V>> for Capsule<V> {
type Vector = V;
fn collision_info(&self, ray: &Ray<V>) -> Option<CollisionInfo<V>> {
let axis = self.end - self.start;
if axis.is_zero() {
return ray
.intersect_sphere(self.start, self.rad)
.map(|(_, info)| -info);
}
let offset = ray.origin - self.start;
let axis_squared = axis.magnitude2();
let direction_dot_axis = ray.direction.dot(&axis);
let offset_dot_axis = offset.dot(&axis);
let direction_squared = ray.direction.magnitude2();
let a = direction_squared * axis_squared - direction_dot_axis * direction_dot_axis;
let b = (ray.direction.dot(&offset) * axis_squared - direction_dot_axis * offset_dot_axis)
* (V::Scalar::one() + V::Scalar::one());
let c = offset.magnitude2() * axis_squared
- offset_dot_axis * offset_dot_axis
- self.rad * self.rad * axis_squared;
let zero = V::Scalar::zero();
let one = V::Scalar::one();
let mut best: Option<(V::Scalar, CollisionInfo<V>)> = None;
let mut try_update = |result: Option<(V::Scalar, CollisionInfo<V>)>| {
if let Some((parameter, info)) = result
&& best
.as_ref()
.is_none_or(|(best_parameter, _)| parameter < *best_parameter)
{
best = Some((parameter, info));
}
};
if !a.is_zero() {
let discriminant = b * b - (one + one + one + one) * a * c;
if discriminant >= zero {
let sqrt_discriminant = discriminant.sqrt();
for sign in [V::Scalar::one(), -V::Scalar::one()] {
let parameter = (-b + sqrt_discriminant * sign) / (a + a);
if parameter >= zero {
let segment_parameter =
(direction_dot_axis * parameter + offset_dot_axis) / axis_squared;
if segment_parameter >= zero && segment_parameter <= one {
let hit_point = ray.origin + ray.direction * parameter;
let axis_point = self.start + axis * segment_parameter;
let to_hit = hit_point - axis_point;
let distance = to_hit.magnitude();
let surface_point = if distance > zero {
axis_point + to_hit / distance * self.rad
} else {
hit_point
};
try_update(Some((
parameter,
CollisionInfo {
self_contact: surface_point,
other_contact: hit_point,
vector: -(ray.direction * parameter),
},
)));
}
}
}
}
}
let sphere_hit = |center| {
ray.intersect_sphere(center, self.rad)
.map(|(p, info)| (p, -info))
};
try_update(sphere_hit(self.start));
try_update(sphere_hit(self.end));
best.map(|(_, info)| info)
}
}
#[cfg(test)]
mod tests {
use super::*;
use simple_vectors::Vector;
type Vec3 = Vector<f32, 3>;
#[test]
fn ray_hits_sphere() {
let ray = Ray::new(Vec3::from([-5.0, 0.0, 0.0]), Vec3::from([1.0, 0.0, 0.0]));
let capsule = Capsule::sphere(Vec3::from([0.0, 0.0, 0.0]), 1.0);
let info = capsule.collision_info(&ray).unwrap();
let distance = info.vector.magnitude();
assert!((distance - 4.0).abs() < 0.001);
}
#[test]
fn ray_misses_sphere() {
let ray = Ray::new(Vec3::from([-5.0, 0.0, 0.0]), Vec3::from([0.0, 1.0, 0.0]));
let capsule = Capsule::sphere(Vec3::from([0.0, 0.0, 0.0]), 1.0);
assert!(capsule.collision_info(&ray).is_none());
}
#[test]
fn ray_hits_capsule_side() {
let ray = Ray::new(Vec3::from([0.0, 5.0, 0.0]), Vec3::from([0.0, -1.0, 0.0]));
let capsule = Capsule::new(
1.0,
Vec3::from([-2.0, 0.0, 0.0]),
Vec3::from([2.0, 0.0, 0.0]),
);
let info = capsule.collision_info(&ray).unwrap();
let distance = info.vector.magnitude();
assert!((distance - 4.0).abs() < 0.001);
}
#[test]
fn ray_hits_capsule_endcap() {
let ray = Ray::new(Vec3::from([-5.0, 0.0, 0.0]), Vec3::from([1.0, 0.0, 0.0]));
let capsule = Capsule::new(
1.0,
Vec3::from([0.0, 0.0, 0.0]),
Vec3::from([0.0, 5.0, 0.0]),
);
let info = capsule.collision_info(&ray).unwrap();
let hit_distance = info.vector.magnitude();
assert!((hit_distance - 4.0).abs() < 0.01);
}
#[test]
fn ray_behind_origin_misses() {
let ray = Ray::new(Vec3::from([5.0, 0.0, 0.0]), Vec3::from([1.0, 0.0, 0.0]));
let capsule = Capsule::sphere(Vec3::from([0.0, 0.0, 0.0]), 1.0);
assert!(capsule.collision_info(&ray).is_none());
}
}