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::vec3::*;
use crate::math;
use core::marker::PhantomData;

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Rect3<Unit: Copy = (), Space: Copy = ()> {
    pub pos: Vec3<Unit, Space>,
    pub dim: Vec3<Unit, Space>,
    #[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 Rect3f32 = Rect3<(),()>;
pub type Rect3Meters = Rect3<Meters,()>;
pub type Rect3Pixels = Rect3<Pixels,()>;
pub type Rect3World = Rect3<(),World>;
pub type Rect3Local = Rect3<(),Local>;
pub type Rect3Screen = Rect3<(),Screen>;
pub type Rect3MetersWorld = Rect3<Meters,World>;
pub type Rect3PixelsScreen = Rect3<Pixels,Screen>;

impl<Unit: Copy, Space: Copy> Rect3<Unit, Space> {
    #[inline]
    pub const fn new(pos: Vec3<Unit, Space>, dim: Vec3<Unit, Space>) -> Self {
        Self { pos, dim, _unit: PhantomData, _space: PhantomData }
    }

    /// Creates a Rect3 from an array of 6 floats: [pos.x, pos.y, pos.z, dim.x, dim.y, dim.z].
    #[inline]
    pub const fn from_array(v: [f32; 6]) -> Self {
        Self {
            pos: Vec3::new(v[0], v[1], v[2]),
            dim: Vec3::new(v[3], v[4], v[5]),
            _unit: PhantomData,
            _space: PhantomData,
        }
    }

    /// Checks if a point (x, y, z) is contained within the rectangle.
    /// The rectangle's position is its minimum corner, and its dimensions are positive.
    /// Points on the minimum edges are considered inside, points on the maximum edges are not.
    #[inline]
    pub fn contains_point_coords(&self, x: f32, y: f32, z: f32) -> bool {
        let min_x = self.pos.x.min(self.pos.x + self.dim.x);
        let max_x = self.pos.x.max(self.pos.x + self.dim.x);
        let min_y = self.pos.y.min(self.pos.y + self.dim.y);
        let max_y = self.pos.y.max(self.pos.y + self.dim.y);
        let min_z = self.pos.z.min(self.pos.z + self.dim.z);
        let max_z = self.pos.z.max(self.pos.z + self.dim.z);
        x >= min_x && x < max_x && y >= min_y && y < max_y && z >= min_z && z < max_z
    }

    /// Checks if a Vec3 point is contained within the rectangle.
    #[inline]
    pub fn contains_point(&self, p: Vec3<Unit, Space>) -> bool {
        self.contains_point_coords(p.x, p.y, p.z)
    }

    /// Checks if this rectangle intersects with another rectangle.
    #[inline]
    pub fn intersects(&self, other: &Self) -> bool {
        self.intersection(other).is_some()
    }

    /// Calculates the intersection of this rectangle with another.
    /// Returns Some(Rect3) if they intersect, None otherwise.
    /// The resulting rectangle will have its position as the minimum corner
    /// and positive dimensions.
    #[inline]
    pub fn intersection(&self, other: &Self) -> Option<Self> {
        let a_min_x = self.pos.x.min(self.pos.x + self.dim.x);
        let a_max_x = self.pos.x.max(self.pos.x + self.dim.x);
        let a_min_y = self.pos.y.min(self.pos.y + self.dim.y);
        let a_max_y = self.pos.y.max(self.pos.y + self.dim.y);
        let a_min_z = self.pos.z.min(self.pos.z + self.dim.z);
        let a_max_z = self.pos.z.max(self.pos.z + self.dim.z);

        let b_min_x = other.pos.x.min(other.pos.x + other.dim.x);
        let b_max_x = other.pos.x.max(other.pos.x + other.dim.x);
        let b_min_y = other.pos.y.min(other.pos.y + other.dim.y);
        let b_max_y = other.pos.y.max(other.pos.y + other.dim.y);
        let b_min_z = other.pos.z.min(other.pos.z + other.dim.z);
        let b_max_z = other.pos.z.max(other.pos.z + other.dim.z);

        let x0 = a_min_x.max(b_min_x);
        let y0 = a_min_y.max(b_min_y);
        let z0 = a_min_z.max(b_min_z);
        let x1 = a_max_x.min(b_max_x);
        let y1 = a_max_y.min(b_max_y);
        let z1 = a_max_z.min(b_max_z);

        if x0 < x1 && y0 < y1 && z0 < z1 {
            Some(Self::new(
                Vec3::new(x0, y0, z0),
                Vec3::new(x1 - x0, y1 - y0, z1 - z0),
            ))
        } else {
            None
        }
    }

    /// Returns the minimum corner of the rectangle.
    pub const fn min(&self) -> Vec3<Unit, Space> {
        Vec3::new(
            if self.pos.x < self.pos.x + self.dim.x { self.pos.x } else { self.pos.x + self.dim.x },
            if self.pos.y < self.pos.y + self.dim.y { self.pos.y } else { self.pos.y + self.dim.y },
            if self.pos.z < self.pos.z + self.dim.z { self.pos.z } else { self.pos.z + self.dim.z },
        )
    }

    /// Returns the maximum corner of the rectangle.
    pub const fn max(&self) -> Vec3<Unit, Space> {
        Vec3::new(
            if self.pos.x > self.pos.x + self.dim.x { self.pos.x } else { self.pos.x + self.dim.x },
            if self.pos.y > self.pos.y + self.dim.y { self.pos.y } else { self.pos.y + self.dim.y },
            if self.pos.z > self.pos.z + self.dim.z { self.pos.z } else { self.pos.z + self.dim.z },
        )
    }

    /// Returns the size (width, height, depth) of the rectangle (always positive).
    pub fn size(&self) -> Vec3<Unit, Space> {
        Vec3::new(self.dim.x.abs(), self.dim.y.abs(), self.dim.z.abs())
    }

    /// Returns the volume of the rectangle.
    pub fn volume(&self) -> f32 {
        let s = self.size();
        s.x * s.y * s.z
    }

    /// Returns true if the rectangle is empty (any size == 0).
    pub fn is_empty(&self) -> bool {
        self.dim.x == 0.0 || self.dim.y == 0.0 || self.dim.z == 0.0
    }

    /// Constructs a Rect3 from min and max corners.
    pub fn from_min_max(min: Vec3<Unit, Space>, max: Vec3<Unit, Space>) -> Self {
        Self {
            pos: min,
            dim: max - min,
            _unit: PhantomData,
            _space: PhantomData,
        }
    }

    /// Returns a new Rect3 expanded to include the given point.
    pub fn expand_to_include(&self, point: Vec3<Unit, Space>) -> Self {
        let min = self.min();
        let max = self.max();
        let new_min = Vec3::new(min.x.min(point.x), min.y.min(point.y), min.z.min(point.z));
        let new_max = Vec3::new(max.x.max(point.x), max.y.max(point.y), max.z.max(point.z));
        Self::from_min_max(new_min, new_max)
    }

    /// Returns the union (smallest Rect containing both) of self and other.
    pub fn union(&self, other: &Self) -> Self {
        let min = self.min().min(other.min());
        let max = self.max().max(other.max());
        Self::from_min_max(min, max)
    }

    /// Transforms the rectangle by a Mat4. Returns the smallest Rect3 containing the transformed corners.
    #[cfg(feature = "mat4")]
    pub fn transform(&self, mat: &crate::mat4::Mat4) -> Self {
        let min = self.min();
        let max = self.max();
        let corners: [Vec3; 8] = [
            Vec3::new(min.x, min.y, min.z),
            Vec3::new(min.x, min.y, max.z),
            Vec3::new(min.x, max.y, min.z),
            Vec3::new(min.x, max.y, max.z),
            Vec3::new(max.x, min.y, min.z),
            Vec3::new(max.x, min.y, max.z),
            Vec3::new(max.x, max.y, min.z),
            Vec3::new(max.x, max.y, max.z),
        ];
        let mut tmin = Vec3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY);
        let mut tmax = Vec3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY);
        for &c in &corners {
            let v = mat.transform_point(c);
            let v_typed = Vec3::<Unit, Space>::new(v.x, v.y, v.z);
            tmin = tmin.min(v_typed);
            tmax = tmax.max(v_typed);
        }
        Self::from_min_max(tmin, tmax)
    }

    /// Returns the closest point in the rectangle to the given point.
    pub fn closest_point(&self, point: Vec3<Unit, Space>) -> Vec3<Unit, Space> {
        let min = self.min();
        let max = self.max();
        point.clamp(min, max)
    }

    /// Returns the distance from the rectangle to the given point (0 if inside).
    pub fn distance(&self, point: Vec3<Unit, Space>) -> f32 {
        let min = self.min();
        let max = self.max();
        let dx = if point.x < min.x {
            min.x - point.x
        } else if point.x > max.x {
            point.x - max.x
        } else {
            0.0
        };
        let dy = if point.y < min.y {
            min.y - point.y
        } else if point.y > max.y {
            point.y - max.y
        } else {
            0.0
        };
        let dz = if point.z < min.z {
            min.z - point.z
        } else if point.z > max.z {
            point.z - max.z
        } else {
            0.0
        };
        math::sqrt(dx * dx + dy * dy + dz * dz)
    }

    /// Ray-Rect intersection (slab method). Returns Some(t) for the first intersection, or None if no hit.
    /// Ray: origin + t * dir, t >= 0. Returns smallest t >= 0.
    pub fn intersect_ray(&self, origin: Vec3<Unit, Space>, dir: Vec3<Unit, Space>) -> Option<f32> {
        let min = self.min();
        let max = self.max();
        let mut tmin = (min.x - origin.x) / dir.x;
        let mut tmax = (max.x - origin.x) / dir.x;
        if tmin > tmax {
            core::mem::swap(&mut tmin, &mut tmax);
        }
        let mut tymin = (min.y - origin.y) / dir.y;
        let mut tymax = (max.y - origin.y) / dir.y;
        if tymin > tymax {
            core::mem::swap(&mut tymin, &mut tymax);
        }
        if (tmin > tymax) || (tymin > tmax) {
            return None;
        }
        tmin = tmin.max(tymin);
        tmax = tmax.min(tymax);
        let mut tzmin = (min.z - origin.z) / dir.z;
        let mut tzmax = (max.z - origin.z) / dir.z;
        if tzmin > tzmax {
            core::mem::swap(&mut tzmin, &mut tzmax);
        }
        if (tmin > tzmax) || (tzmin > tmax) {
            return None;
        }
        tmin = tmin.max(tzmin);
        tmax = tmax.min(tzmax);
        if tmax < 0.0 {
            return None;
        }
        Some(if tmin >= 0.0 { tmin } else { tmax })
    }
}

// Example: conversion from Rect3<Meters> to Rect3<Pixels>
impl<Space: Copy> Rect3<Meters, Space> {
    pub fn to_pixels(self, scale: f32) -> Rect3<Pixels, Space> {
        Rect3 {
            pos: self.pos.to_pixels(scale),
            dim: self.dim.to_pixels(scale),
            _unit: PhantomData,
            _space: PhantomData,
        }
    }
}