bevy_math/
compass.rs

1use core::ops::Neg;
2
3use crate::Dir2;
4#[cfg(feature = "bevy_reflect")]
5use bevy_reflect::Reflect;
6#[cfg(all(feature = "serialize", feature = "bevy_reflect"))]
7use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
8
9/// A compass enum with 4 directions.
10/// ```text
11///          N (North)
12///          ▲
13///          │
14///          │
15/// W (West) ┼─────► E (East)
16///          │
17///          │
18///          ▼
19///          S (South)
20/// ```
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
23#[cfg_attr(
24    feature = "bevy_reflect",
25    derive(Reflect),
26    reflect(Debug, PartialEq, Hash, Clone)
27)]
28#[cfg_attr(
29    all(feature = "serialize", feature = "bevy_reflect"),
30    reflect(Deserialize, Serialize)
31)]
32pub enum CompassQuadrant {
33    /// Corresponds to [`Dir2::Y`] and [`Dir2::NORTH`]
34    North,
35    /// Corresponds to [`Dir2::X`] and [`Dir2::EAST`]
36    East,
37    /// Corresponds to [`Dir2::NEG_X`] and [`Dir2::SOUTH`]
38    South,
39    /// Corresponds to [`Dir2::NEG_Y`] and [`Dir2::WEST`]
40    West,
41}
42
43impl CompassQuadrant {
44    /// Converts a standard index to a [`CompassQuadrant`].
45    ///
46    /// Starts at 0 for [`CompassQuadrant::North`] and increments clockwise.
47    pub const fn from_index(index: usize) -> Option<Self> {
48        match index {
49            0 => Some(Self::North),
50            1 => Some(Self::East),
51            2 => Some(Self::South),
52            3 => Some(Self::West),
53            _ => None,
54        }
55    }
56
57    /// Converts a [`CompassQuadrant`] to a standard index.
58    ///
59    /// Starts at 0 for [`CompassQuadrant::North`] and increments clockwise.
60    pub const fn to_index(self) -> usize {
61        match self {
62            Self::North => 0,
63            Self::East => 1,
64            Self::South => 2,
65            Self::West => 3,
66        }
67    }
68
69    /// Returns the opposite [`CompassQuadrant`], located 180 degrees from `self`.
70    ///
71    /// This can also be accessed via the `-` operator, using the [`Neg`] trait.
72    pub const fn opposite(&self) -> CompassQuadrant {
73        match self {
74            Self::North => Self::South,
75            Self::East => Self::West,
76            Self::South => Self::North,
77            Self::West => Self::East,
78        }
79    }
80}
81
82/// A compass enum with 8 directions.
83/// ```text
84///          N (North)
85///          ▲
86///     NW   │   NE
87///        ╲ │ ╱
88/// W (West) ┼─────► E (East)
89///        ╱ │ ╲
90///     SW   │   SE
91///          ▼
92///          S (South)
93/// ```
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
95#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
96#[cfg_attr(
97    feature = "bevy_reflect",
98    derive(Reflect),
99    reflect(Debug, PartialEq, Hash, Clone)
100)]
101#[cfg_attr(
102    all(feature = "serialize", feature = "bevy_reflect"),
103    reflect(Deserialize, Serialize)
104)]
105pub enum CompassOctant {
106    /// Corresponds to [`Dir2::Y`] and [`Dir2::NORTH`]
107    North,
108    /// Corresponds to [`Dir2::NORTH_EAST`]
109    NorthEast,
110    /// Corresponds to [`Dir2::X`] and [`Dir2::EAST`]
111    East,
112    /// Corresponds to [`Dir2::SOUTH_EAST`]
113    SouthEast,
114    /// Corresponds to [`Dir2::NEG_X`] and [`Dir2::SOUTH`]
115    South,
116    /// Corresponds to [`Dir2::SOUTH_WEST`]
117    SouthWest,
118    /// Corresponds to [`Dir2::NEG_Y`] and [`Dir2::WEST`]
119    West,
120    /// Corresponds to [`Dir2::NORTH_WEST`]
121    NorthWest,
122}
123
124impl CompassOctant {
125    /// Converts a standard index to a [`CompassOctant`].
126    ///
127    /// Starts at 0 for [`CompassOctant::North`] and increments clockwise.
128    pub const fn from_index(index: usize) -> Option<Self> {
129        match index {
130            0 => Some(Self::North),
131            1 => Some(Self::NorthEast),
132            2 => Some(Self::East),
133            3 => Some(Self::SouthEast),
134            4 => Some(Self::South),
135            5 => Some(Self::SouthWest),
136            6 => Some(Self::West),
137            7 => Some(Self::NorthWest),
138            _ => None,
139        }
140    }
141
142    /// Converts a [`CompassOctant`] to a standard index.
143    ///
144    /// Starts at 0 for [`CompassOctant::North`] and increments clockwise.
145    pub const fn to_index(self) -> usize {
146        match self {
147            Self::North => 0,
148            Self::NorthEast => 1,
149            Self::East => 2,
150            Self::SouthEast => 3,
151            Self::South => 4,
152            Self::SouthWest => 5,
153            Self::West => 6,
154            Self::NorthWest => 7,
155        }
156    }
157
158    /// Returns the opposite [`CompassOctant`], located 180 degrees from `self`.
159    ///
160    /// This can also be accessed via the `-` operator, using the [`Neg`] trait.
161    pub const fn opposite(&self) -> CompassOctant {
162        match self {
163            Self::North => Self::South,
164            Self::NorthEast => Self::SouthWest,
165            Self::East => Self::West,
166            Self::SouthEast => Self::NorthWest,
167            Self::South => Self::North,
168            Self::SouthWest => Self::NorthEast,
169            Self::West => Self::East,
170            Self::NorthWest => Self::SouthEast,
171        }
172    }
173}
174
175impl From<CompassQuadrant> for Dir2 {
176    fn from(q: CompassQuadrant) -> Self {
177        match q {
178            CompassQuadrant::North => Dir2::NORTH,
179            CompassQuadrant::East => Dir2::EAST,
180            CompassQuadrant::South => Dir2::SOUTH,
181            CompassQuadrant::West => Dir2::WEST,
182        }
183    }
184}
185
186impl From<Dir2> for CompassQuadrant {
187    /// Converts a [`Dir2`] to a [`CompassQuadrant`] in a lossy manner.
188    /// Converting back to a [`Dir2`] is not guaranteed to yield the same value.
189    fn from(dir: Dir2) -> Self {
190        let angle = dir.to_angle().to_degrees();
191
192        match angle {
193            -135.0..=-45.0 => Self::South,
194            -45.0..=45.0 => Self::East,
195            45.0..=135.0 => Self::North,
196            135.0..=180.0 | -180.0..=-135.0 => Self::West,
197            _ => unreachable!(),
198        }
199    }
200}
201
202impl From<CompassOctant> for Dir2 {
203    fn from(o: CompassOctant) -> Self {
204        match o {
205            CompassOctant::North => Dir2::NORTH,
206            CompassOctant::NorthEast => Dir2::NORTH_EAST,
207            CompassOctant::East => Dir2::EAST,
208            CompassOctant::SouthEast => Dir2::SOUTH_EAST,
209            CompassOctant::South => Dir2::SOUTH,
210            CompassOctant::SouthWest => Dir2::SOUTH_WEST,
211            CompassOctant::West => Dir2::WEST,
212            CompassOctant::NorthWest => Dir2::NORTH_WEST,
213        }
214    }
215}
216
217impl From<Dir2> for CompassOctant {
218    /// Converts a [`Dir2`] to a [`CompassOctant`] in a lossy manner.
219    /// Converting back to a [`Dir2`] is not guaranteed to yield the same value.
220    fn from(dir: Dir2) -> Self {
221        let angle = dir.to_angle().to_degrees();
222
223        match angle {
224            -112.5..=-67.5 => Self::South,
225            -67.5..=-22.5 => Self::SouthEast,
226            -22.5..=22.5 => Self::East,
227            22.5..=67.5 => Self::NorthEast,
228            67.5..=112.5 => Self::North,
229            112.5..=157.5 => Self::NorthWest,
230            157.5..=180.0 | -180.0..=-157.5 => Self::West,
231            -157.5..=-112.5 => Self::SouthWest,
232            _ => unreachable!(),
233        }
234    }
235}
236
237impl Neg for CompassQuadrant {
238    type Output = CompassQuadrant;
239
240    fn neg(self) -> Self::Output {
241        self.opposite()
242    }
243}
244
245impl Neg for CompassOctant {
246    type Output = CompassOctant;
247
248    fn neg(self) -> Self::Output {
249        self.opposite()
250    }
251}
252
253#[cfg(test)]
254mod test_compass_quadrant {
255    use crate::{CompassQuadrant, Dir2, Vec2};
256
257    #[test]
258    fn test_cardinal_directions() {
259        let tests = [
260            (
261                Dir2::new(Vec2::new(1.0, 0.0)).unwrap(),
262                CompassQuadrant::East,
263            ),
264            (
265                Dir2::new(Vec2::new(0.0, 1.0)).unwrap(),
266                CompassQuadrant::North,
267            ),
268            (
269                Dir2::new(Vec2::new(-1.0, 0.0)).unwrap(),
270                CompassQuadrant::West,
271            ),
272            (
273                Dir2::new(Vec2::new(0.0, -1.0)).unwrap(),
274                CompassQuadrant::South,
275            ),
276        ];
277
278        for (dir, expected) in tests {
279            assert_eq!(CompassQuadrant::from(dir), expected);
280        }
281    }
282
283    #[test]
284    fn test_north_pie_slice() {
285        let tests = [
286            (
287                Dir2::new(Vec2::new(-0.1, 0.9)).unwrap(),
288                CompassQuadrant::North,
289            ),
290            (
291                Dir2::new(Vec2::new(0.1, 0.9)).unwrap(),
292                CompassQuadrant::North,
293            ),
294        ];
295
296        for (dir, expected) in tests {
297            assert_eq!(CompassQuadrant::from(dir), expected);
298        }
299    }
300
301    #[test]
302    fn test_east_pie_slice() {
303        let tests = [
304            (
305                Dir2::new(Vec2::new(0.9, 0.1)).unwrap(),
306                CompassQuadrant::East,
307            ),
308            (
309                Dir2::new(Vec2::new(0.9, -0.1)).unwrap(),
310                CompassQuadrant::East,
311            ),
312        ];
313
314        for (dir, expected) in tests {
315            assert_eq!(CompassQuadrant::from(dir), expected);
316        }
317    }
318
319    #[test]
320    fn test_south_pie_slice() {
321        let tests = [
322            (
323                Dir2::new(Vec2::new(-0.1, -0.9)).unwrap(),
324                CompassQuadrant::South,
325            ),
326            (
327                Dir2::new(Vec2::new(0.1, -0.9)).unwrap(),
328                CompassQuadrant::South,
329            ),
330        ];
331
332        for (dir, expected) in tests {
333            assert_eq!(CompassQuadrant::from(dir), expected);
334        }
335    }
336
337    #[test]
338    fn test_west_pie_slice() {
339        let tests = [
340            (
341                Dir2::new(Vec2::new(-0.9, -0.1)).unwrap(),
342                CompassQuadrant::West,
343            ),
344            (
345                Dir2::new(Vec2::new(-0.9, 0.1)).unwrap(),
346                CompassQuadrant::West,
347            ),
348        ];
349
350        for (dir, expected) in tests {
351            assert_eq!(CompassQuadrant::from(dir), expected);
352        }
353    }
354
355    #[test]
356    fn out_of_bounds_indexes_return_none() {
357        assert_eq!(CompassQuadrant::from_index(4), None);
358        assert_eq!(CompassQuadrant::from_index(5), None);
359        assert_eq!(CompassQuadrant::from_index(usize::MAX), None);
360    }
361
362    #[test]
363    fn compass_indexes_are_reversible() {
364        for i in 0..4 {
365            let quadrant = CompassQuadrant::from_index(i).unwrap();
366            assert_eq!(quadrant.to_index(), i);
367        }
368    }
369
370    #[test]
371    fn opposite_directions_reverse_themselves() {
372        for i in 0..4 {
373            let quadrant = CompassQuadrant::from_index(i).unwrap();
374            assert_eq!(-(-quadrant), quadrant);
375        }
376    }
377}
378
379#[cfg(test)]
380mod test_compass_octant {
381    use crate::{CompassOctant, Dir2, Vec2};
382
383    #[test]
384    fn test_cardinal_directions() {
385        let tests = [
386            (
387                Dir2::new(Vec2::new(-0.5, 0.5)).unwrap(),
388                CompassOctant::NorthWest,
389            ),
390            (
391                Dir2::new(Vec2::new(0.0, 1.0)).unwrap(),
392                CompassOctant::North,
393            ),
394            (
395                Dir2::new(Vec2::new(0.5, 0.5)).unwrap(),
396                CompassOctant::NorthEast,
397            ),
398            (Dir2::new(Vec2::new(1.0, 0.0)).unwrap(), CompassOctant::East),
399            (
400                Dir2::new(Vec2::new(0.5, -0.5)).unwrap(),
401                CompassOctant::SouthEast,
402            ),
403            (
404                Dir2::new(Vec2::new(0.0, -1.0)).unwrap(),
405                CompassOctant::South,
406            ),
407            (
408                Dir2::new(Vec2::new(-0.5, -0.5)).unwrap(),
409                CompassOctant::SouthWest,
410            ),
411            (
412                Dir2::new(Vec2::new(-1.0, 0.0)).unwrap(),
413                CompassOctant::West,
414            ),
415        ];
416
417        for (dir, expected) in tests {
418            assert_eq!(CompassOctant::from(dir), expected);
419        }
420    }
421
422    #[test]
423    fn test_north_pie_slice() {
424        let tests = [
425            (
426                Dir2::new(Vec2::new(-0.1, 0.9)).unwrap(),
427                CompassOctant::North,
428            ),
429            (
430                Dir2::new(Vec2::new(0.1, 0.9)).unwrap(),
431                CompassOctant::North,
432            ),
433        ];
434
435        for (dir, expected) in tests {
436            assert_eq!(CompassOctant::from(dir), expected);
437        }
438    }
439
440    #[test]
441    fn test_north_east_pie_slice() {
442        let tests = [
443            (
444                Dir2::new(Vec2::new(0.4, 0.6)).unwrap(),
445                CompassOctant::NorthEast,
446            ),
447            (
448                Dir2::new(Vec2::new(0.6, 0.4)).unwrap(),
449                CompassOctant::NorthEast,
450            ),
451        ];
452
453        for (dir, expected) in tests {
454            assert_eq!(CompassOctant::from(dir), expected);
455        }
456    }
457
458    #[test]
459    fn test_east_pie_slice() {
460        let tests = [
461            (Dir2::new(Vec2::new(0.9, 0.1)).unwrap(), CompassOctant::East),
462            (
463                Dir2::new(Vec2::new(0.9, -0.1)).unwrap(),
464                CompassOctant::East,
465            ),
466        ];
467
468        for (dir, expected) in tests {
469            assert_eq!(CompassOctant::from(dir), expected);
470        }
471    }
472
473    #[test]
474    fn test_south_east_pie_slice() {
475        let tests = [
476            (
477                Dir2::new(Vec2::new(0.4, -0.6)).unwrap(),
478                CompassOctant::SouthEast,
479            ),
480            (
481                Dir2::new(Vec2::new(0.6, -0.4)).unwrap(),
482                CompassOctant::SouthEast,
483            ),
484        ];
485
486        for (dir, expected) in tests {
487            assert_eq!(CompassOctant::from(dir), expected);
488        }
489    }
490
491    #[test]
492    fn test_south_pie_slice() {
493        let tests = [
494            (
495                Dir2::new(Vec2::new(-0.1, -0.9)).unwrap(),
496                CompassOctant::South,
497            ),
498            (
499                Dir2::new(Vec2::new(0.1, -0.9)).unwrap(),
500                CompassOctant::South,
501            ),
502        ];
503
504        for (dir, expected) in tests {
505            assert_eq!(CompassOctant::from(dir), expected);
506        }
507    }
508
509    #[test]
510    fn test_south_west_pie_slice() {
511        let tests = [
512            (
513                Dir2::new(Vec2::new(-0.4, -0.6)).unwrap(),
514                CompassOctant::SouthWest,
515            ),
516            (
517                Dir2::new(Vec2::new(-0.6, -0.4)).unwrap(),
518                CompassOctant::SouthWest,
519            ),
520        ];
521
522        for (dir, expected) in tests {
523            assert_eq!(CompassOctant::from(dir), expected);
524        }
525    }
526
527    #[test]
528    fn test_west_pie_slice() {
529        let tests = [
530            (
531                Dir2::new(Vec2::new(-0.9, -0.1)).unwrap(),
532                CompassOctant::West,
533            ),
534            (
535                Dir2::new(Vec2::new(-0.9, 0.1)).unwrap(),
536                CompassOctant::West,
537            ),
538        ];
539
540        for (dir, expected) in tests {
541            assert_eq!(CompassOctant::from(dir), expected);
542        }
543    }
544
545    #[test]
546    fn test_north_west_pie_slice() {
547        let tests = [
548            (
549                Dir2::new(Vec2::new(-0.4, 0.6)).unwrap(),
550                CompassOctant::NorthWest,
551            ),
552            (
553                Dir2::new(Vec2::new(-0.6, 0.4)).unwrap(),
554                CompassOctant::NorthWest,
555            ),
556        ];
557
558        for (dir, expected) in tests {
559            assert_eq!(CompassOctant::from(dir), expected);
560        }
561    }
562
563    #[test]
564    fn out_of_bounds_indexes_return_none() {
565        assert_eq!(CompassOctant::from_index(8), None);
566        assert_eq!(CompassOctant::from_index(9), None);
567        assert_eq!(CompassOctant::from_index(usize::MAX), None);
568    }
569
570    #[test]
571    fn compass_indexes_are_reversible() {
572        for i in 0..8 {
573            let octant = CompassOctant::from_index(i).unwrap();
574            assert_eq!(octant.to_index(), i);
575        }
576    }
577
578    #[test]
579    fn opposite_directions_reverse_themselves() {
580        for i in 0..8 {
581            let octant = CompassOctant::from_index(i).unwrap();
582            assert_eq!(-(-octant), octant);
583        }
584    }
585}