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 = "geometry")]

use gemath::*;

const EPS: f32 = if cfg!(feature = "libm") { 1e-5 } else { 1e-6 };

fn approx_eq(a: f32, b: f32) -> bool {
    (a - b).abs() <= EPS
}

#[test]
fn obb2_contains_and_closest_point_axis_aligned() {
    let obb: Obb2<(), ()> = Obb2::from_center_half_extents(Vec2::new(0.0, 0.0), Vec2::new(1.0, 2.0));

    assert!(obb.contains_point(Vec2::new(0.0, 0.0)));
    assert!(obb.contains_point(Vec2::new(1.0, 2.0))); // touching
    assert!(!obb.contains_point(Vec2::new(1.0001, 0.0)));

    let p = obb.closest_point(Vec2::new(10.0, -10.0));
    assert!(approx_eq(p.x, 1.0));
    assert!(approx_eq(p.y, -2.0));
}

#[test]
fn obb2_rotated_contains_touching_and_closest_point() {
    let center = Vec2::new(0.0, 0.0);
    let half = Vec2::new(2.0, 1.0);
    let angle = Radians(core::f32::consts::FRAC_PI_4);

    let obb: Obb2<(), ()> = Obb2::from_center_half_extents_rotation_radians(center, half, angle);

    // Point exactly on +X face along rotated axis.
    let axis_x = Vec2::new(1.0, 0.0).rotate(angle);
    let on_face = center + axis_x * half.x;
    assert!(obb.contains_point(on_face));

    let outside = center + axis_x * (half.x + 0.25);
    assert!(!obb.contains_point(outside));
    let cp = obb.closest_point(outside);
    assert!((cp - on_face).length() <= EPS * 10.0);
}

#[test]
fn obb2_try_from_axes_rejects_non_orthonormal() {
    let center: Vec2<(), ()> = Vec2::new(0.0, 0.0);
    let half: Vec2<(), ()> = Vec2::new(1.0, 1.0);
    let axis_x: Vec2<(), ()> = Vec2::new(2.0, 0.0); // not unit
    let axis_y: Vec2<(), ()> = Vec2::new(0.0, 1.0);
    assert!(Obb2::try_from_axes(center, half, axis_x, axis_y).is_none());
}

#[test]
fn obb3_contains_and_closest_point_axis_aligned() {
    let obb: Obb3<(), ()> =
        Obb3::from_center_half_extents(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 2.0, 3.0));

    assert!(obb.contains_point(Vec3::new(1.0, 2.0, 3.0))); // touching
    assert!(!obb.contains_point(Vec3::new(1.0, 2.0, 3.0001)));

    let p = obb.closest_point(Vec3::new(-10.0, 10.0, 10.0));
    assert!(approx_eq(p.x, -1.0));
    assert!(approx_eq(p.y, 2.0));
    assert!(approx_eq(p.z, 3.0));
}

#[test]
fn obb3_from_axis_angle_matches_vec3_rotation() {
    let center: Vec3<(), ()> = Vec3::new(0.0, 0.0, 0.0);
    let half: Vec3<(), ()> = Vec3::new(1.0, 1.0, 1.0);
    let axis: Vec3<(), ()> = Vec3::new(0.0, 1.0, 0.0);
    let angle = Radians(core::f32::consts::FRAC_PI_2);

    let obb = Obb3::from_axis_angle_radians(center, half, axis, angle).unwrap();

    // The +X local axis should be the rotated X basis.
    let expected = Vec3::new(1.0, 0.0, 0.0).rotate_axis(axis.normalize(), angle);
    assert!((obb.axis_x - expected).length() <= EPS * 10.0);
}

#[test]
fn obb3_from_axis_angle_deg_works() {
    let center: Vec3<(), ()> = Vec3::new(0.0, 0.0, 0.0);
    let half: Vec3<(), ()> = Vec3::new(1.0, 1.0, 1.0);
    let axis: Vec3<(), ()> = Vec3::new(0.0, 1.0, 0.0);
    let obb = Obb3::from_axis_angle_deg(center, half, axis, Degrees(90.0)).unwrap();
    assert!(obb.contains_point(Vec3::new(0.0, 0.0, 0.0)));
}

#[cfg(feature = "aabb2")]
#[test]
fn obb2_to_aabb_contains_all_corners() {
    use gemath::Aabb2;
    let obb: Obb2<(), ()> =
        Obb2::from_center_half_extents_rotation_radians(Vec2::new(1.0, -2.0), Vec2::new(2.0, 1.0), Radians(0.7));
    let aabb: Aabb2<(), ()> = obb.to_aabb();
    let min = aabb.min();
    let max = aabb.max();
    for c in obb.corners() {
        assert!(c.x + EPS >= min.x && c.x - EPS <= max.x);
        assert!(c.y + EPS >= min.y && c.y - EPS <= max.y);
    }
}

#[cfg(feature = "aabb3")]
#[test]
fn obb3_to_aabb_contains_all_corners() {
    use gemath::Aabb3;
    let obb: Obb3<(), ()> = Obb3::from_axis_angle_radians(
        Vec3::new(1.0, 2.0, 3.0),
        Vec3::new(1.0, 2.0, 1.0),
        Vec3::new(1.0, 1.0, 0.0),
        Radians(0.8),
    )
    .unwrap();
    let aabb: Aabb3<(), ()> = obb.to_aabb();
    let min = aabb.min();
    let max = aabb.max();
    for c in obb.corners() {
        assert!(c.x + EPS >= min.x && c.x - EPS <= max.x);
        assert!(c.y + EPS >= min.y && c.y - EPS <= max.y);
        assert!(c.z + EPS >= min.z && c.z - EPS <= max.z);
    }
}