Skip to main content

steel_utils/
rotation.rs

1//! Vanilla's `Rotation` — horizontal rotations around the Y axis.
2
3use glam::IVec3;
4
5use crate::Direction;
6use crate::geometry::BoundingBox;
7use crate::random::Random;
8
9/// Horizontal rotation around the Y axis.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Rotation {
12    /// 0°.
13    None,
14    /// 90° clockwise.
15    Clockwise90,
16    /// 180°.
17    Clockwise180,
18    /// 270° clockwise (= 90° counter-clockwise).
19    CounterClockwise90,
20}
21
22const ALL_ROTATIONS: [Rotation; 4] = [
23    Rotation::None,
24    Rotation::Clockwise90,
25    Rotation::Clockwise180,
26    Rotation::CounterClockwise90,
27];
28
29impl Rotation {
30    /// Matches vanilla's `Rotation.getRandom(random)`.
31    #[must_use]
32    pub fn get_random(rng: &mut impl Random) -> Self {
33        ALL_ROTATIONS[rng.next_i32_bounded(4) as usize]
34    }
35
36    /// Matches vanilla's `Util.shuffledCopy(values(), random)` (reverse Fisher-Yates).
37    #[must_use]
38    pub fn get_shuffled(rng: &mut impl Random) -> [Rotation; 4] {
39        let mut rotations = ALL_ROTATIONS;
40        for i in (1..4).rev() {
41            let j = rng.next_i32_bounded((i + 1) as i32) as usize;
42            rotations.swap(i, j);
43        }
44        rotations
45    }
46
47    /// Vertical directions (Up/Down) are unchanged.
48    #[must_use]
49    pub const fn rotate(self, dir: Direction) -> Direction {
50        match self {
51            Self::None => dir,
52            Self::Clockwise90 => dir.rotate_y_clockwise(),
53            Self::Clockwise180 => dir.rotate_y_clockwise().rotate_y_clockwise(),
54            Self::CounterClockwise90 => dir.rotate_y_counter_clockwise(),
55        }
56    }
57
58    /// `self.then(other)` = apply self first, then other.
59    #[must_use]
60    pub const fn then(self, other: Self) -> Self {
61        ALL_ROTATIONS[((self as u8 + other as u8) % 4) as usize]
62    }
63
64    /// Matches vanilla's `StructureTemplate.transform(pos, Mirror.NONE, rotation, pivot)`.
65    ///
66    /// `pivot.y` is ignored (only the XZ plane matters).
67    #[must_use]
68    pub const fn transform_pos(self, pos: IVec3, pivot: IVec3) -> IVec3 {
69        let (x, y, z) = (pos.x, pos.y, pos.z);
70        let (px, pz) = (pivot.x, pivot.z);
71        match self {
72            Self::None => IVec3::new(x, y, z),
73            Self::Clockwise90 => IVec3::new(px + pz - z, y, pz - px + x),
74            Self::Clockwise180 => IVec3::new(px + px - x, y, pz + pz - z),
75            Self::CounterClockwise90 => IVec3::new(px - pz + z, y, px + pz - x),
76        }
77    }
78
79    /// 90°/270° swap the X and Z dimensions.
80    #[must_use]
81    pub const fn rotate_size(self, size: IVec3) -> IVec3 {
82        match self {
83            Self::Clockwise90 | Self::CounterClockwise90 => IVec3::new(size.z, size.y, size.x),
84            Self::None | Self::Clockwise180 => size,
85        }
86    }
87
88    /// Matches vanilla's `StructureTemplate.transform(pos, Mirror.FRONT_BACK, rotation, pivot)`.
89    #[must_use]
90    pub const fn transform_pos_mirrored(
91        self,
92        pos: IVec3,
93        pivot: IVec3,
94        mirror_front_back: bool,
95    ) -> IVec3 {
96        let mx = if mirror_front_back { -pos.x } else { pos.x };
97        let mirrored_pos = IVec3::new(mx, pos.y, pos.z);
98        self.transform_pos(mirrored_pos, pivot)
99    }
100
101    /// Matches vanilla's `StructureTemplate.getBoundingBox(position, rotation, pivot, mirror, size)`.
102    #[must_use]
103    pub fn get_bounding_box_full(
104        self,
105        pos: IVec3,
106        size: IVec3,
107        pivot: IVec3,
108        mirror_front_back: bool,
109    ) -> BoundingBox {
110        let c1 = self.transform_pos_mirrored(IVec3::ZERO, pivot, mirror_front_back);
111        let c2 = self.transform_pos_mirrored(
112            IVec3::new(size.x - 1, size.y - 1, size.z - 1),
113            pivot,
114            mirror_front_back,
115        );
116        BoundingBox::new(c1.min(c2) + pos, c1.max(c2) + pos)
117    }
118
119    /// [`get_bounding_box_full`] with `mirror=NONE`.
120    #[must_use]
121    pub fn get_bounding_box_with_pivot(self, pos: IVec3, size: IVec3, pivot: IVec3) -> BoundingBox {
122        self.get_bounding_box_full(pos, size, pivot, false)
123    }
124
125    /// [`get_bounding_box_full`] with `pivot=ZERO` and `mirror=NONE`. Used by jigsaw pool elements.
126    #[must_use]
127    pub fn get_bounding_box(self, pos: IVec3, size: IVec3) -> BoundingBox {
128        self.get_bounding_box_full(pos, size, IVec3::ZERO, false)
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn rotate_direction() {
138        assert_eq!(Rotation::None.rotate(Direction::North), Direction::North);
139        assert_eq!(
140            Rotation::Clockwise90.rotate(Direction::North),
141            Direction::East
142        );
143        assert_eq!(
144            Rotation::Clockwise180.rotate(Direction::North),
145            Direction::South
146        );
147        assert_eq!(
148            Rotation::CounterClockwise90.rotate(Direction::North),
149            Direction::West
150        );
151    }
152
153    #[test]
154    fn compose_rotations() {
155        assert_eq!(
156            Rotation::Clockwise90.then(Rotation::Clockwise90),
157            Rotation::Clockwise180
158        );
159        assert_eq!(
160            Rotation::Clockwise90.then(Rotation::CounterClockwise90),
161            Rotation::None
162        );
163        assert_eq!(
164            Rotation::Clockwise180.then(Rotation::Clockwise180),
165            Rotation::None
166        );
167    }
168
169    #[test]
170    fn vertical_unchanged() {
171        assert_eq!(Rotation::Clockwise90.rotate(Direction::Up), Direction::Up);
172        assert_eq!(
173            Rotation::Clockwise180.rotate(Direction::Down),
174            Direction::Down
175        );
176    }
177
178    #[test]
179    fn transform_pos_pivot_zero() {
180        assert_eq!(
181            Rotation::None.transform_pos(IVec3::new(3, 5, 7), IVec3::ZERO),
182            IVec3::new(3, 5, 7)
183        );
184        assert_eq!(
185            Rotation::Clockwise90.transform_pos(IVec3::new(3, 5, 7), IVec3::ZERO),
186            IVec3::new(-7, 5, 3)
187        );
188        assert_eq!(
189            Rotation::Clockwise180.transform_pos(IVec3::new(3, 5, 7), IVec3::ZERO),
190            IVec3::new(-3, 5, -7)
191        );
192        assert_eq!(
193            Rotation::CounterClockwise90.transform_pos(IVec3::new(3, 5, 7), IVec3::ZERO),
194            IVec3::new(7, 5, -3)
195        );
196    }
197
198    #[test]
199    fn bounding_box_none() {
200        let bb = Rotation::None.get_bounding_box(IVec3::new(0, 0, 0), IVec3::new(6, 10, 6));
201        assert_eq!((bb.min_x(), bb.min_y(), bb.min_z()), (0, 0, 0));
202        assert_eq!((bb.max_x(), bb.max_y(), bb.max_z()), (5, 9, 5));
203    }
204
205    #[test]
206    fn bounding_box_cw90() {
207        let bb =
208            Rotation::Clockwise90.get_bounding_box(IVec3::new(100, 50, 200), IVec3::new(6, 10, 8));
209        assert_eq!((bb.min_x(), bb.min_y(), bb.min_z()), (93, 50, 200));
210        assert_eq!((bb.max_x(), bb.max_y(), bb.max_z()), (100, 59, 205));
211    }
212
213    #[test]
214    fn bounding_box_cw180() {
215        let bb = Rotation::Clockwise180.get_bounding_box(IVec3::new(0, 0, 0), IVec3::new(6, 10, 8));
216        assert_eq!((bb.min_x(), bb.min_y(), bb.min_z()), (-5, 0, -7));
217        assert_eq!((bb.max_x(), bb.max_y(), bb.max_z()), (0, 9, 0));
218    }
219
220    #[test]
221    fn rotate_size() {
222        assert_eq!(
223            Rotation::None.rotate_size(IVec3::new(6, 10, 8)),
224            IVec3::new(6, 10, 8)
225        );
226        assert_eq!(
227            Rotation::Clockwise90.rotate_size(IVec3::new(6, 10, 8)),
228            IVec3::new(8, 10, 6)
229        );
230        assert_eq!(
231            Rotation::Clockwise180.rotate_size(IVec3::new(6, 10, 8)),
232            IVec3::new(6, 10, 8)
233        );
234        assert_eq!(
235            Rotation::CounterClockwise90.rotate_size(IVec3::new(6, 10, 8)),
236            IVec3::new(8, 10, 6)
237        );
238    }
239}