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};
6use core::ops::Neg;
7use glam::Vec2;
8
9#[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 North,
35 East,
37 South,
39 West,
41}
42
43impl CompassQuadrant {
44 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 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 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 pub fn is_in_direction(self, origin: Vec2, candidate: Vec2) -> bool {
110 let dir = Dir2::from(self);
111 let to_candidate = candidate - origin;
112 to_candidate.dot(*dir) > 0.0
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
129#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
130#[cfg_attr(
131 feature = "bevy_reflect",
132 derive(Reflect),
133 reflect(Debug, PartialEq, Hash, Clone)
134)]
135#[cfg_attr(
136 all(feature = "serialize", feature = "bevy_reflect"),
137 reflect(Deserialize, Serialize)
138)]
139pub enum CompassOctant {
140 North,
142 NorthEast,
144 East,
146 SouthEast,
148 South,
150 SouthWest,
152 West,
154 NorthWest,
156}
157
158impl CompassOctant {
159 pub const fn from_index(index: usize) -> Option<Self> {
163 match index {
164 0 => Some(Self::North),
165 1 => Some(Self::NorthEast),
166 2 => Some(Self::East),
167 3 => Some(Self::SouthEast),
168 4 => Some(Self::South),
169 5 => Some(Self::SouthWest),
170 6 => Some(Self::West),
171 7 => Some(Self::NorthWest),
172 _ => None,
173 }
174 }
175
176 pub const fn to_index(self) -> usize {
180 match self {
181 Self::North => 0,
182 Self::NorthEast => 1,
183 Self::East => 2,
184 Self::SouthEast => 3,
185 Self::South => 4,
186 Self::SouthWest => 5,
187 Self::West => 6,
188 Self::NorthWest => 7,
189 }
190 }
191
192 pub const fn opposite(&self) -> CompassOctant {
196 match self {
197 Self::North => Self::South,
198 Self::NorthEast => Self::SouthWest,
199 Self::East => Self::West,
200 Self::SouthEast => Self::NorthWest,
201 Self::South => Self::North,
202 Self::SouthWest => Self::NorthEast,
203 Self::West => Self::East,
204 Self::NorthWest => Self::SouthEast,
205 }
206 }
207
208 pub fn is_in_direction(self, origin: Vec2, candidate: Vec2) -> bool {
237 let dir = Dir2::from(self);
238 let to_candidate = candidate - origin;
239 to_candidate.dot(*dir) > 0.0
240 }
241}
242
243impl From<CompassQuadrant> for Dir2 {
244 fn from(q: CompassQuadrant) -> Self {
245 match q {
246 CompassQuadrant::North => Dir2::NORTH,
247 CompassQuadrant::East => Dir2::EAST,
248 CompassQuadrant::South => Dir2::SOUTH,
249 CompassQuadrant::West => Dir2::WEST,
250 }
251 }
252}
253
254impl From<Dir2> for CompassQuadrant {
255 fn from(dir: Dir2) -> Self {
258 let angle = dir.to_angle().to_degrees();
259
260 match angle {
261 -135.0..=-45.0 => Self::South,
262 -45.0..=45.0 => Self::East,
263 45.0..=135.0 => Self::North,
264 135.0..=180.0 | -180.0..=-135.0 => Self::West,
265 _ => unreachable!(),
266 }
267 }
268}
269
270impl From<CompassOctant> for Dir2 {
271 fn from(o: CompassOctant) -> Self {
272 match o {
273 CompassOctant::North => Dir2::NORTH,
274 CompassOctant::NorthEast => Dir2::NORTH_EAST,
275 CompassOctant::East => Dir2::EAST,
276 CompassOctant::SouthEast => Dir2::SOUTH_EAST,
277 CompassOctant::South => Dir2::SOUTH,
278 CompassOctant::SouthWest => Dir2::SOUTH_WEST,
279 CompassOctant::West => Dir2::WEST,
280 CompassOctant::NorthWest => Dir2::NORTH_WEST,
281 }
282 }
283}
284
285impl From<Dir2> for CompassOctant {
286 fn from(dir: Dir2) -> Self {
289 let angle = dir.to_angle().to_degrees();
290
291 match angle {
292 -112.5..=-67.5 => Self::South,
293 -67.5..=-22.5 => Self::SouthEast,
294 -22.5..=22.5 => Self::East,
295 22.5..=67.5 => Self::NorthEast,
296 67.5..=112.5 => Self::North,
297 112.5..=157.5 => Self::NorthWest,
298 157.5..=180.0 | -180.0..=-157.5 => Self::West,
299 -157.5..=-112.5 => Self::SouthWest,
300 _ => unreachable!(),
301 }
302 }
303}
304
305impl Neg for CompassQuadrant {
306 type Output = CompassQuadrant;
307
308 fn neg(self) -> Self::Output {
309 self.opposite()
310 }
311}
312
313impl Neg for CompassOctant {
314 type Output = CompassOctant;
315
316 fn neg(self) -> Self::Output {
317 self.opposite()
318 }
319}
320
321#[cfg(test)]
322mod test_compass_quadrant {
323 use crate::{CompassQuadrant, Dir2, Vec2};
324
325 #[test]
326 fn test_cardinal_directions() {
327 let tests = [
328 (
329 Dir2::new(Vec2::new(1.0, 0.0)).unwrap(),
330 CompassQuadrant::East,
331 ),
332 (
333 Dir2::new(Vec2::new(0.0, 1.0)).unwrap(),
334 CompassQuadrant::North,
335 ),
336 (
337 Dir2::new(Vec2::new(-1.0, 0.0)).unwrap(),
338 CompassQuadrant::West,
339 ),
340 (
341 Dir2::new(Vec2::new(0.0, -1.0)).unwrap(),
342 CompassQuadrant::South,
343 ),
344 ];
345
346 for (dir, expected) in tests {
347 assert_eq!(CompassQuadrant::from(dir), expected);
348 }
349 }
350
351 #[test]
352 fn test_north_pie_slice() {
353 let tests = [
354 (
355 Dir2::new(Vec2::new(-0.1, 0.9)).unwrap(),
356 CompassQuadrant::North,
357 ),
358 (
359 Dir2::new(Vec2::new(0.1, 0.9)).unwrap(),
360 CompassQuadrant::North,
361 ),
362 ];
363
364 for (dir, expected) in tests {
365 assert_eq!(CompassQuadrant::from(dir), expected);
366 }
367 }
368
369 #[test]
370 fn test_east_pie_slice() {
371 let tests = [
372 (
373 Dir2::new(Vec2::new(0.9, 0.1)).unwrap(),
374 CompassQuadrant::East,
375 ),
376 (
377 Dir2::new(Vec2::new(0.9, -0.1)).unwrap(),
378 CompassQuadrant::East,
379 ),
380 ];
381
382 for (dir, expected) in tests {
383 assert_eq!(CompassQuadrant::from(dir), expected);
384 }
385 }
386
387 #[test]
388 fn test_south_pie_slice() {
389 let tests = [
390 (
391 Dir2::new(Vec2::new(-0.1, -0.9)).unwrap(),
392 CompassQuadrant::South,
393 ),
394 (
395 Dir2::new(Vec2::new(0.1, -0.9)).unwrap(),
396 CompassQuadrant::South,
397 ),
398 ];
399
400 for (dir, expected) in tests {
401 assert_eq!(CompassQuadrant::from(dir), expected);
402 }
403 }
404
405 #[test]
406 fn test_west_pie_slice() {
407 let tests = [
408 (
409 Dir2::new(Vec2::new(-0.9, -0.1)).unwrap(),
410 CompassQuadrant::West,
411 ),
412 (
413 Dir2::new(Vec2::new(-0.9, 0.1)).unwrap(),
414 CompassQuadrant::West,
415 ),
416 ];
417
418 for (dir, expected) in tests {
419 assert_eq!(CompassQuadrant::from(dir), expected);
420 }
421 }
422
423 #[test]
424 fn out_of_bounds_indexes_return_none() {
425 assert_eq!(CompassQuadrant::from_index(4), None);
426 assert_eq!(CompassQuadrant::from_index(5), None);
427 assert_eq!(CompassQuadrant::from_index(usize::MAX), None);
428 }
429
430 #[test]
431 fn compass_indexes_are_reversible() {
432 for i in 0..4 {
433 let quadrant = CompassQuadrant::from_index(i).unwrap();
434 assert_eq!(quadrant.to_index(), i);
435 }
436 }
437
438 #[test]
439 fn opposite_directions_reverse_themselves() {
440 for i in 0..4 {
441 let quadrant = CompassQuadrant::from_index(i).unwrap();
442 assert_eq!(-(-quadrant), quadrant);
443 }
444 }
445}
446
447#[cfg(test)]
448mod test_compass_octant {
449 use crate::{CompassOctant, Dir2, Vec2};
450
451 #[test]
452 fn test_cardinal_directions() {
453 let tests = [
454 (
455 Dir2::new(Vec2::new(-0.5, 0.5)).unwrap(),
456 CompassOctant::NorthWest,
457 ),
458 (
459 Dir2::new(Vec2::new(0.0, 1.0)).unwrap(),
460 CompassOctant::North,
461 ),
462 (
463 Dir2::new(Vec2::new(0.5, 0.5)).unwrap(),
464 CompassOctant::NorthEast,
465 ),
466 (Dir2::new(Vec2::new(1.0, 0.0)).unwrap(), CompassOctant::East),
467 (
468 Dir2::new(Vec2::new(0.5, -0.5)).unwrap(),
469 CompassOctant::SouthEast,
470 ),
471 (
472 Dir2::new(Vec2::new(0.0, -1.0)).unwrap(),
473 CompassOctant::South,
474 ),
475 (
476 Dir2::new(Vec2::new(-0.5, -0.5)).unwrap(),
477 CompassOctant::SouthWest,
478 ),
479 (
480 Dir2::new(Vec2::new(-1.0, 0.0)).unwrap(),
481 CompassOctant::West,
482 ),
483 ];
484
485 for (dir, expected) in tests {
486 assert_eq!(CompassOctant::from(dir), expected);
487 }
488 }
489
490 #[test]
491 fn test_north_pie_slice() {
492 let tests = [
493 (
494 Dir2::new(Vec2::new(-0.1, 0.9)).unwrap(),
495 CompassOctant::North,
496 ),
497 (
498 Dir2::new(Vec2::new(0.1, 0.9)).unwrap(),
499 CompassOctant::North,
500 ),
501 ];
502
503 for (dir, expected) in tests {
504 assert_eq!(CompassOctant::from(dir), expected);
505 }
506 }
507
508 #[test]
509 fn test_north_east_pie_slice() {
510 let tests = [
511 (
512 Dir2::new(Vec2::new(0.4, 0.6)).unwrap(),
513 CompassOctant::NorthEast,
514 ),
515 (
516 Dir2::new(Vec2::new(0.6, 0.4)).unwrap(),
517 CompassOctant::NorthEast,
518 ),
519 ];
520
521 for (dir, expected) in tests {
522 assert_eq!(CompassOctant::from(dir), expected);
523 }
524 }
525
526 #[test]
527 fn test_east_pie_slice() {
528 let tests = [
529 (Dir2::new(Vec2::new(0.9, 0.1)).unwrap(), CompassOctant::East),
530 (
531 Dir2::new(Vec2::new(0.9, -0.1)).unwrap(),
532 CompassOctant::East,
533 ),
534 ];
535
536 for (dir, expected) in tests {
537 assert_eq!(CompassOctant::from(dir), expected);
538 }
539 }
540
541 #[test]
542 fn test_south_east_pie_slice() {
543 let tests = [
544 (
545 Dir2::new(Vec2::new(0.4, -0.6)).unwrap(),
546 CompassOctant::SouthEast,
547 ),
548 (
549 Dir2::new(Vec2::new(0.6, -0.4)).unwrap(),
550 CompassOctant::SouthEast,
551 ),
552 ];
553
554 for (dir, expected) in tests {
555 assert_eq!(CompassOctant::from(dir), expected);
556 }
557 }
558
559 #[test]
560 fn test_south_pie_slice() {
561 let tests = [
562 (
563 Dir2::new(Vec2::new(-0.1, -0.9)).unwrap(),
564 CompassOctant::South,
565 ),
566 (
567 Dir2::new(Vec2::new(0.1, -0.9)).unwrap(),
568 CompassOctant::South,
569 ),
570 ];
571
572 for (dir, expected) in tests {
573 assert_eq!(CompassOctant::from(dir), expected);
574 }
575 }
576
577 #[test]
578 fn test_south_west_pie_slice() {
579 let tests = [
580 (
581 Dir2::new(Vec2::new(-0.4, -0.6)).unwrap(),
582 CompassOctant::SouthWest,
583 ),
584 (
585 Dir2::new(Vec2::new(-0.6, -0.4)).unwrap(),
586 CompassOctant::SouthWest,
587 ),
588 ];
589
590 for (dir, expected) in tests {
591 assert_eq!(CompassOctant::from(dir), expected);
592 }
593 }
594
595 #[test]
596 fn test_west_pie_slice() {
597 let tests = [
598 (
599 Dir2::new(Vec2::new(-0.9, -0.1)).unwrap(),
600 CompassOctant::West,
601 ),
602 (
603 Dir2::new(Vec2::new(-0.9, 0.1)).unwrap(),
604 CompassOctant::West,
605 ),
606 ];
607
608 for (dir, expected) in tests {
609 assert_eq!(CompassOctant::from(dir), expected);
610 }
611 }
612
613 #[test]
614 fn test_north_west_pie_slice() {
615 let tests = [
616 (
617 Dir2::new(Vec2::new(-0.4, 0.6)).unwrap(),
618 CompassOctant::NorthWest,
619 ),
620 (
621 Dir2::new(Vec2::new(-0.6, 0.4)).unwrap(),
622 CompassOctant::NorthWest,
623 ),
624 ];
625
626 for (dir, expected) in tests {
627 assert_eq!(CompassOctant::from(dir), expected);
628 }
629 }
630
631 #[test]
632 fn out_of_bounds_indexes_return_none() {
633 assert_eq!(CompassOctant::from_index(8), None);
634 assert_eq!(CompassOctant::from_index(9), None);
635 assert_eq!(CompassOctant::from_index(usize::MAX), None);
636 }
637
638 #[test]
639 fn compass_indexes_are_reversible() {
640 for i in 0..8 {
641 let octant = CompassOctant::from_index(i).unwrap();
642 assert_eq!(octant.to_index(), i);
643 }
644 }
645
646 #[test]
647 fn opposite_directions_reverse_themselves() {
648 for i in 0..8 {
649 let octant = CompassOctant::from_index(i).unwrap();
650 assert_eq!(-(-octant), octant);
651 }
652 }
653}