collide-capsule 0.5.1

Capsule collider implementation for the collide crate
Documentation
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());
    }
}