Skip to main content

ling_graphics/
math.rs

1pub use glam::{Vec2, Vec3, Vec4, Mat3, Mat4, Quat, IVec2, IVec3, UVec2};
2
3// ── 4D vector (spatial, not homogeneous) ─────────────────────────────────────
4
5#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
6pub struct Vec4H {
7    pub x: f32,
8    pub y: f32,
9    pub z: f32,
10    pub w: f32,
11}
12
13impl Vec4H {
14    pub const ZERO: Self = Self { x: 0.0, y: 0.0, z: 0.0, w: 0.0 };
15    pub const ONE: Self  = Self { x: 1.0, y: 1.0, z: 1.0, w: 1.0 };
16
17    pub fn new(x: f32, y: f32, z: f32, w: f32) -> Self { Self { x, y, z, w } }
18
19    pub fn dot(&self, other: Self) -> f32 {
20        self.x * other.x + self.y * other.y + self.z * other.z + self.w * other.w
21    }
22
23    /// Minkowski inner product: -x₀y₀ + x₁y₁ + x₂y₂ + x₃y₃ (hyperboloid model)
24    pub fn minkowski_dot(&self, other: Self) -> f32 {
25        -self.x * other.x + self.y * other.y + self.z * other.z + self.w * other.w
26    }
27
28    pub fn length(&self) -> f32 { self.dot(*self).sqrt() }
29
30    pub fn normalize(&self) -> Self {
31        let l = self.length();
32        if l < 1e-8 { return *self; }
33        Self::new(self.x / l, self.y / l, self.z / l, self.w / l)
34    }
35
36    pub fn lerp(&self, other: Self, t: f32) -> Self {
37        Self::new(
38            self.x + (other.x - self.x) * t,
39            self.y + (other.y - self.y) * t,
40            self.z + (other.z - self.z) * t,
41            self.w + (other.w - self.w) * t,
42        )
43    }
44
45    pub fn xyz(&self) -> Vec3 { Vec3::new(self.x, self.y, self.z) }
46}
47
48impl std::ops::Add for Vec4H {
49    type Output = Self;
50    fn add(self, r: Self) -> Self { Self::new(self.x+r.x, self.y+r.y, self.z+r.z, self.w+r.w) }
51}
52impl std::ops::Sub for Vec4H {
53    type Output = Self;
54    fn sub(self, r: Self) -> Self { Self::new(self.x-r.x, self.y-r.y, self.z-r.z, self.w-r.w) }
55}
56impl std::ops::Mul<f32> for Vec4H {
57    type Output = Self;
58    fn mul(self, s: f32) -> Self { Self::new(self.x*s, self.y*s, self.z*s, self.w*s) }
59}
60impl std::ops::Neg for Vec4H {
61    type Output = Self;
62    fn neg(self) -> Self { Self::new(-self.x, -self.y, -self.z, -self.w) }
63}
64
65// ── 5×5 matrix for homogeneous 4D transforms ─────────────────────────────────
66
67#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
68pub struct Mat5(pub [[f32; 5]; 5]);
69
70impl Mat5 {
71    pub fn identity() -> Self {
72        let mut m = [[0f32; 5]; 5];
73        for i in 0..5 { m[i][i] = 1.0; }
74        Self(m)
75    }
76
77    pub fn zero() -> Self { Self([[0f32; 5]; 5]) }
78
79    pub fn mul_vec(&self, v: [f32; 5]) -> [f32; 5] {
80        let mut out = [0f32; 5];
81        for i in 0..5 {
82            for j in 0..5 { out[i] += self.0[i][j] * v[j]; }
83        }
84        out
85    }
86
87    pub fn mul(&self, rhs: &Self) -> Self {
88        let mut out = [[0f32; 5]; 5];
89        for i in 0..5 {
90            for j in 0..5 {
91                for k in 0..5 { out[i][j] += self.0[i][k] * rhs.0[k][j]; }
92            }
93        }
94        Self(out)
95    }
96
97    pub fn transpose(&self) -> Self {
98        let mut out = [[0f32; 5]; 5];
99        for i in 0..5 {
100            for j in 0..5 { out[i][j] = self.0[j][i]; }
101        }
102        Self(out)
103    }
104
105    /// 4D translation matrix (shifts along the w axis of hyperbolic space)
106    pub fn translation_4d(delta: Vec4H) -> Self {
107        let mut m = Self::identity();
108        m.0[0][4] = delta.x;
109        m.0[1][4] = delta.y;
110        m.0[2][4] = delta.z;
111        m.0[3][4] = delta.w;
112        m
113    }
114}
115
116// ── Axis-aligned bounding box ─────────────────────────────────────────────────
117
118#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
119pub struct Aabb {
120    pub min: Vec3,
121    pub max: Vec3,
122}
123
124impl Aabb {
125    pub fn new(min: Vec3, max: Vec3) -> Self { Self { min, max } }
126
127    pub fn from_points(points: &[Vec3]) -> Self {
128        let mut min = Vec3::splat(f32::INFINITY);
129        let mut max = Vec3::splat(f32::NEG_INFINITY);
130        for &p in points {
131            min = min.min(p);
132            max = max.max(p);
133        }
134        Self { min, max }
135    }
136
137    pub fn center(&self) -> Vec3 { (self.min + self.max) * 0.5 }
138    pub fn half_extents(&self) -> Vec3 { (self.max - self.min) * 0.5 }
139    pub fn size(&self) -> Vec3 { self.max - self.min }
140
141    pub fn contains(&self, p: Vec3) -> bool {
142        p.x >= self.min.x && p.x <= self.max.x
143            && p.y >= self.min.y && p.y <= self.max.y
144            && p.z >= self.min.z && p.z <= self.max.z
145    }
146
147    pub fn union(&self, other: &Self) -> Self {
148        Self { min: self.min.min(other.min), max: self.max.max(other.max) }
149    }
150
151    pub fn expand(&self, by: f32) -> Self {
152        Self {
153            min: self.min - Vec3::splat(by),
154            max: self.max + Vec3::splat(by),
155        }
156    }
157}
158
159// ── Ray ───────────────────────────────────────────────────────────────────────
160
161#[derive(Debug, Clone, Copy, PartialEq)]
162pub struct Ray3 {
163    pub origin: Vec3,
164    pub direction: Vec3,
165}
166
167impl Ray3 {
168    pub fn new(origin: Vec3, direction: Vec3) -> Self {
169        Self { origin, direction: direction.normalize() }
170    }
171
172    pub fn at(&self, t: f32) -> Vec3 { self.origin + self.direction * t }
173
174    pub fn intersect_sphere(&self, center: Vec3, radius: f32) -> Option<f32> {
175        let oc = self.origin - center;
176        let b = oc.dot(self.direction);
177        let c = oc.dot(oc) - radius * radius;
178        let disc = b * b - c;
179        if disc < 0.0 { return None; }
180        let t = -b - disc.sqrt();
181        if t > 0.0 { Some(t) } else {
182            let t2 = -b + disc.sqrt();
183            if t2 > 0.0 { Some(t2) } else { None }
184        }
185    }
186
187    pub fn intersect_aabb(&self, aabb: &Aabb) -> Option<f32> {
188        let inv_d = Vec3::ONE / self.direction;
189        let t1 = (aabb.min - self.origin) * inv_d;
190        let t2 = (aabb.max - self.origin) * inv_d;
191        let tmin = t1.min(t2);
192        let tmax = t1.max(t2);
193        let enter = tmin.x.max(tmin.y).max(tmin.z);
194        let exit  = tmax.x.min(tmax.y).min(tmax.z);
195        if exit >= enter && exit >= 0.0 { Some(enter.max(0.0)) } else { None }
196    }
197
198    pub fn intersect_plane(&self, plane: &Plane) -> Option<f32> {
199        let denom = plane.normal.dot(self.direction);
200        if denom.abs() < 1e-8 { return None; }
201        let t = -(plane.normal.dot(self.origin) + plane.d) / denom;
202        if t >= 0.0 { Some(t) } else { None }
203    }
204}
205
206// ── Plane ─────────────────────────────────────────────────────────────────────
207
208#[derive(Debug, Clone, Copy, PartialEq)]
209pub struct Plane {
210    pub normal: Vec3,
211    pub d: f32,
212}
213
214impl Plane {
215    pub fn new(normal: Vec3, d: f32) -> Self { Self { normal: normal.normalize(), d } }
216
217    pub fn from_point_normal(point: Vec3, normal: Vec3) -> Self {
218        let n = normal.normalize();
219        Self { normal: n, d: -n.dot(point) }
220    }
221
222    pub fn from_three_points(a: Vec3, b: Vec3, c: Vec3) -> Self {
223        let n = (b - a).cross(c - a).normalize();
224        Self::from_point_normal(a, n)
225    }
226
227    pub fn signed_distance(&self, p: Vec3) -> f32 { self.normal.dot(p) + self.d }
228}
229
230// ── View frustum ──────────────────────────────────────────────────────────────
231
232pub struct Frustum {
233    planes: [Plane; 6],
234}
235
236impl Frustum {
237    /// Extract frustum planes from a combined view-projection matrix
238    /// using the Gribb/Hartmann method.
239    pub fn from_view_proj(vp: Mat4) -> Self {
240        let cols = vp.to_cols_array_2d(); // [col][row]
241        let get_row = |i: usize| Vec4::new(cols[0][i], cols[1][i], cols[2][i], cols[3][i]);
242        let r0 = get_row(0);
243        let r1 = get_row(1);
244        let r2 = get_row(2);
245        let r3 = get_row(3);
246
247        let make = |v: Vec4| Plane::new(Vec3::new(v.x, v.y, v.z), v.w);
248
249        Self {
250            planes: [
251                make(r3 + r0), // left
252                make(r3 - r0), // right
253                make(r3 + r1), // bottom
254                make(r3 - r1), // top
255                make(r3 + r2), // near
256                make(r3 - r2), // far
257            ],
258        }
259    }
260
261    pub fn contains_point(&self, p: Vec3) -> bool {
262        self.planes.iter().all(|plane| plane.signed_distance(p) >= 0.0)
263    }
264
265    pub fn contains_aabb(&self, aabb: &Aabb) -> bool {
266        for plane in &self.planes {
267            let positive = Vec3::new(
268                if plane.normal.x > 0.0 { aabb.max.x } else { aabb.min.x },
269                if plane.normal.y > 0.0 { aabb.max.y } else { aabb.min.y },
270                if plane.normal.z > 0.0 { aabb.max.z } else { aabb.min.z },
271            );
272            if plane.signed_distance(positive) < 0.0 { return false; }
273        }
274        true
275    }
276
277    pub fn contains_sphere(&self, center: Vec3, radius: f32) -> bool {
278        self.planes.iter().all(|p| p.signed_distance(center) >= -radius)
279    }
280}