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