Skip to main content

steel_utils/
rotation.rs

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