Skip to main content

game_gem/math/
mod.rs

1//! # game-gem Math
2//!
3//! A comprehensive 2D/3D math library built on `glam` with game-specific extensions.
4//!
5//! Re-exports all necessary `glam` types and adds:
6//! - [`Rect`] — axis-aligned rectangle with intersection/containment helpers
7//! - [`Transform`] — 2D transform (position, rotation, scale) with hierarchy support
8//! - [`Lerp`] trait — generic linear interpolation
9//! - [`Angle`] — type-safe angle wrapper (radians, degrees)
10
11mod rect;
12mod transform;
13mod lerp;
14mod angle;
15
16pub use glam::{Vec2, Vec3, Vec4, Mat4, Quat};
17pub use rect::Rect;
18pub use transform::Transform;
19pub use lerp::Lerp;
20pub use angle::Angle;
21
22// --- Convenience constructors & extension methods ---
23
24/// Shorthand: `vec2(x, y)`
25#[inline(always)]
26pub const fn vec2(x: f32, y: f32) -> Vec2 {
27    Vec2::new(x, y)
28}
29
30/// Shorthand: `vec3(x, y, z)`
31#[inline(always)]
32pub const fn vec3(x: f32, y: f32, z: f32) -> Vec3 {
33    Vec3::new(x, y, z)
34}
35
36/// Shorthand: `ivec2(x, y)`
37#[inline(always)]
38pub const fn ivec2(x: i32, y: i32) -> glam::IVec2 {
39    glam::IVec2::new(x, y)
40}
41
42/// Common constants.
43pub mod consts {
44    use super::*;
45
46    /// (0, 0)
47    pub const ZERO: Vec2 = Vec2::new(0.0, 0.0);
48    /// (1, 0)
49    pub const RIGHT: Vec2 = Vec2::new(1.0, 0.0);
50    /// (-1, 0)
51    pub const LEFT: Vec2 = Vec2::new(-1.0, 0.0);
52    /// (0, -1) — screen-space down (Y-down convention)
53    pub const UP: Vec2 = Vec2::new(0.0, -1.0);
54    /// (0, 1) — screen-space down (Y-down convention)
55    pub const DOWN: Vec2 = Vec2::new(0.0, 1.0);
56    /// (1, 1) normalized
57    pub const ONE: Vec2 = Vec2::new(1.0, 1.0);
58
59    /// Useful approximation of π.
60    pub const PI: f32 = std::f32::consts::PI;
61    /// π / 2
62    pub const HALF_PI: f32 = PI / 2.0;
63    /// 2π
64    pub const TAU: f32 = PI * 2.0;
65}
66
67/// Extension trait for `Vec2` with game-oriented helpers.
68pub trait Vec2Ext {
69    /// Euclidean distance to another point.
70    fn distance_to(self, other: Vec2) -> f32;
71    /// Squared Euclidean distance (avoids sqrt).
72    fn distance_squared_to(self, other: Vec2) -> f32;
73    /// Angle from `self` to `other` in radians.
74    fn angle_to(self, other: Vec2) -> f32;
75    /// Rotate this vector by `angle` radians.
76    fn rotated(self, angle: f32) -> Vec2;
77    /// Move toward `target` by at most `max_delta`.
78    fn move_toward(self, target: Vec2, max_delta: f32) -> Vec2;
79    /// Reflect this vector off a surface with the given `normal`.
80    fn reflect(self, normal: Vec2) -> Vec2;
81    /// Project `self` onto `onto`.
82    fn projected_onto(self, onto: Vec2) -> Vec2;
83    /// Perpendicular vector (rotated 90° clockwise in Y-down).
84    fn perp(self) -> Vec2;
85    /// Convert to integer pixel coordinates.
86    fn to_ivec(self) -> glam::IVec2;
87}
88
89impl Vec2Ext for Vec2 {
90    #[inline]
91    fn distance_to(self, other: Vec2) -> f32 {
92        (self - other).length()
93    }
94
95    #[inline]
96    fn distance_squared_to(self, other: Vec2) -> f32 {
97        (self - other).length_squared()
98    }
99
100    #[inline]
101    fn angle_to(self, other: Vec2) -> f32 {
102        let diff = other - self;
103        diff.y.atan2(diff.x)
104    }
105
106    #[inline]
107    fn rotated(self, angle: f32) -> Vec2 {
108        let (sin, cos) = angle.sin_cos();
109        Vec2::new(
110            self.x * cos - self.y * sin,
111            self.x * sin + self.y * cos,
112        )
113    }
114
115    #[inline]
116    fn move_toward(self, target: Vec2, max_delta: f32) -> Vec2 {
117        let diff = target - self;
118        let dist = diff.length();
119        if dist <= max_delta || dist < 1e-6 {
120            target
121        } else {
122            self + diff * (max_delta / dist)
123        }
124    }
125
126    #[inline]
127    fn reflect(self, normal: Vec2) -> Vec2 {
128        self - 2.0 * self.dot(normal) * normal
129    }
130
131    #[inline]
132    fn projected_onto(self, onto: Vec2) -> Vec2 {
133        onto * (self.dot(onto) / onto.length_squared())
134    }
135
136    #[inline]
137    fn perp(self) -> Vec2 {
138        Vec2::new(self.y, -self.x)
139    }
140
141    #[inline]
142    fn to_ivec(self) -> glam::IVec2 {
143        glam::IVec2::new(self.x as i32, self.y as i32)
144    }
145}
146
147/// Extension trait for `f32` with game-oriented helpers.
148pub trait FloatExt {
149    /// Linear interpolation between `self` and `other` by `t` (clamped 0..1).
150    fn lerp(self, other: f32, t: f32) -> f32;
151    /// Clamp value between `min` and `max`.
152    fn clamp(self, min: f32, max: f32) -> f32;
153    /// Map value from `in_min..=in_max` to `out_min..=out_max`.
154    fn map_range(self, in_min: f32, in_max: f32, out_min: f32, out_max: f32) -> f32;
155    /// Deadzone filter — returns 0.0 if within `deadzone` of zero.
156    fn deadzone(self, deadzone: f32) -> f32;
157    /// Snap to nearest increment of `step`.
158    fn snap(self, step: f32) -> f32;
159}
160
161impl FloatExt for f32 {
162    #[inline]
163    fn lerp(self, other: f32, t: f32) -> f32 {
164        self + (other - self) * t.clamp(0.0, 1.0)
165    }
166
167    #[inline]
168    fn clamp(self, min: f32, max: f32) -> f32 {
169        Self::clamp(self, min, max)
170    }
171
172    #[inline]
173    fn map_range(self, in_min: f32, in_max: f32, out_min: f32, out_max: f32) -> f32 {
174        (self - in_min) / (in_max - in_min) * (out_max - out_min) + out_min
175    }
176
177    #[inline]
178    fn deadzone(self, deadzone: f32) -> f32 {
179        if self.abs() < deadzone { 0.0 } else { self }
180    }
181
182    #[inline]
183    fn snap(self, step: f32) -> f32 {
184        (self / step).round() * step
185    }
186}