gemath 0.1.0

Type-safe game math with type-level units/spaces, typed angles, and explicit fallible ops (plus optional geometry/collision).
Documentation
//! Collision / intersection helpers for `geometry` primitives.

use crate::aabb2::Aabb2;
use crate::aabb3::Aabb3;
use crate::geometry::{Circle, Ray2, Ray3, Segment2, Segment3, Sphere};
use crate::math;
use crate::rect2::Rect2;
use crate::rect3::Rect3;
use crate::vec2::Vec2;
use crate::vec3::Vec3;

const EPS: f32 = 1e-6;

#[inline]
fn approx_zero(x: f32) -> bool {
    x.abs() <= EPS
}

/// Returns true if two 2D segments intersect (including touching at endpoints and collinear overlaps).
pub fn segment2_intersects<Unit: Copy, Space: Copy>(a: Segment2<Unit, Space>, b: Segment2<Unit, Space>) -> bool {
    segment2_intersection_kind(a, b).is_some()
}

#[derive(Copy, Clone, Debug, PartialEq)]
enum Segment2IntersectionKind<Unit: Copy, Space: Copy> {
    Point(Vec2<Unit, Space>),
    Overlap,
}

fn segment2_intersection_kind<Unit: Copy, Space: Copy>(
    a: Segment2<Unit, Space>,
    b: Segment2<Unit, Space>,
) -> Option<Segment2IntersectionKind<Unit, Space>> {
    let p = a.a;
    let r = a.b - a.a;
    let q = b.a;
    let s = b.b - b.a;

    // Handle degenerate segments (points).
    if approx_zero(r.length_squared()) && approx_zero(s.length_squared()) {
        // Both are points.
        return if (p - q).length_squared() <= EPS * EPS {
            Some(Segment2IntersectionKind::Point(p))
        } else {
            None
        };
    }
    if approx_zero(r.length_squared()) {
        // a is a point.
        return if point_on_segment2(p, b) {
            Some(Segment2IntersectionKind::Point(p))
        } else {
            None
        };
    }
    if approx_zero(s.length_squared()) {
        // b is a point.
        return if point_on_segment2(q, a) {
            Some(Segment2IntersectionKind::Point(q))
        } else {
            None
        };
    }

    let rxs = r.cross(s);
    let q_p = q - p;
    let q_pxr = q_p.cross(r);

    if approx_zero(rxs) && approx_zero(q_pxr) {
        // Collinear: compute overlap on the axis of greatest magnitude of `r`.
        let (r_comp, qp_comp, qs_comp) = if r.x.abs() >= r.y.abs() {
            (r.x, q_p.x, (q + s - p).x)
        } else {
            (r.y, q_p.y, (q + s - p).y)
        };

        // r_comp cannot be ~0 because segment a isn't degenerate (handled above).
        let t0 = qp_comp / r_comp;
        let t1 = qs_comp / r_comp;
        let tmin = t0.min(t1).max(0.0);
        let tmax = t0.max(t1).min(1.0);
        if tmin > tmax + EPS {
            return None;
        }
        if (tmin - tmax).abs() <= EPS {
            return Some(Segment2IntersectionKind::Point(p + r * tmin));
        }
        return Some(Segment2IntersectionKind::Overlap);
    }

    if approx_zero(rxs) && !approx_zero(q_pxr) {
        // Parallel, non-intersecting.
        return None;
    }

    // Proper intersection of two non-parallel lines.
    let t = q_p.cross(s) / rxs;
    let u = q_p.cross(r) / rxs;
    if t >= -EPS && t <= 1.0 + EPS && u >= -EPS && u <= 1.0 + EPS {
        Some(Segment2IntersectionKind::Point(p + r * t))
    } else {
        None
    }
}

#[inline]
fn point_on_segment2<Unit: Copy, Space: Copy>(p: Vec2<Unit, Space>, s: Segment2<Unit, Space>) -> bool {
    // Collinearity check + bounding box.
    let a = s.a;
    let b = s.b;
    let ab = b - a;
    let ap = p - a;
    if !approx_zero(ab.cross(ap)) {
        return false;
    }
    let min_x = a.x.min(b.x) - EPS;
    let max_x = a.x.max(b.x) + EPS;
    let min_y = a.y.min(b.y) - EPS;
    let max_y = a.y.max(b.y) + EPS;
    p.x >= min_x && p.x <= max_x && p.y >= min_y && p.y <= max_y
}

/// If two 2D segments intersect at a single point, returns that point.
///
/// - Returns `None` if they do not intersect.
/// - Returns `None` if they overlap over an interval (collinear overlap with non-zero length).
pub fn segment2_intersection_point<Unit: Copy, Space: Copy>(
    a: Segment2<Unit, Space>,
    b: Segment2<Unit, Space>,
) -> Option<Vec2<Unit, Space>> {
    match segment2_intersection_kind(a, b)? {
        Segment2IntersectionKind::Point(p) => Some(p),
        Segment2IntersectionKind::Overlap => None,
    }
}

/// Circle vs Rect2 intersection (touching counts as intersecting).
#[inline]
pub fn circle_rect2_intersects<Unit: Copy, Space: Copy>(circle: Circle<Unit, Space>, rect: &Rect2<Unit, Space>) -> bool {
    let c = rect.closest_point(circle.center);
    (c - circle.center).length_squared() <= circle.radius * circle.radius
}

/// Circle vs Aabb2 intersection (touching counts as intersecting).
#[inline]
pub fn circle_aabb2_intersects<Unit: Copy, Space: Copy>(circle: Circle<Unit, Space>, aabb: &Aabb2<Unit, Space>) -> bool {
    let c = aabb.closest_point(circle.center);
    (c - circle.center).length_squared() <= circle.radius * circle.radius
}

/// Sphere vs Aabb3 intersection (touching counts as intersecting).
#[inline]
pub fn sphere_aabb3_intersects<Unit: Copy, Space: Copy>(sphere: Sphere<Unit, Space>, aabb: &Aabb3<Unit, Space>) -> bool {
    let c = aabb.closest_point(sphere.center);
    (c - sphere.center).length_squared() <= sphere.radius * sphere.radius
}

/// Sphere vs Rect3 intersection (touching counts as intersecting).
#[inline]
pub fn sphere_rect3_intersects<Unit: Copy, Space: Copy>(sphere: Sphere<Unit, Space>, rect: &Rect3<Unit, Space>) -> bool {
    let c = rect.closest_point(sphere.center);
    (c - sphere.center).length_squared() <= sphere.radius * sphere.radius
}

/// Segment2 vs Circle intersection (touching counts as intersecting).
#[inline]
pub fn segment2_circle_intersects<Unit: Copy, Space: Copy>(seg: Segment2<Unit, Space>, circle: Circle<Unit, Space>) -> bool {
    seg.distance_to_point(circle.center) <= circle.radius
}

/// Segment3 vs Sphere intersection (touching counts as intersecting).
#[inline]
pub fn segment3_sphere_intersects<Unit: Copy, Space: Copy>(seg: Segment3<Unit, Space>, sphere: Sphere<Unit, Space>) -> bool {
    seg.distance_to_point(sphere.center) <= sphere.radius
}

/// Ray2 vs Circle intersection.
///
/// Returns the smallest `t >= 0` such that `ray.point_at(t)` lies on the circle boundary.
/// If the ray starts inside the circle, this returns the first exit intersection.
pub fn ray2_circle_intersect<Unit: Copy, Space: Copy>(ray: Ray2<Unit, Space>, circle: Circle<Unit, Space>) -> Option<f32> {
    let m = ray.origin - circle.center;
    let d = ray.dir;
    let a = d.dot(d);
    let c = m.dot(m) - circle.radius * circle.radius;

    if approx_zero(a) {
        // Degenerate ray direction: treat as a point query.
        return if c <= 0.0 { Some(0.0) } else { None };
    }

    let b = m.dot(d);
    let disc = b * b - a * c;
    if disc < 0.0 {
        return None;
    }

    let sqrt_disc = math::sqrt(disc);
    let t0 = (-b - sqrt_disc) / a;
    let t1 = (-b + sqrt_disc) / a;

    if t0 >= 0.0 {
        Some(t0)
    } else if t1 >= 0.0 {
        Some(t1)
    } else {
        None
    }
}

/// A raycast “hit” result in 2D.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct RayHit2<Unit: Copy = (), Space: Copy = ()> {
    /// Ray parameter (distance along the ray direction vector).
    pub t: f32,
    pub point: Vec2<Unit, Space>,
    /// Outward surface normal at `point` (unit length when well-defined).
    pub normal: Vec2<Unit, Space>,
}

/// Ray2 vs Circle raycast.
///
/// Returns a structured hit (`t`, `point`, `normal`) for the first intersection.
#[inline]
pub fn ray2_circle_cast<Unit: Copy, Space: Copy>(ray: Ray2<Unit, Space>, circle: Circle<Unit, Space>) -> Option<RayHit2<Unit, Space>> {
    let t = ray2_circle_intersect(ray, circle)?;
    let point = ray.point_at(t);
    let normal = (point - circle.center)
        .try_normalize()
        .unwrap_or_else(|| Vec2::new(1.0, 0.0));
    Some(RayHit2 { t, point, normal })
}

/// Ray3 vs Sphere intersection.
///
/// Returns the smallest `t >= 0` such that `ray.point_at(t)` lies on the sphere boundary.
/// If the ray starts inside the sphere, this returns the first exit intersection.
pub fn ray3_sphere_intersect<Unit: Copy, Space: Copy>(ray: Ray3<Unit, Space>, sphere: Sphere<Unit, Space>) -> Option<f32> {
    let m = ray.origin - sphere.center;
    let d = ray.dir;
    let a = d.dot(d);
    let c = m.dot(m) - sphere.radius * sphere.radius;

    if approx_zero(a) {
        return if c <= 0.0 { Some(0.0) } else { None };
    }

    let b = m.dot(d);
    let disc = b * b - a * c;
    if disc < 0.0 {
        return None;
    }

    let sqrt_disc = math::sqrt(disc);
    let t0 = (-b - sqrt_disc) / a;
    let t1 = (-b + sqrt_disc) / a;

    if t0 >= 0.0 {
        Some(t0)
    } else if t1 >= 0.0 {
        Some(t1)
    } else {
        None
    }
}

/// A raycast “hit” result in 3D.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct RayHit3<Unit: Copy = (), Space: Copy = ()> {
    /// Ray parameter (distance along the ray direction vector).
    pub t: f32,
    pub point: Vec3<Unit, Space>,
    /// Outward surface normal at `point` (unit length when well-defined).
    pub normal: Vec3<Unit, Space>,
}

/// Ray3 vs Sphere raycast.
///
/// Returns a structured hit (`t`, `point`, `normal`) for the first intersection.
#[inline]
pub fn ray3_sphere_cast<Unit: Copy, Space: Copy>(ray: Ray3<Unit, Space>, sphere: Sphere<Unit, Space>) -> Option<RayHit3<Unit, Space>> {
    let t = ray3_sphere_intersect(ray, sphere)?;
    let point = ray.point_at(t);
    let normal = (point - sphere.center)
        .try_normalize()
        .unwrap_or_else(|| Vec3::new(1.0, 0.0, 0.0));
    Some(RayHit3 { t, point, normal })
}