all_is_cubes_base/math/
rotation.rs

1//! Rotations which exchange axes (thus not leaving the integer grid).
2//! This module is private but reexported by its parent.
3
4#![allow(
5    clippy::large_stack_arrays,
6    reason = "effectively-false positive on Arbitrary derive"
7)]
8use core::ops::Mul;
9
10use euclid::vec3;
11
12use crate::math::{Face6, GridCoordinate, GridMatrix, GridSize, GridVector, Gridgid, Vector3D};
13
14#[cfg(doc)]
15use crate::math::GridAab;
16
17/// Represents a discrete (grid-aligned) rotation, or exchange of axes.
18///
19/// Compared to a [`GridMatrix`], this cannot specify scale, translation, or skew;
20/// it is used for identifying the rotations of blocks.
21///
22/// Each of the variant names specifies the three unit vectors which (*x*, *y*, *z*),
23/// respectively, should be multiplied by to perform the rotation.
24/// Lowercase refers to a negated unit vector.
25///
26/// See also:
27///
28/// * [`Face6`] is less general, in that it specifies a single axis but not
29///   rotation about that axis.
30///     * [`Face6::clockwise()`] and [`Face6::counterclockwise()`] can be used to obtain
31///       [`GridRotation`] values.
32/// * [`GridMatrix`] is more general, specifying an affine transformation.
33///
34#[doc = include_str!("../serde-warning.md")]
35#[rustfmt::skip]
36#[expect(clippy::exhaustive_enums, clippy::upper_case_acronyms)]
37#[allow(missing_docs)]
38#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
39#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
40#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41#[repr(u8)]
42pub enum GridRotation {
43    // TODO: shuffle or explicitly number these to choose a meaningful numbering
44    RXYZ, RXYz, RXyZ, RXyz, RxYZ, RxYz, RxyZ, Rxyz,
45    RXZY, RXZy, RXzY, RXzy, RxZY, RxZy, RxzY, Rxzy,
46    RYXZ, RYXz, RYxZ, RYxz, RyXZ, RyXz, RyxZ, Ryxz,
47    RYZX, RYZx, RYzX, RYzx, RyZX, RyZx, RyzX, Ryzx,
48    RZXY, RZXy, RZxY, RZxy, RzXY, RzXy, RzxY, Rzxy,
49    RZYX, RZYx, RZyX, RZyx, RzYX, RzYx, RzyX, Rzyx,
50}
51
52impl GridRotation {
53    /// All 48 possible rotations.
54    ///
55    /// Warning: TODO: The ordering of these rotations is not yet stable.
56    /// The current ordering is based on the six axis permutations followed by rotations.
57    #[rustfmt::skip]
58    pub const ALL: [Self; 48] = {
59        use GridRotation::*;
60        [
61            RXYZ, RXYz, RXyZ, RXyz, RxYZ, RxYz, RxyZ, Rxyz,
62            RXZY, RXZy, RXzY, RXzy, RxZY, RxZy, RxzY, Rxzy,
63            RYXZ, RYXz, RYxZ, RYxz, RyXZ, RyXz, RyxZ, Ryxz,
64            RYZX, RYZx, RYzX, RYzx, RyZX, RyZx, RyzX, Ryzx,
65            RZXY, RZXy, RZxY, RZxy, RzXY, RzXy, RzxY, Rzxy,
66            RZYX, RZYx, RZyX, RZyx, RzYX, RzYx, RzyX, Rzyx,
67        ]
68    };
69
70    /// All possible rotations that are not reflections.
71    ///
72    /// Warning: TODO: The ordering of these rotations is not yet stable.
73    #[rustfmt::skip]
74    pub const ALL_BUT_REFLECTIONS: [Self; 24] = {
75        use GridRotation::*;
76        [
77            RXYZ, RXyz, RxYz, RxyZ,
78            RXZy, RXzY, RxZY, Rxzy,
79            RYXz, RYxZ, RyXZ, Ryxz,
80            RYZX, RYzx, RyZx, RyzX,
81            RZXY, RZxy, RzXy, RzxY,
82            RZYx, RZyX, RzYX, Rzyx,
83        ]
84    };
85
86    /// The identity rotation, also known as [`RXYZ`](Self::RXYZ).
87    pub const IDENTITY: Self = Self::RXYZ;
88
89    /// Constructs a rotation from a basis: that is, the returned rotation will
90    /// rotate `PX` into `basis[0]`, `PY` into `basis[1]`, and `PZ` into `basis[2]`.
91    ///
92    /// Panics if the three provided axes are not mutually perpendicular.
93    #[inline]
94    pub fn from_basis(basis: impl Into<[Face6; 3]>) -> Self {
95        let basis = basis.into();
96        Self::try_from_basis_const(basis)
97            .unwrap_or_else(|| panic!("Invalid basis given to GridRotation::from_basis: {basis:?}"))
98    }
99
100    // TODO: This is public-hidden because we need the const form.
101    // When const traits are available, make regular `from_basis()` const.
102    #[doc(hidden)]
103    #[inline]
104    pub const fn try_from_basis_const(basis: [Face6; 3]) -> Option<Self> {
105        use {Face6::*, GridRotation::*};
106        Some(match basis {
107            [PX, PY, PZ] => RXYZ,
108            [PX, PZ, PY] => RXZY,
109            [PY, PX, PZ] => RYXZ,
110            [PY, PZ, PX] => RYZX,
111            [PZ, PX, PY] => RZXY,
112            [PZ, PY, PX] => RZYX,
113
114            [PX, PY, NZ] => RXYz,
115            [PX, PZ, NY] => RXZy,
116            [PY, PX, NZ] => RYXz,
117            [PY, PZ, NX] => RYZx,
118            [PZ, PX, NY] => RZXy,
119            [PZ, PY, NX] => RZYx,
120
121            [PX, NY, PZ] => RXyZ,
122            [PX, NZ, PY] => RXzY,
123            [PY, NX, PZ] => RYxZ,
124            [PY, NZ, PX] => RYzX,
125            [PZ, NX, PY] => RZxY,
126            [PZ, NY, PX] => RZyX,
127
128            [PX, NY, NZ] => RXyz,
129            [PX, NZ, NY] => RXzy,
130            [PY, NX, NZ] => RYxz,
131            [PY, NZ, NX] => RYzx,
132            [PZ, NX, NY] => RZxy,
133            [PZ, NY, NX] => RZyx,
134
135            [NX, PY, PZ] => RxYZ,
136            [NX, PZ, PY] => RxZY,
137            [NY, PX, PZ] => RyXZ,
138            [NY, PZ, PX] => RyZX,
139            [NZ, PX, PY] => RzXY,
140            [NZ, PY, PX] => RzYX,
141
142            [NX, PY, NZ] => RxYz,
143            [NX, PZ, NY] => RxZy,
144            [NY, PX, NZ] => RyXz,
145            [NY, PZ, NX] => RyZx,
146            [NZ, PX, NY] => RzXy,
147            [NZ, PY, NX] => RzYx,
148
149            [NX, NY, PZ] => RxyZ,
150            [NX, NZ, PY] => RxzY,
151            [NY, NX, PZ] => RyxZ,
152            [NY, NZ, PX] => RyzX,
153            [NZ, NX, PY] => RzxY,
154            [NZ, NY, PX] => RzyX,
155
156            [NX, NY, NZ] => Rxyz,
157            [NX, NZ, NY] => Rxzy,
158            [NY, NX, NZ] => Ryxz,
159            [NY, NZ, NX] => Ryzx,
160            [NZ, NX, NY] => Rzxy,
161            [NZ, NY, NX] => Rzyx,
162
163            _ => return None,
164        })
165    }
166
167    /// Find the rotation (without reflection) which rotates `source` to `destination`.
168    /// and leaves `up` unaffected. (This might also be considered a “look at” operation).
169    ///
170    /// If it is not possible to leave `up` unaffected, returns [`None`]. (Trying two
171    /// perpendicular `up` directions will always succeed.)
172    #[allow(clippy::missing_inline_in_public_items)]
173    pub fn from_to(source: Face6, destination: Face6, up: Face6) -> Option<Self> {
174        let perpendicular = source.cross(up);
175        if source == destination {
176            Some(Self::IDENTITY)
177        } else if let Ok(perpendicular) = Face6::try_from(perpendicular) {
178            // Find rotation from the frame source=NZ up=PY to the actual given one.
179            let canonical_to_given = Self::from_basis([perpendicular, up, source.opposite()]);
180            let given_to_canonical = canonical_to_given.inverse();
181            debug_assert!(!canonical_to_given.is_reflection());
182
183            // The destination expressed in that frame.
184            let canonical_destination = given_to_canonical.transform(destination);
185            // Find which of the four rotations in a plane matches.
186            use Face6::*;
187            let canonical_rotation = match canonical_destination {
188                NY | PY => {
189                    // Tried to rotate into the up vector.
190                    return None;
191                }
192                NZ => Self::IDENTITY,
193                PX => PY.clockwise(),
194                PZ => PY.r180(),
195                NX => PY.counterclockwise(),
196            };
197            // Transform that rotation into the given frame.
198            Some(canonical_to_given * canonical_rotation * given_to_canonical)
199        } else {
200            // perpendicular == Face7::Within, therefore
201            // up was parallel to source, or one of them was Within.
202            None
203        }
204    }
205
206    /// Returns the basis vectors for the unrotated coordinate system in
207    /// the rotated coordinate system.
208    // TODO: properly public API? It’s very useful for building operations but kind of wacky.
209    #[doc(hidden)]
210    #[inline]
211    pub const fn to_basis(self) -> Vector3D<Face6, ()> {
212        // Compute an explicit lookup table to ensure the program uses a nice dense 144 bytes of
213        // data instead of a switch table with separate machine code to load each value.
214        // (Yes, that actually happened.)
215        static TABLE: [Vector3D<Face6, ()>; 48] = {
216            let mut table = [Vector3D::new(Face6::PX, Face6::PX, Face6::PX); 48];
217            let mut i = 0;
218            while i < table.len() {
219                use {Face6::*, GridRotation::*};
220                let rot = GridRotation::ALL[i];
221                table[rot as usize] = match rot {
222                    // Note that each entry in this table is just reiterating the name of the
223                    // variant as its components.
224                    RXYZ => vec3(PX, PY, PZ),
225                    RXZY => vec3(PX, PZ, PY),
226                    RYXZ => vec3(PY, PX, PZ),
227                    RYZX => vec3(PY, PZ, PX),
228                    RZXY => vec3(PZ, PX, PY),
229                    RZYX => vec3(PZ, PY, PX),
230
231                    RXYz => vec3(PX, PY, NZ),
232                    RXZy => vec3(PX, PZ, NY),
233                    RYXz => vec3(PY, PX, NZ),
234                    RYZx => vec3(PY, PZ, NX),
235                    RZXy => vec3(PZ, PX, NY),
236                    RZYx => vec3(PZ, PY, NX),
237
238                    RXyZ => vec3(PX, NY, PZ),
239                    RXzY => vec3(PX, NZ, PY),
240                    RYxZ => vec3(PY, NX, PZ),
241                    RYzX => vec3(PY, NZ, PX),
242                    RZxY => vec3(PZ, NX, PY),
243                    RZyX => vec3(PZ, NY, PX),
244
245                    RXyz => vec3(PX, NY, NZ),
246                    RXzy => vec3(PX, NZ, NY),
247                    RYxz => vec3(PY, NX, NZ),
248                    RYzx => vec3(PY, NZ, NX),
249                    RZxy => vec3(PZ, NX, NY),
250                    RZyx => vec3(PZ, NY, NX),
251
252                    RxYZ => vec3(NX, PY, PZ),
253                    RxZY => vec3(NX, PZ, PY),
254                    RyXZ => vec3(NY, PX, PZ),
255                    RyZX => vec3(NY, PZ, PX),
256                    RzXY => vec3(NZ, PX, PY),
257                    RzYX => vec3(NZ, PY, PX),
258
259                    RxYz => vec3(NX, PY, NZ),
260                    RxZy => vec3(NX, PZ, NY),
261                    RyXz => vec3(NY, PX, NZ),
262                    RyZx => vec3(NY, PZ, NX),
263                    RzXy => vec3(NZ, PX, NY),
264                    RzYx => vec3(NZ, PY, NX),
265
266                    RxyZ => vec3(NX, NY, PZ),
267                    RxzY => vec3(NX, NZ, PY),
268                    RyxZ => vec3(NY, NX, PZ),
269                    RyzX => vec3(NY, NZ, PX),
270                    RzxY => vec3(NZ, NX, PY),
271                    RzyX => vec3(NZ, NY, PX),
272
273                    Rxyz => vec3(NX, NY, NZ),
274                    Rxzy => vec3(NX, NZ, NY),
275                    Ryxz => vec3(NY, NX, NZ),
276                    Ryzx => vec3(NY, NZ, NX),
277                    Rzxy => vec3(NZ, NX, NY),
278                    Rzyx => vec3(NZ, NY, NX),
279                };
280                i += 1;
281            }
282            table
283        };
284
285        TABLE[self as usize]
286    }
287
288    /// Expresses this rotation as a [`Gridgid`] transform which rotates “in place” the
289    /// points within the volume defined by coordinates in the range [0, size].
290    ///
291    /// That is, a [`GridAab`] of that volume will be unchanged by rotation:
292    ///
293    /// ```
294    /// # mod all_is_cubes {
295    /// #   pub mod block { pub use all_is_cubes_base::resolution::Resolution; }
296    /// #   pub use all_is_cubes_base::math;
297    /// # }
298    /// use all_is_cubes::block::Resolution;
299    /// use all_is_cubes::math::{Face6, GridAab, GridRotation};
300    ///
301    /// let b = GridAab::for_block(Resolution::R8);
302    /// let rotation = Face6::PY.clockwise().to_positive_octant_transform(8);
303    /// assert_eq!(b.transform(rotation), Some(b));
304    /// ```
305    ///
306    /// Such matrices are suitable for rotating the voxels of a block, provided
307    /// that the voxel coordinates are then transformed with [`GridMatrix::transform_cube`],
308    /// *not* [`GridMatrix::transform_point`]
309    /// (due to the lower-corner format of cube coordinates).
310    /// ```
311    /// # extern crate all_is_cubes_base as all_is_cubes;
312    /// # use all_is_cubes::math::{Cube, Face6, GridAab, GridRotation};
313    ///
314    /// let rotation = Face6::PY.clockwise().to_positive_octant_transform(4);
315    /// assert_eq!(rotation.transform_cube(Cube::new(0, 0, 0)), Cube::new(3, 0, 0));
316    /// assert_eq!(rotation.transform_cube(Cube::new(3, 0, 0)), Cube::new(3, 0, 3));
317    /// assert_eq!(rotation.transform_cube(Cube::new(3, 0, 3)), Cube::new(0, 0, 3));
318    /// assert_eq!(rotation.transform_cube(Cube::new(0, 0, 3)), Cube::new(0, 0, 0));
319    /// ```
320    //
321    // TODO: add tests
322    #[inline]
323    pub const fn to_positive_octant_transform(self, size: GridCoordinate) -> Gridgid {
324        #[inline(always)]
325        const fn offset(face: Face6, size: GridCoordinate) -> GridVector {
326            if face.is_positive() {
327                GridVector::new(0, 0, 0)
328            } else {
329                // const scalar multiplication
330                let mut v = face.into7().normal_vector_const();
331                v.x *= -size;
332                v.y *= -size;
333                v.z *= -size;
334                v
335            }
336        }
337        #[inline(always)]
338        const fn add(mut a: GridVector, b: GridVector) -> GridVector {
339            a.x += b.x;
340            a.y += b.y;
341            a.z += b.z;
342            a
343        }
344        let basis = self.to_basis();
345        Gridgid {
346            rotation: self,
347            translation: add(
348                add(offset(basis.x, size), offset(basis.y, size)),
349                offset(basis.z, size),
350            ),
351        }
352    }
353
354    /// Expresses this rotation as a matrix without any translation.
355    // TODO: add tests
356    #[inline]
357    pub fn to_rotation_matrix(self) -> GridMatrix {
358        let basis = self.to_basis();
359        GridMatrix {
360            x: basis.x.normal_vector(),
361            y: basis.y.normal_vector(),
362            z: basis.z.normal_vector(),
363            w: Vector3D::zero(),
364        }
365    }
366
367    /// Rotate the face by this rotation.
368    // TODO: test equivalence with matrix
369    #[inline]
370    pub fn transform(self, face: Face6) -> Face6 {
371        // TODO: there ought to be a much cleaner way to express this
372        // ... and it should be a const fn, too
373        let p = self.to_basis()[face.axis()];
374        if face.is_negative() { p.opposite() } else { p }
375    }
376
377    /// Rotate the vector by this rotation.
378    ///
379    /// May panic or wrap if `vector` has any components equal to [`GridCoordinate::MIN`].
380    #[inline]
381    #[track_caller]
382    pub fn transform_vector(self, vector: GridVector) -> GridVector {
383        // Shared handling to print the vector, and also to generate only one set of panic code
384        // rather than three.
385        // TODO: when overflow_checks disabled, don't panic
386        match self.checked_transform_vector(vector) {
387            Some(v) => v,
388            None => panic!(
389                "overflow due to sign change in GridVector::transform_vector({self:?}, {vector:?})"
390            ),
391        }
392    }
393
394    /// Rotate the vector by this rotation.
395    ///
396    /// Returns [`None`] if `vector` has any components equal to [`GridCoordinate::MIN`],
397    /// which would overflow.
398    #[inline]
399    pub fn checked_transform_vector(self, vector: GridVector) -> Option<GridVector> {
400        let basis = self.to_basis();
401
402        let mut result = GridVector::zero();
403        result[basis.x.axis()] = vector.x.checked_mul(basis.x.signum())?;
404        result[basis.y.axis()] = vector.y.checked_mul(basis.y.signum())?;
405        result[basis.z.axis()] = vector.z.checked_mul(basis.z.signum())?;
406
407        Some(result)
408
409        // Implementation note: The following code would seem to be simpler and avoid
410        // a zero initialization,
411        //
412        // let inverse_basis = self.inverse().to_basis();
413        // GridVector {
414        //     x: inverse_basis.x.dot(vector),
415        //     y: inverse_basis.y.dot(vector),
416        //     z: inverse_basis.z.dot(vector),
417        //     _unit: PhantomData,
418        // }
419        //
420        // but the actual generated machine code is larger and involves computed jumps.
421    }
422
423    /// Rotate the size value by this rotation.
424    ///
425    /// This is similar to [`GridRotation::transform_vector()`] except that the components
426    /// are only swapped, not negated, and there is no possibility of numeric overflow.
427    #[inline]
428    pub fn transform_size(self, size: GridSize) -> GridSize {
429        let basis = self.to_basis();
430
431        let mut result = GridSize::zero();
432        result[basis.x.axis()] = size.width;
433        result[basis.y.axis()] = size.height;
434        result[basis.z.axis()] = size.depth;
435        result
436    }
437
438    /// Returns whether this is a reflection.
439    ///
440    /// ```
441    /// # extern crate all_is_cubes_base as all_is_cubes;
442    /// use all_is_cubes::math::{GridRotation, Face6::*};
443    ///
444    /// assert!(!GridRotation::IDENTITY.is_reflection());
445    /// assert!(!GridRotation::from_basis([PX, PZ, NY]).is_reflection());
446    /// assert!(GridRotation::from_basis([PX, PZ, PY]).is_reflection());
447    /// ```
448    #[inline]
449    pub const fn is_reflection(self) -> bool {
450        // In a coordinate system of the *same handedness*, the cross product computes
451        // the same
452
453        let Vector3D { x, y, z, _unit } = self.to_basis();
454        // u8 casts are a kludge to make == work as a const fn.
455        x.cross(y) as u8 != z as u8
456    }
457
458    /// Returns the inverse of this rotation; the one which undoes this.
459    ///
460    /// ```
461    /// # extern crate all_is_cubes_base as all_is_cubes;
462    /// use all_is_cubes::math::GridRotation;
463    ///
464    /// for &rotation in &GridRotation::ALL {
465    ///     assert_eq!(rotation * rotation.inverse(), GridRotation::IDENTITY);
466    /// }
467    /// ```
468    #[rustfmt::skip]
469    #[must_use]
470    #[inline]
471    pub const fn inverse(self) -> Self {
472        use GridRotation::*;
473        match self {
474            RXYZ => RXYZ, RXYz => RXYz, RXyZ => RXyZ, RXyz => RXyz, RxYZ => RxYZ,
475            RxYz => RxYz, RxyZ => RxyZ, Rxyz => Rxyz, RXZY => RXZY, RXZy => RXzY,
476            RXzY => RXZy, RXzy => RXzy, RxZY => RxZY, RxZy => RxzY, RxzY => RxZy,
477            Rxzy => Rxzy, RYXZ => RYXZ, RYXz => RYXz, RYxZ => RyXZ, RYxz => RyXz,
478            RyXZ => RYxZ, RyXz => RYxz, RyxZ => RyxZ, Ryxz => Ryxz, RYZX => RZXY,
479            RYZx => RzXY, RYzX => RZXy, RYzx => RzXy, RyZX => RZxY, RyZx => RzxY,
480            RyzX => RZxy, Ryzx => Rzxy, RZXY => RYZX, RZXy => RYzX, RZxY => RyZX,
481            RZxy => RyzX, RzXY => RYZx, RzXy => RYzx, RzxY => RyZx, Rzxy => Ryzx,
482            RZYX => RZYX, RZYx => RzYX, RZyX => RZyX, RZyx => RzyX, RzYX => RZYx,
483            RzYx => RzYx, RzyX => RZyx, Rzyx => Rzyx,
484        }
485    }
486
487    /// Generates the sequence of rotations that may be obtained by concatenating/multiplying
488    /// this rotation with itself repeatedly.
489    ///
490    /// The first element of the iterator will always be the identity, i.e. this rotation
491    /// applied zero times. The iterator ends when the sequence would repeat itself, i.e.
492    /// just before it would produce the identity again.
493    ///
494    /// # Example results of iteration
495    ///
496    /// ```
497    /// # extern crate all_is_cubes_base as all_is_cubes;
498    /// use all_is_cubes::math::{Face6::*, GridRotation};
499    ///
500    /// // The identity rotation remains itself when iterated.
501    /// assert_eq!(
502    ///     GridRotation::IDENTITY.iterate().collect::<Vec<_>>(),
503    ///     vec![GridRotation::IDENTITY],
504    /// );
505    ///
506    /// // Any reflection or 180° rotation will produce itself and the identity.
507    /// let x_reflection = GridRotation::from_basis([NX, PY, PZ]);
508    /// assert_eq!(
509    ///     x_reflection.iterate().collect::<Vec<_>>(),
510    ///     vec![GridRotation::IDENTITY, x_reflection],
511    /// );
512    ///
513    /// // Any 90° rotation produces four distinct rotations.
514    /// assert_eq!(
515    ///     PY.clockwise().iterate().collect::<Vec<_>>(),
516    ///     vec![
517    ///         GridRotation::IDENTITY,
518    ///         PY.clockwise(),
519    ///         PY.r180(),
520    ///         PY.counterclockwise(),
521    ///    ],
522    /// );
523    /// ```
524    #[allow(clippy::missing_inline_in_public_items)]
525    pub fn iterate(self) -> impl Iterator<Item = Self> {
526        let mut state = Some(Self::IDENTITY);
527        core::iter::from_fn(move || {
528            let current = state?;
529            let next = current * self;
530            if next == Self::IDENTITY {
531                // If we would produce the identity *next time*, then we have produced the
532                // complete cycle and should end iteration.
533                state = None;
534            } else {
535                state = Some(next);
536            }
537            Some(current)
538        })
539    }
540}
541
542impl Default for GridRotation {
543    /// Returns the identity (no rotation).
544    #[inline]
545    fn default() -> Self {
546        Self::IDENTITY
547    }
548}
549
550impl num_traits::One for GridRotation {
551    /// Returns the identity (no rotation).
552    #[inline]
553    fn one() -> Self {
554        Self::IDENTITY
555    }
556}
557
558impl Mul<Self> for GridRotation {
559    type Output = Self;
560
561    /// Multiplication is concatenation: `self * rhs` is equivalent to
562    /// applying `rhs` and then applying `self`.
563    ///
564    /// ```
565    /// # extern crate all_is_cubes_base as all_is_cubes;
566    /// use all_is_cubes::math::{Face6, Face6::*, GridRotation, GridPoint};
567    ///
568    /// let transform_1 = GridRotation::from_basis([NY, PX, PZ]);
569    /// let transform_2 = GridRotation::from_basis([PY, PZ, PX]);
570    ///
571    /// // Demonstrate the directionality of concatenation.
572    /// for face in Face6::ALL {
573    ///     assert_eq!(
574    ///         (transform_1 * transform_2).transform(face),
575    ///         transform_1.transform(transform_2.transform(face)),
576    ///     );
577    /// }
578    /// ```
579    #[inline]
580    fn mul(self, rhs: Self) -> Self::Output {
581        MULTIPLICATION_TABLE[rhs as usize][self as usize]
582    }
583}
584
585/// Indices are the discriminants of the first (RHS) and second (LHS) rotations to be composed.
586/// `tests::regenerate_multiplication_table()` may generate a new version of this table (while
587/// checking the current one).
588/// 
589/// Note that this table *must* be a `static` or we’ll get excess copies of it significantly
590/// increasing the binary size.
591#[rustfmt::skip]
592static MULTIPLICATION_TABLE: [[GridRotation; 48]; 48] = {
593    use GridRotation::*;
594    [
595        [RXYZ,RXYz,RXyZ,RXyz,RxYZ,RxYz,RxyZ,Rxyz,RXZY,RXZy,RXzY,RXzy,RxZY,RxZy,RxzY,Rxzy,RYXZ,RYXz,RYxZ,RYxz,RyXZ,RyXz,RyxZ,Ryxz,RYZX,RYZx,RYzX,RYzx,RyZX,RyZx,RyzX,Ryzx,RZXY,RZXy,RZxY,RZxy,RzXY,RzXy,RzxY,Rzxy,RZYX,RZYx,RZyX,RZyx,RzYX,RzYx,RzyX,Rzyx,],
596        [RXYz,RXYZ,RXyz,RXyZ,RxYz,RxYZ,Rxyz,RxyZ,RXZy,RXZY,RXzy,RXzY,RxZy,RxZY,Rxzy,RxzY,RYXz,RYXZ,RYxz,RYxZ,RyXz,RyXZ,Ryxz,RyxZ,RYZx,RYZX,RYzx,RYzX,RyZx,RyZX,Ryzx,RyzX,RZXy,RZXY,RZxy,RZxY,RzXy,RzXY,Rzxy,RzxY,RZYx,RZYX,RZyx,RZyX,RzYx,RzYX,Rzyx,RzyX,],
597        [RXyZ,RXyz,RXYZ,RXYz,RxyZ,Rxyz,RxYZ,RxYz,RXzY,RXzy,RXZY,RXZy,RxzY,Rxzy,RxZY,RxZy,RYxZ,RYxz,RYXZ,RYXz,RyxZ,Ryxz,RyXZ,RyXz,RYzX,RYzx,RYZX,RYZx,RyzX,Ryzx,RyZX,RyZx,RZxY,RZxy,RZXY,RZXy,RzxY,Rzxy,RzXY,RzXy,RZyX,RZyx,RZYX,RZYx,RzyX,Rzyx,RzYX,RzYx,],
598        [RXyz,RXyZ,RXYz,RXYZ,Rxyz,RxyZ,RxYz,RxYZ,RXzy,RXzY,RXZy,RXZY,Rxzy,RxzY,RxZy,RxZY,RYxz,RYxZ,RYXz,RYXZ,Ryxz,RyxZ,RyXz,RyXZ,RYzx,RYzX,RYZx,RYZX,Ryzx,RyzX,RyZx,RyZX,RZxy,RZxY,RZXy,RZXY,Rzxy,RzxY,RzXy,RzXY,RZyx,RZyX,RZYx,RZYX,Rzyx,RzyX,RzYx,RzYX,],
599        [RxYZ,RxYz,RxyZ,Rxyz,RXYZ,RXYz,RXyZ,RXyz,RxZY,RxZy,RxzY,Rxzy,RXZY,RXZy,RXzY,RXzy,RyXZ,RyXz,RyxZ,Ryxz,RYXZ,RYXz,RYxZ,RYxz,RyZX,RyZx,RyzX,Ryzx,RYZX,RYZx,RYzX,RYzx,RzXY,RzXy,RzxY,Rzxy,RZXY,RZXy,RZxY,RZxy,RzYX,RzYx,RzyX,Rzyx,RZYX,RZYx,RZyX,RZyx,],
600        [RxYz,RxYZ,Rxyz,RxyZ,RXYz,RXYZ,RXyz,RXyZ,RxZy,RxZY,Rxzy,RxzY,RXZy,RXZY,RXzy,RXzY,RyXz,RyXZ,Ryxz,RyxZ,RYXz,RYXZ,RYxz,RYxZ,RyZx,RyZX,Ryzx,RyzX,RYZx,RYZX,RYzx,RYzX,RzXy,RzXY,Rzxy,RzxY,RZXy,RZXY,RZxy,RZxY,RzYx,RzYX,Rzyx,RzyX,RZYx,RZYX,RZyx,RZyX,],
601        [RxyZ,Rxyz,RxYZ,RxYz,RXyZ,RXyz,RXYZ,RXYz,RxzY,Rxzy,RxZY,RxZy,RXzY,RXzy,RXZY,RXZy,RyxZ,Ryxz,RyXZ,RyXz,RYxZ,RYxz,RYXZ,RYXz,RyzX,Ryzx,RyZX,RyZx,RYzX,RYzx,RYZX,RYZx,RzxY,Rzxy,RzXY,RzXy,RZxY,RZxy,RZXY,RZXy,RzyX,Rzyx,RzYX,RzYx,RZyX,RZyx,RZYX,RZYx,],
602        [Rxyz,RxyZ,RxYz,RxYZ,RXyz,RXyZ,RXYz,RXYZ,Rxzy,RxzY,RxZy,RxZY,RXzy,RXzY,RXZy,RXZY,Ryxz,RyxZ,RyXz,RyXZ,RYxz,RYxZ,RYXz,RYXZ,Ryzx,RyzX,RyZx,RyZX,RYzx,RYzX,RYZx,RYZX,Rzxy,RzxY,RzXy,RzXY,RZxy,RZxY,RZXy,RZXY,Rzyx,RzyX,RzYx,RzYX,RZyx,RZyX,RZYx,RZYX,],
603        [RXZY,RXzY,RXZy,RXzy,RxZY,RxzY,RxZy,Rxzy,RXYZ,RXyZ,RXYz,RXyz,RxYZ,RxyZ,RxYz,Rxyz,RYZX,RYzX,RYZx,RYzx,RyZX,RyzX,RyZx,Ryzx,RYXZ,RYxZ,RYXz,RYxz,RyXZ,RyxZ,RyXz,Ryxz,RZYX,RZyX,RZYx,RZyx,RzYX,RzyX,RzYx,Rzyx,RZXY,RZxY,RZXy,RZxy,RzXY,RzxY,RzXy,Rzxy,],
604        [RXZy,RXzy,RXZY,RXzY,RxZy,Rxzy,RxZY,RxzY,RXYz,RXyz,RXYZ,RXyZ,RxYz,Rxyz,RxYZ,RxyZ,RYZx,RYzx,RYZX,RYzX,RyZx,Ryzx,RyZX,RyzX,RYXz,RYxz,RYXZ,RYxZ,RyXz,Ryxz,RyXZ,RyxZ,RZYx,RZyx,RZYX,RZyX,RzYx,Rzyx,RzYX,RzyX,RZXy,RZxy,RZXY,RZxY,RzXy,Rzxy,RzXY,RzxY,],
605        [RXzY,RXZY,RXzy,RXZy,RxzY,RxZY,Rxzy,RxZy,RXyZ,RXYZ,RXyz,RXYz,RxyZ,RxYZ,Rxyz,RxYz,RYzX,RYZX,RYzx,RYZx,RyzX,RyZX,Ryzx,RyZx,RYxZ,RYXZ,RYxz,RYXz,RyxZ,RyXZ,Ryxz,RyXz,RZyX,RZYX,RZyx,RZYx,RzyX,RzYX,Rzyx,RzYx,RZxY,RZXY,RZxy,RZXy,RzxY,RzXY,Rzxy,RzXy,],
606        [RXzy,RXZy,RXzY,RXZY,Rxzy,RxZy,RxzY,RxZY,RXyz,RXYz,RXyZ,RXYZ,Rxyz,RxYz,RxyZ,RxYZ,RYzx,RYZx,RYzX,RYZX,Ryzx,RyZx,RyzX,RyZX,RYxz,RYXz,RYxZ,RYXZ,Ryxz,RyXz,RyxZ,RyXZ,RZyx,RZYx,RZyX,RZYX,Rzyx,RzYx,RzyX,RzYX,RZxy,RZXy,RZxY,RZXY,Rzxy,RzXy,RzxY,RzXY,],
607        [RxZY,RxzY,RxZy,Rxzy,RXZY,RXzY,RXZy,RXzy,RxYZ,RxyZ,RxYz,Rxyz,RXYZ,RXyZ,RXYz,RXyz,RyZX,RyzX,RyZx,Ryzx,RYZX,RYzX,RYZx,RYzx,RyXZ,RyxZ,RyXz,Ryxz,RYXZ,RYxZ,RYXz,RYxz,RzYX,RzyX,RzYx,Rzyx,RZYX,RZyX,RZYx,RZyx,RzXY,RzxY,RzXy,Rzxy,RZXY,RZxY,RZXy,RZxy,],
608        [RxZy,Rxzy,RxZY,RxzY,RXZy,RXzy,RXZY,RXzY,RxYz,Rxyz,RxYZ,RxyZ,RXYz,RXyz,RXYZ,RXyZ,RyZx,Ryzx,RyZX,RyzX,RYZx,RYzx,RYZX,RYzX,RyXz,Ryxz,RyXZ,RyxZ,RYXz,RYxz,RYXZ,RYxZ,RzYx,Rzyx,RzYX,RzyX,RZYx,RZyx,RZYX,RZyX,RzXy,Rzxy,RzXY,RzxY,RZXy,RZxy,RZXY,RZxY,],
609        [RxzY,RxZY,Rxzy,RxZy,RXzY,RXZY,RXzy,RXZy,RxyZ,RxYZ,Rxyz,RxYz,RXyZ,RXYZ,RXyz,RXYz,RyzX,RyZX,Ryzx,RyZx,RYzX,RYZX,RYzx,RYZx,RyxZ,RyXZ,Ryxz,RyXz,RYxZ,RYXZ,RYxz,RYXz,RzyX,RzYX,Rzyx,RzYx,RZyX,RZYX,RZyx,RZYx,RzxY,RzXY,Rzxy,RzXy,RZxY,RZXY,RZxy,RZXy,],
610        [Rxzy,RxZy,RxzY,RxZY,RXzy,RXZy,RXzY,RXZY,Rxyz,RxYz,RxyZ,RxYZ,RXyz,RXYz,RXyZ,RXYZ,Ryzx,RyZx,RyzX,RyZX,RYzx,RYZx,RYzX,RYZX,Ryxz,RyXz,RyxZ,RyXZ,RYxz,RYXz,RYxZ,RYXZ,Rzyx,RzYx,RzyX,RzYX,RZyx,RZYx,RZyX,RZYX,Rzxy,RzXy,RzxY,RzXY,RZxy,RZXy,RZxY,RZXY,],
611        [RYXZ,RYXz,RyXZ,RyXz,RYxZ,RYxz,RyxZ,Ryxz,RZXY,RZXy,RzXY,RzXy,RZxY,RZxy,RzxY,Rzxy,RXYZ,RXYz,RxYZ,RxYz,RXyZ,RXyz,RxyZ,Rxyz,RZYX,RZYx,RzYX,RzYx,RZyX,RZyx,RzyX,Rzyx,RXZY,RXZy,RxZY,RxZy,RXzY,RXzy,RxzY,Rxzy,RYZX,RYZx,RyZX,RyZx,RYzX,RYzx,RyzX,Ryzx,],
612        [RYXz,RYXZ,RyXz,RyXZ,RYxz,RYxZ,Ryxz,RyxZ,RZXy,RZXY,RzXy,RzXY,RZxy,RZxY,Rzxy,RzxY,RXYz,RXYZ,RxYz,RxYZ,RXyz,RXyZ,Rxyz,RxyZ,RZYx,RZYX,RzYx,RzYX,RZyx,RZyX,Rzyx,RzyX,RXZy,RXZY,RxZy,RxZY,RXzy,RXzY,Rxzy,RxzY,RYZx,RYZX,RyZx,RyZX,RYzx,RYzX,Ryzx,RyzX,],
613        [RYxZ,RYxz,RyxZ,Ryxz,RYXZ,RYXz,RyXZ,RyXz,RZxY,RZxy,RzxY,Rzxy,RZXY,RZXy,RzXY,RzXy,RXyZ,RXyz,RxyZ,Rxyz,RXYZ,RXYz,RxYZ,RxYz,RZyX,RZyx,RzyX,Rzyx,RZYX,RZYx,RzYX,RzYx,RXzY,RXzy,RxzY,Rxzy,RXZY,RXZy,RxZY,RxZy,RYzX,RYzx,RyzX,Ryzx,RYZX,RYZx,RyZX,RyZx,],
614        [RYxz,RYxZ,Ryxz,RyxZ,RYXz,RYXZ,RyXz,RyXZ,RZxy,RZxY,Rzxy,RzxY,RZXy,RZXY,RzXy,RzXY,RXyz,RXyZ,Rxyz,RxyZ,RXYz,RXYZ,RxYz,RxYZ,RZyx,RZyX,Rzyx,RzyX,RZYx,RZYX,RzYx,RzYX,RXzy,RXzY,Rxzy,RxzY,RXZy,RXZY,RxZy,RxZY,RYzx,RYzX,Ryzx,RyzX,RYZx,RYZX,RyZx,RyZX,],
615        [RyXZ,RyXz,RYXZ,RYXz,RyxZ,Ryxz,RYxZ,RYxz,RzXY,RzXy,RZXY,RZXy,RzxY,Rzxy,RZxY,RZxy,RxYZ,RxYz,RXYZ,RXYz,RxyZ,Rxyz,RXyZ,RXyz,RzYX,RzYx,RZYX,RZYx,RzyX,Rzyx,RZyX,RZyx,RxZY,RxZy,RXZY,RXZy,RxzY,Rxzy,RXzY,RXzy,RyZX,RyZx,RYZX,RYZx,RyzX,Ryzx,RYzX,RYzx,],
616        [RyXz,RyXZ,RYXz,RYXZ,Ryxz,RyxZ,RYxz,RYxZ,RzXy,RzXY,RZXy,RZXY,Rzxy,RzxY,RZxy,RZxY,RxYz,RxYZ,RXYz,RXYZ,Rxyz,RxyZ,RXyz,RXyZ,RzYx,RzYX,RZYx,RZYX,Rzyx,RzyX,RZyx,RZyX,RxZy,RxZY,RXZy,RXZY,Rxzy,RxzY,RXzy,RXzY,RyZx,RyZX,RYZx,RYZX,Ryzx,RyzX,RYzx,RYzX,],
617        [RyxZ,Ryxz,RYxZ,RYxz,RyXZ,RyXz,RYXZ,RYXz,RzxY,Rzxy,RZxY,RZxy,RzXY,RzXy,RZXY,RZXy,RxyZ,Rxyz,RXyZ,RXyz,RxYZ,RxYz,RXYZ,RXYz,RzyX,Rzyx,RZyX,RZyx,RzYX,RzYx,RZYX,RZYx,RxzY,Rxzy,RXzY,RXzy,RxZY,RxZy,RXZY,RXZy,RyzX,Ryzx,RYzX,RYzx,RyZX,RyZx,RYZX,RYZx,],
618        [Ryxz,RyxZ,RYxz,RYxZ,RyXz,RyXZ,RYXz,RYXZ,Rzxy,RzxY,RZxy,RZxY,RzXy,RzXY,RZXy,RZXY,Rxyz,RxyZ,RXyz,RXyZ,RxYz,RxYZ,RXYz,RXYZ,Rzyx,RzyX,RZyx,RZyX,RzYx,RzYX,RZYx,RZYX,Rxzy,RxzY,RXzy,RXzY,RxZy,RxZY,RXZy,RXZY,Ryzx,RyzX,RYzx,RYzX,RyZx,RyZX,RYZx,RYZX,],
619        [RYZX,RYzX,RyZX,RyzX,RYZx,RYzx,RyZx,Ryzx,RZYX,RZyX,RzYX,RzyX,RZYx,RZyx,RzYx,Rzyx,RXZY,RXzY,RxZY,RxzY,RXZy,RXzy,RxZy,Rxzy,RZXY,RZxY,RzXY,RzxY,RZXy,RZxy,RzXy,Rzxy,RXYZ,RXyZ,RxYZ,RxyZ,RXYz,RXyz,RxYz,Rxyz,RYXZ,RYxZ,RyXZ,RyxZ,RYXz,RYxz,RyXz,Ryxz,],
620        [RYZx,RYzx,RyZx,Ryzx,RYZX,RYzX,RyZX,RyzX,RZYx,RZyx,RzYx,Rzyx,RZYX,RZyX,RzYX,RzyX,RXZy,RXzy,RxZy,Rxzy,RXZY,RXzY,RxZY,RxzY,RZXy,RZxy,RzXy,Rzxy,RZXY,RZxY,RzXY,RzxY,RXYz,RXyz,RxYz,Rxyz,RXYZ,RXyZ,RxYZ,RxyZ,RYXz,RYxz,RyXz,Ryxz,RYXZ,RYxZ,RyXZ,RyxZ,],
621        [RYzX,RYZX,RyzX,RyZX,RYzx,RYZx,Ryzx,RyZx,RZyX,RZYX,RzyX,RzYX,RZyx,RZYx,Rzyx,RzYx,RXzY,RXZY,RxzY,RxZY,RXzy,RXZy,Rxzy,RxZy,RZxY,RZXY,RzxY,RzXY,RZxy,RZXy,Rzxy,RzXy,RXyZ,RXYZ,RxyZ,RxYZ,RXyz,RXYz,Rxyz,RxYz,RYxZ,RYXZ,RyxZ,RyXZ,RYxz,RYXz,Ryxz,RyXz,],
622        [RYzx,RYZx,Ryzx,RyZx,RYzX,RYZX,RyzX,RyZX,RZyx,RZYx,Rzyx,RzYx,RZyX,RZYX,RzyX,RzYX,RXzy,RXZy,Rxzy,RxZy,RXzY,RXZY,RxzY,RxZY,RZxy,RZXy,Rzxy,RzXy,RZxY,RZXY,RzxY,RzXY,RXyz,RXYz,Rxyz,RxYz,RXyZ,RXYZ,RxyZ,RxYZ,RYxz,RYXz,Ryxz,RyXz,RYxZ,RYXZ,RyxZ,RyXZ,],
623        [RyZX,RyzX,RYZX,RYzX,RyZx,Ryzx,RYZx,RYzx,RzYX,RzyX,RZYX,RZyX,RzYx,Rzyx,RZYx,RZyx,RxZY,RxzY,RXZY,RXzY,RxZy,Rxzy,RXZy,RXzy,RzXY,RzxY,RZXY,RZxY,RzXy,Rzxy,RZXy,RZxy,RxYZ,RxyZ,RXYZ,RXyZ,RxYz,Rxyz,RXYz,RXyz,RyXZ,RyxZ,RYXZ,RYxZ,RyXz,Ryxz,RYXz,RYxz,],
624        [RyZx,Ryzx,RYZx,RYzx,RyZX,RyzX,RYZX,RYzX,RzYx,Rzyx,RZYx,RZyx,RzYX,RzyX,RZYX,RZyX,RxZy,Rxzy,RXZy,RXzy,RxZY,RxzY,RXZY,RXzY,RzXy,Rzxy,RZXy,RZxy,RzXY,RzxY,RZXY,RZxY,RxYz,Rxyz,RXYz,RXyz,RxYZ,RxyZ,RXYZ,RXyZ,RyXz,Ryxz,RYXz,RYxz,RyXZ,RyxZ,RYXZ,RYxZ,],
625        [RyzX,RyZX,RYzX,RYZX,Ryzx,RyZx,RYzx,RYZx,RzyX,RzYX,RZyX,RZYX,Rzyx,RzYx,RZyx,RZYx,RxzY,RxZY,RXzY,RXZY,Rxzy,RxZy,RXzy,RXZy,RzxY,RzXY,RZxY,RZXY,Rzxy,RzXy,RZxy,RZXy,RxyZ,RxYZ,RXyZ,RXYZ,Rxyz,RxYz,RXyz,RXYz,RyxZ,RyXZ,RYxZ,RYXZ,Ryxz,RyXz,RYxz,RYXz,],
626        [Ryzx,RyZx,RYzx,RYZx,RyzX,RyZX,RYzX,RYZX,Rzyx,RzYx,RZyx,RZYx,RzyX,RzYX,RZyX,RZYX,Rxzy,RxZy,RXzy,RXZy,RxzY,RxZY,RXzY,RXZY,Rzxy,RzXy,RZxy,RZXy,RzxY,RzXY,RZxY,RZXY,Rxyz,RxYz,RXyz,RXYz,RxyZ,RxYZ,RXyZ,RXYZ,Ryxz,RyXz,RYxz,RYXz,RyxZ,RyXZ,RYxZ,RYXZ,],
627        [RZXY,RzXY,RZXy,RzXy,RZxY,RzxY,RZxy,Rzxy,RYXZ,RyXZ,RYXz,RyXz,RYxZ,RyxZ,RYxz,Ryxz,RZYX,RzYX,RZYx,RzYx,RZyX,RzyX,RZyx,Rzyx,RXYZ,RxYZ,RXYz,RxYz,RXyZ,RxyZ,RXyz,Rxyz,RYZX,RyZX,RYZx,RyZx,RYzX,RyzX,RYzx,Ryzx,RXZY,RxZY,RXZy,RxZy,RXzY,RxzY,RXzy,Rxzy,],
628        [RZXy,RzXy,RZXY,RzXY,RZxy,Rzxy,RZxY,RzxY,RYXz,RyXz,RYXZ,RyXZ,RYxz,Ryxz,RYxZ,RyxZ,RZYx,RzYx,RZYX,RzYX,RZyx,Rzyx,RZyX,RzyX,RXYz,RxYz,RXYZ,RxYZ,RXyz,Rxyz,RXyZ,RxyZ,RYZx,RyZx,RYZX,RyZX,RYzx,Ryzx,RYzX,RyzX,RXZy,RxZy,RXZY,RxZY,RXzy,Rxzy,RXzY,RxzY,],
629        [RZxY,RzxY,RZxy,Rzxy,RZXY,RzXY,RZXy,RzXy,RYxZ,RyxZ,RYxz,Ryxz,RYXZ,RyXZ,RYXz,RyXz,RZyX,RzyX,RZyx,Rzyx,RZYX,RzYX,RZYx,RzYx,RXyZ,RxyZ,RXyz,Rxyz,RXYZ,RxYZ,RXYz,RxYz,RYzX,RyzX,RYzx,Ryzx,RYZX,RyZX,RYZx,RyZx,RXzY,RxzY,RXzy,Rxzy,RXZY,RxZY,RXZy,RxZy,],
630        [RZxy,Rzxy,RZxY,RzxY,RZXy,RzXy,RZXY,RzXY,RYxz,Ryxz,RYxZ,RyxZ,RYXz,RyXz,RYXZ,RyXZ,RZyx,Rzyx,RZyX,RzyX,RZYx,RzYx,RZYX,RzYX,RXyz,Rxyz,RXyZ,RxyZ,RXYz,RxYz,RXYZ,RxYZ,RYzx,Ryzx,RYzX,RyzX,RYZx,RyZx,RYZX,RyZX,RXzy,Rxzy,RXzY,RxzY,RXZy,RxZy,RXZY,RxZY,],
631        [RzXY,RZXY,RzXy,RZXy,RzxY,RZxY,Rzxy,RZxy,RyXZ,RYXZ,RyXz,RYXz,RyxZ,RYxZ,Ryxz,RYxz,RzYX,RZYX,RzYx,RZYx,RzyX,RZyX,Rzyx,RZyx,RxYZ,RXYZ,RxYz,RXYz,RxyZ,RXyZ,Rxyz,RXyz,RyZX,RYZX,RyZx,RYZx,RyzX,RYzX,Ryzx,RYzx,RxZY,RXZY,RxZy,RXZy,RxzY,RXzY,Rxzy,RXzy,],
632        [RzXy,RZXy,RzXY,RZXY,Rzxy,RZxy,RzxY,RZxY,RyXz,RYXz,RyXZ,RYXZ,Ryxz,RYxz,RyxZ,RYxZ,RzYx,RZYx,RzYX,RZYX,Rzyx,RZyx,RzyX,RZyX,RxYz,RXYz,RxYZ,RXYZ,Rxyz,RXyz,RxyZ,RXyZ,RyZx,RYZx,RyZX,RYZX,Ryzx,RYzx,RyzX,RYzX,RxZy,RXZy,RxZY,RXZY,Rxzy,RXzy,RxzY,RXzY,],
633        [RzxY,RZxY,Rzxy,RZxy,RzXY,RZXY,RzXy,RZXy,RyxZ,RYxZ,Ryxz,RYxz,RyXZ,RYXZ,RyXz,RYXz,RzyX,RZyX,Rzyx,RZyx,RzYX,RZYX,RzYx,RZYx,RxyZ,RXyZ,Rxyz,RXyz,RxYZ,RXYZ,RxYz,RXYz,RyzX,RYzX,Ryzx,RYzx,RyZX,RYZX,RyZx,RYZx,RxzY,RXzY,Rxzy,RXzy,RxZY,RXZY,RxZy,RXZy,],
634        [Rzxy,RZxy,RzxY,RZxY,RzXy,RZXy,RzXY,RZXY,Ryxz,RYxz,RyxZ,RYxZ,RyXz,RYXz,RyXZ,RYXZ,Rzyx,RZyx,RzyX,RZyX,RzYx,RZYx,RzYX,RZYX,Rxyz,RXyz,RxyZ,RXyZ,RxYz,RXYz,RxYZ,RXYZ,Ryzx,RYzx,RyzX,RYzX,RyZx,RYZx,RyZX,RYZX,Rxzy,RXzy,RxzY,RXzY,RxZy,RXZy,RxZY,RXZY,],
635        [RZYX,RzYX,RZyX,RzyX,RZYx,RzYx,RZyx,Rzyx,RYZX,RyZX,RYzX,RyzX,RYZx,RyZx,RYzx,Ryzx,RZXY,RzXY,RZxY,RzxY,RZXy,RzXy,RZxy,Rzxy,RXZY,RxZY,RXzY,RxzY,RXZy,RxZy,RXzy,Rxzy,RYXZ,RyXZ,RYxZ,RyxZ,RYXz,RyXz,RYxz,Ryxz,RXYZ,RxYZ,RXyZ,RxyZ,RXYz,RxYz,RXyz,Rxyz,],
636        [RZYx,RzYx,RZyx,Rzyx,RZYX,RzYX,RZyX,RzyX,RYZx,RyZx,RYzx,Ryzx,RYZX,RyZX,RYzX,RyzX,RZXy,RzXy,RZxy,Rzxy,RZXY,RzXY,RZxY,RzxY,RXZy,RxZy,RXzy,Rxzy,RXZY,RxZY,RXzY,RxzY,RYXz,RyXz,RYxz,Ryxz,RYXZ,RyXZ,RYxZ,RyxZ,RXYz,RxYz,RXyz,Rxyz,RXYZ,RxYZ,RXyZ,RxyZ,],
637        [RZyX,RzyX,RZYX,RzYX,RZyx,Rzyx,RZYx,RzYx,RYzX,RyzX,RYZX,RyZX,RYzx,Ryzx,RYZx,RyZx,RZxY,RzxY,RZXY,RzXY,RZxy,Rzxy,RZXy,RzXy,RXzY,RxzY,RXZY,RxZY,RXzy,Rxzy,RXZy,RxZy,RYxZ,RyxZ,RYXZ,RyXZ,RYxz,Ryxz,RYXz,RyXz,RXyZ,RxyZ,RXYZ,RxYZ,RXyz,Rxyz,RXYz,RxYz,],
638        [RZyx,Rzyx,RZYx,RzYx,RZyX,RzyX,RZYX,RzYX,RYzx,Ryzx,RYZx,RyZx,RYzX,RyzX,RYZX,RyZX,RZxy,Rzxy,RZXy,RzXy,RZxY,RzxY,RZXY,RzXY,RXzy,Rxzy,RXZy,RxZy,RXzY,RxzY,RXZY,RxZY,RYxz,Ryxz,RYXz,RyXz,RYxZ,RyxZ,RYXZ,RyXZ,RXyz,Rxyz,RXYz,RxYz,RXyZ,RxyZ,RXYZ,RxYZ,],
639        [RzYX,RZYX,RzyX,RZyX,RzYx,RZYx,Rzyx,RZyx,RyZX,RYZX,RyzX,RYzX,RyZx,RYZx,Ryzx,RYzx,RzXY,RZXY,RzxY,RZxY,RzXy,RZXy,Rzxy,RZxy,RxZY,RXZY,RxzY,RXzY,RxZy,RXZy,Rxzy,RXzy,RyXZ,RYXZ,RyxZ,RYxZ,RyXz,RYXz,Ryxz,RYxz,RxYZ,RXYZ,RxyZ,RXyZ,RxYz,RXYz,Rxyz,RXyz,],
640        [RzYx,RZYx,Rzyx,RZyx,RzYX,RZYX,RzyX,RZyX,RyZx,RYZx,Ryzx,RYzx,RyZX,RYZX,RyzX,RYzX,RzXy,RZXy,Rzxy,RZxy,RzXY,RZXY,RzxY,RZxY,RxZy,RXZy,Rxzy,RXzy,RxZY,RXZY,RxzY,RXzY,RyXz,RYXz,Ryxz,RYxz,RyXZ,RYXZ,RyxZ,RYxZ,RxYz,RXYz,Rxyz,RXyz,RxYZ,RXYZ,RxyZ,RXyZ,],
641        [RzyX,RZyX,RzYX,RZYX,Rzyx,RZyx,RzYx,RZYx,RyzX,RYzX,RyZX,RYZX,Ryzx,RYzx,RyZx,RYZx,RzxY,RZxY,RzXY,RZXY,Rzxy,RZxy,RzXy,RZXy,RxzY,RXzY,RxZY,RXZY,Rxzy,RXzy,RxZy,RXZy,RyxZ,RYxZ,RyXZ,RYXZ,Ryxz,RYxz,RyXz,RYXz,RxyZ,RXyZ,RxYZ,RXYZ,Rxyz,RXyz,RxYz,RXYz,],
642        [Rzyx,RZyx,RzYx,RZYx,RzyX,RZyX,RzYX,RZYX,Ryzx,RYzx,RyZx,RYZx,RyzX,RYzX,RyZX,RYZX,Rzxy,RZxy,RzXy,RZXy,RzxY,RZxY,RzXY,RZXY,Rxzy,RXzy,RxZy,RXZy,RxzY,RXzY,RxZY,RXZY,Ryxz,RYxz,RyXz,RYXz,RyxZ,RYxZ,RyXZ,RYXZ,Rxyz,RXyz,RxYz,RXYz,RxyZ,RXyZ,RxYZ,RXYZ,],
643    ]
644};
645
646#[cfg(test)]
647mod tests {
648    use super::*;
649    use crate::math::{FaceMap, GridPoint};
650    use crate::util::MultiFailure;
651    use Face6::*;
652    use num_traits::One;
653    use std::collections::HashSet;
654
655    #[test]
656    fn identity() {
657        assert_eq!(GridRotation::IDENTITY, GridRotation::one());
658        assert_eq!(GridRotation::IDENTITY, GridRotation::default());
659        assert_eq!(
660            GridRotation::IDENTITY,
661            GridRotation::from_basis([PX, PY, PZ])
662        );
663    }
664
665    #[test]
666    fn inverse_axioms() {
667        assert_eq!(GridRotation::IDENTITY.inverse(), GridRotation::IDENTITY);
668        for rot in GridRotation::ALL {
669            assert_eq!(rot * rot.inverse(), GridRotation::IDENTITY, "{rot:?}");
670            assert_eq!(rot.inverse().inverse(), rot, "{rot:?}");
671        }
672    }
673
674    #[test]
675    fn inverse_effect() {
676        let v = GridVector::new(1, 5, 100);
677        for rot in GridRotation::ALL {
678            assert_eq!(
679                rot.transform_vector(rot.inverse().transform_vector(v)),
680                v,
681                "{rot:?}"
682            );
683            assert_eq!(
684                rot.inverse().transform_vector(rot.transform_vector(v)),
685                v,
686                "{rot:?}"
687            );
688        }
689    }
690
691    /// We can compute the inverse via `iterate()`.
692    /// This test also serves to regenerate the inverse table.
693    #[test]
694    fn inverse_from_iterate() {
695        let mut ok = true;
696        for rot in GridRotation::ALL {
697            let iter_inv = rot.iterate().last().unwrap();
698            let inv = rot.inverse();
699            println!("{rot:?} => {iter_inv:?}, // {inv:?}");
700            ok = ok && iter_inv == inv;
701        }
702        assert!(ok);
703    }
704
705    /// Check that the effect of composing two rotations with the `*` operator is equal to
706    /// the effect of applying both.
707    #[test]
708    fn composition_consistency() {
709        let mut f = MultiFailure::new();
710        for first in GridRotation::ALL {
711            for second in GridRotation::ALL {
712                f.catch(|| {
713                    let composed = second * first;
714                    assert_eq!(
715                        FaceMap::from_fn(|face| { composed.transform(face) }),
716                        FaceMap::from_fn(|face| { second.transform(first.transform(face)) }),
717                        "{second:?} * {first:?}",
718                    );
719                });
720            }
721        }
722    }
723
724    #[test]
725    fn is_reflection_consistency() {
726        for a in GridRotation::ALL {
727            for b in GridRotation::ALL {
728                assert_eq!(
729                    a.is_reflection() ^ b.is_reflection(),
730                    (a * b).is_reflection(),
731                    "{a:?}, {b:?}",
732                );
733            }
734        }
735    }
736
737    /// Test that `GridRotation::ALL` is complete.
738    /// TODO: Also test numbering/ordering properties when that is stable.
739    #[test]
740    fn enumeration() {
741        let mut set = HashSet::new();
742        for rot in GridRotation::ALL {
743            set.insert(rot);
744        }
745        assert_eq!(set.len(), GridRotation::ALL.len());
746        assert_eq!(48, GridRotation::ALL.len());
747    }
748
749    /// Test that `GridRotation::ALL_BUT_REFLECTIONS` is complete.
750    #[test]
751    fn all_but_reflections() {
752        let mut set = HashSet::new();
753        for rot in GridRotation::ALL_BUT_REFLECTIONS {
754            assert!(!rot.is_reflection(), "{rot:?} is a reflection");
755            set.insert(rot);
756        }
757        assert_eq!(set.len(), GridRotation::ALL_BUT_REFLECTIONS.len());
758        // Half of all possible axis transformations have no reflection
759        assert_eq!(
760            GridRotation::ALL.len(),
761            GridRotation::ALL_BUT_REFLECTIONS.len() * 2,
762        );
763    }
764
765    /// Test that `transform_vector()` and `to_rotation_matrix()` do the same thing.
766    #[test]
767    fn equivalent_rotation_matrix() {
768        for rot in GridRotation::ALL {
769            let point = GridPoint::new(1, 20, 300);
770            assert_eq!(
771                rot.transform_vector(point.to_vector()).to_point(),
772                rot.to_rotation_matrix().transform_point(point),
773            );
774        }
775    }
776
777    #[test]
778    fn equivalent_transform_vector_transform_size() {
779        for rot in GridRotation::ALL {
780            let vector = GridVector::new(1, 20, 300);
781            assert_eq!(
782                rot.transform_vector(vector).abs().to_u32(),
783                rot.transform_size(GridSize::from(vector.to_u32())).to_vector()
784            )
785        }
786    }
787
788    /// The set of possible inputs is small enough to test its properties exhaustively
789    #[test]
790    fn from_to_exhaustive() {
791        let mut f = MultiFailure::new();
792        for from_face in Face6::ALL {
793            for to_face in Face6::ALL {
794                for up_face in Face6::ALL {
795                    f.catch(|| {
796                        let result = GridRotation::from_to(from_face, to_face, up_face);
797                        let info = (from_face, to_face, up_face, result);
798                        match result {
799                            Some(result) => {
800                                assert!(!result.is_reflection());
801                                assert_eq!(
802                                    result.transform(from_face),
803                                    to_face,
804                                    "wrong from-to: {info:?}"
805                                );
806                                assert_eq!(
807                                    result.transform(up_face),
808                                    up_face,
809                                    "did not preserve up vector: {info:?}"
810                                );
811                            }
812                            None => {
813                                assert!(
814                                    up_face.axis() == from_face.axis()
815                                        || up_face.axis() == to_face.axis(),
816                                    "returned None incorrectly: {info:?}"
817                                );
818                            }
819                        }
820                    });
821                }
822            }
823        }
824    }
825
826    #[test]
827    fn regenerate_multiplication_table() {
828        let mut failed = false;
829        println!(indoc::indoc! {
830            "
831            #[rustfmt::skip]
832            static MULTIPLICATION_TABLE: [[GridRotation; 48]; 48] = {{
833                use GridRotation::*;
834                ["
835        });
836        for first in GridRotation::ALL {
837            print!("        [");
838            for second in GridRotation::ALL {
839                // Calculate the multiplication via transformation of basis vectors.
840                let result =
841                    GridRotation::from_basis(first.to_basis().map(|v| second.transform(v)));
842
843                print!("{result:?},");
844                // Check whether the current implementation is correct.
845                if result != second * first {
846                    failed = true;
847                }
848            }
849            println!("],");
850        }
851        println!("    ]\n}};");
852        if failed {
853            panic!("multiplication results were not as expected");
854        }
855    }
856}