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