Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_math/src/compass.rs
9331 views
1
use crate::Dir2;
2
#[cfg(feature = "bevy_reflect")]
3
use bevy_reflect::Reflect;
4
#[cfg(all(feature = "serialize", feature = "bevy_reflect"))]
5
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
6
use core::ops::Neg;
7
use glam::Vec2;
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
)]
32
pub 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_Y`] and [`Dir2::SOUTH`]
38
South,
39
/// Corresponds to [`Dir2::NEG_X`] and [`Dir2::WEST`]
40
West,
41
}
42
43
impl 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
/// Checks if a point is in the direction represented by this [`CompassQuadrant`] from an origin.
82
///
83
/// This uses a cone-based check: the vector from origin to the candidate point
84
/// must have a positive dot product with the direction vector.
85
///
86
/// Uses standard mathematical coordinates where Y increases upward.
87
///
88
/// # Arguments
89
///
90
/// * `origin` - The starting position
91
/// * `candidate` - The target position to check
92
///
93
/// # Returns
94
///
95
/// `true` if the candidate is generally in the direction of this quadrant from the origin.
96
///
97
/// # Example
98
///
99
/// ```
100
/// use bevy_math::{CompassQuadrant, Vec2};
101
///
102
/// let origin = Vec2::new(0.0, 0.0);
103
/// let north_point = Vec2::new(0.0, 10.0); // Above origin (Y+ = up)
104
/// let east_point = Vec2::new(10.0, 0.0); // Right of origin
105
///
106
/// assert!(CompassQuadrant::North.is_in_direction(origin, north_point));
107
/// assert!(!CompassQuadrant::North.is_in_direction(origin, east_point));
108
/// ```
109
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
/// A compass enum with 8 directions.
117
/// ```text
118
/// N (North)
119
/// ▲
120
/// NW │ NE
121
/// ╲ │ ╱
122
/// W (West) ┼─────► E (East)
123
/// ╱ │ ╲
124
/// SW │ SE
125
/// ▼
126
/// S (South)
127
/// ```
128
#[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
)]
139
pub enum CompassOctant {
140
/// Corresponds to [`Dir2::Y`] and [`Dir2::NORTH`]
141
North,
142
/// Corresponds to [`Dir2::NORTH_EAST`]
143
NorthEast,
144
/// Corresponds to [`Dir2::X`] and [`Dir2::EAST`]
145
East,
146
/// Corresponds to [`Dir2::SOUTH_EAST`]
147
SouthEast,
148
/// Corresponds to [`Dir2::NEG_Y`] and [`Dir2::SOUTH`]
149
South,
150
/// Corresponds to [`Dir2::SOUTH_WEST`]
151
SouthWest,
152
/// Corresponds to [`Dir2::NEG_X`] and [`Dir2::WEST`]
153
West,
154
/// Corresponds to [`Dir2::NORTH_WEST`]
155
NorthWest,
156
}
157
158
impl CompassOctant {
159
/// Converts a standard index to a [`CompassOctant`].
160
///
161
/// Starts at 0 for [`CompassOctant::North`] and increments clockwise.
162
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
/// Converts a [`CompassOctant`] to a standard index.
177
///
178
/// Starts at 0 for [`CompassOctant::North`] and increments clockwise.
179
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
/// Returns the opposite [`CompassOctant`], located 180 degrees from `self`.
193
///
194
/// This can also be accessed via the `-` operator, using the [`Neg`] trait.
195
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
/// Checks if a point is in the direction represented by this [`CompassOctant`] from an origin.
209
///
210
/// This uses a cone-based check: the vector from origin to the candidate point
211
/// must have a positive dot product with the direction vector.
212
///
213
/// Uses standard mathematical coordinates where Y increases upward.
214
///
215
/// # Arguments
216
///
217
/// * `origin` - The starting position
218
/// * `candidate` - The target position to check
219
///
220
/// # Returns
221
///
222
/// `true` if the candidate is generally in the direction of this octant from the origin.
223
///
224
/// # Example
225
///
226
/// ```
227
/// use bevy_math::{CompassOctant, Vec2};
228
///
229
/// let origin = Vec2::new(0.0, 0.0);
230
/// let north_point = Vec2::new(0.0, 10.0); // Above origin (Y+ = up)
231
/// let east_point = Vec2::new(10.0, 0.0); // Right of origin
232
///
233
/// assert!(CompassOctant::North.is_in_direction(origin, north_point));
234
/// assert!(!CompassOctant::North.is_in_direction(origin, east_point));
235
/// ```
236
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
243
impl 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
254
impl From<Dir2> for CompassQuadrant {
255
/// Converts a [`Dir2`] to a [`CompassQuadrant`] in a lossy manner.
256
/// Converting back to a [`Dir2`] is not guaranteed to yield the same value.
257
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
270
impl 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
285
impl From<Dir2> for CompassOctant {
286
/// Converts a [`Dir2`] to a [`CompassOctant`] in a lossy manner.
287
/// Converting back to a [`Dir2`] is not guaranteed to yield the same value.
288
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
305
impl Neg for CompassQuadrant {
306
type Output = CompassQuadrant;
307
308
fn neg(self) -> Self::Output {
309
self.opposite()
310
}
311
}
312
313
impl Neg for CompassOctant {
314
type Output = CompassOctant;
315
316
fn neg(self) -> Self::Output {
317
self.opposite()
318
}
319
}
320
321
#[cfg(test)]
322
mod 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)]
448
mod 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
}
654
655