Skip to main content

arris_math/
frame.rs

1//! Right-handed 3D frames, and 2D frames of either handedness.
2
3use crate::real;
4use core::fmt;
5
6use nalgebra::UnitQuaternion;
7
8use crate::{Isometry, Point2, Point3, UnitVec2, UnitVec3, Vec2, Vec3};
9
10/// Why an origin and some directions are not a frame.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum FrameError {
13    /// A coordinate is NaN or infinite.
14    NonFinite,
15    /// The axis has zero length.
16    ZeroAxis,
17    /// The `x` hint has no component perpendicular to the axis beyond
18    /// rounding: it is zero, or parallel to `z` to rounding.
19    DegenerateHint,
20    /// Axes given as a frame are not unit and mutually perpendicular to
21    /// rounding, or `z ≠ x × y` for a 3D frame.
22    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/// A right-handed orthonormal frame: an origin and axes `x`, `y`, `z` with
39/// `x × y = z`, each unit to within rounding, whatever the constructor was
40/// given. Every analytic surface and curve is placed by one, so a
41/// transform is a frame change and nothing else (`docs/DATA-MODEL.md`
42/// §Conventions).
43///
44/// Only the validating constructors build one; there is no way to hold a
45/// `Frame` whose axes are not orthonormal.
46///
47/// ```
48/// use arris_math::{Frame, Point3, Vec3};
49///
50/// let f = Frame::new(Point3::new(1.0, 2.0, 3.0), Vec3::z(), Vec3::new(1.0, 1.0, 0.0)).unwrap();
51/// let local = Point3::new(1.0, 0.0, 0.0);
52/// let world = f.to_world(local);
53/// assert!((f.to_local(world) - local).norm() < 1e-15);
54/// assert!((f.x().dot(&f.y())).abs() < 1e-15);
55/// ```
56#[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/// The wire form of a [`Frame`]: its four fields as given, validated by
67/// [`Frame::from_orthonormal`] on the way in so a stored frame is never
68/// less of a frame than a built one.
69#[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    /// The world frame: origin at zero, axes the coordinate axes.
101    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    /// A frame with axis `z` (normalised) and `x` the direction of
111    /// `x_hint`'s component perpendicular to `z`; `y = z × x`.
112    ///
113    /// Errors: a non-finite input, a zero `z`, or a hint with no
114    /// perpendicular component beyond rounding
115    /// ([`crate::RELATIVE_ROUNDING`] of the hint's length): what is left
116    /// of a hint along `z` is noise whose direction means nothing, and
117    /// may lie along `z` itself. A hint that is *nearly* parallel to `z`,
118    /// past rounding, still yields an orthonormal frame, but its `x` is
119    /// only as good as the few bits of the perpendicular component; a
120    /// caller who wants to reject that compares the hint against `z` with
121    /// its own `Tolerance` first.
122    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    /// A frame with axis `z` and `x` chosen by the rule Open CASCADE's
139    /// `gp_Ax3(P, N)` uses (read in the reference tree's `gp` package,
140    /// reimplemented): zero the axis coordinate of smallest magnitude, swap
141    /// the other two with the sign that keeps the larger one, so a
142    /// cylinder built from an axis alone seams where the oracle's does.
143    /// For `z` along a coordinate axis: `+z ↦ x = +x`, `+x ↦ x = +z`,
144    /// `+y ↦ x = +z`.
145    ///
146    /// Errors: a non-finite input or a zero `z`.
147    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        // `hint` is perpendicular to `z` by construction and has the norm of
174        // the two larger coordinates, so it is never zero for a unit `z`.
175        let x = UnitVec3::try_new(hint, 0.0).ok_or(FrameError::ZeroAxis)?;
176        Ok(Self::orthonormalised(origin, x, z))
177    }
178
179    /// A frame from axes that already are one: each unit, mutually
180    /// perpendicular and `z = x × y`, all to rounding
181    /// ([`crate::RELATIVE_ROUNDING`]), stored bit for bit — what the
182    /// native format reads a frame back through, so a round trip changes
183    /// nothing. Errors: a non-finite input, or
184    /// [`FrameError::NotOrthonormal`].
185    ///
186    /// ```
187    /// use arris_math::{Frame, FrameError, Point3, Vec3};
188    ///
189    /// let f = Frame::from_orthonormal(Point3::origin(), Vec3::y(), Vec3::z(), Vec3::x()).unwrap();
190    /// assert_eq!(f.x().into_inner(), Vec3::y());
191    /// let bad = Frame::from_orthonormal(Point3::origin(), Vec3::x(), Vec3::x(), Vec3::z());
192    /// assert_eq!(bad, Err(FrameError::NotOrthonormal));
193    /// ```
194    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    /// The frame whose axes are the images of the coordinate axes under
215    /// `rotation`. Infallible: a unit quaternion's basis is orthonormal.
216    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    /// Rebuilds `y` and `x` from `z` and an `x` that is unit and nearly
223    /// perpendicular to it, so the result is orthonormal to rounding
224    /// regardless of how good the input was.
225    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    /// The same axes at another origin, bit for bit: a translation that
232    /// leaves the orientation untouched, where `transformed` by a pure
233    /// translation would re-round the axes through the identity rotation.
234    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    /// The origin.
244    pub const fn origin(&self) -> Point3 {
245        self.origin
246    }
247
248    /// The `x` axis.
249    pub const fn x(&self) -> UnitVec3 {
250        self.x
251    }
252
253    /// The `y` axis, `z × x`.
254    pub const fn y(&self) -> UnitVec3 {
255        self.y
256    }
257
258    /// The `z` axis, `x × y`.
259    pub const fn z(&self) -> UnitVec3 {
260        self.z
261    }
262
263    /// The coordinates of a world point in this frame.
264    pub fn to_local(&self, p: Point3) -> Point3 {
265        Point3::from(self.vec_to_local(p - self.origin))
266    }
267
268    /// The world point at local coordinates `p`.
269    pub fn to_world(&self, p: Point3) -> Point3 {
270        self.origin + self.vec_to_world(p.coords)
271    }
272
273    /// The components of a world vector along the axes.
274    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    /// The world vector with local components `v`.
279    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    /// The rotation taking the coordinate axes onto this frame's axes.
284    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    /// The rigid motion taking local coordinates to world coordinates:
293    /// `as_isometry().apply(p) == to_world(p)` to rounding.
294    pub fn as_isometry(&self) -> Isometry {
295        Isometry::new(self.rotation(), self.origin.coords)
296    }
297
298    /// This frame moved by `motion`. Moving geometry is moving its frame,
299    /// and this is that.
300    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
313/// `v` scaled by a power of two that brings its largest coordinate into
314/// `[1, 2)`: the same direction, so the squares its normalisation sums
315/// neither underflow nor overflow. A vector whose coordinates are near
316/// `1e-154` has squares in the subnormal range, and normalised directly
317/// it comes out a few parts in ten thousand off unit length. A power of
318/// two scales exactly, so a vector already in range normalises to the
319/// same bits as without it. `None` for the zero vector.
320pub(crate) fn rescaled(v: Vec3) -> Option<Vec3> {
321    let largest = v.amax();
322    if largest <= 0.0 {
323        return None;
324    }
325    // In [-1023, 1074]: applied in two halves, since `2^1074` is not an
326    // `f64` and neither half overflows or underflows on the way.
327    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/// Which way a [`Frame2`]'s `y` turns from its `x`.
332#[derive(Debug, Clone, Copy, PartialEq, Eq)]
333pub enum Handedness {
334    /// `y` is `x` rotated by +90°: `(u, v)` axes in their natural order.
335    Right,
336    /// `y` is `x` rotated by −90°: a reflection of the right-handed frame.
337    Left,
338}
339
340/// An orthonormal frame in a surface's (u, v) plane, of either handedness.
341/// A pcurve placed by a left-handed `Frame2` is traversed clockwise in
342/// (u, v) — the case of a circle shared by a cap and a wall whose normal
343/// opposes the circle's `Z` (`docs/DATA-MODEL.md` §Pcurves).
344///
345/// ```
346/// use arris_math::{Frame2, Handedness, Point2, Vec2};
347///
348/// let f = Frame2::new(Point2::new(1.0, 1.0), Vec2::new(0.0, 2.0), Handedness::Left).unwrap();
349/// assert!(!f.is_right_handed());
350/// assert_eq!(f.y().into_inner(), Vec2::new(1.0, 0.0));
351/// assert_eq!(f.to_world(Point2::new(1.0, 1.0)), Point2::new(2.0, 2.0));
352/// ```
353#[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/// The wire form of a [`Frame2`], validated by [`Frame2::from_orthonormal`]
363/// on the way in.
364#[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    /// Origin at zero, `x` along `u`, `y` along `v`: right-handed.
394    pub fn identity() -> Self {
395        Frame2 {
396            origin: Point2::origin(),
397            x: Vec2::x_axis(),
398            y: Vec2::y_axis(),
399        }
400    }
401
402    /// A frame with `x` along `x` (normalised) and `y` perpendicular to it
403    /// on the side `handedness` says. Errors: a non-finite input or a zero
404    /// `x`.
405    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    /// A frame from axes that already are one — each unit and
422    /// perpendicular to rounding ([`crate::RELATIVE_ROUNDING`]), of either
423    /// handedness — stored bit for bit; what the native format reads a
424    /// `Frame2` back through. Errors: a non-finite input, or
425    /// [`FrameError::NotOrthonormal`].
426    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    /// The origin.
443    pub const fn origin(&self) -> Point2 {
444        self.origin
445    }
446
447    /// The same axes at the origin moved by `by`.
448    ///
449    /// ```
450    /// use arris_math::{Frame2, Point2, Vec2};
451    ///
452    /// let f = Frame2::identity().translated(Vec2::new(1.0, 2.0));
453    /// assert_eq!(f.origin(), Point2::new(1.0, 2.0));
454    /// assert_eq!(f.x(), Frame2::identity().x());
455    /// ```
456    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    /// The image under `(u, v) ↦ (turn − u, v)`: the origin reflected
465    /// about `turn / 2` in `u` and each axis's `u` component negated. The
466    /// reflection reverses handedness, and an orthonormal pair stays one
467    /// exactly (negating a coordinate is exact).
468    ///
469    /// ```
470    /// use arris_math::{Frame2, Point2};
471    ///
472    /// let f = Frame2::identity().reflected_u(6.0);
473    /// assert_eq!(f.origin(), Point2::new(6.0, 0.0));
474    /// assert!(!f.is_right_handed());
475    /// ```
476    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    /// The `x` axis.
485    pub const fn x(&self) -> UnitVec2 {
486        self.x
487    }
488
489    /// The `y` axis: `x` rotated by ±90° according to the handedness.
490    pub const fn y(&self) -> UnitVec2 {
491        self.y
492    }
493
494    /// Which way `y` turns from `x`.
495    pub fn handedness(&self) -> Handedness {
496        if self.is_right_handed() {
497            Handedness::Right
498        } else {
499            Handedness::Left
500        }
501    }
502
503    /// `true` when `x × y > 0`, the (u, v) plane's own orientation.
504    pub fn is_right_handed(&self) -> bool {
505        self.x.perp(&self.y) > 0.0
506    }
507
508    /// The coordinates of a (u, v) point in this frame.
509    pub fn to_local(&self, p: Point2) -> Point2 {
510        Point2::from(self.vec_to_local(p - self.origin))
511    }
512
513    /// The (u, v) point at local coordinates `p`.
514    pub fn to_world(&self, p: Point2) -> Point2 {
515        self.origin + self.vec_to_world(p.coords)
516    }
517
518    /// The components of a (u, v) vector along the axes.
519    pub fn vec_to_local(&self, v: Vec2) -> Vec2 {
520        Vec2::new(v.dot(&self.x), v.dot(&self.y))
521    }
522
523    /// The (u, v) vector with local components `v`.
524    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    /// Found by the `intersect_surfaces` fuzz target (`fuzz/`,
534    /// ADR-0024 §5): a hint along `z` up to rounding leaves a residue
535    /// that can itself lie along `z`, whose cross product with it is zero
536    /// and normalises to NaN axes.
537    #[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    /// Found by the `intersect_surfaces` fuzz target beside the one
564    /// above: an axis whose coordinates are near `1e-154` has squares in
565    /// the subnormal range, and normalised as given it came out 8e-4 off
566    /// unit length — a cylinder about it was a different cylinder.
567    #[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}