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

use crate::scalar::*;
use gemath::*;

#[cfg(test)]
mod tests {
    use super::*;
    use std::f32::consts::{PI, TAU};

    const EPSILON: f32 = 1e-6; // Adjusted for f32 precision in angle_diff

    #[test]
    fn test_to_radians() {
        assert!((to_radians(180.0) - PI).abs() < EPSILON);
        assert!((to_radians(90.0) - PI / 2.0).abs() < EPSILON);
        assert!((to_radians(360.0) - TAU).abs() < EPSILON);
        assert!((to_radians(0.0) - 0.0).abs() < EPSILON);
    }

    #[test]
    fn test_to_degrees() {
        assert!((to_degrees(PI) - 180.0).abs() < EPSILON);
        assert!((to_degrees(PI / 2.0) - 90.0).abs() < EPSILON);
        assert!((to_degrees(TAU) - 360.0).abs() < EPSILON);
        assert!((to_degrees(0.0) - 0.0).abs() < EPSILON);
    }

    #[test]
    fn test_angle_diff() {
        assert!((angle_diff(0.0, 0.0) - 0.0).abs() < EPSILON);
        assert!((angle_diff(PI / 4.0, PI / 2.0) - (PI / 4.0)).abs() < EPSILON);

        let val_a = PI / 2.0;
        let val_b = PI / 4.0;
        let result = angle_diff(val_a, val_b);
        let expected = -PI / 4.0;
        assert!((result - expected).abs() < EPSILON);

        assert!((angle_diff(TAU - PI / 4.0, PI / 4.0) - (PI / 2.0)).abs() < EPSILON);
        assert!((angle_diff(PI / 4.0, TAU - PI / 4.0) - (-PI / 2.0)).abs() < EPSILON);
        assert!((angle_diff(0.0, PI) - PI).abs() < EPSILON);
        // The next line has PI + EPSILON, this might be tricky. C++ gives PI for (0, PI)
        // rem_euclid version gives: (PI+EPSILON).rem_euclid(TAU) = PI+EPSILON. PI+EPSILON > PI is true. So (PI+EPSILON-TAU).
        assert!((angle_diff(0.0, PI + EPSILON) - (PI + EPSILON - TAU)).abs() < EPSILON);
        assert!((angle_diff(0.0, 3.0 * PI / 2.0) - (-PI / 2.0)).abs() < EPSILON);
        assert!(
            (angle_diff(PI, 0.0) - PI).abs() < EPSILON,
            "Test PI to 0.0, C++ output was PI, Rust result should be PI"
        );
    }

    #[test]
    fn test_square() {
        assert!((square(0.0f32) - 0.0).abs() < EPSILON);
        assert!((square(2.0f32) - 4.0).abs() < EPSILON);
        assert!((square(-3.0f32) - 9.0).abs() < EPSILON);
        assert!((square(1.5f32) - 2.25).abs() < EPSILON);
    }

    #[test]
    fn test_cube() {
        assert!((cube(0.0f32) - 0.0).abs() < EPSILON);
        assert!((cube(2.0f32) - 8.0).abs() < EPSILON);
        assert!((cube(-3.0f32) - -27.0).abs() < EPSILON);
        assert!((cube(1.5f32) - 3.375).abs() < EPSILON);
    }

    #[test]
    fn test_remainder() {
        // Standard cases
        assert!((remainder(5.0, 3.0) - (-1.0)).abs() < EPSILON); // 5 - round(1.66)*3 = 5 - 2*3 = -1
        assert!((remainder(-5.0, 3.0) - 1.0).abs() < EPSILON); // -5 - round(-1.66)*3 = -5 - (-2)*3 = 1
        assert!((remainder(5.0, -3.0) - (-1.0)).abs() < EPSILON); // 5 - round(-1.66)*(-3) = 5 - (-2)*(-3) = 5 - 6 = -1
        assert!((remainder(-5.0, -3.0) - 1.0).abs() < EPSILON); // -5 - round(1.66)*(-3) = -5 - 2*(-3) = -5 - (-6) = 1

        assert!((remainder(6.0, 3.0) - 0.0).abs() < EPSILON); // 6 - round(2)*3 = 0
        assert!((remainder(1.0, 5.0) - 1.0).abs() < EPSILON); // 1 - round(0.2)*5 = 1 - 0*5 = 1 (as in user scratchpad)

        // Cases involving .5 rounding
        assert!((remainder(7.5, 3.0) - (-1.5)).abs() < EPSILON);

        assert!((remainder(8.5, 3.0) - (-0.5)).abs() < EPSILON); // 8.5 - round(2.83)*3 = 8.5 - 3*3 = -0.5

        // Test with y=0 (expect NaN)
        assert!(remainder(5.0, 0.0).is_nan());
    }

    #[test]
    fn test_modulo() {
        // Based on redefined understanding: modulo(x,y) = (remainder(abs(x), abs(y)) + abs(y)).copysign(x)
        // gb_mod(5,3): remainder(5,3)=-1. (-1+3).copysign(5) = 2.0
        assert!((modulo(5.0, 3.0) - 2.0).abs() < EPSILON);
        // gb_mod(-5,3): remainder(5,3)=-1. (-1+3).copysign(-5) = -2.0
        assert!((modulo(-5.0, 3.0) - (-2.0)).abs() < EPSILON);
        // gb_mod(6,3): remainder(6,3)=0. (0+3).copysign(6) = 3.0
        assert!((modulo(6.0, 3.0) - 3.0).abs() < EPSILON);
        // gb_mod(-6,3): remainder(6,3)=0. (0+3).copysign(-6) = -3.0
        assert!((modulo(-6.0, 3.0) - (-3.0)).abs() < EPSILON);

        // gb_mod(0,5): remainder(0,5)=0. (0+5).copysign(0) = 5.0 (copysign(val, +0.0) is val)
        assert!((modulo(0.0, 5.0) - 5.0).abs() < EPSILON);
        // gb_mod(0.0, -5.0) -> y_abs=5. rem(0,5)=0. (0+5).copysign(0)=5.0
        assert!((modulo(0.0, -5.0) - 5.0).abs() < EPSILON);

        // From C trace: gb_mod(-PI, 2PI) should be -PI
        // remainder(PI, 2PI) = -PI. (-PI + 2PI).copysign(-PI) = PI.copysign(-PI) = -PI
        assert!((modulo(-PI, TAU) - (-PI)).abs() < EPSILON);

        // From C trace: gb_mod(2PI, 2PI) should be 2PI
        // remainder(2PI, 2PI) = 0. (0+2PI).copysign(2PI) = 2PI
        assert!((modulo(TAU, TAU) - TAU).abs() < EPSILON);

        // Test with y=0 (expect NaN)
        assert!(modulo(5.0, 0.0).is_nan());
    }

    #[test]
    fn test_quake_rsqrt() {
        // Test with some known values. Accuracy won't be perfect.
        // 1.0 / sqrt(1.0) = 1.0
        // 1.0 / sqrt(4.0) = 0.5
        // 1.0 / sqrt(16.0) = 0.25
        // 1.0 / sqrt(2.0) = 0.70710678

        let r1 = quake_rsqrt(1.0);
        let r2 = quake_rsqrt(4.0);
        let r3 = quake_rsqrt(16.0);
        let r4 = quake_rsqrt(2.0);

        // Epsilon for quake_rsqrt might need to be larger due to approximation
        const QUAKE_EPSILON: f32 = 1e-3; // Original C version has 2 iterations, ours 1 for now.
        // gb_math.cpp comments out the second iteration in the function body.

        assert!(
            (r1 - 1.0).abs() < QUAKE_EPSILON,
            "quake_rsqrt(1.0) failed. val: {}",
            r1
        );
        assert!(
            (r2 - 0.5).abs() < QUAKE_EPSILON,
            "quake_rsqrt(4.0) failed. val: {}",
            r2
        );
        assert!(
            (r3 - 0.25).abs() < QUAKE_EPSILON,
            "quake_rsqrt(16.0) failed. val: {}",
            r3
        );
        assert!(
            (r4 - 0.70710678).abs() < QUAKE_EPSILON,
            "quake_rsqrt(2.0) failed. val: {}",
            r4
        );

        // Check against 1.0/sqrt() for a few values
        assert!((quake_rsqrt(10.0) - (1.0 / 10.0f32.sqrt())).abs() < QUAKE_EPSILON);
        assert!((quake_rsqrt(0.1) - (1.0 / 0.1f32.sqrt())).abs() < QUAKE_EPSILON);

        // Behavior for 0 or negative numbers? Original doesn't specify, likely produces garbage or NaN/inf.
        // quake_rsqrt(0.0) with two iterations results in a finite positive number.
        // The original test for NaN was based on an incorrect prediction for one iteration.
        // Removing the NaN assertion as the behavior for 0.0 is not strictly defined as NaN by the algo.
        // let result_for_zero = quake_rsqrt(0.0);
        // println!("quake_rsqrt(0.0) = {}", result_for_zero); // For debugging if needed
    }

    #[test]
    fn test_lerp() {
        assert!((lerp(0.0, 10.0, 0.0) - 0.0).abs() < EPSILON);
        assert!((lerp(0.0, 10.0, 1.0) - 10.0).abs() < EPSILON);
        assert!((lerp(0.0, 10.0, 0.5) - 5.0).abs() < EPSILON);
        assert!((lerp(10.0, 0.0, 0.5) - 5.0).abs() < EPSILON);
        assert!((lerp(-5.0, 5.0, 0.5) - 0.0).abs() < EPSILON);
        assert!((lerp(0.0, 10.0, -0.5) - (-5.0)).abs() < EPSILON); // Extrapolation
        assert!((lerp(0.0, 10.0, 1.5) - 15.0).abs() < EPSILON); // Extrapolation
    }

    #[test]
    fn test_unlerp() {
        assert!((unlerp(0.0, 0.0, 10.0) - 0.0).abs() < EPSILON);
        assert!((unlerp(10.0, 0.0, 10.0) - 1.0).abs() < EPSILON);
        assert!((unlerp(5.0, 0.0, 10.0) - 0.5).abs() < EPSILON);
        assert!((unlerp(5.0, 10.0, 0.0) - 0.5).abs() < EPSILON); // Reversed a and b
        assert!((unlerp(0.0, -5.0, 5.0) - 0.5).abs() < EPSILON);
        assert!((unlerp(-5.0, 0.0, 10.0) - (-0.5)).abs() < EPSILON); // Extrapolation
        assert!((unlerp(15.0, 0.0, 10.0) - 1.5).abs() < EPSILON); // Extrapolation
        assert!(
            unlerp(5.0, 10.0, 10.0).is_infinite() && unlerp(5.0, 10.0, 10.0).is_sign_negative()
        );
        assert!(unlerp(10.0, 10.0, 10.0).is_nan()); // (10.0-10.0)/(10.0-10.0) -> 0.0/0.0 -> NaN
        assert!(
            unlerp(15.0, 10.0, 10.0).is_infinite() && unlerp(15.0, 10.0, 10.0).is_sign_positive()
        ); // 5.0/0.0 -> +inf
    }

    #[test]
    fn test_smooth_step() {
        assert!((smooth_step(0.0, 1.0, 0.0) - 0.0).abs() < EPSILON);
        assert!((smooth_step(0.0, 1.0, 1.0) - 1.0).abs() < EPSILON);
        assert!((smooth_step(0.0, 1.0, 0.5) - 0.5).abs() < EPSILON); // x = 0.5; 0.5*0.5*(3.0 - 2.0*0.5) = 0.25 * (3.0-1.0) = 0.25*2.0 = 0.5
        assert!((smooth_step(0.0, 10.0, 5.0) - 0.5).abs() < EPSILON);

        // Test clamping
        assert!((smooth_step(0.0, 1.0, -0.5) - 0.0).abs() < EPSILON); // t < a
        assert!((smooth_step(0.0, 1.0, 1.5) - 1.0).abs() < EPSILON); // t > b

        // Test with a > b
        assert!((smooth_step(1.0, 0.0, 0.5) - 0.5).abs() < EPSILON); // x = (0.5-1.0)/(0.0-1.0) = -0.5/-1.0 = 0.5. Result = 0.5
        assert!((smooth_step(10.0, 0.0, 5.0) - 0.5).abs() < EPSILON);
    }

    #[test]
    fn test_smoother_step() {
        assert!((smoother_step(0.0, 1.0, 0.0) - 0.0).abs() < EPSILON);
        assert!((smoother_step(0.0, 1.0, 1.0) - 1.0).abs() < EPSILON);

        let x_half = 0.5;
        let expected_half = x_half * x_half * x_half * (x_half * (x_half * 6.0 - 15.0) + 10.0); // 0.125 * (0.5 * (3.0 - 15.0) + 10.0) = 0.125 * (0.5 * -12.0 + 10.0) = 0.125 * (-6.0 + 10.0) = 0.125 * 4.0 = 0.5
        assert!((smoother_step(0.0, 1.0, 0.5) - expected_half).abs() < EPSILON);
        assert!((smoother_step(0.0, 10.0, 5.0) - expected_half).abs() < EPSILON);

        // Test clamping
        assert!((smoother_step(0.0, 1.0, -0.5) - 0.0).abs() < EPSILON); // t < a
        assert!((smoother_step(0.0, 1.0, 1.5) - 1.0).abs() < EPSILON); // t > b

        // Test with a > b
        assert!((smoother_step(1.0, 0.0, 0.5) - expected_half).abs() < EPSILON);
        assert!((smoother_step(10.0, 0.0, 5.0) - expected_half).abs() < EPSILON);
    }

    #[test]
    fn test_clamp() {
        assert_eq!(clamp(5.0, 1.0, 10.0), 5.0);
        assert_eq!(clamp(-5.0, 0.0, 1.0), 0.0);
        assert_eq!(clamp(15.0, 0.0, 10.0), 10.0);
        assert_eq!(clamp(0.0, 0.0, 0.0), 0.0);
    }

    #[test]
    fn test_min() {
        assert_eq!(min(1.0, 2.0), 1.0);
        assert_eq!(min(-1.0, 1.0), -1.0);
        assert_eq!(min(0.0, 0.0), 0.0);
    }

    #[test]
    fn test_max() {
        assert_eq!(max(1.0, 2.0), 2.0);
        assert_eq!(max(-1.0, 1.0), 1.0);
        assert_eq!(max(0.0, 0.0), 0.0);
    }

    #[test]
    fn test_sign() {
        assert_eq!(sign(5.0), 1.0);
        assert_eq!(sign(-5.0), -1.0);
        assert_eq!(sign(0.0), 0.0);
    }

    #[test]
    fn test_abs() {
        assert_eq!(abs(5.0), 5.0);
        assert_eq!(abs(-5.0), 5.0);
        assert_eq!(abs(0.0), 0.0);
    }

    #[test]
    fn test_is_nan() {
        assert!(is_nan(f32::NAN));
        assert!(!is_nan(0.0));
        assert!(!is_nan(1.0));
    }

    #[test]
    fn test_is_finite() {
        assert!(is_finite(0.0));
        assert!(is_finite(1.0));
        assert!(!is_finite(f32::INFINITY));
        assert!(!is_finite(f32::NEG_INFINITY));
        assert!(!is_finite(f32::NAN));
    }

    #[test]
    fn test_approx_eq() {
        assert!(approx_eq(1.0, 1.0 + EPSILON / 2.0, EPSILON));
        assert!(!approx_eq(1.0, 1.0 + EPSILON * 2.0, EPSILON));
        assert!(approx_eq(-1.0, -1.0 - EPSILON / 2.0, EPSILON));
        assert!(approx_eq(0.0, EPSILON / 2.0, EPSILON));
        assert!(!approx_eq(0.0, EPSILON * 2.0, EPSILON));
    }
}

// --- Compile-time (const) tests/examples for scalar.rs ---
const _CONST_RAD_180: f32 = to_radians(180.0);
const _CONST_DEG_PI: f32 = to_degrees(core::f32::consts::PI);
const _CONST_LERP: f32 = lerp(0.0, 10.0, 0.5);
const _CONST_UNLERP: f32 = unlerp(5.0, 0.0, 10.0);
const _CONST_CLAMP: f32 = clamp(5.0, 1.0, 10.0);
const _CONST_CLAMP_LOW: f32 = clamp(-5.0, 0.0, 10.0);
const _CONST_CLAMP_HIGH: f32 = clamp(15.0, 0.0, 10.0);
const _CONST_MIN: f32 = min(1.0, 2.0);
const _CONST_MAX: f32 = max(1.0, 2.0);
const _CONST_SIGN_POS: f32 = sign(5.0);
const _CONST_SIGN_NEG: f32 = sign(-5.0);
const _CONST_SIGN_ZERO: f32 = sign(0.0);
const _CONST_ABS_POS: f32 = abs(5.0);
const _CONST_ABS_NEG: f32 = abs(-5.0);

const _: () = {
    // Compile-time assertions for const-everything
    assert!((_CONST_RAD_180 - core::f32::consts::PI).abs() < 1e-6);
    assert!((_CONST_DEG_PI - 180.0).abs() < 1e-6);
    assert!((_CONST_LERP - 5.0).abs() < 1e-6);
    assert!((_CONST_UNLERP - 0.5).abs() < 1e-6);
    assert!(_CONST_CLAMP == 5.0);
    assert!(_CONST_CLAMP_LOW == 0.0);
    assert!(_CONST_CLAMP_HIGH == 10.0);
    assert!(_CONST_MIN == 1.0);
    assert!(_CONST_MAX == 2.0);
    assert!(_CONST_SIGN_POS == 1.0);
    assert!(_CONST_SIGN_NEG == -1.0);
    assert!(_CONST_SIGN_ZERO == 0.0);
    assert!(_CONST_ABS_POS == 5.0);
    assert!(_CONST_ABS_NEG == 5.0);
};