bevy_math/
compass.rs

1use crate::Dir2;
2#[cfg(feature = "bevy_reflect")]
3use bevy_reflect::Reflect;
4#[cfg(all(feature = "serialize", feature = "bevy_reflect"))]
5use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
6
7/// A compass enum with 4 directions.
8/// ```text
9///          N (North)
10///          ▲
11///          │
12///          │
13/// W (West) ┼─────► E (East)
14///          │
15///          │
16///          ▼
17///          S (South)
18/// ```
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
21#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
22#[cfg_attr(
23    all(feature = "serialize", feature = "bevy_reflect"),
24    reflect(Deserialize, Serialize)
25)]
26pub enum CompassQuadrant {
27    /// Corresponds to [`Dir2::Y`] and [`Dir2::NORTH`]
28    North,
29    /// Corresponds to [`Dir2::X`] and [`Dir2::EAST`]
30    East,
31    /// Corresponds to [`Dir2::NEG_X`] and [`Dir2::SOUTH`]
32    South,
33    /// Corresponds to [`Dir2::NEG_Y`] and [`Dir2::WEST`]
34    West,
35}
36
37/// A compass enum with 8 directions.
38/// ```text
39///          N (North)
40///          ▲
41///     NW   │   NE
42///        ╲ │ ╱
43/// W (West) ┼─────► E (East)
44///        ╱ │ ╲
45///     SW   │   SE
46///          ▼
47///          S (South)
48/// ```
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
51#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
52#[cfg_attr(
53    all(feature = "serialize", feature = "bevy_reflect"),
54    reflect(Deserialize, Serialize)
55)]
56pub enum CompassOctant {
57    /// Corresponds to [`Dir2::Y`] and [`Dir2::NORTH`]
58    North,
59    /// Corresponds to [`Dir2::NORTH_EAST`]
60    NorthEast,
61    /// Corresponds to [`Dir2::X`] and [`Dir2::EAST`]
62    East,
63    /// Corresponds to [`Dir2::SOUTH_EAST`]
64    SouthEast,
65    /// Corresponds to [`Dir2::NEG_X`] and [`Dir2::SOUTH`]
66    South,
67    /// Corresponds to [`Dir2::SOUTH_WEST`]
68    SouthWest,
69    /// Corresponds to [`Dir2::NEG_Y`] and [`Dir2::WEST`]
70    West,
71    /// Corresponds to [`Dir2::NORTH_WEST`]
72    NorthWest,
73}
74
75impl From<CompassQuadrant> for Dir2 {
76    fn from(q: CompassQuadrant) -> Self {
77        match q {
78            CompassQuadrant::North => Dir2::NORTH,
79            CompassQuadrant::East => Dir2::EAST,
80            CompassQuadrant::South => Dir2::SOUTH,
81            CompassQuadrant::West => Dir2::WEST,
82        }
83    }
84}
85
86impl From<Dir2> for CompassQuadrant {
87    /// Converts a [`Dir2`] to a [`CompassQuadrant`] in a lossy manner.
88    /// Converting back to a [`Dir2`] is not guaranteed to yield the same value.
89    fn from(dir: Dir2) -> Self {
90        let angle = dir.to_angle().to_degrees();
91
92        match angle {
93            -135.0..=-45.0 => Self::South,
94            -45.0..=45.0 => Self::East,
95            45.0..=135.0 => Self::North,
96            135.0..=180.0 | -180.0..=-135.0 => Self::West,
97            _ => unreachable!(),
98        }
99    }
100}
101
102impl From<CompassOctant> for Dir2 {
103    fn from(o: CompassOctant) -> Self {
104        match o {
105            CompassOctant::North => Dir2::NORTH,
106            CompassOctant::NorthEast => Dir2::NORTH_EAST,
107            CompassOctant::East => Dir2::EAST,
108            CompassOctant::SouthEast => Dir2::SOUTH_EAST,
109            CompassOctant::South => Dir2::SOUTH,
110            CompassOctant::SouthWest => Dir2::SOUTH_WEST,
111            CompassOctant::West => Dir2::WEST,
112            CompassOctant::NorthWest => Dir2::NORTH_WEST,
113        }
114    }
115}
116
117impl From<Dir2> for CompassOctant {
118    /// Converts a [`Dir2`] to a [`CompassOctant`] in a lossy manner.
119    /// Converting back to a [`Dir2`] is not guaranteed to yield the same value.
120    fn from(dir: Dir2) -> Self {
121        let angle = dir.to_angle().to_degrees();
122
123        match angle {
124            -112.5..=-67.5 => Self::South,
125            -67.5..=-22.5 => Self::SouthEast,
126            -22.5..=22.5 => Self::East,
127            22.5..=67.5 => Self::NorthEast,
128            67.5..=112.5 => Self::North,
129            112.5..=157.5 => Self::NorthWest,
130            157.5..=180.0 | -180.0..=-157.5 => Self::West,
131            -157.5..=-112.5 => Self::SouthWest,
132            _ => unreachable!(),
133        }
134    }
135}
136
137#[cfg(test)]
138mod test_compass_quadrant {
139    use crate::{CompassQuadrant, Dir2, Vec2};
140
141    #[test]
142    fn test_cardinal_directions() {
143        let tests = vec![
144            (
145                Dir2::new(Vec2::new(1.0, 0.0)).unwrap(),
146                CompassQuadrant::East,
147            ),
148            (
149                Dir2::new(Vec2::new(0.0, 1.0)).unwrap(),
150                CompassQuadrant::North,
151            ),
152            (
153                Dir2::new(Vec2::new(-1.0, 0.0)).unwrap(),
154                CompassQuadrant::West,
155            ),
156            (
157                Dir2::new(Vec2::new(0.0, -1.0)).unwrap(),
158                CompassQuadrant::South,
159            ),
160        ];
161
162        for (dir, expected) in tests {
163            assert_eq!(CompassQuadrant::from(dir), expected);
164        }
165    }
166
167    #[test]
168    fn test_north_pie_slice() {
169        let tests = vec![
170            (
171                Dir2::new(Vec2::new(-0.1, 0.9)).unwrap(),
172                CompassQuadrant::North,
173            ),
174            (
175                Dir2::new(Vec2::new(0.1, 0.9)).unwrap(),
176                CompassQuadrant::North,
177            ),
178        ];
179
180        for (dir, expected) in tests {
181            assert_eq!(CompassQuadrant::from(dir), expected);
182        }
183    }
184
185    #[test]
186    fn test_east_pie_slice() {
187        let tests = vec![
188            (
189                Dir2::new(Vec2::new(0.9, 0.1)).unwrap(),
190                CompassQuadrant::East,
191            ),
192            (
193                Dir2::new(Vec2::new(0.9, -0.1)).unwrap(),
194                CompassQuadrant::East,
195            ),
196        ];
197
198        for (dir, expected) in tests {
199            assert_eq!(CompassQuadrant::from(dir), expected);
200        }
201    }
202
203    #[test]
204    fn test_south_pie_slice() {
205        let tests = vec![
206            (
207                Dir2::new(Vec2::new(-0.1, -0.9)).unwrap(),
208                CompassQuadrant::South,
209            ),
210            (
211                Dir2::new(Vec2::new(0.1, -0.9)).unwrap(),
212                CompassQuadrant::South,
213            ),
214        ];
215
216        for (dir, expected) in tests {
217            assert_eq!(CompassQuadrant::from(dir), expected);
218        }
219    }
220
221    #[test]
222    fn test_west_pie_slice() {
223        let tests = vec![
224            (
225                Dir2::new(Vec2::new(-0.9, -0.1)).unwrap(),
226                CompassQuadrant::West,
227            ),
228            (
229                Dir2::new(Vec2::new(-0.9, 0.1)).unwrap(),
230                CompassQuadrant::West,
231            ),
232        ];
233
234        for (dir, expected) in tests {
235            assert_eq!(CompassQuadrant::from(dir), expected);
236        }
237    }
238}
239
240#[cfg(test)]
241mod test_compass_octant {
242    use crate::{CompassOctant, Dir2, Vec2};
243
244    #[test]
245    fn test_cardinal_directions() {
246        let tests = vec![
247            (
248                Dir2::new(Vec2::new(-0.5, 0.5)).unwrap(),
249                CompassOctant::NorthWest,
250            ),
251            (
252                Dir2::new(Vec2::new(0.0, 1.0)).unwrap(),
253                CompassOctant::North,
254            ),
255            (
256                Dir2::new(Vec2::new(0.5, 0.5)).unwrap(),
257                CompassOctant::NorthEast,
258            ),
259            (Dir2::new(Vec2::new(1.0, 0.0)).unwrap(), CompassOctant::East),
260            (
261                Dir2::new(Vec2::new(0.5, -0.5)).unwrap(),
262                CompassOctant::SouthEast,
263            ),
264            (
265                Dir2::new(Vec2::new(0.0, -1.0)).unwrap(),
266                CompassOctant::South,
267            ),
268            (
269                Dir2::new(Vec2::new(-0.5, -0.5)).unwrap(),
270                CompassOctant::SouthWest,
271            ),
272            (
273                Dir2::new(Vec2::new(-1.0, 0.0)).unwrap(),
274                CompassOctant::West,
275            ),
276        ];
277
278        for (dir, expected) in tests {
279            assert_eq!(CompassOctant::from(dir), expected);
280        }
281    }
282
283    #[test]
284    fn test_north_pie_slice() {
285        let tests = vec![
286            (
287                Dir2::new(Vec2::new(-0.1, 0.9)).unwrap(),
288                CompassOctant::North,
289            ),
290            (
291                Dir2::new(Vec2::new(0.1, 0.9)).unwrap(),
292                CompassOctant::North,
293            ),
294        ];
295
296        for (dir, expected) in tests {
297            assert_eq!(CompassOctant::from(dir), expected);
298        }
299    }
300
301    #[test]
302    fn test_north_east_pie_slice() {
303        let tests = vec![
304            (
305                Dir2::new(Vec2::new(0.4, 0.6)).unwrap(),
306                CompassOctant::NorthEast,
307            ),
308            (
309                Dir2::new(Vec2::new(0.6, 0.4)).unwrap(),
310                CompassOctant::NorthEast,
311            ),
312        ];
313
314        for (dir, expected) in tests {
315            assert_eq!(CompassOctant::from(dir), expected);
316        }
317    }
318
319    #[test]
320    fn test_east_pie_slice() {
321        let tests = vec![
322            (Dir2::new(Vec2::new(0.9, 0.1)).unwrap(), CompassOctant::East),
323            (
324                Dir2::new(Vec2::new(0.9, -0.1)).unwrap(),
325                CompassOctant::East,
326            ),
327        ];
328
329        for (dir, expected) in tests {
330            assert_eq!(CompassOctant::from(dir), expected);
331        }
332    }
333
334    #[test]
335    fn test_south_east_pie_slice() {
336        let tests = vec![
337            (
338                Dir2::new(Vec2::new(0.4, -0.6)).unwrap(),
339                CompassOctant::SouthEast,
340            ),
341            (
342                Dir2::new(Vec2::new(0.6, -0.4)).unwrap(),
343                CompassOctant::SouthEast,
344            ),
345        ];
346
347        for (dir, expected) in tests {
348            assert_eq!(CompassOctant::from(dir), expected);
349        }
350    }
351
352    #[test]
353    fn test_south_pie_slice() {
354        let tests = vec![
355            (
356                Dir2::new(Vec2::new(-0.1, -0.9)).unwrap(),
357                CompassOctant::South,
358            ),
359            (
360                Dir2::new(Vec2::new(0.1, -0.9)).unwrap(),
361                CompassOctant::South,
362            ),
363        ];
364
365        for (dir, expected) in tests {
366            assert_eq!(CompassOctant::from(dir), expected);
367        }
368    }
369
370    #[test]
371    fn test_south_west_pie_slice() {
372        let tests = vec![
373            (
374                Dir2::new(Vec2::new(-0.4, -0.6)).unwrap(),
375                CompassOctant::SouthWest,
376            ),
377            (
378                Dir2::new(Vec2::new(-0.6, -0.4)).unwrap(),
379                CompassOctant::SouthWest,
380            ),
381        ];
382
383        for (dir, expected) in tests {
384            assert_eq!(CompassOctant::from(dir), expected);
385        }
386    }
387
388    #[test]
389    fn test_west_pie_slice() {
390        let tests = vec![
391            (
392                Dir2::new(Vec2::new(-0.9, -0.1)).unwrap(),
393                CompassOctant::West,
394            ),
395            (
396                Dir2::new(Vec2::new(-0.9, 0.1)).unwrap(),
397                CompassOctant::West,
398            ),
399        ];
400
401        for (dir, expected) in tests {
402            assert_eq!(CompassOctant::from(dir), expected);
403        }
404    }
405
406    #[test]
407    fn test_north_west_pie_slice() {
408        let tests = vec![
409            (
410                Dir2::new(Vec2::new(-0.4, 0.6)).unwrap(),
411                CompassOctant::NorthWest,
412            ),
413            (
414                Dir2::new(Vec2::new(-0.6, 0.4)).unwrap(),
415                CompassOctant::NorthWest,
416            ),
417        ];
418
419        for (dir, expected) in tests {
420            assert_eq!(CompassOctant::from(dir), expected);
421        }
422    }
423}