Skip to main content

brepkit_math/
vec.rs

1//! Vector and point types for geometric computation.
2//!
3//! [`Vector`] represents directions and displacements; [`Position`] represents
4//! positions. Both are newtypes over fixed-size `f64` arrays, parameterized by
5//! dimension via const generics.
6//!
7//! Ergonomic type aliases are provided for common dimensions:
8//!
9//! | Alias    | Underlying type |
10//! |----------|-----------------|
11//! | [`Vec2`] | `Vector<2>` |
12//! | [`Vec3`] | `Vector<3>` |
13//! | [`Point2`] | `Position<2>` |
14//! | [`Point3`] | `Position<3>` |
15
16use std::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign};
17
18use crate::MathError;
19
20// ===========================================================================
21// Vector<N>
22// ===========================================================================
23
24/// An N-dimensional vector representing a direction or displacement.
25#[derive(Debug, Clone, Copy, PartialEq)]
26pub struct Vector<const N: usize>(pub [f64; N]);
27
28/// A 2D vector.
29pub type Vec2 = Vector<2>;
30
31/// A 3D vector.
32pub type Vec3 = Vector<3>;
33
34// ---------------------------------------------------------------------------
35// Shared methods (all dimensions)
36// ---------------------------------------------------------------------------
37
38impl<const N: usize> Vector<N> {
39    /// Squared Euclidean length (avoids a sqrt).
40    #[must_use]
41    pub fn length_squared(self) -> f64 {
42        let mut sum = 0.0;
43        let mut i = 0;
44        while i < N {
45            sum = self.0[i].mul_add(self.0[i], sum);
46            i += 1;
47        }
48        sum
49    }
50
51    /// Euclidean length.
52    #[must_use]
53    pub fn length(self) -> f64 {
54        self.length_squared().sqrt()
55    }
56
57    /// Return a unit-length vector in the same direction.
58    ///
59    /// # Errors
60    ///
61    /// Returns [`MathError::ZeroVector`] if the vector is zero, has
62    /// denormalized length (< [`f64::MIN_POSITIVE`]), or has overflowed
63    /// length (non-finite).
64    pub fn normalize(self) -> Result<Self, MathError> {
65        let len = self.length();
66        if !len.is_finite() || len < f64::MIN_POSITIVE {
67            return Err(MathError::ZeroVector);
68        }
69        let inv = 1.0 / len;
70        Ok(Self(std::array::from_fn(|i| self.0[i] * inv)))
71    }
72}
73
74// ---------------------------------------------------------------------------
75// 2D-specific methods
76// ---------------------------------------------------------------------------
77
78impl Vector<2> {
79    /// Create a new 2D vector.
80    #[must_use]
81    pub const fn new(x: f64, y: f64) -> Self {
82        Self([x, y])
83    }
84
85    /// X component.
86    #[must_use]
87    pub const fn x(self) -> f64 {
88        self.0[0]
89    }
90
91    /// Y component.
92    #[must_use]
93    pub const fn y(self) -> f64 {
94        self.0[1]
95    }
96
97    /// Dot product of two 2D vectors.
98    #[must_use]
99    pub fn dot(self, rhs: Self) -> f64 {
100        self.0[0].mul_add(rhs.0[0], self.0[1] * rhs.0[1])
101    }
102}
103
104// ---------------------------------------------------------------------------
105// 3D-specific methods
106// ---------------------------------------------------------------------------
107
108impl Vector<3> {
109    /// Create a new 3D vector.
110    #[must_use]
111    pub const fn new(x: f64, y: f64, z: f64) -> Self {
112        Self([x, y, z])
113    }
114
115    /// X component.
116    #[must_use]
117    pub const fn x(self) -> f64 {
118        self.0[0]
119    }
120
121    /// Y component.
122    #[must_use]
123    pub const fn y(self) -> f64 {
124        self.0[1]
125    }
126
127    /// Z component.
128    #[must_use]
129    pub const fn z(self) -> f64 {
130        self.0[2]
131    }
132
133    /// Dot product of two 3D vectors.
134    #[must_use]
135    pub fn dot(self, rhs: Self) -> f64 {
136        self.0[0].mul_add(rhs.0[0], self.0[1].mul_add(rhs.0[1], self.0[2] * rhs.0[2]))
137    }
138
139    /// Cross product of two 3D vectors.
140    #[must_use]
141    pub fn cross(self, rhs: Self) -> Self {
142        Self([
143            self.0[1].mul_add(rhs.0[2], -(self.0[2] * rhs.0[1])),
144            self.0[2].mul_add(rhs.0[0], -(self.0[0] * rhs.0[2])),
145            self.0[0].mul_add(rhs.0[1], -(self.0[1] * rhs.0[0])),
146        ])
147    }
148}
149
150// ---------------------------------------------------------------------------
151// Vector operators (all dimensions)
152// ---------------------------------------------------------------------------
153
154impl<const N: usize> Add for Vector<N> {
155    type Output = Self;
156
157    fn add(self, rhs: Self) -> Self {
158        Self(std::array::from_fn(|i| self.0[i] + rhs.0[i]))
159    }
160}
161
162impl<const N: usize> AddAssign for Vector<N> {
163    fn add_assign(&mut self, rhs: Self) {
164        for i in 0..N {
165            self.0[i] += rhs.0[i];
166        }
167    }
168}
169
170impl<const N: usize> Sub for Vector<N> {
171    type Output = Self;
172
173    fn sub(self, rhs: Self) -> Self {
174        Self(std::array::from_fn(|i| self.0[i] - rhs.0[i]))
175    }
176}
177
178impl<const N: usize> SubAssign for Vector<N> {
179    fn sub_assign(&mut self, rhs: Self) {
180        for i in 0..N {
181            self.0[i] -= rhs.0[i];
182        }
183    }
184}
185
186/// `vector * scalar`
187impl<const N: usize> Mul<f64> for Vector<N> {
188    type Output = Self;
189
190    fn mul(self, s: f64) -> Self {
191        Self(std::array::from_fn(|i| self.0[i] * s))
192    }
193}
194
195/// `scalar * vector`
196impl<const N: usize> Mul<Vector<N>> for f64 {
197    type Output = Vector<N>;
198
199    fn mul(self, rhs: Vector<N>) -> Vector<N> {
200        Vector(std::array::from_fn(|i| self * rhs.0[i]))
201    }
202}
203
204impl<const N: usize> Neg for Vector<N> {
205    type Output = Self;
206
207    fn neg(self) -> Self {
208        Self(std::array::from_fn(|i| -self.0[i]))
209    }
210}
211
212// ===========================================================================
213// Position<N>
214// ===========================================================================
215
216/// An N-dimensional point representing a position.
217#[derive(Debug, Clone, Copy, PartialEq)]
218pub struct Position<const N: usize>(pub [f64; N]);
219
220/// A 2D point.
221pub type Point2 = Position<2>;
222
223/// A 3D point.
224pub type Point3 = Position<3>;
225
226// ---------------------------------------------------------------------------
227// 2D-specific methods
228// ---------------------------------------------------------------------------
229
230impl Position<2> {
231    /// Create a new 2D point.
232    #[must_use]
233    pub const fn new(x: f64, y: f64) -> Self {
234        Self([x, y])
235    }
236
237    /// X coordinate.
238    #[must_use]
239    pub const fn x(self) -> f64 {
240        self.0[0]
241    }
242
243    /// Y coordinate.
244    #[must_use]
245    pub const fn y(self) -> f64 {
246        self.0[1]
247    }
248}
249
250// ---------------------------------------------------------------------------
251// 3D-specific methods
252// ---------------------------------------------------------------------------
253
254impl Position<3> {
255    /// Create a new 3D point.
256    #[must_use]
257    pub const fn new(x: f64, y: f64, z: f64) -> Self {
258        Self([x, y, z])
259    }
260
261    /// X coordinate.
262    #[must_use]
263    pub const fn x(self) -> f64 {
264        self.0[0]
265    }
266
267    /// Y coordinate.
268    #[must_use]
269    pub const fn y(self) -> f64 {
270        self.0[1]
271    }
272
273    /// Z coordinate.
274    #[must_use]
275    pub const fn z(self) -> f64 {
276        self.0[2]
277    }
278}
279
280// ---------------------------------------------------------------------------
281// Position operators (all dimensions)
282// ---------------------------------------------------------------------------
283
284/// Translate a point by a vector: `point + vector → point`.
285impl<const N: usize> Add<Vector<N>> for Position<N> {
286    type Output = Self;
287
288    fn add(self, rhs: Vector<N>) -> Self {
289        Self(std::array::from_fn(|i| self.0[i] + rhs.0[i]))
290    }
291}
292
293/// Translate a point by a negative vector: `point - vector → point`.
294impl<const N: usize> Sub<Vector<N>> for Position<N> {
295    type Output = Self;
296
297    fn sub(self, rhs: Vector<N>) -> Self {
298        Self(std::array::from_fn(|i| self.0[i] - rhs.0[i]))
299    }
300}
301
302/// Displacement from one point to another: `point - point → vector`.
303impl<const N: usize> Sub for Position<N> {
304    type Output = Vector<N>;
305
306    fn sub(self, rhs: Self) -> Vector<N> {
307        Vector(std::array::from_fn(|i| self.0[i] - rhs.0[i]))
308    }
309}
310
311// ===========================================================================
312// Serde support
313// ===========================================================================
314
315/// Implements `Serialize` and `Deserialize` for a const-generic newtype
316/// wrapping `[f64; N]`. Serde's derive macro cannot handle `[T; N]` for
317/// arbitrary `N`, so we provide manual impls that serialize as a tuple.
318macro_rules! impl_serde_for_array_newtype {
319    ($ty:ident, $name:expr) => {
320        #[cfg(feature = "serde")]
321        impl<const N: usize> serde::Serialize for $ty<N> {
322            fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
323                use serde::ser::SerializeTuple;
324                let mut tup = ser.serialize_tuple(N)?;
325                for &val in &self.0 {
326                    tup.serialize_element(&val)?;
327                }
328                tup.end()
329            }
330        }
331
332        #[cfg(feature = "serde")]
333        impl<'de, const N: usize> serde::Deserialize<'de> for $ty<N> {
334            fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
335                struct ArrayVisitor<const M: usize>;
336
337                impl<'de, const M: usize> serde::de::Visitor<'de> for ArrayVisitor<M> {
338                    type Value = [f64; M];
339
340                    fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
341                        write!(f, "an array of {} floats", M)
342                    }
343
344                    fn visit_seq<A: serde::de::SeqAccess<'de>>(
345                        self,
346                        mut seq: A,
347                    ) -> Result<Self::Value, A::Error> {
348                        let mut arr = [0.0; M];
349                        for (i, slot) in arr.iter_mut().enumerate() {
350                            *slot = seq
351                                .next_element()?
352                                .ok_or_else(|| serde::de::Error::invalid_length(i, &self))?;
353                        }
354                        Ok(arr)
355                    }
356                }
357
358                de.deserialize_tuple(N, ArrayVisitor::<N>).map($ty)
359            }
360        }
361    };
362}
363
364impl_serde_for_array_newtype!(Vector, "Vector");
365impl_serde_for_array_newtype!(Position, "Position");
366
367// ===========================================================================
368// Tests
369// ===========================================================================
370
371#[cfg(test)]
372#[allow(clippy::unwrap_used, clippy::expect_used)]
373mod tests {
374    use super::*;
375
376    #[test]
377    fn vec3_dot() {
378        let a = Vec3::new(1.0, 2.0, 3.0);
379        let b = Vec3::new(4.0, 5.0, 6.0);
380        assert!((a.dot(b) - 32.0).abs() < 1e-14);
381    }
382
383    #[test]
384    fn vec3_cross() {
385        let x = Vec3::new(1.0, 0.0, 0.0);
386        let y = Vec3::new(0.0, 1.0, 0.0);
387        let z = x.cross(y);
388        assert!((z.x()).abs() < 1e-14);
389        assert!((z.y()).abs() < 1e-14);
390        assert!((z.z() - 1.0).abs() < 1e-14);
391    }
392
393    #[test]
394    fn vec3_normalize() {
395        let v = Vec3::new(3.0, 4.0, 0.0);
396        let n = v.normalize().expect("non-zero");
397        assert!((n.length() - 1.0).abs() < 1e-14);
398    }
399
400    #[test]
401    fn vec3_zero_normalize_fails() {
402        let v = Vec3::new(0.0, 0.0, 0.0);
403        assert!(v.normalize().is_err());
404    }
405
406    #[test]
407    fn vec3_denormal_normalize_fails() {
408        // Denormalized-length vector should be rejected (A1 fix)
409        let v = Vec3::new(1e-320, 0.0, 0.0);
410        assert!(v.normalize().is_err());
411    }
412
413    #[test]
414    fn vec2_dot() {
415        let a = Vec2::new(3.0, 4.0);
416        let b = Vec2::new(1.0, 2.0);
417        assert!((a.dot(b) - 11.0).abs() < 1e-14);
418    }
419
420    #[test]
421    fn point3_sub_gives_vec3() {
422        let a = Point3::new(3.0, 4.0, 5.0);
423        let b = Point3::new(1.0, 1.0, 1.0);
424        let v = a - b;
425        assert!((v.x() - 2.0).abs() < 1e-14);
426        assert!((v.y() - 3.0).abs() < 1e-14);
427        assert!((v.z() - 4.0).abs() < 1e-14);
428    }
429
430    #[test]
431    fn point3_add_vec3() {
432        let p = Point3::new(1.0, 2.0, 3.0);
433        let v = Vec3::new(1.0, 1.0, 1.0);
434        let q = p + v;
435        assert!((q.x() - 2.0).abs() < 1e-14);
436    }
437
438    #[test]
439    fn point3_sub_vec3() {
440        let p = Point3::new(3.0, 4.0, 5.0);
441        let v = Vec3::new(1.0, 1.0, 1.0);
442        let q = p - v;
443        assert!((q.x() - 2.0).abs() < 1e-14);
444        assert!((q.y() - 3.0).abs() < 1e-14);
445        assert!((q.z() - 4.0).abs() < 1e-14);
446    }
447
448    #[test]
449    fn scalar_times_vec3() {
450        let v = Vec3::new(1.0, 2.0, 3.0);
451        let scaled = 2.0 * v;
452        assert!((scaled.x() - 2.0).abs() < 1e-14);
453        assert!((scaled.y() - 4.0).abs() < 1e-14);
454        assert!((scaled.z() - 6.0).abs() < 1e-14);
455    }
456
457    #[test]
458    fn vec3_add_assign() {
459        let mut a = Vec3::new(1.0, 2.0, 3.0);
460        a += Vec3::new(4.0, 5.0, 6.0);
461        assert!((a.x() - 5.0).abs() < 1e-14);
462        assert!((a.y() - 7.0).abs() < 1e-14);
463        assert!((a.z() - 9.0).abs() < 1e-14);
464    }
465
466    #[test]
467    fn vec3_sub_assign() {
468        let mut a = Vec3::new(5.0, 7.0, 9.0);
469        a -= Vec3::new(1.0, 2.0, 3.0);
470        assert!((a.x() - 4.0).abs() < 1e-14);
471        assert!((a.y() - 5.0).abs() < 1e-14);
472        assert!((a.z() - 6.0).abs() < 1e-14);
473    }
474
475    use proptest::prelude::*;
476
477    proptest! {
478        #[test]
479        fn prop_normalize_unit_length(x in -10.0f64..10.0, y in -10.0f64..10.0, z in -10.0f64..10.0) {
480            let v = Vec3::new(x, y, z);
481            if let Ok(n) = v.normalize() {
482                prop_assert!((n.length() - 1.0).abs() < 1e-12, "length = {}", n.length());
483            }
484        }
485
486        #[test]
487        fn prop_cross_anticommutative(
488            ax in -10.0f64..10.0, ay in -10.0f64..10.0, az in -10.0f64..10.0,
489            bx in -10.0f64..10.0, by in -10.0f64..10.0, bz in -10.0f64..10.0,
490        ) {
491            let a = Vec3::new(ax, ay, az);
492            let b = Vec3::new(bx, by, bz);
493            let ab = a.cross(b);
494            let ba = b.cross(a);
495            // a×b = -(b×a)
496            prop_assert!((ab.x() + ba.x()).abs() < 1e-10);
497            prop_assert!((ab.y() + ba.y()).abs() < 1e-10);
498            prop_assert!((ab.z() + ba.z()).abs() < 1e-10);
499        }
500
501        #[test]
502        fn prop_point_sub_vec_inverse_of_add(
503            px in -100.0f64..100.0, py in -100.0f64..100.0, pz in -100.0f64..100.0,
504            vx in -100.0f64..100.0, vy in -100.0f64..100.0, vz in -100.0f64..100.0,
505        ) {
506            let p = Point3::new(px, py, pz);
507            let v = Vec3::new(vx, vy, vz);
508            // (p + v) - v == p
509            let roundtrip = (p + v) - v;
510            prop_assert!((roundtrip.x() - p.x()).abs() < 1e-10);
511            prop_assert!((roundtrip.y() - p.y()).abs() < 1e-10);
512            prop_assert!((roundtrip.z() - p.z()).abs() < 1e-10);
513        }
514    }
515}