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