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}