leafwing_input_manager/input_processing/dual_axis/
circle.rs1use std::fmt::Debug;
4use std::hash::{Hash, Hasher};
5
6use bevy::{
7 math::FloatOrd,
8 prelude::{Reflect, Vec2},
9};
10use serde::{Deserialize, Serialize};
11
12use super::DualAxisProcessor;
13
14#[doc(alias = "RadialBounds")]
36#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
37#[must_use]
38pub struct CircleBounds {
39 pub(crate) radius: f32,
41}
42
43impl CircleBounds {
44 pub const FULL_RANGE: Self = Self { radius: f32::MAX };
46
47 #[doc(alias = "magnitude")]
57 #[doc(alias = "from_radius")]
58 #[inline]
59 pub fn new(max: f32) -> Self {
60 assert!(max >= 0.0);
61 Self { radius: max }
62 }
63
64 #[must_use]
66 #[inline]
67 pub fn radius(&self) -> f32 {
68 self.radius
69 }
70
71 #[must_use]
73 #[inline]
74 pub fn contains(&self, input_value: Vec2) -> bool {
75 input_value.length() <= self.radius
76 }
77
78 #[must_use]
80 #[inline]
81 pub fn clamp(&self, input_value: Vec2) -> Vec2 {
82 input_value.clamp_length_max(self.radius)
83 }
84}
85
86impl Default for CircleBounds {
87 #[inline]
89 fn default() -> Self {
90 Self::new(1.0)
91 }
92}
93
94impl From<CircleBounds> for DualAxisProcessor {
95 fn from(value: CircleBounds) -> Self {
96 Self::CircleBounds(value)
97 }
98}
99
100impl Eq for CircleBounds {}
101
102impl Hash for CircleBounds {
103 fn hash<H: Hasher>(&self, state: &mut H) {
104 FloatOrd(self.radius).hash(state);
105 }
106}
107
108#[doc(alias = "RadialExclusion")]
136#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
137#[must_use]
138pub struct CircleExclusion {
139 pub(crate) radius_squared: f32,
141}
142
143impl CircleExclusion {
144 pub const ZERO: Self = Self {
146 radius_squared: 0.0,
147 };
148
149 #[doc(alias = "magnitude")]
159 #[doc(alias = "from_radius")]
160 #[inline]
161 pub fn new(threshold: f32) -> Self {
162 assert!(threshold >= 0.0);
163 Self {
164 radius_squared: threshold.powi(2),
165 }
166 }
167
168 #[must_use]
170 #[inline]
171 pub fn radius(&self) -> f32 {
172 self.radius_squared.sqrt()
173 }
174
175 #[must_use]
177 #[inline]
178 pub fn contains(&self, input_value: Vec2) -> bool {
179 input_value.length_squared() <= self.radius_squared
180 }
181
182 #[inline]
184 pub fn scaled(self) -> CircleDeadZone {
185 CircleDeadZone::new(self.radius())
186 }
187
188 #[must_use]
190 #[inline]
191 pub fn exclude(&self, input_value: Vec2) -> Vec2 {
192 if self.contains(input_value) {
193 Vec2::ZERO
194 } else {
195 input_value
196 }
197 }
198}
199
200impl Default for CircleExclusion {
201 fn default() -> Self {
203 Self::new(0.1)
204 }
205}
206
207impl From<CircleExclusion> for DualAxisProcessor {
208 fn from(value: CircleExclusion) -> Self {
209 Self::CircleExclusion(value)
210 }
211}
212
213impl Eq for CircleExclusion {}
214
215impl Hash for CircleExclusion {
216 fn hash<H: Hasher>(&self, state: &mut H) {
217 FloatOrd(self.radius_squared).hash(state);
218 }
219}
220
221#[doc(alias = "RadialDeadZone")]
272#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
273#[must_use]
274pub struct CircleDeadZone {
275 pub(crate) radius: f32,
277
278 pub(crate) livezone_recip: f32,
280}
281
282impl CircleDeadZone {
283 pub const ZERO: Self = Self {
285 radius: 0.0,
286 livezone_recip: 1.0,
287 };
288
289 #[doc(alias = "magnitude")]
299 #[doc(alias = "from_radius")]
300 #[inline]
301 pub fn new(threshold: f32) -> Self {
302 let bounds = CircleBounds::default();
303 Self {
304 radius: threshold,
305 livezone_recip: (bounds.radius - threshold).recip(),
306 }
307 }
308
309 #[must_use]
311 #[inline]
312 pub fn radius(&self) -> f32 {
313 self.radius
314 }
315
316 #[inline]
318 pub fn exclusion(&self) -> CircleExclusion {
319 CircleExclusion::new(self.radius)
320 }
321
322 #[inline]
324 pub fn bounds(&self) -> CircleBounds {
325 CircleBounds::default()
326 }
327
328 #[must_use]
332 #[inline]
333 pub fn livezone_min_max(&self) -> (f32, f32) {
334 (self.radius, self.bounds().radius)
335 }
336
337 #[must_use]
339 #[inline]
340 pub fn within_exclusion(&self, input_value: Vec2) -> bool {
341 self.exclusion().contains(input_value)
342 }
343
344 #[must_use]
346 #[inline]
347 pub fn within_bounds(&self, input_value: Vec2) -> bool {
348 self.bounds().contains(input_value)
349 }
350
351 #[must_use]
353 #[inline]
354 pub fn within_livezone(&self, input_value: Vec2) -> bool {
355 let input_length = input_value.length();
356 let (min, max) = self.livezone_min_max();
357 min <= input_length && input_length <= max
358 }
359
360 #[must_use]
362 pub fn normalize(&self, input_value: Vec2) -> Vec2 {
363 let input_length = input_value.length();
364 if input_length == 0.0 {
365 return Vec2::ZERO;
366 }
367
368 let (deadzone, bound) = self.livezone_min_max();
372 let clamped_input_length = input_length.min(bound);
373 let offset_to_deadzone = (clamped_input_length - deadzone).max(0.0);
374 let magnitude_scale = (offset_to_deadzone * self.livezone_recip) / input_length;
375 input_value * magnitude_scale
376 }
377}
378
379impl Default for CircleDeadZone {
380 #[inline]
382 fn default() -> Self {
383 CircleDeadZone::new(0.1)
384 }
385}
386
387impl From<CircleDeadZone> for DualAxisProcessor {
388 fn from(value: CircleDeadZone) -> Self {
389 Self::CircleDeadZone(value)
390 }
391}
392
393impl Eq for CircleDeadZone {}
394
395impl Hash for CircleDeadZone {
396 fn hash<H: Hasher>(&self, state: &mut H) {
397 FloatOrd(self.radius).hash(state);
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404 use bevy::prelude::FloatExt;
405
406 #[test]
407 fn test_circle_value_bounds() {
408 fn test_bounds(bounds: CircleBounds, radius: f32) {
409 assert_eq!(bounds.radius(), radius);
410
411 let processor = DualAxisProcessor::CircleBounds(bounds);
412 assert_eq!(DualAxisProcessor::from(bounds), processor);
413
414 for x in -300..300 {
415 let x = x as f32 * 0.01;
416 for y in -300..300 {
417 let y = y as f32 * 0.01;
418 let value = Vec2::new(x, y);
419
420 assert_eq!(processor.process(value), bounds.clamp(value));
421
422 if value.length() <= radius {
423 assert!(bounds.contains(value));
424 } else {
425 assert!(!bounds.contains(value));
426 }
427
428 let expected = value.clamp_length_max(radius);
429 let delta = (bounds.clamp(value) - expected).abs();
430 assert!(delta.x <= f32::EPSILON);
431 assert!(delta.y <= f32::EPSILON);
432 }
433 }
434 }
435
436 let bounds = CircleBounds::FULL_RANGE;
437 test_bounds(bounds, f32::MAX);
438
439 let bounds = CircleBounds::default();
440 test_bounds(bounds, 1.0);
441
442 let bounds = CircleBounds::new(2.0);
443 test_bounds(bounds, 2.0);
444 }
445
446 #[test]
447 fn test_circle_exclusion() {
448 fn test_exclusion(exclusion: CircleExclusion, radius: f32) {
449 assert_eq!(exclusion.radius(), radius);
450
451 let processor = DualAxisProcessor::CircleExclusion(exclusion);
452 assert_eq!(DualAxisProcessor::from(exclusion), processor);
453
454 for x in -300..300 {
455 let x = x as f32 * 0.01;
456 for y in -300..300 {
457 let y = y as f32 * 0.01;
458 let value = Vec2::new(x, y);
459
460 assert_eq!(processor.process(value), exclusion.exclude(value));
461
462 if value.length() <= radius {
463 assert!(exclusion.contains(value));
464 assert_eq!(exclusion.exclude(value), Vec2::ZERO);
465 } else {
466 assert!(!exclusion.contains(value));
467 assert_eq!(exclusion.exclude(value), value);
468 }
469 }
470 }
471 }
472
473 let exclusion = CircleExclusion::ZERO;
474 test_exclusion(exclusion, 0.0);
475
476 let exclusion = CircleExclusion::default();
477 test_exclusion(exclusion, 0.1);
478
479 let exclusion = CircleExclusion::new(0.5);
480 test_exclusion(exclusion, 0.5);
481 }
482
483 #[test]
484 fn test_circle_deadzone() {
485 fn test_deadzone(deadzone: CircleDeadZone, radius: f32) {
486 assert_eq!(deadzone.radius(), radius);
487
488 let exclusion = CircleExclusion::new(radius);
489 assert_eq!(exclusion.scaled(), deadzone);
490
491 let processor = DualAxisProcessor::CircleDeadZone(deadzone);
492 assert_eq!(DualAxisProcessor::from(deadzone), processor);
493
494 for x in -300..300 {
495 let x = x as f32 * 0.01;
496 for y in -300..300 {
497 let y = y as f32 * 0.01;
498 let value = Vec2::new(x, y);
499
500 assert_eq!(processor.process(value), deadzone.normalize(value));
501
502 if value.length() <= radius {
504 assert!(deadzone.within_exclusion(value));
505 assert_eq!(deadzone.normalize(value), Vec2::ZERO);
506 }
507 else if value.length() <= 1.0 {
509 assert!(deadzone.within_livezone(value));
510
511 let expected_scale = f32::inverse_lerp(radius, 1.0, value.length());
512 let expected = value.normalize() * expected_scale;
513 let delta = (deadzone.normalize(value) - expected).abs();
514 assert!(delta.x <= 0.00001);
515 assert!(delta.y <= 0.00001);
516 }
517 else {
519 assert!(!deadzone.within_bounds(value));
520
521 let expected = value.clamp_length_max(1.0);
522 let delta = (deadzone.normalize(value) - expected).abs();
523 assert!(delta.x <= 0.00001);
524 assert!(delta.y <= 0.00001);
525 }
526 }
527 }
528 }
529
530 let deadzone = CircleDeadZone::ZERO;
531 test_deadzone(deadzone, 0.0);
532
533 let deadzone = CircleDeadZone::default();
534 test_deadzone(deadzone, 0.1);
535
536 let deadzone = CircleDeadZone::new(0.5);
537 test_deadzone(deadzone, 0.5);
538 }
539}