all_is_cubes_base/math/
rigid.rs1use 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#[expect(clippy::exhaustive_structs)]
21#[derive(Clone, Copy, Eq, Hash, PartialEq)]
22pub struct Gridgid {
23 pub rotation: GridRotation,
25 pub translation: GridVector,
27}
28
29impl Gridgid {
30 pub const IDENTITY: Self = Self {
32 rotation: GridRotation::IDENTITY,
33 translation: GridVector::new(0, 0, 0),
34 };
35
36 #[doc(hidden)] pub const FLIP_Y: Self = Self {
39 rotation: GridRotation::RXyZ,
40 translation: GridVector::new(0, 0, 0),
41 };
42
43 #[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 #[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 #[inline]
68 pub fn to_matrix(self) -> GridMatrix {
69 GridMatrix::from_translation(self.translation) * self.rotation.to_rotation_matrix()
70 }
71
72 #[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 #[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 #[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 #[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 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}