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#[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
82#[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 North,
108 NorthEast,
110 East,
112 SouthEast,
114 South,
116 SouthWest,
118 West,
120 NorthWest,
122}
123
124impl CompassOctant {
125 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 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 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 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 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}