apollonius 0.1.0

N-dimensional Euclidean geometry for Rust: points, vectors, lines, segments, hyperspheres, hyperplanes, AABBs, triangles, and a unified intersection API—all with const generics.
Documentation
pub mod aabb;
pub mod hyperplane;
pub mod hypersphere;
pub mod line;
pub mod segment;
pub mod triangle;

use crate::{AABB, FloatSign, Point, VectorMetricSquared, classify_to_zero};
use num_traits::Float;

/// Defines spatial queries for geometric entities in N-dimensional space.
///
/// This trait provides a unified interface to calculate projections, distances,
/// and containment checks between points and various geometric primitives.
pub trait SpatialRelation<T, const N: usize> {
    /// Projects a point onto the nearest location on the geometric entity.
    ///
    /// For infinite entities like lines, this is the orthogonal projection.
    /// For finite shapes like segments or spheres, the result is clamped
    /// to the boundaries or surface.
    ///
    /// # Examples
    ///
    /// ```
    /// use apollonius::{Point, Circle, SpatialRelation};
    ///
    /// let circle = Circle::new(Point::new([0.0, 0.0]), 1.0);
    /// let p = Point::new([2.0, 0.0]);
    ///
    /// // Projects the point onto the circle's boundary
    /// let closest = circle.closest_point(&p);
    /// assert_eq!(closest.coords_ref()[0], 1.0);
    /// ```
    fn closest_point(&self, p: &Point<T, N>) -> Point<T, N>;

    /// Returns the squared Euclidean distance from the point to the entity.
    ///
    /// This method is provided by default and relies on [`Self::closest_point`].
    /// Prefer this over [`Self::distance_to_point`] when comparing distances (e.g.
    /// threshold checks), since it avoids a square root and is cheaper.
    ///
    /// # Constraints
    /// * `T` must implement [`Float`] and [`std::iter::Sum`].
    ///
    /// # Example
    ///
    /// ```
    /// use apollonius::{Point, Line, Vector, SpatialRelation};
    ///
    /// let line = Line::new(Point::new([0.0, 0.0]), Vector::new([1.0, 0.0]));
    /// let p = Point::new([5.0, 3.0]);
    /// let d_sq = line.distance_to_point_squared(&p);
    /// assert!(((d_sq - 9.0) as f64).abs() < 1e-10); // 3² = 9
    /// ```
    #[inline]
    fn distance_to_point_squared(&self, p: &Point<T, N>) -> T
    where
        T: Float + std::iter::Sum,
    {
        (self.closest_point(p) - *p).magnitude_squared()
    }

    /// Calculates the minimum Euclidean distance between the entity and a point.
    ///
    /// This method is provided by default as the square root of
    /// [`Self::distance_to_point_squared`]. Use [`Self::distance_to_point_squared`]
    /// when only comparing distances to avoid the cost of the square root.
    ///
    /// # Constraints
    /// * `T` must implement [`Float`] and [`std::iter::Sum`].
    #[inline]
    fn distance_to_point(&self, p: &Point<T, N>) -> T
    where
        T: Float + std::iter::Sum,
    {
        self.distance_to_point_squared(p).sqrt()
    }

    /// Checks if a point lies on the boundary or structure of the entity.
    ///
    /// This uses the engine's internal epsilon tolerance to account for
    /// floating-point inaccuracies.
    ///
    /// # Examples
    ///
    /// ```
    /// use apollonius::{Point, Circle, SpatialRelation};
    ///
    /// let circle = Circle::new(Point::new([0.0, 0.0]), 1.0);
    /// assert!(circle.contains(&Point::new([1.0, 0.0])));
    /// ```
    #[inline]
    fn contains(&self, point: &Point<T, N>) -> bool
    where
        T: Float + std::iter::Sum,
    {
        match classify_to_zero(
            (self.closest_point(point) - *point).magnitude_squared(),
            None,
        ) {
            FloatSign::Zero => true,
            _ => false,
        }
    }

    /// Checks if a point is within the volume or area defined by the entity.
    ///
    /// For 1D entities (Line, Segment) or boundaries (Hyperplane), this
    /// usually defaults to [`Self::contains`]. For volumes (Hypersphere),
    /// it includes the interior.
    #[inline]
    fn is_inside(&self, point: &Point<T, N>) -> bool
    where
        T: Float + std::iter::Sum,
    {
        self.contains(point)
    }
}

/// Represents an entity that can be enclosed within an Axis-Aligned Bounding Box.
///
/// Implemented by [`Hypersphere`], [`Segment`], and other primitives that
/// compute an AABB for broad-phase collision detection.
///
/// # Examples
///
/// ```
/// use apollonius::{Point, Hypersphere, Bounded};
///
/// let sphere = Hypersphere::new(Point::new([0.0, 0.0, 0.0]), 5.0);
/// let aabb = sphere.aabb();
/// assert_eq!(aabb.min_ref().coords_ref()[0], -5.0);
/// assert_eq!(aabb.max_ref().coords_ref()[0], 5.0);
/// ```
pub trait Bounded<T, const N: usize> {
    /// Returns the minimum Axis-Aligned Bounding Box that encloses the entity.
    ///
    /// Used for broad-phase collision detection to quickly prune non-colliding objects.
    fn aabb(&self) -> AABB<T, N>;
}

/// Represents the outcome of an intersection query between geometric primitives.
///
/// This enum accounts for the different ways entities can interact in N-dimensional space,
/// distinguishing between points of contact, boundary crossings, and overlapping structures.
///
/// **Note:** Not every intersection method returns every variant. Each method that returns
/// `IntersectionResult` documents in its `# Returns` section exactly which variants it can
/// produce (e.g. Line–Hypersphere returns only `None`, `Tangent`, or `Secant`; Segment–Segment
/// returns only `None`, `Single`, or `Collinear`).
#[derive(Debug, Clone, PartialEq, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(bound(serialize = "T: serde::Serialize", deserialize = "T: serde::Deserialize<'de>")))]
pub enum IntersectionResult<T, const N: usize> {
    /// No intersection occurs between the entities.
    None,

    /// The entities touch at exactly one point without crossing boundaries.
    ///
    /// In physics simulations, this typically represents a grazing contact.
    Tangent(Point<T, N>),

    /// The entities intersect at two distinct points.
    ///
    /// Typical of a line or segment that enters and then exits a volume
    /// (e.g., a secant line through a hypersphere).
    Secant(Point<T, N>, Point<T, N>),

    /// The entities are coincident or overlap over a continuous range.
    ///
    /// This occurs when two lines are identical or a segment lies
    /// entirely within a hyperplane.
    Collinear,

    /// Exactly one point of intersection that is not a tangency.
    ///
    /// Usually occurs when a finite primitive (like a segment) starts
    /// outside and ends inside another entity's volume.
    Single(Point<T, N>),

    /// The primitive penetrates the half-space defined by a boundary (e.g., hyperplane).
    /// The associated value represents the penetration depth along the normal.
    HalfSpacePenetration(T),
}

#[cfg(all(test, feature = "serde"))]
mod serde_tests {
    use super::*;
    use crate::Point;
    use serde_json;

    #[test]
    fn test_intersection_result_serialization_roundtrip() {
        let none: IntersectionResult<f64, 2> = IntersectionResult::None;
        let json = serde_json::to_string(&none).unwrap();
        let restored: IntersectionResult<f64, 2> = serde_json::from_str(&json).unwrap();
        assert_eq!(none, restored);

        let tangent = IntersectionResult::Tangent(Point::new([1.0, 2.0]));
        let json = serde_json::to_string(&tangent).unwrap();
        let restored: IntersectionResult<f64, 2> = serde_json::from_str(&json).unwrap();
        assert_eq!(tangent, restored);

        let secant = IntersectionResult::Secant(
            Point::new([0.0, 0.0]),
            Point::new([1.0, 1.0]),
        );
        let json = serde_json::to_string(&secant).unwrap();
        let restored: IntersectionResult<f64, 2> = serde_json::from_str(&json).unwrap();
        assert_eq!(secant, restored);

        let collinear: IntersectionResult<f64, 2> = IntersectionResult::Collinear;
        let json = serde_json::to_string(&collinear).unwrap();
        let restored: IntersectionResult<f64, 2> = serde_json::from_str(&json).unwrap();
        assert_eq!(collinear, restored);

        let single = IntersectionResult::Single(Point::new([3.0, 4.0]));
        let json = serde_json::to_string(&single).unwrap();
        let restored: IntersectionResult<f64, 2> = serde_json::from_str(&json).unwrap();
        assert_eq!(single, restored);

        let penetration = IntersectionResult::HalfSpacePenetration(2.5);
        let json = serde_json::to_string(&penetration).unwrap();
        let restored: IntersectionResult<f64, 2> = serde_json::from_str(&json).unwrap();
        assert_eq!(penetration, restored);
    }
}