Skip to main content

nexcore_softrender/math/
vec.rs

1//! Vector types: Vec2, Vec3, Vec4 (homogeneous coordinates)
2//!
3//! Pure math. Every field is f64 for precision.
4//! Vec4 uses homogeneous coordinates: actual position = (x/w, y/w, z/w).
5
6use core::ops::{Add, Div, Mul, Neg, Sub};
7
8// ============================================================================
9// Vec2
10// ============================================================================
11
12#[derive(Debug, Clone, Copy, PartialEq)]
13#[non_exhaustive]
14pub struct Vec2 {
15    pub x: f64,
16    pub y: f64,
17}
18
19impl Vec2 {
20    pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
21    pub const ONE: Self = Self { x: 1.0, y: 1.0 };
22
23    pub const fn new(x: f64, y: f64) -> Self {
24        Self { x, y }
25    }
26
27    pub fn dot(self, rhs: Self) -> f64 {
28        self.x * rhs.x + self.y * rhs.y
29    }
30
31    /// 2D cross product (returns scalar z-component)
32    pub fn cross(self, rhs: Self) -> f64 {
33        self.x * rhs.y - self.y * rhs.x
34    }
35
36    pub fn length_sq(self) -> f64 {
37        self.dot(self)
38    }
39
40    pub fn length(self) -> f64 {
41        self.length_sq().sqrt()
42    }
43
44    pub fn normalize(self) -> Self {
45        let len = self.length();
46        if len < f64::EPSILON {
47            Self::ZERO
48        } else {
49            self * (1.0 / len)
50        }
51    }
52
53    /// Perpendicular vector (90° counter-clockwise)
54    pub fn perp(self) -> Self {
55        Self::new(-self.y, self.x)
56    }
57
58    pub fn lerp(self, other: Self, t: f64) -> Self {
59        Self::new(
60            self.x + (other.x - self.x) * t,
61            self.y + (other.y - self.y) * t,
62        )
63    }
64}
65
66impl Add for Vec2 {
67    type Output = Self;
68    fn add(self, rhs: Self) -> Self {
69        Self::new(self.x + rhs.x, self.y + rhs.y)
70    }
71}
72
73impl Sub for Vec2 {
74    type Output = Self;
75    fn sub(self, rhs: Self) -> Self {
76        Self::new(self.x - rhs.x, self.y - rhs.y)
77    }
78}
79
80impl Mul<f64> for Vec2 {
81    type Output = Self;
82    fn mul(self, s: f64) -> Self {
83        Self::new(self.x * s, self.y * s)
84    }
85}
86
87impl Mul<Vec2> for f64 {
88    type Output = Vec2;
89    fn mul(self, v: Vec2) -> Vec2 {
90        Vec2::new(self * v.x, self * v.y)
91    }
92}
93
94impl Neg for Vec2 {
95    type Output = Self;
96    fn neg(self) -> Self {
97        Self::new(-self.x, -self.y)
98    }
99}
100
101// ============================================================================
102// Vec3
103// ============================================================================
104
105#[derive(Debug, Clone, Copy, PartialEq)]
106#[non_exhaustive]
107pub struct Vec3 {
108    pub x: f64,
109    pub y: f64,
110    pub z: f64,
111}
112
113impl Vec3 {
114    pub const ZERO: Self = Self {
115        x: 0.0,
116        y: 0.0,
117        z: 0.0,
118    };
119    pub const ONE: Self = Self {
120        x: 1.0,
121        y: 1.0,
122        z: 1.0,
123    };
124    pub const X: Self = Self {
125        x: 1.0,
126        y: 0.0,
127        z: 0.0,
128    };
129    pub const Y: Self = Self {
130        x: 0.0,
131        y: 1.0,
132        z: 0.0,
133    };
134    pub const Z: Self = Self {
135        x: 0.0,
136        y: 0.0,
137        z: 1.0,
138    };
139
140    pub const fn new(x: f64, y: f64, z: f64) -> Self {
141        Self { x, y, z }
142    }
143
144    pub fn from_vec2(v: Vec2, z: f64) -> Self {
145        Self::new(v.x, v.y, z)
146    }
147
148    pub fn xy(self) -> Vec2 {
149        Vec2::new(self.x, self.y)
150    }
151
152    pub fn dot(self, rhs: Self) -> f64 {
153        self.x * rhs.x + self.y * rhs.y + self.z * rhs.z
154    }
155
156    pub fn cross(self, rhs: Self) -> Self {
157        Self::new(
158            self.y * rhs.z - self.z * rhs.y,
159            self.z * rhs.x - self.x * rhs.z,
160            self.x * rhs.y - self.y * rhs.x,
161        )
162    }
163
164    pub fn length_sq(self) -> f64 {
165        self.dot(self)
166    }
167
168    pub fn length(self) -> f64 {
169        self.length_sq().sqrt()
170    }
171
172    pub fn normalize(self) -> Self {
173        let len = self.length();
174        if len < f64::EPSILON {
175            Self::ZERO
176        } else {
177            self * (1.0 / len)
178        }
179    }
180
181    pub fn lerp(self, other: Self, t: f64) -> Self {
182        Self::new(
183            self.x + (other.x - self.x) * t,
184            self.y + (other.y - self.y) * t,
185            self.z + (other.z - self.z) * t,
186        )
187    }
188}
189
190impl Add for Vec3 {
191    type Output = Self;
192    fn add(self, rhs: Self) -> Self {
193        Self::new(self.x + rhs.x, self.y + rhs.y, self.z + rhs.z)
194    }
195}
196
197impl Sub for Vec3 {
198    type Output = Self;
199    fn sub(self, rhs: Self) -> Self {
200        Self::new(self.x - rhs.x, self.y - rhs.y, self.z - rhs.z)
201    }
202}
203
204impl Mul<f64> for Vec3 {
205    type Output = Self;
206    fn mul(self, s: f64) -> Self {
207        Self::new(self.x * s, self.y * s, self.z * s)
208    }
209}
210
211impl Mul<Vec3> for f64 {
212    type Output = Vec3;
213    fn mul(self, v: Vec3) -> Vec3 {
214        Vec3::new(self * v.x, self * v.y, self * v.z)
215    }
216}
217
218impl Neg for Vec3 {
219    type Output = Self;
220    fn neg(self) -> Self {
221        Self::new(-self.x, -self.y, -self.z)
222    }
223}
224
225// ============================================================================
226// Vec4 — Homogeneous coordinates
227// ============================================================================
228
229#[derive(Debug, Clone, Copy, PartialEq)]
230#[non_exhaustive]
231pub struct Vec4 {
232    pub x: f64,
233    pub y: f64,
234    pub z: f64,
235    pub w: f64,
236}
237
238impl Vec4 {
239    pub const ZERO: Self = Self {
240        x: 0.0,
241        y: 0.0,
242        z: 0.0,
243        w: 0.0,
244    };
245
246    pub const fn new(x: f64, y: f64, z: f64, w: f64) -> Self {
247        Self { x, y, z, w }
248    }
249
250    /// Create homogeneous point (w=1)
251    pub fn point(x: f64, y: f64, z: f64) -> Self {
252        Self::new(x, y, z, 1.0)
253    }
254
255    /// Create homogeneous direction (w=0)
256    pub fn direction(x: f64, y: f64, z: f64) -> Self {
257        Self::new(x, y, z, 0.0)
258    }
259
260    pub fn from_vec3(v: Vec3, w: f64) -> Self {
261        Self::new(v.x, v.y, v.z, w)
262    }
263
264    /// Perspective divide: (x/w, y/w, z/w)
265    pub fn to_vec3(self) -> Vec3 {
266        if self.w.abs() < f64::EPSILON {
267            Vec3::new(self.x, self.y, self.z)
268        } else {
269            Vec3::new(self.x / self.w, self.y / self.w, self.z / self.w)
270        }
271    }
272
273    pub fn xyz(self) -> Vec3 {
274        Vec3::new(self.x, self.y, self.z)
275    }
276
277    pub fn dot(self, rhs: Self) -> f64 {
278        self.x * rhs.x + self.y * rhs.y + self.z * rhs.z + self.w * rhs.w
279    }
280}
281
282impl Add for Vec4 {
283    type Output = Self;
284    fn add(self, rhs: Self) -> Self {
285        Self::new(
286            self.x + rhs.x,
287            self.y + rhs.y,
288            self.z + rhs.z,
289            self.w + rhs.w,
290        )
291    }
292}
293
294impl Sub for Vec4 {
295    type Output = Self;
296    fn sub(self, rhs: Self) -> Self {
297        Self::new(
298            self.x - rhs.x,
299            self.y - rhs.y,
300            self.z - rhs.z,
301            self.w - rhs.w,
302        )
303    }
304}
305
306impl Mul<f64> for Vec4 {
307    type Output = Self;
308    fn mul(self, s: f64) -> Self {
309        Self::new(self.x * s, self.y * s, self.z * s, self.w * s)
310    }
311}
312
313impl Div<f64> for Vec4 {
314    type Output = Self;
315    fn div(self, s: f64) -> Self {
316        Self::new(self.x / s, self.y / s, self.z / s, self.w / s)
317    }
318}
319
320// ============================================================================
321// Tests
322// ============================================================================
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn vec2_basic_ops() {
330        let a = Vec2::new(3.0, 4.0);
331        let b = Vec2::new(1.0, 2.0);
332        let sum = a + b;
333        assert!((sum.x - 4.0).abs() < f64::EPSILON);
334        assert!((sum.y - 6.0).abs() < f64::EPSILON);
335    }
336
337    #[test]
338    fn vec2_length() {
339        let v = Vec2::new(3.0, 4.0);
340        assert!((v.length() - 5.0).abs() < 1e-10);
341    }
342
343    #[test]
344    fn vec2_normalize() {
345        let v = Vec2::new(3.0, 4.0).normalize();
346        assert!((v.length() - 1.0).abs() < 1e-10);
347    }
348
349    #[test]
350    fn vec2_cross() {
351        let a = Vec2::new(1.0, 0.0);
352        let b = Vec2::new(0.0, 1.0);
353        assert!((a.cross(b) - 1.0).abs() < f64::EPSILON);
354    }
355
356    #[test]
357    fn vec2_perp() {
358        let v = Vec2::new(1.0, 0.0);
359        let p = v.perp();
360        assert!((p.x - 0.0).abs() < f64::EPSILON);
361        assert!((p.y - 1.0).abs() < f64::EPSILON);
362    }
363
364    #[test]
365    fn vec3_cross_product() {
366        let x = Vec3::X;
367        let y = Vec3::Y;
368        let z = x.cross(y);
369        assert!((z.x - 0.0).abs() < f64::EPSILON);
370        assert!((z.y - 0.0).abs() < f64::EPSILON);
371        assert!((z.z - 1.0).abs() < f64::EPSILON);
372    }
373
374    #[test]
375    fn vec3_dot() {
376        let a = Vec3::new(1.0, 2.0, 3.0);
377        let b = Vec3::new(4.0, 5.0, 6.0);
378        assert!((a.dot(b) - 32.0).abs() < f64::EPSILON);
379    }
380
381    #[test]
382    fn vec4_perspective_divide() {
383        let v = Vec4::new(4.0, 6.0, 8.0, 2.0);
384        let p = v.to_vec3();
385        assert!((p.x - 2.0).abs() < f64::EPSILON);
386        assert!((p.y - 3.0).abs() < f64::EPSILON);
387        assert!((p.z - 4.0).abs() < f64::EPSILON);
388    }
389
390    #[test]
391    fn vec2_lerp() {
392        let a = Vec2::new(0.0, 0.0);
393        let b = Vec2::new(10.0, 10.0);
394        let mid = a.lerp(b, 0.5);
395        assert!((mid.x - 5.0).abs() < f64::EPSILON);
396        assert!((mid.y - 5.0).abs() < f64::EPSILON);
397    }
398
399    #[test]
400    fn vec3_normalize_zero() {
401        let v = Vec3::ZERO.normalize();
402        assert!((v.length()).abs() < f64::EPSILON);
403    }
404}