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 { None } else { Some(Self(v / len)) }
28    }
29
30    /// Construct without checking — caller asserts `|v| ≈ 1`.
31    ///
32    /// # Safety
33    /// Violating the unit-length invariant leads to incorrect geometric results,
34    /// not unsound memory, so this is not `unsafe` in the Rust sense.
35    #[inline]
36    pub fn new_unchecked(v: Vec3) -> Self { Self(v) }
37
38    /// Access the underlying `Vec3`.
39    #[inline]
40    pub fn as_vec(self) -> Vec3 { self.0 }
41
42    /// Dot product with another unit vector.
43    #[inline]
44    pub fn dot(self, rhs: Self) -> f64 { self.0.dot(rhs.0) }
45
46    /// Dot product with a free `Vec3`.
47    #[inline]
48    pub fn dot_vec(self, v: Vec3) -> f64 { self.0.dot(v) }
49
50    /// Cross product — result is a plain `Vec3` (may not be unit length).
51    #[inline]
52    pub fn cross(self, rhs: Self) -> Vec3 { self.0.cross(rhs.0) }
53
54    /// Return the opposite direction.
55    #[inline]
56    pub fn flip(self) -> Self { Self(-self.0) }
57
58    /// Angle between two unit vectors (radians, always in `[0, π]`).
59    #[inline]
60    pub fn angle_to(self, other: Self) -> f64 {
61        self.dot(other).clamp(-1.0, 1.0).acos()
62    }
63
64    /// Build an orthonormal basis `(self, u, v)` where `u` and `v` span the
65    /// plane perpendicular to `self`.  `u` is arbitrary but consistent.
66    pub fn perp_basis(self) -> (Self, Self) {
67        let u = self.0.any_perp().normalize();
68        let v_vec = self.0.cross(u);
69        (Self(u), Self(v_vec))
70    }
71}
72
73impl Neg for UnitVec3 {
74    type Output = Self;
75    #[inline] fn neg(self) -> Self { self.flip() }
76}
77
78impl Mul<f64> for UnitVec3 {
79    type Output = Vec3;
80    #[inline] fn mul(self, s: f64) -> Vec3 { self.0 * s }
81}
82impl Mul<UnitVec3> for f64 {
83    type Output = Vec3;
84    #[inline] fn mul(self, u: UnitVec3) -> Vec3 { u.0 * self }
85}
86
87impl From<UnitVec3> for Vec3 {
88    fn from(u: UnitVec3) -> Vec3 { u.0 }
89}
90
91impl fmt::Debug for UnitVec3 {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        write!(f, "UnitVec3({:.6}, {:.6}, {:.6})", self.0.x, self.0.y, self.0.z)
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn unit_length() {
103        let v = Vec3::new(1.0, 2.0, 3.0);
104        let u = UnitVec3::try_from_vec(v).unwrap();
105        assert!((u.as_vec().length() - 1.0).abs() < 1e-12);
106    }
107
108    #[test]
109    fn zero_returns_none() {
110        assert!(UnitVec3::try_from_vec(Vec3::ZERO).is_none());
111    }
112
113    #[test]
114    fn perp_basis_orthogonal() {
115        let u = UnitVec3::Z;
116        let (a, b) = u.perp_basis();
117        assert!(a.dot(u).abs() < 1e-12);
118        assert!(b.dot(u).abs() < 1e-12);
119        assert!(a.dot(b).abs() < 1e-12);
120    }
121}