1use core::fmt;
4
5use nalgebra::UnitQuaternion;
6
7use crate::{Isometry, Point2, Point3, UnitVec2, UnitVec3, Vec2, Vec3};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum FrameError {
12 NonFinite,
14 ZeroAxis,
16 DegenerateHint,
19 NotOrthonormal,
22}
23
24impl fmt::Display for FrameError {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 f.write_str(match self {
27 FrameError::NonFinite => "frame has a non-finite coordinate",
28 FrameError::ZeroAxis => "frame axis has zero length",
29 FrameError::DegenerateHint => "frame x hint is zero or parallel to the axis",
30 FrameError::NotOrthonormal => "frame axes are not orthonormal",
31 })
32 }
33}
34
35impl std::error::Error for FrameError {}
36
37#[derive(Debug, Clone, Copy, PartialEq)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57#[cfg_attr(feature = "serde", serde(try_from = "FrameRepr", into = "FrameRepr"))]
58pub struct Frame {
59 origin: Point3,
60 x: UnitVec3,
61 y: UnitVec3,
62 z: UnitVec3,
63}
64
65#[cfg(feature = "serde")]
69#[derive(serde::Serialize, serde::Deserialize)]
70struct FrameRepr {
71 origin: Point3,
72 x: Vec3,
73 y: Vec3,
74 z: Vec3,
75}
76
77#[cfg(feature = "serde")]
78impl From<Frame> for FrameRepr {
79 fn from(f: Frame) -> Self {
80 FrameRepr {
81 origin: f.origin,
82 x: f.x.into_inner(),
83 y: f.y.into_inner(),
84 z: f.z.into_inner(),
85 }
86 }
87}
88
89#[cfg(feature = "serde")]
90impl TryFrom<FrameRepr> for Frame {
91 type Error = FrameError;
92
93 fn try_from(r: FrameRepr) -> Result<Self, FrameError> {
94 Frame::from_orthonormal(r.origin, r.x, r.y, r.z)
95 }
96}
97
98impl Frame {
99 pub fn world() -> Self {
101 Frame {
102 origin: Point3::origin(),
103 x: Vec3::x_axis(),
104 y: Vec3::y_axis(),
105 z: Vec3::z_axis(),
106 }
107 }
108
109 pub fn new(origin: Point3, z: Vec3, x_hint: Vec3) -> Result<Self, FrameError> {
122 if !(is_finite3(&origin.coords) && is_finite3(&z) && is_finite3(&x_hint)) {
123 return Err(FrameError::NonFinite);
124 }
125 let z = rescaled(z)
126 .and_then(|z| UnitVec3::try_new(z, 0.0))
127 .ok_or(FrameError::ZeroAxis)?;
128 let x_hint = rescaled(x_hint).ok_or(FrameError::DegenerateHint)?;
129 let perpendicular = x_hint - z.dot(&x_hint) * z.into_inner();
130 if crate::is_negligible(perpendicular.norm(), x_hint.norm()) {
131 return Err(FrameError::DegenerateHint);
132 }
133 let x = UnitVec3::try_new(perpendicular, 0.0).ok_or(FrameError::DegenerateHint)?;
134 Ok(Self::orthonormalised(origin, x, z))
135 }
136
137 pub fn from_z(origin: Point3, z: Vec3) -> Result<Self, FrameError> {
147 if !(is_finite3(&origin.coords) && is_finite3(&z)) {
148 return Err(FrameError::NonFinite);
149 }
150 let z = rescaled(z)
151 .and_then(|z| UnitVec3::try_new(z, 0.0))
152 .ok_or(FrameError::ZeroAxis)?;
153 let (a, b, c) = (z.x, z.y, z.z);
154 let (aa, ba, ca) = (a.abs(), b.abs(), c.abs());
155 let hint = if ba <= aa && ba <= ca {
156 if aa > ca {
157 Vec3::new(-c, 0.0, a)
158 } else {
159 Vec3::new(c, 0.0, -a)
160 }
161 } else if aa <= ba && aa <= ca {
162 if ba > ca {
163 Vec3::new(0.0, -c, b)
164 } else {
165 Vec3::new(0.0, c, -b)
166 }
167 } else if aa > ba {
168 Vec3::new(-b, a, 0.0)
169 } else {
170 Vec3::new(b, -a, 0.0)
171 };
172 let x = UnitVec3::try_new(hint, 0.0).ok_or(FrameError::ZeroAxis)?;
175 Ok(Self::orthonormalised(origin, x, z))
176 }
177
178 pub fn from_orthonormal(origin: Point3, x: Vec3, y: Vec3, z: Vec3) -> Result<Self, FrameError> {
194 if !(is_finite3(&origin.coords) && is_finite3(&x) && is_finite3(&y) && is_finite3(&z)) {
195 return Err(FrameError::NonFinite);
196 }
197 let unit = |v: &Vec3| crate::is_negligible(v.norm() - 1.0, 1.0);
198 let perpendicular = |a: &Vec3, b: &Vec3| crate::is_negligible(a.dot(b), 1.0);
199 if !(unit(&x) && unit(&y) && unit(&z))
200 || !(perpendicular(&x, &y) && perpendicular(&y, &z) && perpendicular(&z, &x))
201 || !crate::is_negligible((x.cross(&y) - z).norm(), 1.0)
202 {
203 return Err(FrameError::NotOrthonormal);
204 }
205 Ok(Frame {
206 origin,
207 x: UnitVec3::new_unchecked(x),
208 y: UnitVec3::new_unchecked(y),
209 z: UnitVec3::new_unchecked(z),
210 })
211 }
212
213 pub fn from_rotation(origin: Point3, rotation: &UnitQuaternion<f64>) -> Self {
216 let x = UnitVec3::new_normalize(rotation.transform_vector(&Vec3::x()));
217 let z = UnitVec3::new_normalize(rotation.transform_vector(&Vec3::z()));
218 Self::orthonormalised(origin, x, z)
219 }
220
221 fn orthonormalised(origin: Point3, x: UnitVec3, z: UnitVec3) -> Self {
225 let y = UnitVec3::new_normalize(z.cross(&x));
226 let x = UnitVec3::new_normalize(y.cross(&z));
227 Frame { origin, x, y, z }
228 }
229
230 pub const fn with_origin(&self, origin: Point3) -> Frame {
234 Frame {
235 origin,
236 x: self.x,
237 y: self.y,
238 z: self.z,
239 }
240 }
241
242 pub const fn origin(&self) -> Point3 {
244 self.origin
245 }
246
247 pub const fn x(&self) -> UnitVec3 {
249 self.x
250 }
251
252 pub const fn y(&self) -> UnitVec3 {
254 self.y
255 }
256
257 pub const fn z(&self) -> UnitVec3 {
259 self.z
260 }
261
262 pub fn to_local(&self, p: Point3) -> Point3 {
264 Point3::from(self.vec_to_local(p - self.origin))
265 }
266
267 pub fn to_world(&self, p: Point3) -> Point3 {
269 self.origin + self.vec_to_world(p.coords)
270 }
271
272 pub fn vec_to_local(&self, v: Vec3) -> Vec3 {
274 Vec3::new(v.dot(&self.x), v.dot(&self.y), v.dot(&self.z))
275 }
276
277 pub fn vec_to_world(&self, v: Vec3) -> Vec3 {
279 v.x * self.x.into_inner() + v.y * self.y.into_inner() + v.z * self.z.into_inner()
280 }
281
282 pub fn rotation(&self) -> UnitQuaternion<f64> {
284 UnitQuaternion::from_basis_unchecked(&[
285 self.x.into_inner(),
286 self.y.into_inner(),
287 self.z.into_inner(),
288 ])
289 }
290
291 pub fn as_isometry(&self) -> Isometry {
294 Isometry::new(self.rotation(), self.origin.coords)
295 }
296
297 pub fn transformed(&self, motion: &Isometry) -> Frame {
300 Self::orthonormalised(
301 motion.apply(self.origin),
302 motion.apply_unit(self.x),
303 motion.apply_unit(self.z),
304 )
305 }
306}
307
308fn is_finite3(v: &Vec3) -> bool {
309 v.iter().all(|c| c.is_finite())
310}
311
312fn rescaled(v: Vec3) -> Option<Vec3> {
320 let largest = v.amax();
321 if largest <= 0.0 {
322 return None;
323 }
324 let k = -(largest.log2().floor() as i32);
327 Some(v * 2f64.powi(k / 2) * 2f64.powi(k - k / 2))
328}
329
330#[derive(Debug, Clone, Copy, PartialEq, Eq)]
332pub enum Handedness {
333 Right,
335 Left,
337}
338
339#[derive(Debug, Clone, Copy, PartialEq)]
353#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
354#[cfg_attr(feature = "serde", serde(try_from = "Frame2Repr", into = "Frame2Repr"))]
355pub struct Frame2 {
356 origin: Point2,
357 x: UnitVec2,
358 y: UnitVec2,
359}
360
361#[cfg(feature = "serde")]
364#[derive(serde::Serialize, serde::Deserialize)]
365struct Frame2Repr {
366 origin: Point2,
367 x: Vec2,
368 y: Vec2,
369}
370
371#[cfg(feature = "serde")]
372impl From<Frame2> for Frame2Repr {
373 fn from(f: Frame2) -> Self {
374 Frame2Repr {
375 origin: f.origin,
376 x: f.x.into_inner(),
377 y: f.y.into_inner(),
378 }
379 }
380}
381
382#[cfg(feature = "serde")]
383impl TryFrom<Frame2Repr> for Frame2 {
384 type Error = FrameError;
385
386 fn try_from(r: Frame2Repr) -> Result<Self, FrameError> {
387 Frame2::from_orthonormal(r.origin, r.x, r.y)
388 }
389}
390
391impl Frame2 {
392 pub fn identity() -> Self {
394 Frame2 {
395 origin: Point2::origin(),
396 x: Vec2::x_axis(),
397 y: Vec2::y_axis(),
398 }
399 }
400
401 pub fn new(origin: Point2, x: Vec2, handedness: Handedness) -> Result<Self, FrameError> {
405 if !(origin.coords.iter().all(|c| c.is_finite()) && x.iter().all(|c| c.is_finite())) {
406 return Err(FrameError::NonFinite);
407 }
408 let x = UnitVec2::try_new(x, 0.0).ok_or(FrameError::ZeroAxis)?;
409 let y = match handedness {
410 Handedness::Right => Vec2::new(-x.y, x.x),
411 Handedness::Left => Vec2::new(x.y, -x.x),
412 };
413 Ok(Frame2 {
414 origin,
415 x,
416 y: UnitVec2::new_unchecked(y),
417 })
418 }
419
420 pub fn from_orthonormal(origin: Point2, x: Vec2, y: Vec2) -> Result<Self, FrameError> {
426 let finite = |v: &Vec2| v.iter().all(|c| c.is_finite());
427 if !(finite(&origin.coords) && finite(&x) && finite(&y)) {
428 return Err(FrameError::NonFinite);
429 }
430 let unit = |v: &Vec2| crate::is_negligible(v.norm() - 1.0, 1.0);
431 if !(unit(&x) && unit(&y)) || !crate::is_negligible(x.dot(&y), 1.0) {
432 return Err(FrameError::NotOrthonormal);
433 }
434 Ok(Frame2 {
435 origin,
436 x: UnitVec2::new_unchecked(x),
437 y: UnitVec2::new_unchecked(y),
438 })
439 }
440
441 pub const fn origin(&self) -> Point2 {
443 self.origin
444 }
445
446 pub fn translated(&self, by: Vec2) -> Frame2 {
456 Frame2 {
457 origin: self.origin + by,
458 x: self.x,
459 y: self.y,
460 }
461 }
462
463 pub const fn x(&self) -> UnitVec2 {
465 self.x
466 }
467
468 pub const fn y(&self) -> UnitVec2 {
470 self.y
471 }
472
473 pub fn handedness(&self) -> Handedness {
475 if self.is_right_handed() {
476 Handedness::Right
477 } else {
478 Handedness::Left
479 }
480 }
481
482 pub fn is_right_handed(&self) -> bool {
484 self.x.perp(&self.y) > 0.0
485 }
486
487 pub fn to_local(&self, p: Point2) -> Point2 {
489 Point2::from(self.vec_to_local(p - self.origin))
490 }
491
492 pub fn to_world(&self, p: Point2) -> Point2 {
494 self.origin + self.vec_to_world(p.coords)
495 }
496
497 pub fn vec_to_local(&self, v: Vec2) -> Vec2 {
499 Vec2::new(v.dot(&self.x), v.dot(&self.y))
500 }
501
502 pub fn vec_to_world(&self, v: Vec2) -> Vec2 {
504 v.x * self.x.into_inner() + v.y * self.y.into_inner()
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511
512 #[test]
517 fn a_hint_along_the_axis_to_rounding_is_degenerate_not_nan() {
518 let mut nan = Vec::new();
519 for k in 1..=2000 {
520 let scale = 0.001 * f64::from(k);
521 for z in [
522 Vec3::new(-1.0, -1.0, -1.0),
523 Vec3::new(1.0, 2.0, 3.0),
524 Vec3::new(0.3, -0.7, 0.1),
525 ] {
526 let hint = z * scale;
527 match Frame::new(Point3::origin(), z, hint) {
528 Err(FrameError::DegenerateHint) => {}
529 Ok(f) => nan.push((z, scale, f)),
530 Err(e) => panic!("{z:?} × {scale}: {e}"),
531 }
532 }
533 }
534 assert!(
535 nan.is_empty(),
536 "{} frames built: {:?}",
537 nan.len(),
538 nan.first()
539 );
540 }
541
542 #[test]
547 fn a_tiny_axis_or_hint_still_makes_an_orthonormal_frame() {
548 for scale in [1e-160, 1e-155, 1e-154, 1e-150, 1.0, 1e150, 1e154] {
549 let z = Vec3::new(-0.7, 3e-300, -0.7) * scale;
550 let hint = Vec3::new(0.3, -1.0, 0.2) * scale;
551 for f in [
552 Frame::new(Point3::origin(), z, hint).unwrap(),
553 Frame::from_z(Point3::origin(), z).unwrap(),
554 ] {
555 for axis in [f.x(), f.y(), f.z()] {
556 assert!(
557 (axis.norm() - 1.0).abs() <= crate::RELATIVE_ROUNDING,
558 "{scale:e}: {axis:?}"
559 );
560 }
561 assert!(f.x().dot(&f.z()).abs() <= crate::RELATIVE_ROUNDING);
562 }
563 }
564 }
565
566 #[test]
567 fn world_frame_is_the_identity() {
568 let w = Frame::world();
569 let p = Point3::new(1.0, -2.0, 3.0);
570 assert_eq!(w.to_local(p), p);
571 assert_eq!(w.to_world(p), p);
572 assert_eq!(w.rotation(), UnitQuaternion::identity());
573 }
574
575 #[test]
576 fn from_z_follows_the_axis_rule() {
577 let f = Frame::from_z(Point3::origin(), Vec3::z()).unwrap();
578 assert_eq!(f.x().into_inner(), Vec3::x());
579 assert_eq!(f.y().into_inner(), Vec3::y());
580 let f = Frame::from_z(Point3::origin(), Vec3::x()).unwrap();
581 assert_eq!(f.x().into_inner(), Vec3::z());
582 assert_eq!(f.y().into_inner(), -Vec3::y());
583 let f = Frame::from_z(Point3::origin(), Vec3::y()).unwrap();
584 assert_eq!(f.x().into_inner(), Vec3::z());
585 assert_eq!(f.y().into_inner(), Vec3::x());
586 }
587
588 #[test]
589 fn errors_name_the_problem() {
590 let o = Point3::origin();
591 assert_eq!(
592 Frame::new(o, Vec3::zeros(), Vec3::x()),
593 Err(FrameError::ZeroAxis)
594 );
595 assert_eq!(
596 Frame::new(o, Vec3::z(), Vec3::z() * 2.0),
597 Err(FrameError::DegenerateHint)
598 );
599 assert_eq!(
600 Frame::new(o, Vec3::z(), Vec3::zeros()),
601 Err(FrameError::DegenerateHint)
602 );
603 assert_eq!(
604 Frame::new(o, Vec3::new(f64::NAN, 0.0, 1.0), Vec3::x()),
605 Err(FrameError::NonFinite)
606 );
607 assert_eq!(Frame::from_z(o, Vec3::zeros()), Err(FrameError::ZeroAxis));
608 assert_eq!(
609 Frame2::new(Point2::origin(), Vec2::zeros(), Handedness::Right),
610 Err(FrameError::ZeroAxis)
611 );
612 }
613
614 #[test]
615 fn frame2_handedness_round_trips() {
616 for h in [Handedness::Right, Handedness::Left] {
617 let f = Frame2::new(Point2::new(0.5, -0.5), Vec2::new(3.0, 4.0), h).unwrap();
618 assert_eq!(f.handedness(), h);
619 let p = Point2::new(0.3, 0.9);
620 assert!((f.to_local(f.to_world(p)) - p).norm() < 1e-15);
621 assert!(f.x().dot(&f.y()).abs() < 1e-15);
622 }
623 assert!(Frame2::identity().is_right_handed());
624 }
625}