gemath 0.1.0

Type-safe game math with type-level units/spaces, typed angles, and explicit fallible ops (plus optional geometry/collision).
Documentation
#![cfg(feature = "collision")]

use gemath::*;

#[cfg(test)]
mod tests {
    use super::*;
    const EPS: f32 = if cfg!(feature = "libm") { 1e-5 } else { 1e-6 };

    #[test]
    fn segment2_intersection_basic_and_collinear() {
        let a: Segment2<(), ()> = Segment2::new(Vec2::new(0.0, 0.0), Vec2::new(2.0, 0.0));
        let b: Segment2<(), ()> = Segment2::new(Vec2::new(1.0, -1.0), Vec2::new(1.0, 1.0));
        assert!(segment2_intersects(a, b));
        let p = segment2_intersection_point(a, b).unwrap();
        assert!((p - Vec2::<(), ()>::new(1.0, 0.0)).length() < 1e-6);

        // Touching at a single endpoint (collinear) => intersects + point.
        let c: Segment2<(), ()> = Segment2::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0));
        let d: Segment2<(), ()> = Segment2::new(Vec2::new(1.0, 0.0), Vec2::new(2.0, 0.0));
        assert!(segment2_intersects(c, d));
        let p2 = segment2_intersection_point(c, d).unwrap();
        assert!((p2 - Vec2::<(), ()>::new(1.0, 0.0)).length() < 1e-6);

        // Collinear overlap over an interval => intersects, but no single intersection point.
        let e: Segment2<(), ()> = Segment2::new(Vec2::new(0.0, 0.0), Vec2::new(2.0, 0.0));
        let f: Segment2<(), ()> = Segment2::new(Vec2::new(1.0, 0.0), Vec2::new(3.0, 0.0));
        assert!(segment2_intersects(e, f));
        assert!(segment2_intersection_point(e, f).is_none());
    }

    #[test]
    fn circle_rect2_and_aabb2_intersects() {
        let rect: Rect2<(), ()> = Rect2::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0));
        let circle_inside: Circle<(), ()> = Circle::new(Vec2::new(0.5, 0.5), 0.1);
        assert!(circle_rect2_intersects(circle_inside, &rect));

        // Tangent on the right edge: closest point is (1, 0.5), distance == radius.
        let circle_tangent: Circle<(), ()> = Circle::new(Vec2::new(2.0, 0.5), 1.0);
        assert!(circle_rect2_intersects(circle_tangent, &rect));

        let circle_far: Circle<(), ()> = Circle::new(Vec2::new(10.0, 10.0), 1.0);
        assert!(!circle_rect2_intersects(circle_far, &rect));

        let aabb: Aabb2<(), ()> = Aabb2::from_min_max(Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0));
        assert!(circle_aabb2_intersects(circle_inside, &aabb));
        assert!(circle_aabb2_intersects(circle_tangent, &aabb));
        assert!(!circle_aabb2_intersects(circle_far, &aabb));
    }

    #[test]
    fn sphere_aabb3_and_rect3_intersects() {
        let aabb: Aabb3<(), ()> = Aabb3::from_min_max(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
        let rect: Rect3<(), ()> = Rect3::from_min_max(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));

        let s_inside: Sphere<(), ()> = Sphere::new(Vec3::new(0.0, 0.0, 0.0), 0.5);
        assert!(sphere_aabb3_intersects(s_inside, &aabb));
        assert!(sphere_rect3_intersects(s_inside, &rect));

        // Tangent on +X face at x=1.
        let s_tangent: Sphere<(), ()> = Sphere::new(Vec3::new(2.0, 0.0, 0.0), 1.0);
        assert!(sphere_aabb3_intersects(s_tangent, &aabb));
        assert!(sphere_rect3_intersects(s_tangent, &rect));

        let s_far: Sphere<(), ()> = Sphere::new(Vec3::new(10.0, 0.0, 0.0), 1.0);
        assert!(!sphere_aabb3_intersects(s_far, &aabb));
        assert!(!sphere_rect3_intersects(s_far, &rect));
    }

    #[test]
    fn ray_circle_and_ray_sphere_intersect_t() {
        let circle: Circle<(), ()> = Circle::new(Vec2::new(0.0, 0.0), 1.0);
        let ray: Ray2<(), ()> = Ray2::new(Vec2::new(-2.0, 0.0), Vec2::new(1.0, 0.0));
        let t = ray2_circle_intersect(ray, circle).unwrap();
        assert!((t - 1.0).abs() < 1e-6);

        // Starts inside => returns exit t.
        let ray_inside: Ray2<(), ()> = Ray2::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0));
        let t2 = ray2_circle_intersect(ray_inside, circle).unwrap();
        assert!((t2 - 1.0).abs() < 1e-6);

        // Miss.
        let ray_miss: Ray2<(), ()> = Ray2::new(Vec2::new(-2.0, 2.0), Vec2::new(1.0, 0.0));
        assert!(ray2_circle_intersect(ray_miss, circle).is_none());

        let sphere: Sphere<(), ()> = Sphere::new(Vec3::new(0.0, 0.0, 0.0), 1.0);
        let ray3: Ray3<(), ()> = Ray3::new(Vec3::new(-2.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0));
        let t3 = ray3_sphere_intersect(ray3, sphere).unwrap();
        assert!((t3 - 1.0).abs() < 1e-6);
    }

    #[test]
    fn raycast_hit_results_have_t_point_and_normal() {
        let circle: Circle<(), ()> = Circle::new(Vec2::new(0.0, 0.0), 1.0);

        // Hit.
        let ray: Ray2<(), ()> = Ray2::new(Vec2::new(-2.0, 0.0), Vec2::new(1.0, 0.0));
        let hit = ray2_circle_cast(ray, circle).unwrap();
        assert!((hit.t - 1.0).abs() < EPS);
        assert!((hit.point - Vec2::<(), ()>::new(-1.0, 0.0)).length() < EPS);
        assert!((hit.normal - Vec2::<(), ()>::new(-1.0, 0.0)).length() < EPS);

        // Tangent.
        let ray_tangent: Ray2<(), ()> = Ray2::new(Vec2::new(-2.0, 1.0), Vec2::new(1.0, 0.0));
        let hit_tangent = ray2_circle_cast(ray_tangent, circle).unwrap();
        assert!((hit_tangent.t - 2.0).abs() < EPS);
        assert!((hit_tangent.point - Vec2::<(), ()>::new(0.0, 1.0)).length() < EPS);
        assert!((hit_tangent.normal - Vec2::<(), ()>::new(0.0, 1.0)).length() < EPS);

        // Inside-start => exit hit.
        let ray_inside: Ray2<(), ()> = Ray2::new(Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0));
        let hit_inside = ray2_circle_cast(ray_inside, circle).unwrap();
        assert!((hit_inside.t - 1.0).abs() < EPS);
        assert!((hit_inside.point - Vec2::<(), ()>::new(1.0, 0.0)).length() < EPS);
        assert!((hit_inside.normal - Vec2::<(), ()>::new(1.0, 0.0)).length() < EPS);

        // Miss.
        let ray_miss: Ray2<(), ()> = Ray2::new(Vec2::new(-2.0, 2.0), Vec2::new(1.0, 0.0));
        assert!(ray2_circle_cast(ray_miss, circle).is_none());

        let sphere: Sphere<(), ()> = Sphere::new(Vec3::new(0.0, 0.0, 0.0), 1.0);

        // Hit.
        let ray3: Ray3<(), ()> = Ray3::new(Vec3::new(-2.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0));
        let hit3 = ray3_sphere_cast(ray3, sphere).unwrap();
        assert!((hit3.t - 1.0).abs() < EPS);
        assert!((hit3.point - Vec3::<(), ()>::new(-1.0, 0.0, 0.0)).length() < EPS);
        assert!((hit3.normal - Vec3::<(), ()>::new(-1.0, 0.0, 0.0)).length() < EPS);

        // Tangent.
        let ray3_tangent: Ray3<(), ()> = Ray3::new(Vec3::new(-2.0, 1.0, 0.0), Vec3::new(1.0, 0.0, 0.0));
        let hit3_tangent = ray3_sphere_cast(ray3_tangent, sphere).unwrap();
        assert!((hit3_tangent.t - 2.0).abs() < EPS);
        assert!((hit3_tangent.point - Vec3::<(), ()>::new(0.0, 1.0, 0.0)).length() < EPS);
        assert!((hit3_tangent.normal - Vec3::<(), ()>::new(0.0, 1.0, 0.0)).length() < EPS);

        // Inside-start => exit hit.
        let ray3_inside: Ray3<(), ()> = Ray3::new(Vec3::new(0.0, 0.0, 0.5), Vec3::new(0.0, 0.0, 1.0));
        let hit3_inside = ray3_sphere_cast(ray3_inside, sphere).unwrap();
        assert!((hit3_inside.t - 0.5).abs() < EPS);
        assert!((hit3_inside.point - Vec3::<(), ()>::new(0.0, 0.0, 1.0)).length() < EPS);
        assert!((hit3_inside.normal - Vec3::<(), ()>::new(0.0, 0.0, 1.0)).length() < EPS);

        // Miss.
        let ray3_miss: Ray3<(), ()> = Ray3::new(Vec3::new(-2.0, 2.0, 0.0), Vec3::new(1.0, 0.0, 0.0));
        assert!(ray3_sphere_cast(ray3_miss, sphere).is_none());
    }

    #[test]
    fn segment_circle_and_segment_sphere_intersects() {
        let seg2: Segment2<(), ()> = Segment2::new(Vec2::new(0.0, 0.0), Vec2::new(2.0, 0.0));
        let c_hit: Circle<(), ()> = Circle::new(Vec2::new(1.0, 0.25), 0.5);
        let c_miss: Circle<(), ()> = Circle::new(Vec2::new(3.0, 0.0), 0.5);
        assert!(segment2_circle_intersects(seg2, c_hit));
        assert!(!segment2_circle_intersects(seg2, c_miss));

        let seg3: Segment3<(), ()> = Segment3::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 2.0));
        let s_hit: Sphere<(), ()> = Sphere::new(Vec3::new(0.25, 0.0, 1.0), 0.5);
        let s_miss: Sphere<(), ()> = Sphere::new(Vec3::new(3.0, 0.0, 1.0), 0.5);
        assert!(segment3_sphere_intersects(seg3, s_hit));
        assert!(!segment3_sphere_intersects(seg3, s_miss));
    }
}