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(all(feature = "rect3", feature = "mat4"))]

use gemath::*;
use gemath::rect3::*;
use gemath::vec3::*;

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

    #[test]
    fn test_rect3_new() {
        let r: Rect3<(),()> = Rect3::new(Vec3::new(1.0, 2.0, 3.0), Vec3::new(4.0, 5.0, 6.0));
        assert_eq!(r.pos, Vec3::new(1.0, 2.0, 3.0));
        assert_eq!(r.dim, Vec3::new(4.0, 5.0, 6.0));
    }

    #[test]
    fn test_rect3_default() {
        let r: Rect3 = Default::default();
        assert_eq!(r.pos, Vec3::ZERO);
        assert_eq!(r.dim, Vec3::ZERO);
    }

    #[test]
    fn test_rect3_from_slice() {
        let r: Rect3<(),()> = Rect3::from_array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
        assert_eq!(r.pos, Vec3::new(1.0, 2.0, 3.0));
        assert_eq!(r.dim, Vec3::new(4.0, 5.0, 6.0));
    }

    #[test]
    fn test_rect3_contains_point_coords() {
        let r: Rect3<(),()> = Rect3::new(Vec3::new(1.0, 1.0, 1.0), Vec3::new(3.0, 3.0, 3.0));
        // Point inside
        assert!(r.contains_point_coords(2.0, 2.0, 2.0));
        // Point on min_edge (inclusive)
        assert!(r.contains_point_coords(1.0, 1.0, 1.0));
        assert!(r.contains_point_coords(1.0, 2.5, 2.5));
        assert!(r.contains_point_coords(2.5, 1.0, 2.5));
        assert!(r.contains_point_coords(2.5, 2.5, 1.0));
        // Point on max_edge (exclusive)
        assert!(!r.contains_point_coords(4.0, 4.0, 4.0));
        assert!(!r.contains_point_coords(1.0, 1.0, 4.0));
        assert!(!r.contains_point_coords(1.0, 4.0, 1.0));
        assert!(!r.contains_point_coords(4.0, 1.0, 1.0));
        // Point outside
        assert!(!r.contains_point_coords(0.0, 0.0, 0.0));
        assert!(!r.contains_point_coords(5.0, 5.0, 5.0));

        // Test with negative dimensions (should normalize)
        let r_neg_dim: Rect3<(),()> = Rect3::new(Vec3::new(4.0, 4.0, 4.0), Vec3::new(-3.0, -3.0, -3.0));
        assert!(r_neg_dim.contains_point_coords(2.0, 2.0, 2.0));
        assert!(r_neg_dim.contains_point_coords(1.0, 1.0, 1.0)); // min edge
        assert!(!r_neg_dim.contains_point_coords(4.0, 4.0, 4.0)); // max edge
    }

    #[test]
    fn test_rect3_contains_point() {
        let r: Rect3<(),()> = Rect3::new(Vec3::new(1.0, 1.0, 1.0), Vec3::new(3.0, 3.0, 3.0));
        assert!(r.contains_point(Vec3::new(2.0, 2.0, 2.0)));
        assert!(!r.contains_point(Vec3::new(5.0, 5.0, 5.0)));
    }

    #[test]
    fn test_rect3_intersects_and_intersection() {
        let r1: Rect3<(),()> = Rect3::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(2.0, 2.0, 2.0));
        let r2: Rect3<(),()> = Rect3::new(Vec3::new(1.0, 1.0, 1.0), Vec3::new(2.0, 2.0, 2.0));
        let r3: Rect3<(),()> = Rect3::new(Vec3::new(3.0, 3.0, 3.0), Vec3::new(1.0, 1.0, 1.0));
        let r4: Rect3<(),()> = Rect3::new(Vec3::new(0.5, 0.5, 0.5), Vec3::new(1.0, 1.0, 1.0)); // r4 is inside r1

        // Intersection r1 and r2
        assert!(r1.intersects(&r2));
        assert_eq!(
            r1.intersection(&r2),
            Some(Rect3::new(
                Vec3::new(1.0, 1.0, 1.0),
                Vec3::new(1.0, 1.0, 1.0)
            ))
        );

        // No intersection r1 and r3
        assert!(!r1.intersects(&r3));
        assert_eq!(r1.intersection(&r3), None);

        // r4 is contained in r1
        assert!(r1.intersects(&r4));
        assert_eq!(r1.intersection(&r4), Some(r4));
        assert!(r4.intersects(&r1));
        assert_eq!(r4.intersection(&r1), Some(r4));

        // Touching edges
        let r_touch_x = Rect3::new(Vec3::new(2.0, 0.0, 0.0), Vec3::new(1.0, 2.0, 2.0));
        assert!(!r1.intersects(&r_touch_x));
        assert_eq!(r1.intersection(&r_touch_x), None);

        // Rectangles with negative dimensions
        let r_neg_dim1: Rect3<(),()> = Rect3::new(Vec3::new(2.0, 2.0, 2.0), Vec3::new(-2.0, -2.0, -2.0));
        let r_neg_dim2: Rect3<(),()> = Rect3::new(Vec3::new(3.0, 3.0, 3.0), Vec3::new(-2.0, -2.0, -2.0));
        assert!(r_neg_dim1.intersects(&r_neg_dim2));
        assert_eq!(
            r_neg_dim1.intersection(&r_neg_dim2),
            Some(Rect3::new(
                Vec3::new(1.0, 1.0, 1.0),
                Vec3::new(1.0, 1.0, 1.0)
            ))
        );

        // No overlap, touching at a point
        let r5: Rect3<(),()> = Rect3::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 1.0, 1.0));
        let r6: Rect3<(),()> = Rect3::new(Vec3::new(1.0, 1.0, 1.0), Vec3::new(1.0, 1.0, 1.0));
        assert!(!r5.intersects(&r6));
        assert_eq!(r5.intersection(&r6), None);
    }

    #[test]
    fn test_rect3_min_max_size() {
        let r: Rect3<(),()> = Rect3::new(Vec3::new(3.0, 2.0, 5.0), Vec3::new(-2.0, 5.0, -3.0));
        assert_eq!(r.min(), Vec3::new(1.0, 2.0, 2.0));
        assert_eq!(r.max(), Vec3::new(3.0, 7.0, 5.0));
        assert_eq!(r.size(), Vec3::new(2.0, 5.0, 3.0));
    }

    #[test]
    fn test_rect3_volume() {
        let r: Rect3<(),()> = Rect3::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(3.0, 4.0, 2.0));
        assert_eq!(r.volume(), 24.0);
        let r_neg: Rect3<(),()> = Rect3::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(-3.0, 4.0, 2.0));
        assert_eq!(r_neg.volume(), 24.0);
    }

    #[test]
    fn test_rect3_is_empty() {
        let r: Rect3<(),()> = Rect3::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 4.0, 2.0));
        assert!(r.is_empty());
        let r2: Rect3<(),()> = Rect3::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(3.0, 0.0, 2.0));
        assert!(r2.is_empty());
        let r3: Rect3<(),()> = Rect3::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(3.0, 4.0, 2.0));
        assert!(!r3.is_empty());
    }

    #[test]
    fn test_rect3_from_min_max() {
        let min: Vec3<(),()> = Vec3::new(1.0, 2.0, 3.0);
        let max: Vec3<(),()> = Vec3::new(4.0, 6.0, 7.0);
        let r: Rect3<(),()> = Rect3::from_min_max(min, max);
        assert_eq!(r.min(), min);
        assert_eq!(r.max(), max);
    }

    #[test]
    fn test_rect3_expand_to_include() {
        let r: Rect3<(),()> = Rect3::from_min_max(Vec3::new(1.0, 2.0, 3.0), Vec3::new(3.0, 4.0, 5.0));
        let expanded = r.expand_to_include(Vec3::new(5.0, 0.0, 6.0));
        assert_eq!(expanded.min(), Vec3::new(1.0, 0.0, 3.0));
        assert_eq!(expanded.max(), Vec3::new(5.0, 4.0, 6.0));
    }

    #[test]
    fn test_rect3_union() {
        let a: Rect3<(),()> = Rect3::from_min_max(Vec3::new(0.0, 0.0, 0.0), Vec3::new(2.0, 2.0, 2.0));
        let b: Rect3<(),()> = Rect3::from_min_max(Vec3::new(1.0, -1.0, 1.0), Vec3::new(3.0, 1.0, 4.0));
        let u = a.union(&b);
        assert_eq!(u.min(), Vec3::new(0.0, -1.0, 0.0));
        assert_eq!(u.max(), Vec3::new(3.0, 2.0, 4.0));
    }

    #[test]
    fn test_rect3_closest_point_and_distance() {
        let rect: Rect3<(),()> = Rect3::from_min_max(Vec3::new(1.0, 1.0, 1.0), Vec3::new(3.0, 4.0, 5.0));
        // Inside
        assert_eq!(
            rect.closest_point(Vec3::new(2.0, 2.0, 2.0)),
            Vec3::new(2.0, 2.0, 2.0)
        );
        assert_eq!(rect.distance(Vec3::new(2.0, 2.0, 2.0)), 0.0);
        // Outside
        assert_eq!(
            rect.closest_point(Vec3::new(0.0, 0.0, 0.0)),
            Vec3::new(1.0, 1.0, 1.0)
        );
        assert!((rect.distance(Vec3::new(0.0, 0.0, 0.0)) - (3.0_f32).sqrt()).abs() < 1e-6);
        assert_eq!(
            rect.closest_point(Vec3::new(4.0, 5.0, 6.0)),
            Vec3::new(3.0, 4.0, 5.0)
        );
    }

    #[test]
    fn test_rect3_intersect_ray() {
        let rect: Rect3<(),()> = Rect3::from_min_max(Vec3::new(1.0, 1.0, 1.0), Vec3::new(3.0, 4.0, 5.0));
        // Ray from outside, hits
        let t = rect.intersect_ray(Vec3::new(0.0, 2.0, 2.0), Vec3::new(1.0, 0.0, 0.0));
        assert!(t.is_some() && (t.unwrap() - 1.0).abs() < 1e-6);
        // Ray from inside
        let t2 = rect.intersect_ray(Vec3::new(2.0, 2.0, 2.0), Vec3::new(1.0, 0.0, 0.0));
        assert!(t2.is_some() && (t2.unwrap() - 1.0).abs() < 1e-6);
        // Ray misses
        let t3 = rect.intersect_ray(Vec3::new(0.0, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0));
        assert!(t3.is_none());
    }

    #[test]
    fn test_rect3_transform() {
        use crate::mat4::Mat4;
        let rect: Rect3<(),()> = Rect3::from_min_max(Vec3::new(1.0, 2.0, 3.0), Vec3::new(3.0, 4.0, 5.0));
        let m = Mat4::from_translation(Vec3::new(10.0, 20.0, 30.0));
        let t = rect.transform(&m);
        assert_eq!(t.min(), Vec3::new(11.0, 22.0, 33.0));
        assert_eq!(t.max(), Vec3::new(13.0, 24.0, 35.0));
    }
}

// --- Compile-time (const) tests/examples for Rect3 ---
const _CONST_RECT0: Rect3f32 = Rect3f32::new(Vec3f32::new(1.0, 2.0, 3.0), Vec3f32::new(4.0, 5.0, 6.0));
const _CONST_RECT1: Rect3f32 = Rect3f32::new(Vec3f32::new(7.0, 8.0, 9.0), Vec3f32::new(10.0, 11.0, 12.0));
const _CONST_RECT_FROM_ARRAY: Rect3f32 = Rect3f32::from_array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
const _CONST_RECT_MIN: Vec3f32 = _CONST_RECT0.min();
const _CONST_RECT_MAX: Vec3f32 = _CONST_RECT0.max();
const _CONST_RECT_METERS: Rect3Meters = Rect3Meters::new(Vec3Meters::new(1.0, 2.0, 3.0), Vec3Meters::new(4.0, 5.0, 6.0));
const _CONST_RECT_WORLD: Rect3World = Rect3World::new(Vec3World::new(1.0, 2.0, 3.0), Vec3World::new(4.0, 5.0, 6.0));

const _: () = {
    // Compile-time assertions for const-everything
    assert!(_CONST_RECT0.pos.x == 1.0 && _CONST_RECT0.pos.y == 2.0 && _CONST_RECT0.pos.z == 3.0);
    assert!(_CONST_RECT0.dim.x == 4.0 && _CONST_RECT0.dim.y == 5.0 && _CONST_RECT0.dim.z == 6.0);
    assert!(_CONST_RECT_FROM_ARRAY.pos.x == 1.0 && _CONST_RECT_FROM_ARRAY.pos.y == 2.0 && _CONST_RECT_FROM_ARRAY.pos.z == 3.0);
    assert!(_CONST_RECT_FROM_ARRAY.dim.x == 4.0 && _CONST_RECT_FROM_ARRAY.dim.y == 5.0 && _CONST_RECT_FROM_ARRAY.dim.z == 6.0);
    assert!(_CONST_RECT_MIN.x == 1.0 && _CONST_RECT_MIN.y == 2.0 && _CONST_RECT_MIN.z == 3.0);
    assert!(_CONST_RECT_MAX.x == 5.0 && _CONST_RECT_MAX.y == 7.0 && _CONST_RECT_MAX.z == 9.0);
};

// Compile-time type safety: the following lines would fail to compile if uncommented
// const _FAIL: Rect3Meters = Rect3Pixels::new(Vec3Pixels::new(1.0, 2.0, 3.0), Vec3Pixels::new(4.0, 5.0, 6.0)); // error: mismatched types
// const _FAIL2: Rect3World = Rect3Local::new(Vec3Local::new(1.0, 2.0, 3.0), Vec3Local::new(4.0, 5.0, 6.0)); // error: mismatched types

#[test]
fn test_rect3_meters_to_pixels() {
    let meters: Rect3Meters = Rect3Meters::new(Vec3Meters::new(2.0, 3.0, 4.0), Vec3Meters::new(5.0, 6.0, 7.0));
    let pixels = meters.to_pixels(100.0);
    assert_eq!(pixels.pos, Vec3Pixels::new(200.0, 300.0, 400.0));
    assert_eq!(pixels.dim, Vec3Pixels::new(500.0, 600.0, 700.0));
}