1use crate::{QueryError, Result};
4
5#[derive(Debug, Clone, Copy, PartialEq)]
14pub struct Plane {
15 origin: [f64; 3],
16 normal: [f64; 3],
17 u: [f64; 3],
18 v: [f64; 3],
19}
20
21const AXES_TOLERANCE: f64 = 1e-6;
23
24impl Plane {
25 pub fn new(origin: [f64; 3], normal: [f64; 3]) -> Result<Plane> {
36 check_finite(origin, "plane origin must be finite")?;
37 check_finite(normal, "plane normal must be finite")?;
38 let big = normal.iter().fold(0.0f64, |m, c| m.max(c.abs()));
41 if big == 0.0 {
42 return Err(QueryError::InvalidInput("plane normal must not be zero"));
43 }
44 let n = normalize(normal.map(|c| c / big));
45 let k = (0..3)
46 .min_by(|&i, &j| n[i].abs().total_cmp(&n[j].abs()))
47 .unwrap_or(0);
48 let mut axis = [0.0; 3];
49 axis[k] = 1.0;
50 let u = normalize(std::array::from_fn(|i| axis[i] - n[k] * n[i]));
51 let v = cross(n, u);
52 Ok(Plane {
53 origin,
54 normal: n,
55 u,
56 v,
57 })
58 }
59
60 pub fn with_axes(origin: [f64; 3], u: [f64; 3], v: [f64; 3]) -> Result<Plane> {
67 check_finite(origin, "plane origin must be finite")?;
68 check_finite(u, "plane axes must be finite")?;
69 check_finite(v, "plane axes must be finite")?;
70 let unit = |a: [f64; 3]| (dot(a, a).sqrt() - 1.0).abs() <= AXES_TOLERANCE;
71 if !(unit(u) && unit(v) && dot(u, v).abs() <= AXES_TOLERANCE) {
72 return Err(QueryError::InvalidInput("plane axes must be orthonormal"));
73 }
74 Ok(Plane {
75 origin,
76 normal: normalize(cross(u, v)),
77 u,
78 v,
79 })
80 }
81
82 pub fn origin(&self) -> [f64; 3] {
84 self.origin
85 }
86
87 pub fn normal(&self) -> [f64; 3] {
89 self.normal
90 }
91
92 pub fn u(&self) -> [f64; 3] {
94 self.u
95 }
96
97 pub fn v(&self) -> [f64; 3] {
99 self.v
100 }
101
102 pub fn signed_distance(&self, p: [f64; 3]) -> f64 {
105 dot(self.normal, p) - dot(self.normal, self.origin)
106 }
107
108 pub fn to_uv(&self, p: [f64; 3]) -> [f64; 2] {
111 let d = sub(p, self.origin);
112 [dot(d, self.u), dot(d, self.v)]
113 }
114
115 pub fn from_uv(&self, q: [f64; 2]) -> [f64; 3] {
117 std::array::from_fn(|k| self.origin[k] + q[0] * self.u[k] + q[1] * self.v[k])
118 }
119}
120
121fn check_finite(a: [f64; 3], what: &'static str) -> Result<()> {
122 if a.iter().all(|c| c.is_finite()) {
123 Ok(())
124 } else {
125 Err(QueryError::InvalidInput(what))
126 }
127}
128
129fn normalize(a: [f64; 3]) -> [f64; 3] {
130 let len = dot(a, a).sqrt();
131 a.map(|c| c / len)
132}
133
134pub(crate) fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
135 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
136}
137
138pub(crate) fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
139 [
140 a[1] * b[2] - a[2] * b[1],
141 a[2] * b[0] - a[0] * b[2],
142 a[0] * b[1] - a[1] * b[0],
143 ]
144}
145
146fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
147 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::test_util::{Lcg, random_normal};
154
155 fn random_vec(rng: &mut Lcg, scale: f64) -> [f64; 3] {
156 std::array::from_fn(|_| (f64::from(rng.next()) * 2.0 - 1.0) * scale)
157 }
158
159 fn norm(a: [f64; 3]) -> f64 {
160 dot(a, a).sqrt()
161 }
162
163 #[test]
164 fn plane_new_normalises_and_rejects_zero() {
165 let p = Plane::new([0.0; 3], [0.0, 0.0, 2.0]).unwrap();
166 assert_eq!(p.normal(), [0.0, 0.0, 1.0]);
167 for bad in [[0.0; 3], [f64::NAN, 0.0, 1.0], [f64::INFINITY, 0.0, 0.0]] {
168 assert!(matches!(
169 Plane::new([0.0; 3], bad),
170 Err(QueryError::InvalidInput(_))
171 ));
172 }
173 assert!(matches!(
174 Plane::new([f64::NAN, 0.0, 0.0], [0.0, 0.0, 1.0]),
175 Err(QueryError::InvalidInput(_))
176 ));
177 let p = Plane::new([0.0; 3], [1e300, 1e300, 0.0]).unwrap();
179 assert!((norm(p.normal()) - 1.0).abs() < 1e-15);
180 }
181
182 #[test]
183 fn default_frame_is_orthonormal_right_handed() {
184 let mut rng = Lcg(0x91a7);
185 let mut normals: Vec<[f64; 3]> = (0..1000).map(|_| random_normal(&mut rng)).collect();
186 normals.extend([
188 [1.0, 0.0, 0.0],
189 [0.0, -1.0, 0.0],
190 [0.0, 0.0, 1.0],
191 [1.0, 1.0, 1.0],
192 ]);
193 for n in normals {
194 let p = Plane::new([0.0; 3], n).unwrap();
195 let (n, u, v) = (p.normal(), p.u(), p.v());
196 assert!((norm(u) - 1.0).abs() < 1e-12, "{n:?}");
197 assert!((norm(v) - 1.0).abs() < 1e-12, "{n:?}");
198 assert!(dot(u, n).abs() < 1e-12, "{n:?}");
199 assert!(dot(v, n).abs() < 1e-12, "{n:?}");
200 assert!(dot(u, v).abs() < 1e-12, "{n:?}");
201 let w = cross(u, v);
202 assert!((0..3).all(|k| (w[k] - n[k]).abs() < 1e-12), "{n:?}");
203 }
204 }
205
206 #[test]
207 fn with_axes_rejects_non_orthonormal() {
208 let o = [1.0, 2.0, 3.0];
209 assert!(matches!(
210 Plane::with_axes(o, [1.0, 0.0, 0.0], [1.0, 1.0, 0.0]),
211 Err(QueryError::InvalidInput(_))
212 ));
213 assert!(matches!(
214 Plane::with_axes(o, [2.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
215 Err(QueryError::InvalidInput(_))
216 ));
217 assert!(matches!(
218 Plane::with_axes(o, [f64::NAN, 0.0, 0.0], [0.0, 1.0, 0.0]),
219 Err(QueryError::InvalidInput(_))
220 ));
221 let p = Plane::with_axes(o, [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]).unwrap();
222 assert_eq!(p.normal(), [1.0, 0.0, 0.0]);
223 assert_eq!(
224 (p.origin(), p.u(), p.v()),
225 (o, [0.0, 1.0, 0.0], [0.0, 0.0, 1.0])
226 );
227 assert!(Plane::with_axes(o, [1.0 + 5e-7, 0.0, 0.0], [5e-7, 1.0, 0.0]).is_ok());
229 }
230
231 #[test]
232 fn uv_round_trip() {
233 let mut rng = Lcg(0x3c0f);
234 for _ in 0..1000 {
235 let o = random_vec(&mut rng, 1.0);
236 let plane = Plane::new(o, random_normal(&mut rng)).unwrap();
237 let p = random_vec(&mut rng, 100.0);
238 let s = plane.signed_distance(p);
239 let n = plane.normal();
240 let projected: [f64; 3] = std::array::from_fn(|k| p[k] - s * n[k]);
241 let q = plane.from_uv(plane.to_uv(p));
242 let tol = 1e-12 * norm(p);
243 assert!((0..3).all(|k| (q[k] - projected[k]).abs() <= tol), "{p:?}");
244 }
245 }
246
247 #[test]
248 fn signed_distance_uses_the_side_rule() {
249 let plane = Plane::new([0.0, 0.0, 1.0], [0.0, 0.0, 3.0]).unwrap();
250 assert_eq!(plane.signed_distance([5.0, -2.0, 1.0]), 0.0);
251 assert_eq!(plane.signed_distance([0.0, 0.0, 3.5]), 2.5);
252 assert_eq!(plane.signed_distance([0.0, 0.0, 0.0]), -1.0);
253 }
254}