unicorn_hat/
rotation.rs

1//! Rotation and coordinate system support for the display
2//!
3//! Allows rotating the coordinate system to match different physical orientations
4//! of the Unicorn HAT, and choosing the origin location.
5
6/// Coordinate system origin location
7///
8/// Determines where (0,0) is located on the display.
9///
10/// # Examples
11///
12/// ```
13/// use unicorn_hat::Origin;
14///
15/// let origin = Origin::TopLeft;  // Default: (0,0) at top-left
16/// let origin = Origin::BottomLeft;  // (0,0) at bottom-left (useful for bar graphs)
17/// ```
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum Origin {
20    /// Origin at top-left, Y increases downward (default)
21    ///
22    /// ```text
23    /// (0,0) ──────► (7,0)
24    ///   │
25    ///   ▼
26    /// (0,7)       (7,7)
27    /// ```
28    #[default]
29    TopLeft,
30
31    /// Origin at bottom-left, Y increases upward
32    ///
33    /// Useful for bar graphs and charts where Y represents a value.
34    ///
35    /// ```text
36    /// (0,7)       (7,7)
37    ///   ▲
38    ///   │
39    /// (0,0) ──────► (7,0)
40    /// ```
41    BottomLeft,
42}
43
44impl Origin {
45    /// Transforms coordinates based on origin setting
46    ///
47    /// # Arguments
48    ///
49    /// * `x` - X coordinate (0-7)
50    /// * `y` - Y coordinate (0-7)
51    ///
52    /// # Returns
53    ///
54    /// Tuple of (transformed_x, transformed_y) in top-left coordinate space
55    pub fn apply(&self, x: usize, y: usize) -> (usize, usize) {
56        match self {
57            Origin::TopLeft => (x, y),
58            Origin::BottomLeft => (x, 7 - y),
59        }
60    }
61}
62
63/// Rotation angles for the display
64///
65/// Rotations are applied clockwise from the default orientation.
66///
67/// # Examples
68///
69/// ```
70/// use unicorn_hat::Rotate;
71///
72/// let rotation = Rotate::RotCW90;  // 90° clockwise
73/// ```
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
75pub enum Rotate {
76    /// No rotation (default orientation)
77    #[default]
78    RotNone,
79    /// 90° clockwise rotation
80    RotCW90,
81    /// 90° counter-clockwise rotation (270° clockwise)
82    RotCCW90,
83    /// 180° rotation
84    Rot180,
85}
86
87impl Rotate {
88    /// Applies rotation to an (x, y) coordinate
89    ///
90    /// Transforms coordinates according to the rotation angle.
91    /// The grid is always 8x8, so coordinates stay in 0-7 range.
92    ///
93    /// # Arguments
94    ///
95    /// * `x` - Original x coordinate (0-7)
96    /// * `y` - Original y coordinate (0-7)
97    ///
98    /// # Returns
99    ///
100    /// Tuple of (rotated_x, rotated_y)
101    pub fn apply(&self, x: usize, y: usize) -> (usize, usize) {
102        match self {
103            Rotate::RotNone => (x, y),
104            Rotate::RotCW90 => {
105                // 90° clockwise: (x, y) -> (7-y, x)
106                (7 - y, x)
107            }
108            Rotate::RotCCW90 => {
109                // 90° counter-clockwise: (x, y) -> (y, 7-x)
110                (y, 7 - x)
111            }
112            Rotate::Rot180 => {
113                // 180°: (x, y) -> (7-x, 7-y)
114                (7 - x, 7 - y)
115            }
116        }
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_rotate_none() {
126        let r = Rotate::RotNone;
127        assert_eq!(r.apply(0, 0), (0, 0));
128        assert_eq!(r.apply(7, 0), (7, 0));
129        assert_eq!(r.apply(0, 7), (0, 7));
130        assert_eq!(r.apply(7, 7), (7, 7));
131        assert_eq!(r.apply(3, 4), (3, 4));
132    }
133
134    #[test]
135    fn test_rotate_cw90() {
136        let r = Rotate::RotCW90;
137
138        // Corners: 90° clockwise
139        // Top-left (0,0) -> Top-right (7,0)
140        assert_eq!(r.apply(0, 0), (7, 0));
141
142        // Top-right (7,0) -> Bottom-right (7,7)
143        assert_eq!(r.apply(7, 0), (7, 7));
144
145        // Bottom-right (7,7) -> Bottom-left (0,7)
146        assert_eq!(r.apply(7, 7), (0, 7));
147
148        // Bottom-left (0,7) -> Top-left (0,0)
149        assert_eq!(r.apply(0, 7), (0, 0));
150    }
151
152    #[test]
153    fn test_rotate_ccw90() {
154        let r = Rotate::RotCCW90;
155
156        // Corners: 90° counter-clockwise
157        // Top-left (0,0) -> Bottom-left (0,7)
158        assert_eq!(r.apply(0, 0), (0, 7));
159
160        // Top-right (7,0) -> Top-left (0,0)
161        assert_eq!(r.apply(7, 0), (0, 0));
162
163        // Bottom-right (7,7) -> Top-right (7,0)
164        assert_eq!(r.apply(7, 7), (7, 0));
165
166        // Bottom-left (0,7) -> Bottom-right (7,7)
167        assert_eq!(r.apply(0, 7), (7, 7));
168    }
169
170    #[test]
171    fn test_rotate_180() {
172        let r = Rotate::Rot180;
173
174        // Corners: 180°
175        // Top-left (0,0) -> Bottom-right (7,7)
176        assert_eq!(r.apply(0, 0), (7, 7));
177
178        // Top-right (7,0) -> Bottom-left (0,7)
179        assert_eq!(r.apply(7, 0), (0, 7));
180
181        // Bottom-right (7,7) -> Top-left (0,0)
182        assert_eq!(r.apply(7, 7), (0, 0));
183
184        // Bottom-left (0,7) -> Top-right (7,0)
185        assert_eq!(r.apply(0, 7), (7, 0));
186
187        // Center should stay near center
188        assert_eq!(r.apply(3, 3), (4, 4));
189        assert_eq!(r.apply(4, 4), (3, 3));
190    }
191
192    #[test]
193    fn test_double_rotation_cw90() {
194        let r = Rotate::RotCW90;
195
196        // Two 90° CW rotations = 180°
197        let (x1, y1) = r.apply(2, 3);
198        let (x2, y2) = r.apply(x1, y1);
199
200        assert_eq!((x2, y2), Rotate::Rot180.apply(2, 3));
201    }
202
203    #[test]
204    fn test_four_rotations_cw90() {
205        let r = Rotate::RotCW90;
206
207        // Four 90° CW rotations = back to original
208        let (mut x, mut y) = (2, 3);
209        for _ in 0..4 {
210            (x, y) = r.apply(x, y);
211        }
212
213        assert_eq!((x, y), (2, 3));
214    }
215
216    #[test]
217    fn test_cw90_then_ccw90() {
218        // CW90 followed by CCW90 should return to original
219        let original = (3, 5);
220        let (x, y) = Rotate::RotCW90.apply(original.0, original.1);
221        let (x, y) = Rotate::RotCCW90.apply(x, y);
222
223        assert_eq!((x, y), original);
224    }
225
226    #[test]
227    fn test_default_rotation() {
228        assert_eq!(Rotate::default(), Rotate::RotNone);
229    }
230
231    #[test]
232    fn test_rotation_bounds() {
233        // All rotations should keep coordinates in bounds (0-7)
234        let rotations = [Rotate::RotNone, Rotate::RotCW90, Rotate::RotCCW90, Rotate::Rot180];
235
236        for rotation in &rotations {
237            for y in 0..8 {
238                for x in 0..8 {
239                    let (rx, ry) = rotation.apply(x, y);
240                    assert!(rx < 8, "Rotated x {} out of bounds for ({}, {}) with {:?}", rx, x, y, rotation);
241                    assert!(ry < 8, "Rotated y {} out of bounds for ({}, {}) with {:?}", ry, x, y, rotation);
242                }
243            }
244        }
245    }
246
247    // Origin tests
248    #[test]
249    fn test_origin_top_left() {
250        let origin = Origin::TopLeft;
251        assert_eq!(origin.apply(0, 0), (0, 0));
252        assert_eq!(origin.apply(7, 0), (7, 0));
253        assert_eq!(origin.apply(0, 7), (0, 7));
254        assert_eq!(origin.apply(7, 7), (7, 7));
255        assert_eq!(origin.apply(3, 4), (3, 4));
256    }
257
258    #[test]
259    fn test_origin_bottom_left() {
260        let origin = Origin::BottomLeft;
261        // Bottom-left: (0,0) should map to (0,7) in top-left space
262        assert_eq!(origin.apply(0, 0), (0, 7));
263        assert_eq!(origin.apply(7, 0), (7, 7));
264        assert_eq!(origin.apply(0, 7), (0, 0));
265        assert_eq!(origin.apply(7, 7), (7, 0));
266        assert_eq!(origin.apply(3, 4), (3, 3));
267    }
268
269    #[test]
270    fn test_origin_default() {
271        assert_eq!(Origin::default(), Origin::TopLeft);
272    }
273
274    #[test]
275    fn test_origin_bounds() {
276        let origins = [Origin::TopLeft, Origin::BottomLeft];
277
278        for origin in &origins {
279            for y in 0..8 {
280                for x in 0..8 {
281                    let (ox, oy) = origin.apply(x, y);
282                    assert!(ox < 8, "Origin x {} out of bounds for ({}, {}) with {:?}", ox, x, y, origin);
283                    assert!(oy < 8, "Origin y {} out of bounds for ({}, {}) with {:?}", oy, x, y, origin);
284                }
285            }
286        }
287    }
288}