collide-sphere 0.3.1

Sphere collider implementation for the collide crate
Documentation
#![deny(missing_docs)]

//! Sphere collider implementation for the `collide` crate.
//!
//! A sphere is the simplest collider: just a center point and a radius.
//! Collision detection reduces to a single distance check, making it
//! the most efficient collider type.
//!
//! When using a `CollisionManager` with only spheres, you get a dedicated
//! layer with optimal performance — no unnecessary branching or memory
//! overhead compared to more general collider types like capsules or convex shapes.
//!
//! Enable the `ray` feature to get `Collider<Ray<V>>` for `Sphere<V>`.

use collide::{BoundingVolume, Collider, CollisionInfo, Transform, Transformable};
use inner_space::{InnerSpace, VectorSpace};
use scalars::{One, Sqrt, Zero};

/// A sphere collider defined by a center point and radius.
#[derive(Copy, Clone, Debug)]
pub struct Sphere<V: VectorSpace> {
    /// The center of the sphere.
    pub center: V,
    /// The radius of the sphere.
    pub radius: V::Scalar,
}

impl<V: Copy + InnerSpace> Sphere<V> {
    /// Creates a new sphere collider.
    pub fn new(center: V, radius: V::Scalar) -> Self {
        Self { center, radius }
    }

    /// Creates a sphere with zero radius, representing a single point.
    pub fn point(center: V) -> Self {
        Self {
            center,
            radius: V::Scalar::zero(),
        }
    }
}

impl<V: Copy + InnerSpace> Collider for Sphere<V> {
    type Vector = V;

    fn check_collision(&self, other: &Self) -> bool {
        let displacement = other.center - self.center;
        let combined_radius = self.radius + other.radius;
        displacement.magnitude2() <= combined_radius * combined_radius
    }

    fn collision_info(&self, other: &Self) -> Option<CollisionInfo<V>> {
        let displacement = other.center - self.center;
        let distance_squared = displacement.magnitude2();
        let combined_radius = self.radius + other.radius;

        if distance_squared > combined_radius * combined_radius {
            return None;
        }

        let distance = distance_squared.sqrt();
        let direction = displacement / distance;

        Some(CollisionInfo {
            self_contact: self.center + direction * self.radius,
            other_contact: other.center - direction * other.radius,
            vector: direction * (distance - combined_radius),
        })
    }
}

impl<V: Copy + InnerSpace> BoundingVolume for Sphere<V> {
    fn overlaps(&self, other: &Self) -> bool {
        self.check_collision(other)
    }

    fn merged(&self, other: &Self) -> Self {
        let displacement = other.center - self.center;
        let distance = displacement.magnitude();
        if distance + other.radius <= self.radius {
            return *self;
        }
        if distance + self.radius <= other.radius {
            return *other;
        }
        let new_radius =
            (distance + self.radius + other.radius) / (V::Scalar::one() + V::Scalar::one());
        let direction = if distance.is_zero() {
            V::zero()
        } else {
            displacement / distance
        };
        Self {
            center: self.center + direction * (new_radius - self.radius),
            radius: new_radius,
        }
    }
}

impl<V: Copy + InnerSpace, T: Transform<V>> Transformable<T> for Sphere<V> {
    fn transformed(&self, transform: &T) -> Self {
        Self {
            center: transform.apply_point(self.center),
            radius: self.radius,
        }
    }
}

#[cfg(feature = "ray")]
mod ray;

#[cfg(test)]
mod tests {
    use super::*;
    use simple_vectors::Vector;

    type Vec3 = Vector<f32, 3>;

    #[test]
    fn spheres_collide() {
        let a = Sphere::new(Vec3::from([0.0, 0.0, 0.0]), 1.0);
        let b = Sphere::new(Vec3::from([1.5, 0.0, 0.0]), 1.0);

        let info = a.collision_info(&b).unwrap();
        assert!((info.vector.magnitude() - 0.5).abs() < 0.001);
    }

    #[test]
    fn spheres_dont_collide() {
        let a = Sphere::new(Vec3::from([0.0, 0.0, 0.0]), 1.0);
        let b = Sphere::new(Vec3::from([3.0, 0.0, 0.0]), 1.0);

        assert!(a.collision_info(&b).is_none());
    }

    #[test]
    fn check_collision_matches_collision_info() {
        let a = Sphere::new(Vec3::from([0.0, 0.0, 0.0]), 1.0);
        let close = Sphere::new(Vec3::from([1.5, 0.0, 0.0]), 1.0);
        let far = Sphere::new(Vec3::from([3.0, 0.0, 0.0]), 1.0);

        assert_eq!(
            a.check_collision(&close),
            a.collision_info(&close).is_some()
        );
        assert_eq!(a.check_collision(&far), a.collision_info(&far).is_some());
    }
}