all_is_cubes_base/math/
rigid.rs

1use core::fmt;
2
3use euclid::Vector3D;
4use manyfmt::Refmt;
5
6#[cfg(doc)]
7use crate::math::GridAab;
8use crate::math::{Cube, GridCoordinate, GridMatrix, GridPoint, GridRotation, GridVector};
9
10/// A [rigid transformation] that is composed of a [`GridRotation`] followed by an
11/// integer-valued translation.
12///
13/// That is, mathematically, this may represent any transformation from ℤ³ to ℤ³, that
14/// preserves distances between transformed points.
15/// As [`GridRotation`] includes reflections, so too does this.
16///
17/// These transformations are always invertible except in the case of numeric overflow.
18///
19/// [rigid transformation]: https://en.wikipedia.org/wiki/Rigid_transformation
20#[expect(clippy::exhaustive_structs)]
21#[derive(Clone, Copy, Eq, Hash, PartialEq)]
22pub struct Gridgid {
23    /// Rotation component. Applied before the translation.
24    pub rotation: GridRotation,
25    /// Translation component. Applied after the rotation.
26    pub translation: GridVector,
27}
28
29impl Gridgid {
30    /// The identity transform, which leaves points unchanged.
31    pub const IDENTITY: Self = Self {
32        rotation: GridRotation::IDENTITY,
33        translation: GridVector::new(0, 0, 0),
34    };
35
36    /// For Y-down drawing
37    #[doc(hidden)] // used by all-is-cubes-content - TODO: public?
38    pub const FLIP_Y: Self = Self {
39        rotation: GridRotation::RXyZ,
40        translation: GridVector::new(0, 0, 0),
41    };
42
43    /// Constructs a [`Gridgid`] that only performs rotation.
44    ///
45    /// Note that this is a rotation about the origin _point_ `[0, 0, 0]`, not the _cube_
46    /// that is identified by that point (that is, not the center of [`GridAab::ORIGIN_CUBE`]).
47    ///
48    /// For more general rotations about a center, see [`GridRotation::to_positive_octant_transform()`].
49    #[inline]
50    pub const fn from_rotation_about_origin(rotation: GridRotation) -> Self {
51        Self {
52            rotation,
53            translation: GridVector::new(0, 0, 0),
54        }
55    }
56
57    /// Constructs a [`Gridgid`] that only performs translation.
58    #[inline]
59    pub fn from_translation(translation: impl Into<GridVector>) -> Self {
60        Self {
61            rotation: GridRotation::IDENTITY,
62            translation: translation.into(),
63        }
64    }
65
66    /// Returns the equivalent matrix.
67    #[inline]
68    pub fn to_matrix(self) -> GridMatrix {
69        GridMatrix::from_translation(self.translation) * self.rotation.to_rotation_matrix()
70    }
71
72    /// Applies this transform to the given point.
73    ///
74    /// Note that a point is not a unit cube; if the point identifies a cube then use
75    /// [`Gridgid::transform_cube()`] instead.
76    #[inline]
77    #[track_caller]
78    pub fn transform_point(self, point: GridPoint) -> GridPoint {
79        self.checked_transform_point(point).expect("transformed point overflowed")
80    }
81
82    /// Applies this transform to the given point.
83    /// Returns [`None`] if the resulting point is out of bounds.
84    ///
85    /// Note that a point is not a unit cube; if the point identifies a cube then use
86    /// [`Gridgid::transform_cube()`] instead.
87    #[inline]
88    pub fn checked_transform_point(self, point: GridPoint) -> Option<GridPoint> {
89        Some(
90            transpose_vector_option(
91                self.rotation
92                    .checked_transform_vector(point.to_vector())?
93                    .zip(self.translation, GridCoordinate::checked_add),
94            )?
95            .to_point(),
96        )
97    }
98
99    /// Equivalent to temporarily applying an offset of `[0.5, 0.5, 0.5]` while
100    /// transforming `cube`'s coordinates as per [`Gridgid::transform_point()`], despite
101    /// the fact that integer arithmetic is being used.
102    ///
103    /// This operation thus transforms the [`Cube`] considered as a solid object
104    /// the same as a [`GridAab::single_cube`] containing that cube.
105    ///
106    /// ```
107    /// # extern crate all_is_cubes_base as all_is_cubes;
108    /// use all_is_cubes::math::{Cube, Gridgid, GridPoint, GridRotation, GridVector};
109    ///
110    /// // Translation without rotation has the usual definition.
111    /// let t = Gridgid::from_translation([10, 0, 0]);
112    /// assert_eq!(t.transform_cube(Cube::new(1, 1, 1)), Cube::new(11, 1, 1));
113    ///
114    /// // With a rotation or reflection, the results are different.
115    /// // TODO: Come up with a better example and explanation.
116    /// let reflected = Gridgid {
117    ///     translation: GridVector::new(10, 0, 0),
118    ///     rotation: GridRotation::RxYZ,
119    /// };
120    /// assert_eq!(reflected.transform_point(GridPoint::new(1, 5, 5)), GridPoint::new(9, 5, 5));
121    /// assert_eq!(reflected.transform_cube(Cube::new(1, 5, 5)), Cube::new(8, 5, 5));
122    /// ```
123    ///
124    /// [`GridAab::single_cube`]: crate::math::GridAab::single_cube
125    #[inline]
126    pub fn transform_cube(&self, cube: Cube) -> Cube {
127        Cube::from(
128            self.transform_point(cube.lower_bounds())
129                .min(self.transform_point(cube.upper_bounds())),
130        )
131    }
132
133    /// Returns the transform which maps the outputs of this one to the inputs of this one.
134    ///
135    /// May panic or wrap (as per the Rust `overflow-checks` compilation option)
136    /// if `self.translation` has any components equal to [`GridCoordinate::MIN`].
137    #[must_use]
138    #[inline]
139    pub fn inverse(self) -> Self {
140        let rotation = self.rotation.inverse();
141        Self {
142            rotation,
143            translation: rotation.transform_vector(-self.translation),
144        }
145    }
146}
147
148impl core::ops::Mul for Gridgid {
149    type Output = Self;
150    #[inline]
151    fn mul(self, rhs: Self) -> Self::Output {
152        Self {
153            // TODO: test this
154            rotation: self.rotation * rhs.rotation,
155            translation: self.transform_point(rhs.translation.to_point()).to_vector(),
156        }
157    }
158}
159
160impl From<GridRotation> for Gridgid {
161    #[inline]
162    fn from(value: GridRotation) -> Self {
163        Self::from_rotation_about_origin(value)
164    }
165}
166
167impl From<Gridgid> for GridMatrix {
168    #[inline]
169    fn from(value: Gridgid) -> Self {
170        value.to_matrix()
171    }
172}
173
174impl fmt::Debug for Gridgid {
175    #[inline(never)]
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        if *self == Self::IDENTITY {
178            f.pad("Gridgid::IDENTITY")
179        } else {
180            let &Self {
181                rotation,
182                translation,
183            } = self;
184
185            let mut ds = f.debug_struct("Gridgid");
186            if rotation != GridRotation::IDENTITY {
187                ds.field("rotation", &rotation);
188            }
189            if translation != GridVector::zero() {
190                ds.field(
191                    "translation",
192                    &translation.refmt(&crate::util::ConciseDebug),
193                );
194            }
195            ds.finish()
196        }
197    }
198}
199
200fn transpose_vector_option<T, U>(v: Vector3D<Option<T>, U>) -> Option<Vector3D<T, U>> {
201    Some(Vector3D::new(v.x?, v.y?, v.z?))
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use rand::SeedableRng as _;
208    use rand::seq::IndexedRandom as _;
209    use rand_xoshiro::Xoshiro256Plus;
210
211    fn random_gridgid(mut rng: impl rand::Rng) -> Gridgid {
212        Gridgid {
213            rotation: *GridRotation::ALL.choose(&mut rng).unwrap(),
214            translation: {
215                let mut r = || rng.random_range(-100..=100);
216                GridVector::new(r(), r(), r())
217            },
218        }
219    }
220
221    #[test]
222    fn equivalent_transform() {
223        let mut rng = Xoshiro256Plus::seed_from_u64(2897358920346590823);
224        for _ in 1..100 {
225            let m = random_gridgid(&mut rng);
226            dbg!(m, m.to_matrix());
227            assert_eq!(
228                m.transform_point(GridPoint::new(2, 300, 40000)),
229                m.to_matrix().transform_point(GridPoint::new(2, 300, 40000)),
230            );
231        }
232    }
233
234    #[test]
235    fn equivalent_concat() {
236        let mut rng = Xoshiro256Plus::seed_from_u64(5933089223468901296);
237        for _ in 1..100 {
238            let t1 = random_gridgid(&mut rng);
239            let t2 = random_gridgid(&mut rng);
240            assert_eq!((t1 * t2).to_matrix(), t1.to_matrix() * t2.to_matrix());
241        }
242    }
243
244    #[test]
245    fn equivalent_inverse() {
246        let mut rng = Xoshiro256Plus::seed_from_u64(5933089223468901296);
247        for _ in 1..100 {
248            let t = random_gridgid(&mut rng);
249            assert_eq!(
250                t.inverse().to_matrix(),
251                t.to_matrix().inverse_transform().unwrap(),
252            );
253        }
254    }
255}