1use core::cmp::Ordering;
5use core::ops;
6
7use euclid::Vector3D;
8use num_traits::One;
9
10#[cfg(not(feature = "std"))]
12#[allow(unused_imports)]
13use num_traits::float::FloatCore as _;
14
15use crate::math::{
16 Axis, Cube, Face6, Face7, FreeCoordinate, GridCoordinate, GridPoint, GridRotation, GridVector,
17 Gridgid,
18};
19
20#[expect(clippy::exhaustive_structs)]
25#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
26pub struct GridMatrix {
27 pub x: GridVector,
29 pub y: GridVector,
31 pub z: GridVector,
33 pub w: GridVector,
35}
36
37impl GridMatrix {
38 pub const ZERO: Self = Self {
40 x: Vector3D::new(0, 0, 0),
41 y: Vector3D::new(0, 0, 0),
42 z: Vector3D::new(0, 0, 0),
43 w: Vector3D::new(0, 0, 0),
44 };
45
46 #[doc(hidden)] pub const FLIP_Y: Self = Self {
49 x: Vector3D::new(1, 0, 0),
50 y: Vector3D::new(0, -1, 0),
51 z: Vector3D::new(0, 0, 1),
52 w: Vector3D::new(0, 0, 0),
53 };
54
55 #[expect(clippy::too_many_arguments)]
58 #[inline]
59 pub const fn new(
60 x0: GridCoordinate,
61 x1: GridCoordinate,
62 x2: GridCoordinate,
63 y0: GridCoordinate,
64 y1: GridCoordinate,
65 y2: GridCoordinate,
66 z0: GridCoordinate,
67 z1: GridCoordinate,
68 z2: GridCoordinate,
69 w0: GridCoordinate,
70 w1: GridCoordinate,
71 w2: GridCoordinate,
72 ) -> Self {
73 Self {
74 x: Vector3D::new(x0, x1, x2),
75 y: Vector3D::new(y0, y1, y2),
76 z: Vector3D::new(z0, z1, z2),
77 w: Vector3D::new(w0, w1, w2),
78 }
79 }
80
81 #[inline]
83 pub fn from_translation(offset: impl Into<GridVector>) -> Self {
84 Self {
85 w: offset.into(),
86 ..Self::one()
87 }
88 }
89
90 #[inline]
95 pub fn from_scale(scale: GridCoordinate) -> Self {
96 Self {
97 x: Vector3D::new(scale, 0, 0),
98 y: Vector3D::new(0, scale, 0),
99 z: Vector3D::new(0, 0, scale),
100 w: Vector3D::new(0, 0, 0),
101 }
102 }
103
104 #[inline]
120 pub fn from_origin(origin: impl Into<GridPoint>, x: Face7, y: Face7, z: Face7) -> Self {
121 Self {
122 x: x.normal_vector(),
123 y: y.normal_vector(),
124 z: z.normal_vector(),
125 w: origin.into().to_vector(),
126 }
127 }
128
129 #[inline]
131 #[rustfmt::skip]
132 pub fn to_free(self) -> euclid::Transform3D<FreeCoordinate, Cube, Cube> {
133 euclid::Transform3D::new(
134 self.x.x, self.x.y, self.x.z, 0,
135 self.y.x, self.y.y, self.y.z, 0,
136 self.z.x, self.z.y, self.z.z, 0,
137 self.w.x, self.w.y, self.w.z, 1,
138 ).cast()
139 }
140
141 #[inline(always)]
143 fn row(&self, r: Axis) -> MVector4<GridCoordinate> {
144 MVector4::new(self.x[r], self.y[r], self.z[r], self.w[r])
145 }
146
147 #[inline]
172 pub fn transform_cube(&self, cube: Cube) -> Cube {
173 Cube::from(
174 self.transform_point(cube.lower_bounds())
175 .min(self.transform_point(cube.upper_bounds())),
176 )
177 }
178
179 #[allow(clippy::missing_inline_in_public_items)]
200 pub fn decompose(self) -> Option<Gridgid> {
201 Some(Gridgid {
202 rotation: GridRotation::from_basis([
203 Face6::try_from(self.x).ok()?,
204 Face6::try_from(self.y).ok()?,
205 Face6::try_from(self.z).ok()?,
206 ]),
207 translation: self.w,
208 })
209 }
210
211 #[inline]
214 #[must_use]
215 pub fn transform_vector(&self, vec: GridVector) -> GridVector {
216 GridVector::new(
217 self.row(Axis::X).truncate().dot(vec),
218 self.row(Axis::Y).truncate().dot(vec),
219 self.row(Axis::Z).truncate().dot(vec),
220 )
221 }
222
223 #[inline]
225 #[must_use]
226 pub fn transform_point(&self, point: GridPoint) -> GridPoint {
227 let homogeneous = MVector4::homogeneous(point.to_vector());
228 GridPoint::new(
229 self.row(Axis::X).dot(homogeneous),
230 self.row(Axis::Y).dot(homogeneous),
231 self.row(Axis::Z).dot(homogeneous),
232 )
233 }
234
235 #[must_use]
254 #[inline]
255 pub fn concat(&self, other: &Self) -> Self {
256 GridMatrix {
257 x: self.transform_vector(other.x),
258 y: self.transform_vector(other.y),
259 z: self.transform_vector(other.z),
260 w: self.transform_point(other.w.to_point()).to_vector(),
263 }
264 }
265
266 #[allow(clippy::missing_inline_in_public_items)]
268 pub fn inverse_transform(&self) -> Option<Self> {
269 const INVERSE_EPSILON: FreeCoordinate = 0.25 / (GridCoordinate::MAX as FreeCoordinate);
272 fn try_round(v: [FreeCoordinate; 4], expected_w: FreeCoordinate) -> Option<GridVector> {
273 let mut result = Vector3D::zero();
274 #[expect(clippy::needless_range_loop)]
275 for axis in 0..4 {
276 let rounded = v[axis].round();
277 let remainder = v[axis] - rounded;
278 if remainder.abs().partial_cmp(&INVERSE_EPSILON) != Some(Ordering::Less) {
279 return None;
281 }
282 match axis {
283 0 => result.x = rounded as GridCoordinate,
285 1 => result.y = rounded as GridCoordinate,
286 2 => result.z = rounded as GridCoordinate,
287 3 =>
288 {
289 #[expect(clippy::float_cmp)]
290 if rounded != expected_w {
291 return None;
292 }
293 }
294 _ => unreachable!(),
295 }
296 }
297 Some(result)
298 }
299
300 let fi = self.to_free().inverse()?.to_arrays();
301 Some(GridMatrix {
302 x: try_round(fi[0], 0.0)?,
303 y: try_round(fi[1], 0.0)?,
304 z: try_round(fi[2], 0.0)?,
305 w: try_round(fi[3], 1.0)?,
306 })
307 }
308}
309
310impl ops::Mul<Self> for GridMatrix {
311 type Output = Self;
312 #[inline]
313 fn mul(self, rhs: Self) -> Self::Output {
314 self.concat(&rhs)
316 }
317}
318
319impl One for GridMatrix {
320 #[inline]
321 #[rustfmt::skip]
322 fn one() -> Self {
323 Self::new(
324 1, 0, 0,
325 0, 1, 0,
326 0, 0, 1,
327 0, 0, 0,
328 )
329 }
330}
331
332#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
334struct MVector4<T> {
335 x: T,
336 y: T,
337 z: T,
338 w: T,
339}
340
341impl<T: Copy> MVector4<T> {
342 fn new(x: T, y: T, z: T, w: T) -> Self {
343 Self { x, y, z, w }
344 }
345
346 fn homogeneous<U>(v: Vector3D<T, U>) -> MVector4<T>
347 where
348 T: One,
349 {
350 Self {
351 x: v.x,
352 y: v.y,
353 z: v.z,
354 w: T::one(),
355 }
356 }
357
358 fn dot(self, other: Self) -> T
359 where
360 T: ops::Mul<Output = T>,
361 T: ops::Add<Output = T>,
362 {
363 self.x * other.x + self.y * other.y + self.z * other.z + self.w * other.w
364 }
365
366 fn truncate<U>(self) -> Vector3D<T, U> {
367 Vector3D::new(self.x, self.y, self.z)
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374 use euclid::{Transform3D, point3, vec3};
375 use rand::{Rng, SeedableRng as _};
376 use rand_xoshiro::Xoshiro256Plus;
377
378 fn random_grid_matrix(mut rng: impl Rng) -> GridMatrix {
379 let mut r = || rng.random_range(-100..=100);
380 GridMatrix::new(r(), r(), r(), r(), r(), r(), r(), r(), r(), r(), r(), r())
381 }
382
383 fn random_possibly_invertible_matrix(mut rng: impl Rng) -> GridMatrix {
384 let mut r = |n: GridCoordinate| rng.random_range(-n..=n);
385 GridMatrix::new(
386 r(1),
387 r(1),
388 r(1),
389 r(1),
390 r(1),
391 r(1),
392 r(1),
393 r(1),
394 r(1),
395 r(GridCoordinate::MAX / 10),
396 r(GridCoordinate::MAX / 10),
397 r(GridCoordinate::MAX / 10),
398 )
399 }
400
401 #[test]
402 #[rustfmt::skip]
403 fn equivalent_constructor() {
404 let m = GridMatrix::new(
405 1, 2, 3,
406 5, 6, 7,
407 9, 10, 11,
408 13, 14, 15,
409 );
410 assert_eq!(m.to_free(), Transform3D::new(
411 1., 2., 3., 0.,
412 5., 6., 7., 0.,
413 9., 10., 11., 0.,
414 13., 14., 15., 1.,
415 ));
416 }
417
418 #[test]
419 fn equivalent_transform() {
420 let mut rng = Xoshiro256Plus::seed_from_u64(2897358920346590823);
421 for _ in 1..100 {
422 let m = random_grid_matrix(&mut rng);
423 dbg!(m, m.to_free());
424 assert_eq!(
425 m.transform_point(GridPoint::new(2, 300, 40000)).map(FreeCoordinate::from),
426 m.to_free().transform_point3d(point3(2., 300., 40000.)).unwrap(),
427 );
428 assert_eq!(
429 m.transform_vector(GridVector::new(10, 20, 30)).map(FreeCoordinate::from),
430 m.to_free().transform_vector3d(vec3(10., 20., 30.)),
431 );
432 }
433 }
434
435 #[test]
436 fn equivalent_concat() {
437 let mut rng = Xoshiro256Plus::seed_from_u64(5933089223468901296);
438 for _ in 1..100 {
439 let m1 = random_grid_matrix(&mut rng);
440 let m2 = random_grid_matrix(&mut rng);
441 assert_eq!(m1.concat(&m2).to_free(), m2.to_free().then(&m1.to_free()));
443 }
444 }
445
446 #[test]
447 fn equivalent_inverse() {
448 let mut rng = Xoshiro256Plus::seed_from_u64(0xca9bd0d289b4700e);
449 let mut nontrivial = 0;
450 for _ in 1..500 {
451 let m = random_possibly_invertible_matrix(&mut rng);
452 let inv = m.inverse_transform();
453 if let Some(inv) = inv {
454 assert_eq!(
455 Some(inv.to_free()),
456 m.to_free().inverse(),
457 "inverse of {m:?}",
458 );
459 nontrivial += 1;
460 }
461 }
462 assert!(nontrivial > 100, "got {nontrivial} inverses");
463 }
464}