gemath 0.1.0

Type-safe game math with type-level units/spaces, typed angles, and explicit fallible ops (plus optional geometry/collision).
Documentation
use crate::vec2::{Vec2, Meters, Pixels, World, Local, Screen};
use crate::angle::{Degrees, Radians};
use core::ops::{Add, Mul, Sub};
use core::marker::PhantomData;
use crate::math;

/// 2x2 matrix with type-level unit and coordinate space
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Mat2<Unit: Copy = (), Space: Copy = ()> {
    pub x_col: Vec2<Unit, Space>, // First column
    pub y_col: Vec2<Unit, Space>, // Second column
    #[cfg_attr(feature = "serde", serde(skip))]
    _unit: PhantomData<Unit>,
    #[cfg_attr(feature = "serde", serde(skip))]
    _space: PhantomData<Space>,
}

/// Type aliases for common units and spaces
pub type Mat2f32 = Mat2<(),()>;
pub type Mat2Meters = Mat2<Meters,()>;
pub type Mat2Pixels = Mat2<Pixels,()>;
pub type Mat2World = Mat2<(),World>;
pub type Mat2Local = Mat2<(),Local>;
pub type Mat2Screen = Mat2<(),Screen>;
pub type Mat2MetersWorld = Mat2<Meters,World>;
pub type Mat2PixelsScreen = Mat2<Pixels,Screen>;

impl<Unit: Copy, Space: Copy> Mat2<Unit, Space> {
    pub const ZERO: Self = Self {
        x_col: Vec2::ZERO,
        y_col: Vec2::ZERO,
        _unit: PhantomData,
        _space: PhantomData,
    };

    pub const IDENTITY: Self = Self {
        x_col: Vec2::new(1.0, 0.0),
        y_col: Vec2::new(0.0, 1.0),
        _unit: PhantomData,
        _space: PhantomData,
    };

    #[inline]
    pub const fn new(x_col: Vec2<Unit, Space>, y_col: Vec2<Unit, Space>) -> Self {
        Self { x_col, y_col, _unit: PhantomData, _space: PhantomData }
    }

    /// Creates a matrix from individual elements, assuming column-major order.
    /// m00, m10 are first column, m01, m11 are second column.
    #[inline]
    pub const fn from_cols_array(data: &[f32; 4]) -> Self {
        Self {
            x_col: Vec2::new(data[0], data[1]),
            y_col: Vec2::new(data[2], data[3]),
            _unit: PhantomData,
            _space: PhantomData,
        }
    }

    /// Creates a matrix from individual elements, assuming row-major order for input.
    /// r0c0, r0c1 are first row, r1c0, r1c1 are second row.
    #[inline]
    pub const fn from_rows(r0c0: f32, r0c1: f32, r1c0: f32, r1c1: f32) -> Self {
        Self {
            x_col: Vec2::new(r0c0, r1c0),
            y_col: Vec2::new(r0c1, r1c1),
            _unit: PhantomData,
            _space: PhantomData,
        }
    }

    /// Creates a 2D rotation matrix (CCW) from an angle in radians.
    ///
    /// Column-major:
    /// - x_col = (cos, sin)
    /// - y_col = (-sin, cos)
    #[inline]
    pub fn from_rotation(angle: Radians) -> Self {
        let (sin, cos) = math::sin_cos(angle.0);
        Self {
            x_col: Vec2::new(cos, sin),
            y_col: Vec2::new(-sin, cos),
            _unit: PhantomData,
            _space: PhantomData,
        }
    }

    /// Creates a 2D rotation matrix (CCW) from an angle in degrees.
    #[inline]
    pub fn from_rotation_deg(angle: Degrees) -> Self {
        Self::from_rotation(angle.to_radians())
    }

    /// Creates a 2D scale matrix.
    ///
    /// `scale` is unitless (dimensionless) scaling factors.
    /// This scales vectors of any `Unit`/`Space` without changing their type-level tags.
    #[inline]
    pub const fn from_scale(scale: Vec2) -> Self {
        Self {
            x_col: Vec2::new(scale.x, 0.0),
            y_col: Vec2::new(0.0, scale.y),
            _unit: PhantomData,
            _space: PhantomData,
        }
    }

    /// Calculates the determinant of the matrix.
    /// det(M) = x_col.x * y_col.y - x_col.y * y_col.x
    #[inline]
    pub const fn determinant(&self) -> f32 {
        self.x_col.x * self.y_col.y - self.x_col.y * self.y_col.x
    }

    /// Returns the transpose of the matrix.
    /// The rows become columns and columns become rows.
    #[inline]
    pub const fn transpose(&self) -> Self {
        Self {
            x_col: Vec2::new(self.x_col.x, self.y_col.x),
            y_col: Vec2::new(self.x_col.y, self.y_col.y),
            _unit: PhantomData,
            _space: PhantomData,
        }
    }

    /// Calculates the inverse of the matrix.
    /// Returns `None` if the determinant is zero (or very close to it), as the matrix would not be invertible.
    pub fn inverse(&self) -> Option<Self> {
        let det = self.determinant();
        if det.abs() < 1e-7 {
            None
        } else {
            let inv_det = 1.0 / det;
            Some(Self {
                x_col: Vec2::new(self.y_col.y * inv_det, -self.x_col.y * inv_det),
                y_col: Vec2::new(-self.y_col.x * inv_det, self.x_col.x * inv_det),
                _unit: PhantomData,
                _space: PhantomData,
            })
        }
    }

    /// Alias for [`Mat2::inverse`].
    #[inline]
    pub fn try_inverse(&self) -> Option<Self> {
        self.inverse()
    }

    /// Returns the i-th column (0 or 1).
    pub const fn col(&self, i: usize) -> Option<Vec2<Unit, Space>> {
        match i {
            0 => Some(self.x_col),
            1 => Some(self.y_col),
            _ => None,
        }
    }

    /// Sets the i-th column (0 or 1).
    pub fn set_col(&mut self, i: usize, v: Vec2<Unit, Space>) {
        match i {
            0 => self.x_col = v,
            1 => self.y_col = v,
            _ => {}
        }
    }

    /// Returns the i-th row (0 or 1).
    pub const fn row(&self, i: usize) -> Option<Vec2<Unit, Space>> {
        match i {
            0 => Some(Vec2::new(self.x_col.x, self.y_col.x)),
            1 => Some(Vec2::new(self.x_col.y, self.y_col.y)),
            _ => None,
        }
    }

    /// Sets the i-th row (0 or 1).
    pub fn set_row(&mut self, i: usize, v: Vec2<Unit, Space>) {
        match i {
            0 => {
                self.x_col.x = v.x;
                self.y_col.x = v.y;
            }
            1 => {
                self.x_col.y = v.x;
                self.y_col.y = v.y;
            }
            _ => {}
        }
    }

    /// Returns true if the matrix is orthonormal (columns are unit and perpendicular).
    pub fn is_orthonormal(&self) -> bool {
        let c0 = self.x_col;
        let c1 = self.y_col;
        (c0.length() - 1.0).abs() < 1e-5
            && (c1.length() - 1.0).abs() < 1e-5
            && (c0.dot(c1)).abs() < 1e-5
    }

    /// Returns a shear matrix with shear factors shx, shy.
    pub const fn from_shear(shx: f32, shy: f32) -> Self {
        Self {
            x_col: Vec2::new(1.0, shy),
            y_col: Vec2::new(shx, 1.0),
            _unit: PhantomData,
            _space: PhantomData,
        }
    }
}

// Matrix-Matrix addition
impl<Unit: Copy, Space: Copy> Add for Mat2<Unit, Space> {
    type Output = Self;
    #[inline]
    fn add(self, rhs: Self) -> Self::Output {
        Self {
            x_col: self.x_col + rhs.x_col,
            y_col: self.y_col + rhs.y_col,
            _unit: PhantomData,
            _space: PhantomData,
        }
    }
}

// Matrix-Matrix subtraction
impl<Unit: Copy, Space: Copy> Sub for Mat2<Unit, Space> {
    type Output = Self;
    #[inline]
    fn sub(self, rhs: Self) -> Self::Output {
        Self {
            x_col: self.x_col - rhs.x_col,
            y_col: self.y_col - rhs.y_col,
            _unit: PhantomData,
            _space: PhantomData,
        }
    }
}

// Matrix-Matrix multiplication
impl<Unit: Copy, Space: Copy> Mul for Mat2<Unit, Space> {
    type Output = Self;
    #[inline]
    fn mul(self, rhs: Self) -> Self::Output {
        Self {
            x_col: self * rhs.x_col,
            y_col: self * rhs.y_col,
            _unit: PhantomData,
            _space: PhantomData,
        }
    }
}

// Matrix-Vector multiplication
// M * v
// [a b] * [x] = [ax + by]
// [c d]   [y]   [cx + dy]
// x_col = (a,c), y_col = (b,d)
// res.x = a*x + b*y = x_col.x * v.x + y_col.x * v.y
// res.y = c*x + d*y = x_col.y * v.x + y_col.y * v.y
impl<Unit: Copy, Space: Copy> Mul<Vec2<Unit, Space>> for Mat2<Unit, Space> {
    type Output = Vec2<Unit, Space>;
    #[inline]
    fn mul(self, v: Vec2<Unit, Space>) -> Vec2<Unit, Space> {
        Vec2::new(
            self.x_col.x * v.x + self.y_col.x * v.y,
            self.x_col.y * v.x + self.y_col.y * v.y,
        )
    }
}

// Matrix-Scalar multiplication
impl<Unit: Copy, Space: Copy> Mul<f32> for Mat2<Unit, Space> {
    type Output = Self;
    #[inline]
    fn mul(self, scalar: f32) -> Self::Output {
        Self {
            x_col: self.x_col * scalar,
            y_col: self.y_col * scalar,
            _unit: PhantomData,
            _space: PhantomData,
        }
    }
}

// Scalar-Matrix multiplication
impl<Unit: Copy, Space: Copy> Mul<Mat2<Unit, Space>> for f32 {
    type Output = Mat2<Unit, Space>;
    #[inline]
    fn mul(self, matrix: Mat2<Unit, Space>) -> Self::Output {
        matrix * self // Reuse Mat2 * f32
    }
}