all_is_cubes_base/math/
face.rs

1//! Axis-aligned unit vectors: the [`Face6`] and [`Face7`] types.
2//! This module is private but reexported by its parent.
3
4use core::fmt;
5use core::ops;
6
7use euclid::Vector3D;
8
9use manyfmt::Refmt as _;
10use manyfmt::formats::Unquote;
11
12use crate::math::{
13    Axis, ConciseDebug, Cube, FreeCoordinate, FreeVector, GridCoordinate, GridPoint, GridRotation,
14    GridVector, Gridgid, Zero, lines,
15};
16
17/// Identifies a face of a cube or an orthogonal unit vector.
18///
19/// See also the similar type [`Face7`], which adds a “zero” or “within the cube”
20/// variant. The two enums use the same discriminant numbering.
21///
22#[doc = include_str!("../serde-warning.md")]
23#[expect(clippy::exhaustive_enums)]
24#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, exhaust::Exhaust)]
25#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27#[repr(u8)]
28pub enum Face6 {
29    /// Negative X; the face whose normal vector is `(-1, 0, 0)`.
30    NX = 1,
31    /// Negative Y; the face whose normal vector is `(0, -1, 0)`; downward.
32    NY = 2,
33    /// Negative Z; the face whose normal vector is `(0, 0, -1)`.
34    NZ = 3,
35    /// Positive X; the face whose normal vector is `(1, 0, 0)`.
36    PX = 4,
37    /// Positive Y; the face whose normal vector is `(0, 1, 0)`; upward.
38    PY = 5,
39    /// Positive Z; the face whose normal vector is `(0, 0, 1)`.
40    PZ = 6,
41}
42
43/// Identifies a face of a cube or an orthogonal unit vector, except for
44/// [`Within`](Face7::Within) meaning “zero distance and undefined direction”.
45///
46/// This is essentially `Option<`[`Face6`]`>`, except with `Face`-specific methods
47/// provided. The two enums use the same discriminant numbering.
48///
49#[doc = include_str!("../serde-warning.md")]
50#[expect(clippy::exhaustive_enums)]
51#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, exhaust::Exhaust)]
52#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
53#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
54#[repr(u8)]
55pub enum Face7 {
56    /// The interior volume of a cube, or an undefined direction. Corresponds to the vector `(0, 0, 0)`.
57    Within = 0,
58    /// Negative X; the face whose normal vector is `(-1, 0, 0)`.
59    NX,
60    /// Negative Y; the face whose normal vector is `(0, -1, 0)`; downward.
61    NY,
62    /// Negative Z; the face whose normal vector is `(0, 0, -1)`.
63    NZ,
64    /// Positive X; the face whose normal vector is `(1, 0, 0)`.
65    PX,
66    /// Positive Y; the face whose normal vector is `(0, 1, 0)`; upward.
67    PY,
68    /// Positive Z; the face whose normal vector is `(0, 0, 1)`.
69    PZ,
70}
71
72impl Face6 {
73    /// All the values of [`Face6`].
74    pub const ALL: [Face6; 6] = [
75        Face6::NX,
76        Face6::NY,
77        Face6::NZ,
78        Face6::PX,
79        Face6::PY,
80        Face6::PZ,
81    ];
82
83    /// Inverse function of `face as u8`, converting the number to [`Face6`].
84    #[inline]
85    pub const fn from_discriminant(d: u8) -> Option<Self> {
86        match d {
87            1 => Some(Self::NX),
88            2 => Some(Self::NY),
89            3 => Some(Self::NZ),
90            4 => Some(Self::PX),
91            5 => Some(Self::PY),
92            6 => Some(Self::PZ),
93            _ => None,
94        }
95    }
96
97    /// Returns the [`Face6`] whose normal vector is closest in direction to the given
98    /// vector.
99    ///
100    /// Edge cases:
101    /// *   Ties are broken by preferring Z faces over Y faces, and Y faces over X faces.
102    /// *   If all magnitudes are zero, the Z axis's sign is used. (Remember that floating-point
103    ///     numbers include distinct positive and negative zeroes).
104    /// *   If any coordinate is NaN, returns [`None`].
105    #[allow(clippy::missing_inline_in_public_items, reason = "unsure")]
106    pub fn from_snapped_vector(vector: FreeVector) -> Option<Self> {
107        let Vector3D { x, y, z, _unit } = vector;
108
109        // This isn't the likely case, but if we check it first, the generated code for signum()
110        // can avoid redundant NaN checks.
111        if x.is_nan() || y.is_nan() || z.is_nan() {
112            return None;
113        }
114
115        // Note that the Rust signum() reads the sign of zeroes rather than returning zero for zero
116        // (as would be mathematically conventional --
117        // <https://en.wikipedia.org/w/index.php?title=Sign_function&oldid=1177447019>).
118        // Duplicating the calls in each branch helps avoid redundant NaN checks.
119        let (neg_face, sign) = if x.abs() > y.abs() && x.abs() > z.abs() {
120            (Face6::NX, x.signum())
121        } else if y.abs() > z.abs() {
122            (Face6::NY, y.signum())
123        } else {
124            (Face6::NZ, z.signum())
125        };
126        Some(if sign < 0. {
127            neg_face
128        } else {
129            neg_face.opposite()
130        })
131    }
132
133    /// Returns which axis this face's normal vector is parallel to.
134    #[inline]
135    #[must_use]
136    pub const fn axis(self) -> Axis {
137        match self {
138            Self::NX | Self::PX => Axis::X,
139            Self::NY | Self::PY => Axis::Y,
140            Self::NZ | Self::PZ => Axis::Z,
141        }
142    }
143
144    /// Returns whether this face is a “positive” face: one whose unit vector's nonzero
145    /// coordinate is positive.
146    ///
147    /// ```
148    /// # extern crate all_is_cubes_base as all_is_cubes;
149    /// use all_is_cubes::math::Face6;
150    ///
151    /// assert_eq!(Face6::PX.is_positive(), true);
152    /// assert_eq!(Face6::NX.is_positive(), false);
153    /// ```
154    #[inline]
155    pub const fn is_positive(self) -> bool {
156        matches!(self, Self::PX | Self::PY | Self::PZ)
157    }
158
159    /// Returns whether this face is a negative face: one whose unit vector's nonzero
160    /// coordinate is negative.
161    ///
162    /// ```
163    /// # extern crate all_is_cubes_base as all_is_cubes;
164    /// use all_is_cubes::math::Face6;
165    ///
166    /// assert_eq!(Face6::PX.is_negative(), false);
167    /// assert_eq!(Face6::NX.is_negative(), true);
168    /// ```
169    #[inline]
170    pub fn is_negative(self) -> bool {
171        matches!(self, Self::NX | Self::NY | Self::NZ)
172    }
173
174    #[inline]
175    pub(crate) fn signum(self) -> GridCoordinate {
176        match self {
177            Self::NX | Self::NY | Self::NZ => -1,
178            Self::PX | Self::PY | Self::PZ => 1,
179        }
180    }
181
182    /// Returns the opposite face (maps [`PX`](Self::PX) to [`NX`](Self::NX) and so on).
183    #[inline]
184    #[must_use]
185    pub const fn opposite(self) -> Face6 {
186        match self {
187            Face6::NX => Face6::PX,
188            Face6::NY => Face6::PY,
189            Face6::NZ => Face6::PZ,
190            Face6::PX => Face6::NX,
191            Face6::PY => Face6::NY,
192            Face6::PZ => Face6::NZ,
193        }
194    }
195
196    /// Returns the face whose normal is the cross product of these faces' normals.
197    /// Since cross products may be zero, the result is a [`Face7`].
198    #[inline]
199    #[must_use]
200    pub const fn cross(self, other: Self) -> Face7 {
201        self.into7().cross(other.into7())
202    }
203
204    /// Returns the axis-aligned unit vector normal to this face.
205    ///
206    /// If a vector of a different length is desired, use [`Face6::vector()`] instead of
207    /// multiplying this.
208    #[inline]
209    #[must_use]
210    pub fn normal_vector<S, U>(self) -> Vector3D<S, U>
211    where
212        S: Zero + num_traits::One + ops::Neg<Output = S>,
213    {
214        self.into7().normal_vector()
215    }
216
217    /// Returns an axis-aligned vector normal to this face, whose magnitude, and only nonzero
218    /// component, is `magnitude`.
219    ///
220    /// This is mathematically equivalent to multiplying [`Face6::normal_vector()`] by `magnitude`,
221    /// but does not perform those multiplications, and may have better type inference.
222    ///
223    /// # Example
224    ///
225    /// ```rust
226    /// # extern crate all_is_cubes_base as all_is_cubes;
227    /// use all_is_cubes::math::{Face6, GridVector};
228    ///
229    /// assert_eq!(Face6::PY.vector(3), GridVector::new(0, 3, 0));
230    /// assert_eq!(Face6::NY.vector(3), GridVector::new(0, -3, 0));
231    /// ```
232    // TODO: better name for this operation?
233    #[inline]
234    #[must_use]
235    pub fn vector<S, U>(self, magnitude: S) -> Vector3D<S, U>
236    where
237        S: Zero + ops::Neg<Output = S>,
238    {
239        let zero1 = S::zero();
240        let zero2 = S::zero();
241        match self {
242            Face6::NX => Vector3D::new(-magnitude, zero1, zero2),
243            Face6::NY => Vector3D::new(zero1, -magnitude, zero2),
244            Face6::NZ => Vector3D::new(zero1, zero2, -magnitude),
245            Face6::PX => Vector3D::new(magnitude, zero1, zero2),
246            Face6::PY => Vector3D::new(zero1, magnitude, zero2),
247            Face6::PZ => Vector3D::new(zero1, zero2, magnitude),
248        }
249    }
250
251    /// Dot product of this face as a unit vector and the given vector,
252    /// implemented by selecting the relevant component.
253    ///
254    /// ```
255    /// # extern crate all_is_cubes_base as all_is_cubes;
256    /// use all_is_cubes::math::{Face6, FreeVector};
257    ///
258    /// let sample_vector = FreeVector::new(1.0, 2.0, 5.0_f64);
259    /// for face in Face6::ALL {
260    ///     assert_eq!(face.dot(sample_vector), face.normal_vector().dot(sample_vector));
261    /// }
262    /// ```
263    #[inline]
264    #[must_use]
265    pub fn dot<S, U>(self, vector: Vector3D<S, U>) -> S
266    where
267        S: Zero + ops::Neg<Output = S>,
268    {
269        self.into7().dot(vector)
270    }
271
272    /// Returns a rotation, without reflection, which will rotate `Face6::NZ` to be `self`.
273    ///
274    /// The significance of this rotation is that it may be used to obtain a set of
275    /// coordinate systems for all six faces of some cube. It is arbitrary, but convenient
276    /// to have the arbitrary choice already made.
277    #[inline]
278    pub const fn rotation_from_nz(self) -> GridRotation {
279        match self {
280            Face6::NX => GridRotation::RYZX,
281            Face6::NY => GridRotation::RZXY,
282            Face6::NZ => GridRotation::RXYZ,
283            // Positives have the same axis swaps but an arbitrary choice of 180° rotation.
284            Face6::PX => GridRotation::RyZx, // PX rotates about Y.
285            Face6::PY => GridRotation::RZxy, // PY rotates about X.
286            Face6::PZ => GridRotation::RXyz, // PZ rotates about Y.
287        }
288    }
289
290    /// Returns a [`Gridgid`] transformation which, if given points on the square
291    /// with x ∈ [0, scale], y ∈ [0, scale], and z = 0, converts them to points that lie
292    /// on the faces of the cube with x ∈ [0, scale], y ∈ [0, scale], and z ∈ [0, scale].
293    ///
294    /// Specifically, `Face6::NZ.face_transform()` is the identity and all others are
295    /// consistent with that. Note that there are arbitrary choices in the rotation
296    /// of all other faces. (TODO: Document those choices and test them.)
297    ///
298    /// The rotations used are equal to [`Face6::rotation_from_nz()`],
299    /// and this method is equivalent to
300    /// `self.rotation_from_nz().to_positive_octant_transform(scale)`.
301    // TODO: decide whether to replace this entirely with `rotation_from_nz()`
302    ///
303    /// To work with floating-point coordinates, use `.face_transform().to_matrix().to_free()`.
304    #[must_use]
305    #[rustfmt::skip]
306    #[allow(clippy::missing_inline_in_public_items)]
307    pub const fn face_transform(self, scale: GridCoordinate) -> Gridgid {
308        self.rotation_from_nz().to_positive_octant_transform(scale)
309    }
310
311    /// Returns the rotation which is clockwise,
312    /// when looking towards the face `self` of the rotated object.
313    ///
314    /// # Example
315    ///
316    /// ```
317    /// # extern crate all_is_cubes_base as all_is_cubes;
318    /// use all_is_cubes::math::Face6::*;
319    ///
320    /// assert_eq!(PY.clockwise().transform(PX), PZ);
321    /// ```
322    #[inline]
323    pub const fn clockwise(self) -> GridRotation {
324        match self {
325            Face6::NX => GridRotation::RXzY,
326            Face6::NY => GridRotation::RzYX,
327            Face6::NZ => GridRotation::RYxZ,
328            Face6::PX => GridRotation::RXZy,
329            Face6::PY => GridRotation::RZYx,
330            Face6::PZ => GridRotation::RyXZ,
331        }
332    }
333
334    /// Returns the rotation which is counterclockwise (anticlockwise),
335    /// when looking towards the face `self` of the rotated object.
336    ///
337    /// # Example
338    ///
339    /// ```
340    /// # extern crate all_is_cubes_base as all_is_cubes;
341    /// use all_is_cubes::math::Face6::*;
342    ///
343    /// assert_eq!(PY.counterclockwise().transform(PZ), PX);
344    /// ```
345    #[inline]
346    pub const fn counterclockwise(self) -> GridRotation {
347        self.clockwise().inverse()
348    }
349
350    /// Returns the rotation which is a half turn or 180º,
351    /// when looking towards the face `self` of the rotated object.
352    ///
353    /// This result only depends on the axis, not the direction, but it is available here to
354    /// complete the set
355    /// `[self, self.clockwise(), self.r180(), self.counterclockwise()]`,
356    /// which can also be expressed as
357    /// <code>self.[clockwise][Self::clockwise]().[iterate][GridRotation::iterate]()</code>.
358    ///
359    /// # Example
360    ///
361    /// ```
362    /// # extern crate all_is_cubes_base as all_is_cubes;
363    /// use all_is_cubes::math::Face6::*;
364    ///
365    /// assert_eq!(PY.r180().transform(PX), NX);
366    /// ```
367    #[inline]
368    pub const fn r180(self) -> GridRotation {
369        match self {
370            Face6::NX | Face6::PX => GridRotation::RXyz,
371            Face6::NY | Face6::PY => GridRotation::RxYz,
372            Face6::NZ | Face6::PZ => GridRotation::RxyZ,
373        }
374    }
375
376    /// Helper to convert in const context; equivalent to `.into()`.
377    #[inline]
378    pub(crate) const fn into7(self) -> Face7 {
379        match self {
380            Face6::NX => Face7::NX,
381            Face6::NY => Face7::NY,
382            Face6::NZ => Face7::NZ,
383            Face6::PX => Face7::PX,
384            Face6::PY => Face7::PY,
385            Face6::PZ => Face7::PZ,
386        }
387    }
388}
389
390impl Face7 {
391    /// All the values of [`Face7`], with [`Face7::Within`] listed first.
392    pub const ALL: [Face7; 7] = [
393        Face7::Within,
394        Face7::NX,
395        Face7::NY,
396        Face7::NZ,
397        Face7::PX,
398        Face7::PY,
399        Face7::PZ,
400    ];
401
402    /// Inverse function of `face as u8`, converting the number to [`Face7`].
403    #[inline]
404    pub const fn from_discriminant(d: u8) -> Option<Self> {
405        match d {
406            0 => Some(Self::Within),
407            1 => Some(Self::NX),
408            2 => Some(Self::NY),
409            3 => Some(Self::NZ),
410            4 => Some(Self::PX),
411            5 => Some(Self::PY),
412            6 => Some(Self::PZ),
413            _ => None,
414        }
415    }
416
417    /// Returns which axis this face's normal vector is parallel to,
418    /// or [`None`] if the face is [`Face7::Within`].
419    #[inline]
420    #[must_use]
421    pub const fn axis(self) -> Option<Axis> {
422        match self {
423            Face7::Within => None,
424            Face7::NX | Face7::PX => Some(Axis::X),
425            Face7::NY | Face7::PY => Some(Axis::Y),
426            Face7::NZ | Face7::PZ => Some(Axis::Z),
427        }
428    }
429
430    /// Returns whether this face is a “positive” face: one whose unit vector's nonzero
431    /// coordinate is positive.
432    ///
433    /// ```
434    /// # extern crate all_is_cubes_base as all_is_cubes;
435    /// use all_is_cubes::math::Face7;
436    ///
437    /// assert_eq!(Face7::PX.is_positive(), true);
438    /// assert_eq!(Face7::NX.is_positive(), false);
439    /// assert_eq!(Face7::Within.is_positive(), false);
440    /// ```
441    #[inline]
442    pub fn is_positive(self) -> bool {
443        matches!(self, Face7::PX | Face7::PY | Face7::PZ)
444    }
445
446    /// Returns whether this face is a negative face: one whose unit vector's nonzero
447    /// coordinate is negative.
448    ///
449    /// ```
450    /// # extern crate all_is_cubes_base as all_is_cubes;
451    /// use all_is_cubes::math::Face7;
452    ///
453    /// assert_eq!(Face7::PX.is_negative(), false);
454    /// assert_eq!(Face7::NX.is_negative(), true);
455    /// assert_eq!(Face7::Within.is_negative(), false);
456    /// ```
457    #[inline]
458    pub fn is_negative(self) -> bool {
459        matches!(self, Face7::NX | Face7::NY | Face7::NZ)
460    }
461
462    /// Returns the opposite face (maps [`PX`](Self::PX) to [`NX`](Self::NX) and so on).
463    #[inline]
464    #[must_use]
465    pub const fn opposite(self) -> Face7 {
466        match self {
467            Face7::Within => Face7::Within,
468            Face7::NX => Face7::PX,
469            Face7::NY => Face7::PY,
470            Face7::NZ => Face7::PZ,
471            Face7::PX => Face7::NX,
472            Face7::PY => Face7::NY,
473            Face7::PZ => Face7::NZ,
474        }
475    }
476
477    /// Returns the face whose normal is the cross product of these faces' normals.
478    #[inline]
479    #[must_use]
480    pub const fn cross(self, other: Self) -> Self {
481        use Face7::*;
482        match (self, other) {
483            // Zero input
484            (Within, _) => Within,
485            (_, Within) => Within,
486
487            // Equal vectors
488            (NX, NX) => Within,
489            (NY, NY) => Within,
490            (NZ, NZ) => Within,
491            (PX, PX) => Within,
492            (PY, PY) => Within,
493            (PZ, PZ) => Within,
494
495            // Opposite vectors
496            (NX, PX) => Within,
497            (NY, PY) => Within,
498            (NZ, PZ) => Within,
499            (PX, NX) => Within,
500            (PY, NY) => Within,
501            (PZ, NZ) => Within,
502
503            (NX, NY) => PZ,
504            (NX, NZ) => NY,
505            (NX, PY) => NZ,
506            (NX, PZ) => PY,
507
508            (NY, NX) => NZ,
509            (NY, NZ) => PX,
510            (NY, PX) => PZ,
511            (NY, PZ) => NX,
512
513            (NZ, NX) => PY,
514            (NZ, NY) => NX,
515            (NZ, PX) => NY,
516            (NZ, PY) => PX,
517
518            (PX, NY) => NZ,
519            (PX, NZ) => PY,
520            (PX, PY) => PZ,
521            (PX, PZ) => NY,
522
523            (PY, NX) => PZ,
524            (PY, NZ) => NX,
525            (PY, PX) => NZ,
526            (PY, PZ) => PX,
527
528            (PZ, NX) => NY,
529            (PZ, NY) => PX,
530            (PZ, PX) => PY,
531            (PZ, PY) => NX,
532        }
533    }
534
535    /// Returns the vector normal to this face. [`Within`](Self::Within) is assigned the
536    /// zero vector.
537    ///
538    /// If a vector of a different length is desired, use [`Face6::vector()`] instead of
539    /// multiplying this.
540    #[inline]
541    #[must_use]
542    pub fn normal_vector<S, U>(self) -> Vector3D<S, U>
543    where
544        S: Zero + num_traits::One + ops::Neg<Output = S>,
545    {
546        self.vector(S::one())
547    }
548
549    /// Returns the vector normal to this face. [`Within`](Self::Within) is assigned the
550    /// zero vector.
551    ///
552    /// This version is `const` but not generic.
553    #[inline]
554    #[must_use]
555    pub(crate) const fn normal_vector_const(self) -> GridVector {
556        match self {
557            Face7::Within => Vector3D::new(0, 0, 0),
558            Face7::NX => Vector3D::new(-1, 0, 0),
559            Face7::NY => Vector3D::new(0, -1, 0),
560            Face7::NZ => Vector3D::new(0, 0, -1),
561            Face7::PX => Vector3D::new(1, 0, 0),
562            Face7::PY => Vector3D::new(0, 1, 0),
563            Face7::PZ => Vector3D::new(0, 0, 1),
564        }
565    }
566
567    /// Returns an axis-aligned vector normal to this face, whose magnitude, and only nonzero
568    /// component, is `magnitude` unless `self == Face7::Within`.
569    ///
570    /// This is mathematically equivalent to multiplying [`Face7::normal_vector()`] by `magnitude`,
571    /// but does not perform those multiplications, and may have better type inference.
572    ///
573    /// # Example
574    ///
575    /// ```rust
576    /// # extern crate all_is_cubes_base as all_is_cubes;
577    /// use all_is_cubes::math::{Face7, GridVector};
578    ///
579    /// assert_eq!(Face7::PY.vector(3), GridVector::new(0, 3, 0));
580    /// assert_eq!(Face7::Within.vector(3), GridVector::new(0, 0, 0));
581    /// ```
582    // TODO: better name for this operation?
583    #[inline]
584    #[must_use]
585    pub fn vector<S, U>(self, magnitude: S) -> Vector3D<S, U>
586    where
587        S: Zero + ops::Neg<Output = S>,
588    {
589        let zero1 = S::zero();
590        let zero2 = S::zero();
591        match self {
592            Face7::Within => Vector3D::new(zero1, zero2, S::zero()),
593            Face7::NX => Vector3D::new(-magnitude, zero1, zero2),
594            Face7::NY => Vector3D::new(zero1, -magnitude, zero2),
595            Face7::NZ => Vector3D::new(zero1, zero2, -magnitude),
596            Face7::PX => Vector3D::new(magnitude, zero1, zero2),
597            Face7::PY => Vector3D::new(zero1, magnitude, zero2),
598            Face7::PZ => Vector3D::new(zero1, zero2, magnitude),
599        }
600    }
601
602    /// Dot product of this face as a unit vector and the given vector,
603    /// implemented by selecting the relevant component.
604    ///
605    /// ```
606    /// # extern crate all_is_cubes_base as all_is_cubes;
607    /// use all_is_cubes::math::{Face7, FreeVector};
608    ///
609    /// let sample_vector = FreeVector::new(1.0, 2.0, 5.0_f64);
610    /// for face in Face7::ALL {
611    ///     assert_eq!(face.dot(sample_vector), face.normal_vector().dot(sample_vector));
612    /// }
613    /// ```
614    #[inline]
615    #[must_use]
616    pub fn dot<S, U>(self, vector: Vector3D<S, U>) -> S
617    where
618        S: Zero + ops::Neg<Output = S>,
619    {
620        match self {
621            Face7::Within => S::zero(),
622            Face7::NX => -vector.x,
623            Face7::NY => -vector.y,
624            Face7::NZ => -vector.z,
625            Face7::PX => vector.x,
626            Face7::PY => vector.y,
627            Face7::PZ => vector.z,
628        }
629    }
630}
631
632impl ops::Neg for Face6 {
633    type Output = Self;
634    #[inline]
635    fn neg(self) -> Self::Output {
636        self.opposite()
637    }
638}
639impl ops::Neg for Face7 {
640    type Output = Self;
641    #[inline]
642    fn neg(self) -> Self::Output {
643        self.opposite()
644    }
645}
646
647impl From<Face6> for Face7 {
648    #[inline]
649    fn from(value: Face6) -> Self {
650        value.into7()
651    }
652}
653impl TryFrom<Face7> for Face6 {
654    type Error = Faceless;
655    #[inline]
656    fn try_from(value: Face7) -> Result<Face6, Self::Error> {
657        match value {
658            Face7::Within => Err(Faceless),
659            Face7::NX => Ok(Face6::NX),
660            Face7::NY => Ok(Face6::NY),
661            Face7::NZ => Ok(Face6::NZ),
662            Face7::PX => Ok(Face6::PX),
663            Face7::PY => Ok(Face6::PY),
664            Face7::PZ => Ok(Face6::PZ),
665        }
666    }
667}
668
669impl TryFrom<GridVector> for Face6 {
670    /// Returns the original vector on failure.
671    /// (An error message would probably be too lacking context to be helpful.)
672    type Error = GridVector;
673
674    /// Recovers a `Face6` from its corresponding unit normal vector. All other vectors
675    /// are rejected.
676    ///
677    /// ```
678    /// # extern crate all_is_cubes_base as all_is_cubes;
679    /// use all_is_cubes::math::{Face6, GridVector};
680    ///
681    /// // A Face6 may be converted from its normal vector.
682    /// for face in Face6::ALL {
683    ///     assert_eq!(Face6::try_from(face.normal_vector()), Ok(face));
684    /// }
685    ///
686    /// // If the vector does not correspond to any Face6, it is returned.
687    /// let v = GridVector::new(1, 2, 3);
688    /// assert_eq!(Face6::try_from(v), Err(v));
689    /// ```
690    #[inline]
691    fn try_from(value: GridVector) -> Result<Self, Self::Error> {
692        let f7 = Face7::try_from(value)?;
693        Face6::try_from(f7).map_err(|_| value)
694    }
695}
696impl TryFrom<GridVector> for Face7 {
697    /// Returns the original vector on failure.
698    /// (An error message would probably be too lacking context to be helpful.)
699    type Error = GridVector;
700
701    /// Recovers a [`Face7`] from its corresponding unit normal vector. All other vectors
702    /// are rejected.
703    ///
704    /// ```
705    /// # extern crate all_is_cubes_base as all_is_cubes;
706    /// use all_is_cubes::math::{Face7, GridVector};
707    ///
708    /// // A Face7 may be converted from its normal vector.
709    /// for face in Face7::ALL {
710    ///     assert_eq!(Face7::try_from(face.normal_vector()), Ok(face));
711    /// }
712    ///
713    /// // If the vector does not correspond to any Face7, it is returned.
714    /// let v = GridVector::new(1, 2, 3);
715    /// assert_eq!(Face7::try_from(v), Err(v));
716    /// ```
717    #[rustfmt::skip]
718    #[allow(clippy::missing_inline_in_public_items)] // unsure whether good
719    fn try_from(value: GridVector) -> Result<Self, Self::Error> {
720        use Face7::*;
721        match value {
722            GridVector { _unit: _, x: 0, y: 0, z: 0 } => Ok(Within),
723            GridVector { _unit: _, x: 1, y: 0, z: 0 } => Ok(PX),
724            GridVector { _unit: _, x: 0, y: 1, z: 0 } => Ok(PY),
725            GridVector { _unit: _, x: 0, y: 0, z: 1 } => Ok(PZ),
726            GridVector { _unit: _, x: -1, y: 0, z: 0 } => Ok(NX),
727            GridVector { _unit: _, x: 0, y: -1, z: 0 } => Ok(NY),
728            GridVector { _unit: _, x: 0, y: 0, z: -1 } => Ok(NZ),
729            not_unit_vector => Err(not_unit_vector),
730        }
731    }
732}
733
734/// Error resulting from providing [`Face7::Within`] where a definite nonzero direction
735/// is needed, such as converting to a [`Face6`].
736#[derive(Copy, Clone, Debug, Eq, PartialEq, displaydoc::Display)]
737#[displaydoc("Face7::Within does not have a direction or axis")]
738#[expect(clippy::exhaustive_structs)]
739pub struct Faceless;
740
741#[cfg(feature = "rerun")]
742impl From<Face6> for re_types::view_coordinates::SignedAxis3 {
743    #[inline]
744    fn from(face: Face6) -> Self {
745        use re_types::view_coordinates::{Axis3, Sign, SignedAxis3};
746        match face {
747            Face6::NX => SignedAxis3 {
748                sign: Sign::Negative,
749                axis: Axis3::X,
750            },
751            Face6::NY => SignedAxis3 {
752                sign: Sign::Negative,
753                axis: Axis3::Y,
754            },
755            Face6::NZ => SignedAxis3 {
756                sign: Sign::Negative,
757                axis: Axis3::Z,
758            },
759            Face6::PX => SignedAxis3 {
760                sign: Sign::Positive,
761                axis: Axis3::X,
762            },
763            Face6::PY => SignedAxis3 {
764                sign: Sign::Positive,
765                axis: Axis3::Y,
766            },
767            Face6::PZ => SignedAxis3 {
768                sign: Sign::Positive,
769                axis: Axis3::Z,
770            },
771        }
772    }
773}
774
775/// Container for values keyed by [`Face6`]s. Always holds exactly six elements.
776#[expect(clippy::exhaustive_structs)]
777#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, exhaust::Exhaust)]
778#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
779pub struct FaceMap<V> {
780    /// The value whose key is [`Face6::NX`].
781    pub nx: V,
782    /// The value whose key is [`Face6::NY`].
783    pub ny: V,
784    /// The value whose key is [`Face6::NZ`].
785    pub nz: V,
786    /// The value whose key is [`Face6::PX`].
787    pub px: V,
788    /// The value whose key is [`Face6::PY`].
789    pub py: V,
790    /// The value whose key is [`Face6::PZ`].
791    pub pz: V,
792}
793
794#[allow(
795    clippy::missing_inline_in_public_items,
796    reason = "all methods are generic code"
797)]
798impl<V> FaceMap<V> {
799    /// Constructs a [`FaceMap`] by using the provided function to compute
800    /// a value for each [`Face6`] enum variant.
801    #[inline]
802    pub fn from_fn(mut f: impl FnMut(Face6) -> V) -> Self {
803        Self {
804            nx: f(Face6::NX),
805            ny: f(Face6::NY),
806            nz: f(Face6::NZ),
807            px: f(Face6::PX),
808            py: f(Face6::PY),
809            pz: f(Face6::PZ),
810        }
811    }
812
813    /// Constructs a [`FaceMap`] whose negative and positive directions are equal.
814    // TODO: Evaluate whether this is a good API.
815    #[inline]
816    #[doc(hidden)] // used by all-is-cubes-content
817    pub fn symmetric([x, y, z]: [V; 3]) -> Self
818    where
819        V: Default + Clone,
820    {
821        Self {
822            nx: x.clone(),
823            px: x,
824            ny: y.clone(),
825            py: y,
826            nz: z.clone(),
827            pz: z,
828        }
829    }
830
831    /// Returns a vector containing the values for each negative face.
832    pub fn negatives<U>(self) -> Vector3D<V, U>
833    where
834        V: Copy,
835    {
836        Vector3D::new(self.nx, self.ny, self.nz)
837    }
838
839    /// Returns a vector containing the values for each positive face.
840    pub fn positives<U>(self) -> Vector3D<V, U>
841    where
842        V: Copy,
843    {
844        Vector3D::new(self.px, self.py, self.pz)
845    }
846
847    /// Iterate over the map's key-value pairs by reference, in the same order as [`Face6::ALL`].
848    pub fn iter(&self) -> impl Iterator<Item = (Face6, &V)> {
849        Face6::ALL.iter().copied().map(move |f| (f, &self[f]))
850    }
851
852    /// Iterate over the map's key-value pairs by mutable reference, in the same order as [`Face6::ALL`].
853    pub fn iter_mut(&mut self) -> impl Iterator<Item = (Face6, &mut V)> {
854        [
855            (Face6::NX, &mut self.nx),
856            (Face6::NY, &mut self.ny),
857            (Face6::NZ, &mut self.nz),
858            (Face6::PX, &mut self.px),
859            (Face6::PY, &mut self.py),
860            (Face6::PZ, &mut self.pz),
861        ]
862        .into_iter()
863    }
864
865    /// Iterate over the map values by reference, in the same order as [`Face6::ALL`].
866    pub fn values(&self) -> impl Iterator<Item = &V> + Clone {
867        Face6::ALL.iter().copied().map(move |f| &self[f])
868    }
869
870    /// Iterate over the map values by mutable reference, in the same order as [`Face6::ALL`].
871    pub fn values_mut(&mut self) -> impl Iterator<Item = &mut V> {
872        [
873            &mut self.nx,
874            &mut self.ny,
875            &mut self.nz,
876            &mut self.px,
877            &mut self.py,
878            &mut self.pz,
879        ]
880        .into_iter()
881    }
882
883    /// Convert to an array, whose elements are arranged in the same order as [`Face6::ALL`].
884    pub fn into_values(self) -> [V; 6] {
885        [self.nx, self.ny, self.nz, self.px, self.py, self.pz]
886    }
887
888    /// Convert to an iterator, whose items are arranged in the same order as [`Face6::ALL`].
889    pub fn into_values_iter(self) -> impl Iterator<Item = V> {
890        // TODO: eliminate this as not really useful in Rust 2021
891        self.into_values().into_iter()
892    }
893
894    /// Calculates the sum of all values.
895    ///
896    /// This is semantically equivalent to `.into_values_iter().sum()` but computes the sum
897    /// using the [`ops::Add`] trait. It may be more efficient than involving iterators.
898    #[inline]
899    pub fn sum(self) -> V
900    where
901        V: ops::Add<Output = V>,
902    {
903        // I don’t know what addition order is best for minimizing error,
904        // but this is at least more “balanced” than some.
905        // Perhaps there is a better algorithm?
906        (self.nx + self.px) + (self.ny + self.py) + (self.nz + self.pz)
907    }
908
909    /// Transform values.
910    pub fn map<U>(self, mut f: impl FnMut(Face6, V) -> U) -> FaceMap<U> {
911        FaceMap {
912            nx: f(Face6::NX, self.nx),
913            ny: f(Face6::NY, self.ny),
914            nz: f(Face6::NZ, self.nz),
915            px: f(Face6::PX, self.px),
916            py: f(Face6::PY, self.py),
917            pz: f(Face6::PZ, self.pz),
918        }
919    }
920
921    /// Transform values, taking the input by reference.
922    pub fn map_ref<'map, U>(&'map self, mut f: impl FnMut(Face6, &'map V) -> U) -> FaceMap<U> {
923        FaceMap {
924            nx: f(Face6::NX, &self.nx),
925            ny: f(Face6::NY, &self.ny),
926            nz: f(Face6::NZ, &self.nz),
927            px: f(Face6::PX, &self.px),
928            py: f(Face6::PY, &self.py),
929            pz: f(Face6::PZ, &self.pz),
930        }
931    }
932
933    /// Combine two [`FaceMap`]s using a function applied to each pair of corresponding values.
934    pub fn zip<U, R>(self, other: FaceMap<U>, mut f: impl FnMut(Face6, V, U) -> R) -> FaceMap<R> {
935        FaceMap {
936            nx: f(Face6::NX, self.nx, other.nx),
937            ny: f(Face6::NY, self.ny, other.ny),
938            nz: f(Face6::NZ, self.nz, other.nz),
939            px: f(Face6::PX, self.px, other.px),
940            py: f(Face6::PY, self.py, other.py),
941            pz: f(Face6::PZ, self.pz, other.pz),
942        }
943    }
944
945    /// Returns this map with one entry's value replaced.
946    ///
947    /// This may be used for constructing a map with only one interesting entry:
948    ///
949    /// ```
950    /// # extern crate all_is_cubes_base as all_is_cubes;
951    /// use all_is_cubes::math::{Face6, FaceMap};
952    ///
953    /// assert_eq!(
954    ///     FaceMap::default().with(Face6::PY, 10),
955    ///     {
956    ///         let mut m = FaceMap::default();
957    ///         m[Face6::PY] = 10;
958    ///         m
959    ///     },
960    /// );
961    /// ```
962    #[inline]
963    #[must_use]
964    pub fn with(mut self, face: Face6, value: V) -> Self {
965        self[face] = value;
966        self
967    }
968
969    /// Shuffle the values in this map according to the given rotation.
970    #[must_use]
971    pub fn rotate(self, rotation: GridRotation) -> Self {
972        // TODO: Can we make this cleaner? (If GridRotation had a way to ask it what swaps
973        // it corresponds to, that might also be useful for Vol rotations.)
974        let to_source = rotation.inverse();
975        let mut source = self.map(|_, value| Some(value));
976        Self::from_fn(|face| source[to_source.transform(face)].take().unwrap())
977    }
978}
979
980impl<V: Clone> FaceMap<V> {
981    /// Constructs a [`FaceMap`] containing clones of the provided value.
982    #[inline]
983    pub fn splat(value: V) -> Self {
984        Self {
985            nx: value.clone(),
986            ny: value.clone(),
987            nz: value.clone(),
988            px: value.clone(),
989            py: value.clone(),
990            pz: value,
991        }
992    }
993}
994
995impl<V: Copy> FaceMap<V> {
996    /// Constructs a [`FaceMap`] containing copies of the provided value.
997    ///
998    /// This is practically identical to [`FaceMap::splat()`] except that it is a
999    /// `const fn`. It may be removed from future major versions once Rust supports const
1000    /// trait function calls.
1001    #[inline]
1002    pub const fn splat_copy(value: V) -> Self {
1003        Self {
1004            nx: value,
1005            ny: value,
1006            nz: value,
1007            px: value,
1008            py: value,
1009            pz: value,
1010        }
1011    }
1012}
1013
1014impl<V> ops::Index<Face6> for FaceMap<V> {
1015    type Output = V;
1016    #[inline]
1017    fn index(&self, face: Face6) -> &V {
1018        match face {
1019            Face6::NX => &self.nx,
1020            Face6::NY => &self.ny,
1021            Face6::NZ => &self.nz,
1022            Face6::PX => &self.px,
1023            Face6::PY => &self.py,
1024            Face6::PZ => &self.pz,
1025        }
1026    }
1027}
1028
1029impl<V> ops::IndexMut<Face6> for FaceMap<V> {
1030    #[inline]
1031    fn index_mut(&mut self, face: Face6) -> &mut V {
1032        match face {
1033            Face6::NX => &mut self.nx,
1034            Face6::NY => &mut self.ny,
1035            Face6::NZ => &mut self.nz,
1036            Face6::PX => &mut self.px,
1037            Face6::PY => &mut self.py,
1038            Face6::PZ => &mut self.pz,
1039        }
1040    }
1041}
1042
1043impl<V> fmt::Debug for FaceMap<V>
1044where
1045    V: fmt::Debug + PartialEq,
1046{
1047    /// In addition to the usual formatting behaviors, [`FaceMap`] will detect whether
1048    /// elements are equal and avoid redundant printing.
1049    #[allow(clippy::missing_inline_in_public_items)]
1050    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1051        let FaceMap {
1052            nx,
1053            ny,
1054            nz,
1055            px,
1056            py,
1057            pz,
1058        } = self;
1059
1060        let mut dm = f.debug_map();
1061
1062        if nx == ny && nx == nz && nx == px && nx == py && nx == pz {
1063            dm.entry(&"all".refmt(&Unquote), nx);
1064        } else if nx == ny && nx == nz && px == py && px == pz {
1065            dm.entry(&"−all".refmt(&Unquote), nx);
1066            dm.entry(&"+all".refmt(&Unquote), px);
1067        } else if nx == px && ny == py && nz == pz {
1068            dm.entry(&"x".refmt(&Unquote), nx);
1069            dm.entry(&"y".refmt(&Unquote), ny);
1070            dm.entry(&"z".refmt(&Unquote), nz);
1071        } else {
1072            dm.entry(&"−x".refmt(&Unquote), nx);
1073            dm.entry(&"−y".refmt(&Unquote), ny);
1074            dm.entry(&"−z".refmt(&Unquote), nz);
1075            dm.entry(&"+x".refmt(&Unquote), px);
1076            dm.entry(&"+y".refmt(&Unquote), py);
1077            dm.entry(&"+z".refmt(&Unquote), pz);
1078        }
1079
1080        dm.finish()
1081    }
1082}
1083
1084macro_rules! impl_binary_operator_for_facemap {
1085    ($trait:ident :: $method:ident, $assign_trait:ident :: $assign_method:ident) => {
1086        impl<V: ops::$trait> ops::$trait for FaceMap<V> {
1087            type Output = FaceMap<V::Output>;
1088            /// Apply the operator pairwise to the values for all six faces.
1089            #[inline]
1090            fn $method(self, other: FaceMap<V>) -> FaceMap<V::Output> {
1091                self.zip(other, |_, a, b| <V as ops::$trait>::$method(a, b))
1092            }
1093        }
1094
1095        impl<V: ops::$assign_trait> ops::$assign_trait for FaceMap<V> {
1096            /// Apply the operator pairwise to the values for all six faces.
1097            #[inline]
1098            fn $assign_method(&mut self, rhs: Self) {
1099                self.nx.$assign_method(rhs.nx);
1100                self.ny.$assign_method(rhs.ny);
1101                self.nz.$assign_method(rhs.nz);
1102                self.px.$assign_method(rhs.px);
1103                self.py.$assign_method(rhs.py);
1104                self.pz.$assign_method(rhs.pz);
1105            }
1106        }
1107    };
1108}
1109impl_binary_operator_for_facemap!(BitAnd::bitand, BitAndAssign::bitand_assign);
1110impl_binary_operator_for_facemap!(BitOr::bitor, BitOrAssign::bitor_assign);
1111impl_binary_operator_for_facemap!(BitXor::bitxor, BitXorAssign::bitxor_assign);
1112impl_binary_operator_for_facemap!(Add::add, AddAssign::add_assign);
1113impl_binary_operator_for_facemap!(Mul::mul, MulAssign::mul_assign);
1114impl_binary_operator_for_facemap!(Sub::sub, SubAssign::sub_assign);
1115impl_binary_operator_for_facemap!(Div::div, DivAssign::div_assign);
1116impl_binary_operator_for_facemap!(Rem::rem, RemAssign::rem_assign);
1117
1118impl<V> IntoIterator for FaceMap<V> {
1119    type Item = (Face6, V);
1120
1121    // TODO: use a custom iterator type if this gets more than one-off use
1122    type IntoIter = <[(Face6, V); 6] as IntoIterator>::IntoIter;
1123
1124    #[inline]
1125    fn into_iter(self) -> Self::IntoIter {
1126        [
1127            (Face6::NX, self.nx),
1128            (Face6::NY, self.ny),
1129            (Face6::NZ, self.nz),
1130            (Face6::PX, self.px),
1131            (Face6::PY, self.py),
1132            (Face6::PZ, self.pz),
1133        ]
1134        .into_iter()
1135    }
1136}
1137
1138/// The combination of a [`Cube`] and [`Face7`] identifying one face of it or the interior.
1139/// This pattern appears in cursor selection and collision detection.
1140#[derive(Clone, Copy, Hash, Eq, PartialEq)]
1141#[expect(clippy::exhaustive_structs)]
1142#[allow(missing_docs)]
1143pub struct CubeFace {
1144    pub cube: Cube,
1145    pub face: Face7,
1146}
1147
1148impl CubeFace {
1149    #[allow(missing_docs)]
1150    #[inline]
1151    pub fn new(cube: impl Into<Cube>, face: Face7) -> Self {
1152        Self {
1153            cube: cube.into(),
1154            face,
1155        }
1156    }
1157
1158    /// Computes the cube that is adjacent in the direction of [`self.face`](Self::face).
1159    /// Equal to [`self.cube`](Self::cube) if the face is [`Face7::Within`].
1160    ///
1161    /// May panic if the cube coordinates overflow.
1162    #[inline]
1163    pub fn adjacent(self) -> Cube {
1164        self.cube + self.face.normal_vector()
1165    }
1166
1167    /// Translate `self` by adding `offset` to `self.cube`.
1168    #[inline]
1169    #[must_use]
1170    pub fn translate(mut self, offset: GridVector) -> Self {
1171        self.cube += offset;
1172        self
1173    }
1174}
1175
1176impl fmt::Debug for CubeFace {
1177    #[allow(clippy::missing_inline_in_public_items)]
1178    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
1179        write!(
1180            fmt,
1181            "CubeFace({:?}, {:?})",
1182            self.cube.refmt(&ConciseDebug),
1183            self.face,
1184        )
1185    }
1186}
1187
1188impl lines::Wireframe for CubeFace {
1189    #[allow(clippy::missing_inline_in_public_items)]
1190    fn wireframe_points<E: Extend<[lines::Vertex; 2]>>(&self, output: &mut E) {
1191        // TODO: How much to offset the lines should be a parameter of the wireframe_points process.
1192        let expansion = 0.005;
1193        let aab = self.cube.aab().expand(expansion);
1194        aab.wireframe_points(output);
1195
1196        // Draw an X on the face.
1197        if let Ok(face) = Face6::try_from(self.face) {
1198            let face_transform = face.face_transform(1);
1199            const X_POINTS: [[GridPoint; 2]; 2] = [
1200                [GridPoint::new(0, 0, 0), GridPoint::new(1, 1, 0)],
1201                [GridPoint::new(1, 0, 0), GridPoint::new(0, 1, 0)],
1202            ];
1203            // TODO: this is a messy kludge and really we should be stealing corner points
1204            // from the AAB instead, but there isn't yet a good way to do that.
1205            output.extend(X_POINTS.into_iter().map(|line| {
1206                line.map(|point| {
1207                    lines::Vertex::from(
1208                        (face_transform.transform_point(point))
1209                            .map(|c| (FreeCoordinate::from(c) - 0.5) * (1. + expansion * 2.) + 0.5)
1210                            + self.cube.aab().lower_bounds_v(),
1211                    )
1212                })
1213            }));
1214        }
1215    }
1216}
1217
1218#[cfg(test)]
1219mod tests {
1220    use crate::util::MultiFailure;
1221
1222    use super::*;
1223    use alloc::string::String;
1224    use alloc::vec::Vec;
1225    use exhaust::Exhaust;
1226    use pretty_assertions::assert_eq;
1227
1228    #[test]
1229    fn from_snapped_vector_roundtrip() {
1230        for face in Face6::ALL {
1231            let normal = face.normal_vector();
1232            let snapped = Face6::from_snapped_vector(normal);
1233            assert_eq!(Some(face), snapped, "from {normal:?}");
1234        }
1235    }
1236
1237    #[test]
1238    #[rustfmt::skip]
1239    fn from_snapped_vector_cases() {
1240        let mut f = MultiFailure::new();
1241        for (face, vector, comment) in [
1242            (Some(Face6::PZ), [0., 0., 0.], "zero tie, positive Z, positive other"),
1243            (Some(Face6::PZ), [-0., -0., 0.], "zero tie, positive Z, negative other"),
1244            (Some(Face6::NZ), [0., 0., -0.], "zero tie, negative Z, positive other"),
1245            (Some(Face6::NZ), [-0., -0., -0.], "zero tie, negative Z, negative other"),
1246
1247            (Some(Face6::NZ), [-2., -3., -3.], "2-axis tie YZ, negative"),
1248            (Some(Face6::NY), [-3., -3., -2.], "2-axis tie XY, negative"),
1249            (Some(Face6::PZ), [2., 3., 3.], "2-axis tie YZ, positive"),
1250            (Some(Face6::PY), [3., 3., 2.], "2-axis tie XY, positive"),
1251
1252            (None, [f64::NAN, 1.0, 1.0], "NaN X"),
1253            (None, [1.0, f64::NAN, 1.0], "NaN Y"),
1254            (None, [1.0, 1.0, f64::NAN], "NaN Z"),
1255        ] {
1256            f.catch(|| {
1257                let vector = FreeVector::from(vector);
1258                assert_eq!(face, Face6::from_snapped_vector(vector), "{comment}, {vector:?}");
1259            });
1260        }
1261    }
1262
1263    #[test]
1264    fn cross_6() {
1265        let mut f = MultiFailure::new();
1266        for face1 in Face6::ALL {
1267            for face2 in Face6::ALL {
1268                f.catch(|| {
1269                    // Cross product of faces is identical to cross product of vectors.
1270                    assert_eq!(
1271                        face1.cross(face2).normal_vector::<f64, ()>(),
1272                        face1.normal_vector().cross(face2.normal_vector()),
1273                        "{face1:?} cross {face2:?}",
1274                    );
1275                });
1276            }
1277        }
1278    }
1279
1280    #[test]
1281    fn cross_7() {
1282        let mut f = MultiFailure::new();
1283        for face1 in Face7::ALL {
1284            for face2 in Face7::ALL {
1285                f.catch(|| {
1286                    // Cross product of faces is identical to cross product of vectors.
1287                    assert_eq!(
1288                        face1.cross(face2).normal_vector::<f64, ()>(),
1289                        face1.normal_vector().cross(face2.normal_vector()),
1290                        "{face1:?} cross {face2:?}",
1291                    );
1292                });
1293            }
1294        }
1295    }
1296
1297    #[test]
1298    fn rotation_from_nz() {
1299        for face in Face6::ALL {
1300            let rot = face.rotation_from_nz();
1301            assert_eq!(
1302                rot.transform(Face6::NZ),
1303                face,
1304                "{face:?}: {rot:?} should rotate from NZ"
1305            );
1306            assert!(!rot.is_reflection(), "{face:?}: {rot:?} should not reflect");
1307        }
1308    }
1309
1310    #[test]
1311    fn face_transform_does_not_reflect() {
1312        for face in Face6::ALL {
1313            assert!(!face.face_transform(7).rotation.is_reflection());
1314        }
1315    }
1316
1317    // TODO: More tests of face.face_transform()
1318
1319    #[test]
1320    fn clockwise_properties() {
1321        let mut f = MultiFailure::new();
1322        for face in Face6::ALL {
1323            f.catch(|| {
1324                assert_eq!(
1325                    face.counterclockwise(),
1326                    face.clockwise().inverse(),
1327                    "{face:?} ccw is inverse of cw"
1328                );
1329                assert!(
1330                    !face.clockwise().is_reflection(),
1331                    "{face:?}.clockwise() is not a reflection"
1332                );
1333                assert_eq!(
1334                    face.clockwise().transform(face),
1335                    face,
1336                    "{face:?}.clockwise() leaves itself unchanged"
1337                );
1338                assert_eq!(
1339                    face.clockwise().iterate().count(),
1340                    4,
1341                    "{face:?}.clockwise() is a 90° rotation"
1342                );
1343                assert_eq!(
1344                    face.clockwise() * face.clockwise(),
1345                    face.r180(),
1346                    "{face:?}.clockwise() twice equals 180°"
1347                )
1348            });
1349        }
1350    }
1351
1352    #[test]
1353    fn face_map_debug_cmp() {
1354        let strings =
1355            FaceMap::<bool>::exhaust().map(|fm| format!("{fm:?}")).collect::<Vec<String>>();
1356        assert_eq!(
1357            strings.iter().map(String::as_str).collect::<Vec<_>>(),
1358            vec![
1359                "{all: false}",
1360                "{−x: false, −y: false, −z: false, +x: false, +y: false, +z: true}",
1361                "{−x: false, −y: false, −z: false, +x: false, +y: true, +z: false}",
1362                "{−x: false, −y: false, −z: false, +x: false, +y: true, +z: true}",
1363                "{−x: false, −y: false, −z: false, +x: true, +y: false, +z: false}",
1364                "{−x: false, −y: false, −z: false, +x: true, +y: false, +z: true}",
1365                "{−x: false, −y: false, −z: false, +x: true, +y: true, +z: false}",
1366                "{−all: false, +all: true}",
1367                "{−x: false, −y: false, −z: true, +x: false, +y: false, +z: false}",
1368                "{x: false, y: false, z: true}",
1369                "{−x: false, −y: false, −z: true, +x: false, +y: true, +z: false}",
1370                "{−x: false, −y: false, −z: true, +x: false, +y: true, +z: true}",
1371                "{−x: false, −y: false, −z: true, +x: true, +y: false, +z: false}",
1372                "{−x: false, −y: false, −z: true, +x: true, +y: false, +z: true}",
1373                "{−x: false, −y: false, −z: true, +x: true, +y: true, +z: false}",
1374                "{−x: false, −y: false, −z: true, +x: true, +y: true, +z: true}",
1375                "{−x: false, −y: true, −z: false, +x: false, +y: false, +z: false}",
1376                "{−x: false, −y: true, −z: false, +x: false, +y: false, +z: true}",
1377                "{x: false, y: true, z: false}",
1378                "{−x: false, −y: true, −z: false, +x: false, +y: true, +z: true}",
1379                "{−x: false, −y: true, −z: false, +x: true, +y: false, +z: false}",
1380                "{−x: false, −y: true, −z: false, +x: true, +y: false, +z: true}",
1381                "{−x: false, −y: true, −z: false, +x: true, +y: true, +z: false}",
1382                "{−x: false, −y: true, −z: false, +x: true, +y: true, +z: true}",
1383                "{−x: false, −y: true, −z: true, +x: false, +y: false, +z: false}",
1384                "{−x: false, −y: true, −z: true, +x: false, +y: false, +z: true}",
1385                "{−x: false, −y: true, −z: true, +x: false, +y: true, +z: false}",
1386                "{x: false, y: true, z: true}",
1387                "{−x: false, −y: true, −z: true, +x: true, +y: false, +z: false}",
1388                "{−x: false, −y: true, −z: true, +x: true, +y: false, +z: true}",
1389                "{−x: false, −y: true, −z: true, +x: true, +y: true, +z: false}",
1390                "{−x: false, −y: true, −z: true, +x: true, +y: true, +z: true}",
1391                "{−x: true, −y: false, −z: false, +x: false, +y: false, +z: false}",
1392                "{−x: true, −y: false, −z: false, +x: false, +y: false, +z: true}",
1393                "{−x: true, −y: false, −z: false, +x: false, +y: true, +z: false}",
1394                "{−x: true, −y: false, −z: false, +x: false, +y: true, +z: true}",
1395                "{x: true, y: false, z: false}",
1396                "{−x: true, −y: false, −z: false, +x: true, +y: false, +z: true}",
1397                "{−x: true, −y: false, −z: false, +x: true, +y: true, +z: false}",
1398                "{−x: true, −y: false, −z: false, +x: true, +y: true, +z: true}",
1399                "{−x: true, −y: false, −z: true, +x: false, +y: false, +z: false}",
1400                "{−x: true, −y: false, −z: true, +x: false, +y: false, +z: true}",
1401                "{−x: true, −y: false, −z: true, +x: false, +y: true, +z: false}",
1402                "{−x: true, −y: false, −z: true, +x: false, +y: true, +z: true}",
1403                "{−x: true, −y: false, −z: true, +x: true, +y: false, +z: false}",
1404                "{x: true, y: false, z: true}",
1405                "{−x: true, −y: false, −z: true, +x: true, +y: true, +z: false}",
1406                "{−x: true, −y: false, −z: true, +x: true, +y: true, +z: true}",
1407                "{−x: true, −y: true, −z: false, +x: false, +y: false, +z: false}",
1408                "{−x: true, −y: true, −z: false, +x: false, +y: false, +z: true}",
1409                "{−x: true, −y: true, −z: false, +x: false, +y: true, +z: false}",
1410                "{−x: true, −y: true, −z: false, +x: false, +y: true, +z: true}",
1411                "{−x: true, −y: true, −z: false, +x: true, +y: false, +z: false}",
1412                "{−x: true, −y: true, −z: false, +x: true, +y: false, +z: true}",
1413                "{x: true, y: true, z: false}",
1414                "{−x: true, −y: true, −z: false, +x: true, +y: true, +z: true}",
1415                "{−all: true, +all: false}",
1416                "{−x: true, −y: true, −z: true, +x: false, +y: false, +z: true}",
1417                "{−x: true, −y: true, −z: true, +x: false, +y: true, +z: false}",
1418                "{−x: true, −y: true, −z: true, +x: false, +y: true, +z: true}",
1419                "{−x: true, −y: true, −z: true, +x: true, +y: false, +z: false}",
1420                "{−x: true, −y: true, −z: true, +x: true, +y: false, +z: true}",
1421                "{−x: true, −y: true, −z: true, +x: true, +y: true, +z: false}",
1422                "{all: true}",
1423            ],
1424        );
1425    }
1426
1427    /// Test the ordering of all [`FaceMap`] methods that explicitly produce an ordered result.
1428    #[test]
1429    fn face_map_iter_in_enum_order() {
1430        let mut map = FaceMap::from_fn(core::convert::identity);
1431        let expected_both: Vec<(Face6, Face6)> = Face6::ALL.into_iter().zip(Face6::ALL).collect();
1432
1433        // FaceMap::iter()
1434        assert_eq!(
1435            expected_both,
1436            map.iter().map(|(k, &v)| (k, v)).collect::<Vec<_>>(),
1437        );
1438
1439        // FaceMap::iter_mut()
1440        assert_eq!(
1441            expected_both,
1442            map.iter_mut().map(|(k, &mut v)| (k, v)).collect::<Vec<_>>(),
1443        );
1444
1445        // FaceMap::values()
1446        assert_eq!(
1447            Face6::ALL.to_vec(),
1448            map.values().copied().collect::<Vec<_>>(),
1449        );
1450
1451        // FaceMap::into_values()
1452        assert_eq!(Face6::ALL, map.into_values());
1453    }
1454
1455    #[test]
1456    fn face_map_rotate() {
1457        assert_eq!(
1458            FaceMap {
1459                nx: 10,
1460                px: 20,
1461                ny: 11,
1462                py: 21,
1463                nz: 12,
1464                pz: 22,
1465            }
1466            .rotate(GridRotation::RyXZ),
1467            FaceMap {
1468                nx: 11,
1469                px: 21,
1470                ny: 20,
1471                py: 10,
1472                nz: 12,
1473                pz: 22,
1474            }
1475        )
1476    }
1477
1478    // TODO: More tests of FaceMap
1479
1480    #[test]
1481    fn cubeface_format() {
1482        let cube_face = CubeFace {
1483            cube: Cube::new(1, 2, 3),
1484            face: Face7::NY,
1485        };
1486        assert_eq!(&format!("{cube_face:#?}"), "CubeFace((+1, +2, +3), NY)");
1487    }
1488}