Skip to main content

cadcore_math/
unitvec.rs

1//! Unit vector (`UnitVec3`) — a `Vec3` guaranteed to have length 1.
2
3use std::fmt;
4use std::ops::{Mul, Neg};
5
6use crate::{Vec3, EPS};
7
8/// A `Vec3` that is normalised to unit length at construction time.
9///
10/// Stored as a plain `Vec3` internally — the invariant is only enforced
11/// at the construction boundary.
12#[derive(Clone, Copy, PartialEq)]
13pub struct UnitVec3(Vec3);
14
15impl UnitVec3 {
16    /// +X axis.
17    pub const X: Self = Self(Vec3::X);
18    /// +Y axis.
19    pub const Y: Self = Self(Vec3::Y);
20    /// +Z axis.
21    pub const Z: Self = Self(Vec3::Z);
22
23    /// Construct from a `Vec3`, normalising it.  Returns `None` for zero vectors.
24    #[inline]
25    pub fn try_from_vec(v: Vec3) -> Option<Self> {
26        let len = v.length();
27        if len < EPS {
28            None
29        } else {
30            Some(Self(v / len))
31        }
32    }
33
34    /// Construct without checking — caller asserts `|v| ≈ 1`.
35    ///
36    /// # Safety
37    /// Violating the unit-length invariant leads to incorrect geometric results,
38    /// not unsound memory, so this is not `unsafe` in the Rust sense.
39    #[inline]
40    pub fn new_unchecked(v: Vec3) -> Self {
41        Self(v)
42    }
43
44    /// Access the underlying `Vec3`.
45    #[inline]
46    pub fn as_vec(self) -> Vec3 {
47        self.0
48    }
49
50    /// Dot product with another unit vector.
51    #[inline]
52    pub fn dot(self, rhs: Self) -> f64 {
53        self.0.dot(rhs.0)
54    }
55
56    /// Dot product with a free `Vec3`.
57    #[inline]
58    pub fn dot_vec(self, v: Vec3) -> f64 {
59        self.0.dot(v)
60    }
61
62    /// Cross product — result is a plain `Vec3` (may not be unit length).
63    #[inline]
64    pub fn cross(self, rhs: Self) -> Vec3 {
65        self.0.cross(rhs.0)
66    }
67
68    /// Return the opposite direction.
69    #[inline]
70    pub fn flip(self) -> Self {
71        Self(-self.0)
72    }
73
74    /// Angle between two unit vectors (radians, always in `[0, π]`).
75    #[inline]
76    pub fn angle_to(self, other: Self) -> f64 {
77        self.dot(other).clamp(-1.0, 1.0).acos()
78    }
79
80    /// Build an orthonormal basis `(self, u, v)` where `u` and `v` span the
81    /// plane perpendicular to `self`.  `u` is arbitrary but consistent.
82    pub fn perp_basis(self) -> (Self, Self) {
83        let u = self.0.any_perp().normalize();
84        let v_vec = self.0.cross(u);
85        (Self(u), Self(v_vec))
86    }
87}
88
89impl Neg for UnitVec3 {
90    type Output = Self;
91    #[inline]
92    fn neg(self) -> Self {
93        self.flip()
94    }
95}
96
97impl Mul<f64> for UnitVec3 {
98    type Output = Vec3;
99    #[inline]
100    fn mul(self, s: f64) -> Vec3 {
101        self.0 * s
102    }
103}
104impl Mul<UnitVec3> for f64 {
105    type Output = Vec3;
106    #[inline]
107    fn mul(self, u: UnitVec3) -> Vec3 {
108        u.0 * self
109    }
110}
111
112impl From<UnitVec3> for Vec3 {
113    fn from(u: UnitVec3) -> Vec3 {
114        u.0
115    }
116}
117
118impl fmt::Debug for UnitVec3 {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        write!(
121            f,
122            "UnitVec3({:.6}, {:.6}, {:.6})",
123            self.0.x, self.0.y, self.0.z
124        )
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn unit_length() {
134        let v = Vec3::new(1.0, 2.0, 3.0);
135        let u = UnitVec3::try_from_vec(v).unwrap();
136        assert!((u.as_vec().length() - 1.0).abs() < 1e-12);
137    }
138
139    #[test]
140    fn zero_returns_none() {
141        assert!(UnitVec3::try_from_vec(Vec3::ZERO).is_none());
142    }
143
144    #[test]
145    fn perp_basis_orthogonal() {
146        let u = UnitVec3::Z;
147        let (a, b) = u.perp_basis();
148        assert!(a.dot(u).abs() < 1e-12);
149        assert!(b.dot(u).abs() < 1e-12);
150        assert!(a.dot(b).abs() < 1e-12);
151    }
152}