Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_math/src/compass.rs
6595 views
1
use core::ops::Neg;
2
3
use crate::Dir2;
4
#[cfg(feature = "bevy_reflect")]
5
use bevy_reflect::Reflect;
6
#[cfg(all(feature = "serialize", feature = "bevy_reflect"))]
7
use 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
)]
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_X`] and [`Dir2::SOUTH`]
38
South,
39
/// Corresponds to [`Dir2::NEG_Y`] 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
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
)]
105
pub 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
124
impl 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
175
impl 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
186
impl 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
202
impl 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
217
impl 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
237
impl Neg for CompassQuadrant {
238
type Output = CompassQuadrant;
239
240
fn neg(self) -> Self::Output {
241
self.opposite()
242
}
243
}
244
245
impl Neg for CompassOctant {
246
type Output = CompassOctant;
247
248
fn neg(self) -> Self::Output {
249
self.opposite()
250
}
251
}
252
253
#[cfg(test)]
254
mod 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)]
380
mod 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
}
586
587