1use crate::MathError;
8use crate::vec::{Point3, Vec3};
9
10#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct Frame3 {
17 pub origin: Point3,
19 pub x: Vec3,
21 pub y: Vec3,
23 pub z: Vec3,
25}
26
27impl Frame3 {
28 pub fn from_normal(origin: Point3, normal: Vec3) -> Result<Self, MathError> {
37 let z = normal.normalize()?;
38 let (x, y) = perpendicular_pair(z)?;
39 Ok(Self { origin, x, y, z })
40 }
41
42 pub fn from_normal_and_ref(
53 origin: Point3,
54 normal: Vec3,
55 ref_dir: Vec3,
56 ) -> Result<Self, MathError> {
57 let z = normal.normalize()?;
58 let ref_proj = ref_dir - z * ref_dir.dot(z);
59 let x = if let Ok(v) = ref_proj.normalize() {
60 v
61 } else {
62 let (arb_x, _) = perpendicular_pair(z)?;
64 arb_x
65 };
66 let y = z.cross(x);
67 Ok(Self { origin, x, y, z })
68 }
69}
70
71fn perpendicular_pair(z: Vec3) -> Result<(Vec3, Vec3), MathError> {
74 let candidate = if z.x().abs() < 0.9 {
75 Vec3::new(1.0, 0.0, 0.0)
76 } else {
77 Vec3::new(0.0, 1.0, 0.0)
78 };
79 let x = z.cross(candidate).normalize()?;
80 let y = z.cross(x);
81 Ok((x, y))
82}
83
84#[cfg(test)]
85#[allow(clippy::unwrap_used, clippy::expect_used)]
86mod tests {
87 use super::*;
88 use proptest::prelude::*;
89
90 #[test]
91 fn frame_from_z_axis() {
92 let f = Frame3::from_normal(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 5.0))
93 .expect("non-zero");
94 assert!((f.z.z() - 1.0).abs() < 1e-14);
95 assert!(f.x.dot(f.y).abs() < 1e-14);
96 assert!(f.x.dot(f.z).abs() < 1e-14);
97 assert!(f.y.dot(f.z).abs() < 1e-14);
98 }
99
100 #[test]
101 fn frame_from_x_axis() {
102 let f = Frame3::from_normal(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0))
104 .expect("non-zero");
105 assert!((f.z.x() - 1.0).abs() < 1e-14);
106 assert!(f.x.dot(f.z).abs() < 1e-14);
107 }
108
109 #[test]
110 fn frame_with_ref_dir() {
111 let f = Frame3::from_normal_and_ref(
112 Point3::new(0.0, 0.0, 0.0),
113 Vec3::new(0.0, 0.0, 1.0),
114 Vec3::new(1.0, 0.0, 0.0),
115 )
116 .expect("non-zero");
117 assert!((f.x.x() - 1.0).abs() < 1e-14);
119 assert!(f.x.y().abs() < 1e-14);
120 }
121
122 #[test]
123 fn frame_with_parallel_ref_falls_back() {
124 let f = Frame3::from_normal_and_ref(
126 Point3::new(0.0, 0.0, 0.0),
127 Vec3::new(0.0, 0.0, 1.0),
128 Vec3::new(0.0, 0.0, 1.0),
129 )
130 .expect("non-zero");
131 assert!(f.x.dot(f.z).abs() < 1e-14);
132 assert!((f.x.length() - 1.0).abs() < 1e-14);
133 }
134
135 #[test]
136 fn zero_normal_errors() {
137 let r = Frame3::from_normal(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 0.0));
138 assert!(r.is_err());
139 }
140
141 proptest! {
142 #[test]
143 fn prop_frame_orthonormal(
144 nx in -10.0f64..10.0, ny in -10.0f64..10.0, nz in -10.0f64..10.0,
145 ) {
146 let n = Vec3::new(nx, ny, nz);
147 if let Ok(f) = Frame3::from_normal(Point3::new(0.0, 0.0, 0.0), n) {
148 prop_assert!((f.x.length() - 1.0).abs() < 1e-12);
150 prop_assert!((f.y.length() - 1.0).abs() < 1e-12);
151 prop_assert!((f.z.length() - 1.0).abs() < 1e-12);
152 prop_assert!(f.x.dot(f.y).abs() < 1e-12);
154 prop_assert!(f.x.dot(f.z).abs() < 1e-12);
155 prop_assert!(f.y.dot(f.z).abs() < 1e-12);
156 let cross = f.x.cross(f.y);
158 prop_assert!((cross.x() - f.z.x()).abs() < 1e-12);
159 prop_assert!((cross.y() - f.z.y()).abs() < 1e-12);
160 prop_assert!((cross.z() - f.z.z()).abs() < 1e-12);
161 }
162 }
163 }
164}