leafwing_input_manager/input_processing/dual_axis/
range.rs

1//! Range processors for dual-axis inputs
2
3use std::fmt::Debug;
4use std::hash::Hash;
5
6use bevy::prelude::*;
7use serde::{Deserialize, Serialize};
8
9use super::DualAxisProcessor;
10use crate::input_processing::single_axis::*;
11
12/// Specifies a square-shaped region defining acceptable ranges for valid dual-axis inputs,
13/// with independent min-max ranges for each axis, restricting all values stay within intended limits
14/// to avoid unexpected behavior caused by extreme inputs.
15///
16/// In simple terms, this processor is just the dual-axis version of [`AxisBounds`].
17/// Helpers like [`AxisBounds::extend_dual()`] and its peers can be used to create a [`DualAxisBounds`].
18///
19/// ```rust
20/// use bevy::prelude::*;
21/// use leafwing_input_manager::prelude::*;
22///
23/// // Restrict X to [-2.0, 2.5] and Y to [-1.0, 1.5].
24/// let bounds = DualAxisBounds::new((-2.0, 2.5), (-1.0, 1.5));
25/// assert_eq!(bounds.bounds_x().min_max(), (-2.0, 2.5));
26/// assert_eq!(bounds.bounds_y().min_max(), (-1.0, 1.5));
27///
28/// // Another way to create a DualAxisBounds.
29/// let bounds_x = AxisBounds::new(-2.0, 2.5);
30/// let bounds_y = AxisBounds::new(-1.0, 1.5);
31/// assert_eq!(bounds_x.extend_dual_with_y(bounds_y), bounds);
32///
33/// for x in -300..300 {
34///     let x = x as f32 * 0.01;
35///     for y in -300..300 {
36///         let y = y as f32 * 0.01;
37///         let value = Vec2::new(x, y);
38///
39///         assert_eq!(bounds.clamp(value).x, bounds_x.clamp(x));
40///         assert_eq!(bounds.clamp(value).y, bounds_y.clamp(y));
41///     }
42/// }
43/// ```
44#[doc(alias("SquareBounds", "AxialBounds"))]
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
46#[must_use]
47pub struct DualAxisBounds {
48    /// The [`AxisBounds`] for the X-axis inputs.
49    pub bounds_x: AxisBounds,
50
51    /// The [`AxisBounds`] for the Y-axis inputs.
52    pub bounds_y: AxisBounds,
53}
54
55impl DualAxisBounds {
56    /// Unlimited [`DualAxisBounds`].
57    pub const FULL_RANGE: Self = AxisBounds::FULL_RANGE.extend_dual();
58
59    /// Creates a [`DualAxisBounds`] that restricts values within the range `[min, max]` on each axis.
60    ///
61    /// # Requirements
62    ///
63    /// - `min` <= `max` on each axis.
64    ///
65    /// # Panics
66    ///
67    /// Panics if the requirements aren't met.
68    #[inline]
69    pub fn new((x_min, x_max): (f32, f32), (y_min, y_max): (f32, f32)) -> Self {
70        Self {
71            bounds_x: AxisBounds::new(x_min, x_max),
72            bounds_y: AxisBounds::new(y_min, y_max),
73        }
74    }
75
76    /// Creates a [`DualAxisBounds`] that restricts values within the same range `[min, max]` on both axes.
77    ///
78    /// # Requirements
79    ///
80    /// - `min` <= `max`.
81    ///
82    /// # Panics
83    ///
84    /// Panics if the requirements aren't met.
85    #[inline]
86    pub fn all(min: f32, max: f32) -> Self {
87        let range = (min, max);
88        Self::new(range, range)
89    }
90
91    /// Creates a [`DualAxisBounds`] that only restricts X values within the range `[min, max]`.
92    ///
93    /// # Requirements
94    ///
95    /// - `min` <= `max`.
96    ///
97    /// # Panics
98    ///
99    /// Panics if the requirements aren't met.
100    #[inline]
101    pub fn only_x(min: f32, max: f32) -> Self {
102        Self {
103            bounds_x: AxisBounds::new(min, max),
104            ..Self::FULL_RANGE
105        }
106    }
107
108    /// Creates a [`DualAxisBounds`] that only restricts Y values within the range `[min, max]`.
109    ///
110    /// # Requirements
111    ///
112    /// - `min` <= `max`.
113    ///
114    /// # Panics
115    ///
116    /// Panics if the requirements aren't met.
117    #[inline]
118    pub fn only_y(min: f32, max: f32) -> Self {
119        Self {
120            bounds_y: AxisBounds::new(min, max),
121            ..Self::FULL_RANGE
122        }
123    }
124
125    /// Creates a [`DualAxisBounds`] that restricts values within the range `[-threshold, threshold]` on each axis.
126    ///
127    /// # Requirements
128    ///
129    /// - `threshold` >= `0.0` on each axis.
130    ///
131    /// # Panics
132    ///
133    /// Panics if the requirements aren't met.
134    #[doc(alias = "magnitude")]
135    #[inline]
136    pub fn symmetric(threshold_x: f32, threshold_y: f32) -> Self {
137        Self {
138            bounds_x: AxisBounds::symmetric(threshold_x),
139            bounds_y: AxisBounds::symmetric(threshold_y),
140        }
141    }
142
143    /// Creates a [`DualAxisBounds`] that restricts values within the range `[-threshold, threshold]` on both axes.
144    ///
145    /// # Requirements
146    ///
147    /// - `threshold` >= `0.0`.
148    ///
149    /// # Panics
150    ///
151    /// Panics if the requirements aren't met.
152    #[doc(alias = "magnitude_all")]
153    #[inline]
154    pub fn symmetric_all(threshold: f32) -> Self {
155        Self::symmetric(threshold, threshold)
156    }
157
158    /// Creates a [`DualAxisBounds`] that only restricts X values within the range `[-threshold, threshold]`.
159    ///
160    /// # Requirements
161    ///
162    /// - `threshold` >= `0.0`.
163    ///
164    /// # Panics
165    ///
166    /// Panics if the requirements aren't met.
167    #[doc(alias = "magnitude_only_x")]
168    #[inline]
169    pub fn symmetric_only_x(threshold: f32) -> Self {
170        Self {
171            bounds_x: AxisBounds::symmetric(threshold),
172            ..Self::FULL_RANGE
173        }
174    }
175
176    /// Creates a [`DualAxisBounds`] that only restricts Y values within the range `[-threshold, threshold]`.
177    ///
178    /// # Requirements
179    ///
180    /// - `threshold` >= `0.0`.
181    ///
182    /// # Panics
183    ///
184    /// Panics if the requirements aren't met.
185    #[doc(alias = "magnitude_only_y")]
186    #[inline]
187    pub fn symmetric_only_y(threshold: f32) -> Self {
188        Self {
189            bounds_y: AxisBounds::symmetric(threshold),
190            ..Self::FULL_RANGE
191        }
192    }
193
194    /// Creates a [`DualAxisBounds`] that restricts values to a minimum value on each axis.
195    #[inline]
196    pub const fn at_least(x_min: f32, y_min: f32) -> Self {
197        Self {
198            bounds_x: AxisBounds::at_least(x_min),
199            bounds_y: AxisBounds::at_least(y_min),
200        }
201    }
202
203    /// Creates a [`DualAxisBounds`] that restricts values to a minimum value on both axes.
204    #[inline]
205    pub const fn at_least_all(min: f32) -> Self {
206        AxisBounds::at_least(min).extend_dual()
207    }
208
209    /// Creates a [`DualAxisBounds`] that only restricts X values to a minimum value.
210    #[inline]
211    pub const fn at_least_only_x(min: f32) -> Self {
212        Self {
213            bounds_x: AxisBounds::at_least(min),
214            ..Self::FULL_RANGE
215        }
216    }
217
218    /// Creates a [`DualAxisBounds`] that only restricts Y values to a minimum value.
219    #[inline]
220    pub const fn at_least_only_y(min: f32) -> Self {
221        Self {
222            bounds_y: AxisBounds::at_least(min),
223            ..Self::FULL_RANGE
224        }
225    }
226
227    /// Creates a [`DualAxisBounds`] that restricts values to a maximum value on each axis.
228    #[inline]
229    pub const fn at_most(x_max: f32, y_max: f32) -> Self {
230        Self {
231            bounds_x: AxisBounds::at_most(x_max),
232            bounds_y: AxisBounds::at_most(y_max),
233        }
234    }
235
236    /// Creates a [`DualAxisBounds`] that restricts values to a maximum value on both axes.
237    #[inline]
238    pub const fn at_most_all(max: f32) -> Self {
239        AxisBounds::at_most(max).extend_dual()
240    }
241
242    /// Creates a [`DualAxisBounds`] that only restricts X values to a maximum value.
243    #[inline]
244    pub const fn at_most_only_x(max: f32) -> Self {
245        Self {
246            bounds_x: AxisBounds::at_most(max),
247            ..Self::FULL_RANGE
248        }
249    }
250
251    /// Creates a [`DualAxisBounds`] that only restricts Y values to a maximum value.
252    #[inline]
253    pub const fn at_most_only_y(max: f32) -> Self {
254        Self {
255            bounds_y: AxisBounds::at_most(max),
256            ..Self::FULL_RANGE
257        }
258    }
259
260    /// Returns the bounds for inputs along each axis.
261    #[inline]
262    pub fn bounds(&self) -> (AxisBounds, AxisBounds) {
263        (self.bounds_x, self.bounds_y)
264    }
265
266    /// Returns the bounds for the X-axis inputs.
267    #[inline]
268    pub fn bounds_x(&self) -> AxisBounds {
269        self.bounds().0
270    }
271
272    /// Returns the bounds for the Y-axis inputs.
273    #[inline]
274    pub fn bounds_y(&self) -> AxisBounds {
275        self.bounds().1
276    }
277
278    /// Is `input_value` is within the bounds?
279    #[must_use]
280    #[inline]
281    pub fn contains(&self, input_value: Vec2) -> BVec2 {
282        BVec2::new(
283            self.bounds_x.contains(input_value.x),
284            self.bounds_y.contains(input_value.y),
285        )
286    }
287
288    /// Clamps `input_value` within the bounds.
289    #[must_use]
290    #[inline]
291    pub fn clamp(&self, input_value: Vec2) -> Vec2 {
292        Vec2::new(
293            self.bounds_x.clamp(input_value.x),
294            self.bounds_y.clamp(input_value.y),
295        )
296    }
297}
298
299impl Default for DualAxisBounds {
300    /// Creates a [`DualAxisBounds`] that restricts values within the range `[-1.0, 1.0]` on both axes.
301    #[inline]
302    fn default() -> Self {
303        AxisBounds::default().extend_dual()
304    }
305}
306
307impl From<DualAxisBounds> for DualAxisProcessor {
308    fn from(value: DualAxisBounds) -> Self {
309        Self::ValueBounds(value)
310    }
311}
312
313impl AxisBounds {
314    /// Creates a [`DualAxisBounds`] using `self` for both axes.
315    #[inline]
316    pub const fn extend_dual(self) -> DualAxisBounds {
317        DualAxisBounds {
318            bounds_x: self,
319            bounds_y: self,
320        }
321    }
322
323    /// Creates a [`DualAxisBounds`] only using `self` for the X-axis.
324    #[inline]
325    pub const fn extend_dual_only_x(self) -> DualAxisBounds {
326        DualAxisBounds {
327            bounds_x: self,
328            ..DualAxisBounds::FULL_RANGE
329        }
330    }
331
332    /// Creates a [`DualAxisBounds`] only using `self` to the Y-axis.
333    #[inline]
334    pub const fn extend_dual_only_y(self) -> DualAxisBounds {
335        DualAxisBounds {
336            bounds_y: self,
337            ..DualAxisBounds::FULL_RANGE
338        }
339    }
340
341    /// Creates a [`DualAxisBounds`] using `self` to the Y-axis with the given `bounds_x` to the X-axis.
342    #[inline]
343    pub const fn extend_dual_with_x(self, bounds_x: Self) -> DualAxisBounds {
344        DualAxisBounds {
345            bounds_x,
346            bounds_y: self,
347        }
348    }
349
350    /// Creates a [`DualAxisBounds`] using `self` to the X-axis with the given `bounds_y` to the Y-axis.
351    #[inline]
352    pub const fn extend_dual_with_y(self, bounds_y: Self) -> DualAxisBounds {
353        DualAxisBounds {
354            bounds_x: self,
355            bounds_y,
356        }
357    }
358}
359
360impl From<AxisBounds> for DualAxisProcessor {
361    fn from(bounds: AxisBounds) -> Self {
362        Self::ValueBounds(bounds.extend_dual())
363    }
364}
365
366/// Specifies a cross-shaped region for excluding dual-axis inputs,
367/// with min-max independent min-max ranges for each axis, resulting in a per-axis "snapping" effect,
368/// helping filter out minor fluctuations to enhance control precision for pure axial motion.
369///
370/// In simple terms, this processor is just the dual-axis version of [`AxisExclusion`].
371/// Helpers like [`AxisExclusion::extend_dual()`] and its peers can be used to create a [`DualAxisExclusion`].
372///
373/// ```rust
374/// use bevy::prelude::*;
375/// use leafwing_input_manager::prelude::*;
376///
377/// // Exclude X within [-0.2, 0.3] and Y within [-0.1, 0.4].
378/// let exclusion = DualAxisExclusion::new((-0.2, 0.3), (-0.1, 0.4));
379/// assert_eq!(exclusion.exclusion_x().min_max(), (-0.2, 0.3));
380/// assert_eq!(exclusion.exclusion_y().min_max(), (-0.1, 0.4));
381///
382/// // Another way to create a DualAxisExclusion.
383/// let exclusion_x = AxisExclusion::new(-0.2, 0.3);
384/// let exclusion_y = AxisExclusion::new(-0.1, 0.4);
385/// assert_eq!(exclusion_x.extend_dual_with_y(exclusion_y), exclusion);
386///
387/// for x in -300..300 {
388///     let x = x as f32 * 0.01;
389///     for y in -300..300 {
390///         let y = y as f32 * 0.01;
391///         let value = Vec2::new(x, y);
392///
393///         assert_eq!(exclusion.exclude(value).x, exclusion_x.exclude(x));
394///         assert_eq!(exclusion.exclude(value).y, exclusion_y.exclude(y));
395///     }
396/// }
397/// ```
398#[doc(alias("CrossExclusion", "AxialExclusion"))]
399#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
400#[must_use]
401pub struct DualAxisExclusion {
402    /// The [`AxisExclusion`] for the X-axis inputs.
403    pub exclusion_x: AxisExclusion,
404
405    /// The [`AxisExclusion`] for the Y-axis inputs.
406    pub exclusion_y: AxisExclusion,
407}
408
409impl DualAxisExclusion {
410    /// Zero-size [`DualAxisExclusion`], leaving values as is.
411    pub const ZERO: Self = AxisExclusion::ZERO.extend_dual();
412
413    /// Creates a [`DualAxisExclusion`] that ignores values within the range `[negative_max, positive_min]` on each axis.
414    ///
415    /// # Requirements
416    ///
417    /// - `negative_max` <= `0.0` <= `positive_min` on each axis.
418    ///
419    /// # Panics
420    ///
421    /// Panics if the requirements aren't met.
422    #[inline]
423    pub fn new(
424        (x_negative_max, x_positive_min): (f32, f32),
425        (y_negative_max, y_positive_min): (f32, f32),
426    ) -> Self {
427        Self {
428            exclusion_x: AxisExclusion::new(x_negative_max, x_positive_min),
429            exclusion_y: AxisExclusion::new(y_negative_max, y_positive_min),
430        }
431    }
432
433    /// Creates a [`DualAxisExclusion`] that ignores values within the range `[negative_max, positive_min]` on both axes.
434    ///
435    /// # Requirements
436    ///
437    /// - `negative_max` <= `0.0` <= `positive_min`.
438    ///
439    /// # Panics
440    ///
441    /// Panics if the requirements aren't met.
442    #[inline]
443    pub fn all(negative_max: f32, positive_min: f32) -> Self {
444        AxisExclusion::new(negative_max, positive_min).extend_dual()
445    }
446
447    /// Creates a [`DualAxisExclusion`] that only ignores X values within the range `[negative_max, positive_min]`.
448    ///
449    /// # Requirements
450    ///
451    /// - `negative_max` <= `0.0` <= `positive_min`.
452    ///
453    /// # Panics
454    ///
455    /// Panics if the requirements aren't met.
456    #[inline]
457    pub fn only_x(negative_max: f32, positive_min: f32) -> Self {
458        Self {
459            exclusion_x: AxisExclusion::new(negative_max, positive_min),
460            ..Self::ZERO
461        }
462    }
463
464    /// Creates a [`DualAxisExclusion`] that only ignores Y values within the range `[negative_max, positive_min]`.
465    ///
466    /// # Requirements
467    ///
468    /// - `negative_max` <= `0.0` <= `positive_min`.
469    ///
470    /// # Panics
471    ///
472    /// Panics if the requirements aren't met.
473    #[inline]
474    pub fn only_y(negative_max: f32, positive_min: f32) -> Self {
475        Self {
476            exclusion_y: AxisExclusion::new(negative_max, positive_min),
477            ..Self::ZERO
478        }
479    }
480
481    /// Creates a [`DualAxisExclusion`] that ignores values within the range `[-threshold, threshold]` on each axis.
482    ///
483    /// # Requirements
484    ///
485    /// - `threshold` >= `0.0` on each axis.
486    ///
487    /// # Panics
488    ///
489    /// Panics if the requirements aren't met.
490    #[doc(alias = "magnitude")]
491    #[inline]
492    pub fn symmetric(threshold_x: f32, threshold_y: f32) -> Self {
493        Self {
494            exclusion_x: AxisExclusion::symmetric(threshold_x),
495            exclusion_y: AxisExclusion::symmetric(threshold_y),
496        }
497    }
498
499    /// Creates a [`DualAxisExclusion`] that ignores values within the range `[-threshold, threshold]` on both axes.
500    ///
501    /// # Requirements
502    ///
503    /// - `threshold` >= `0.0`.
504    ///
505    /// # Panics
506    ///
507    /// Panics if the requirements aren't met.
508    #[doc(alias = "magnitude_all")]
509    #[inline]
510    pub fn symmetric_all(threshold: f32) -> Self {
511        AxisExclusion::symmetric(threshold).extend_dual()
512    }
513
514    /// Creates a [`DualAxisExclusion`] that only ignores X values within the range `[-threshold, threshold]`.
515    ///
516    /// # Requirements
517    ///
518    /// - `threshold` >= `0.0`.
519    ///
520    /// # Panics
521    ///
522    /// Panics if the requirements aren't met.
523    #[doc(alias = "magnitude_only_x")]
524    #[inline]
525    pub fn symmetric_only_x(threshold: f32) -> Self {
526        Self {
527            exclusion_x: AxisExclusion::symmetric(threshold),
528            ..Self::ZERO
529        }
530    }
531
532    /// Creates a [`DualAxisExclusion`] that only ignores Y values within the range `[-threshold, threshold]`.
533    ///
534    /// # Requirements
535    ///
536    /// - `threshold` >= `0.0`.
537    ///
538    /// # Panics
539    ///
540    /// Panics if the requirements aren't met.
541    #[doc(alias = "magnitude_only_y")]
542    #[inline]
543    pub fn symmetric_only_y(threshold: f32) -> Self {
544        Self {
545            exclusion_y: AxisExclusion::symmetric(threshold),
546            ..Self::ZERO
547        }
548    }
549
550    /// Creates a [`DualAxisExclusion`] that only passes positive values that greater than `positive_min` on each axis.
551    ///
552    /// # Requirements
553    ///
554    /// - `positive_min` >= `0.0` on each axis.
555    ///
556    /// # Panics
557    ///
558    /// Panics if the requirements aren't met.
559    #[inline]
560    pub fn only_positive(x_positive_min: f32, y_positive_min: f32) -> Self {
561        Self {
562            exclusion_x: AxisExclusion::only_positive(x_positive_min),
563            exclusion_y: AxisExclusion::only_positive(y_positive_min),
564        }
565    }
566
567    /// Creates a [`DualAxisExclusion`] that only passes positive values that greater than `positive_min` on both axes.
568    ///
569    /// # Requirements
570    ///
571    /// - `positive_min` >= `0.0`.
572    ///
573    /// # Panics
574    ///
575    /// Panics if the requirements aren't met.
576    #[inline]
577    pub fn only_positive_all(positive_min: f32) -> Self {
578        AxisExclusion::only_positive(positive_min).extend_dual()
579    }
580
581    /// Creates a [`DualAxisExclusion`] that only passes positive X values that greater than `positive_min`.
582    ///
583    /// # Requirements
584    ///
585    /// - `positive_min` >= `0.0`.
586    ///
587    /// # Panics
588    ///
589    /// Panics if the requirements aren't met.
590    #[inline]
591    pub fn only_positive_x(positive_min: f32) -> Self {
592        Self {
593            exclusion_x: AxisExclusion::only_positive(positive_min),
594            ..Self::ZERO
595        }
596    }
597
598    /// Creates a [`DualAxisExclusion`] that only passes positive Y values that greater than `positive_min`.
599    ///
600    /// # Requirements
601    ///
602    /// - `positive_min` >= `0.0`.
603    ///
604    /// # Panics
605    ///
606    /// Panics if the requirements aren't met.
607    #[inline]
608    pub fn only_positive_y(positive_min: f32) -> Self {
609        Self {
610            exclusion_y: AxisExclusion::only_positive(positive_min),
611            ..Self::ZERO
612        }
613    }
614
615    /// Creates a [`DualAxisExclusion`] that only passes negative values that less than `negative_max` on each axis.
616    ///
617    /// # Requirements
618    ///
619    /// - `negative_max` <= `0.0` on each axis.
620    ///
621    /// # Panics
622    ///
623    /// Panics if the requirements aren't met.
624    #[inline]
625    pub fn only_negative(x_negative_max: f32, y_negative_max: f32) -> Self {
626        Self {
627            exclusion_x: AxisExclusion::only_negative(x_negative_max),
628            exclusion_y: AxisExclusion::only_negative(y_negative_max),
629        }
630    }
631
632    /// Creates a [`DualAxisExclusion`] that only passes negative values that less than `negative_max` on both axes.
633    ///
634    /// # Requirements
635    ///
636    /// - `negative_max` <= `0.0`.
637    ///
638    /// # Panics
639    ///
640    /// Panics if the requirements aren't met.
641    #[inline]
642    pub fn only_negative_all(negative_max: f32) -> Self {
643        AxisExclusion::only_negative(negative_max).extend_dual()
644    }
645
646    /// Creates a [`DualAxisExclusion`] that only passes negative X values that less than `negative_max`.
647    ///
648    /// # Requirements
649    ///
650    /// - `positive_min` >= `0.0`.
651    ///
652    /// # Panics
653    ///
654    /// Panics if the requirements aren't met.
655    #[inline]
656    pub fn only_negative_x(negative_max: f32) -> Self {
657        Self {
658            exclusion_x: AxisExclusion::only_negative(negative_max),
659            ..Self::ZERO
660        }
661    }
662
663    /// Creates a [`DualAxisExclusion`] that only passes negative Y values that less than `negative_max`.
664    ///
665    /// # Requirements
666    ///
667    /// - `positive_min` >= `0.0`.
668    ///
669    /// # Panics
670    ///
671    /// Panics if the requirements aren't met.
672    #[inline]
673    pub fn only_negative_y(negative_max: f32) -> Self {
674        Self {
675            exclusion_y: AxisExclusion::only_negative(negative_max),
676            ..Self::ZERO
677        }
678    }
679
680    /// Returns the exclusion ranges for inputs along each axis.
681    #[inline]
682    pub fn exclusions(&self) -> (AxisExclusion, AxisExclusion) {
683        (self.exclusion_x, self.exclusion_y)
684    }
685
686    /// Returns the exclusion range for the X-axis inputs.
687    #[inline]
688    pub fn exclusion_x(&self) -> AxisExclusion {
689        self.exclusions().0
690    }
691
692    /// Returns the exclusion range for the Y-axis inputs.
693    #[inline]
694    pub fn exclusion_y(&self) -> AxisExclusion {
695        self.exclusions().1
696    }
697
698    /// Is the `input_value` within the exclusion range?
699    #[must_use]
700    #[inline]
701    pub fn contains(&self, input_value: Vec2) -> BVec2 {
702        BVec2::new(
703            self.exclusion_x.contains(input_value.x),
704            self.exclusion_y.contains(input_value.y),
705        )
706    }
707
708    /// Excludes values within the specified region.
709    #[must_use]
710    #[inline]
711    pub fn exclude(&self, input_value: Vec2) -> Vec2 {
712        Vec2::new(
713            self.exclusion_x.exclude(input_value.x),
714            self.exclusion_y.exclude(input_value.y),
715        )
716    }
717
718    /// Creates a [`DualAxisDeadZone`] using `self` as the exclusion range.
719    pub fn scaled(self) -> DualAxisDeadZone {
720        DualAxisDeadZone::new(self.exclusion_x.min_max(), self.exclusion_y.min_max())
721    }
722}
723
724impl Default for DualAxisExclusion {
725    /// Creates a [`DualAxisExclusion`] that excludes input values within `[-1.0, 1.0]` on both axes.
726    #[inline]
727    fn default() -> Self {
728        AxisExclusion::default().extend_dual()
729    }
730}
731
732impl From<DualAxisExclusion> for DualAxisProcessor {
733    fn from(value: DualAxisExclusion) -> Self {
734        Self::Exclusion(value)
735    }
736}
737
738impl AxisExclusion {
739    /// Creates a [`DualAxisExclusion`] using `self` for both axes.
740    #[inline]
741    pub const fn extend_dual(self) -> DualAxisExclusion {
742        DualAxisExclusion {
743            exclusion_x: self,
744            exclusion_y: self,
745        }
746    }
747
748    /// Creates a [`DualAxisExclusion`] only using `self` for the X-axis.
749    #[inline]
750    pub const fn extend_dual_only_x(self) -> DualAxisExclusion {
751        DualAxisExclusion {
752            exclusion_x: self,
753            ..DualAxisExclusion::ZERO
754        }
755    }
756
757    /// Creates a [`DualAxisExclusion`] only using `self` to the Y-axis.
758    #[inline]
759    pub const fn extend_dual_only_y(self) -> DualAxisExclusion {
760        DualAxisExclusion {
761            exclusion_y: self,
762            ..DualAxisExclusion::ZERO
763        }
764    }
765
766    /// Creates a [`DualAxisExclusion`] using `self` to the Y-axis with the given `bounds_x` to the X-axis.
767    #[inline]
768    pub const fn extend_dual_with_x(self, exclusion_x: Self) -> DualAxisExclusion {
769        DualAxisExclusion {
770            exclusion_x,
771            exclusion_y: self,
772        }
773    }
774
775    /// Creates a [`DualAxisExclusion`] using `self` to the X-axis with the given `bounds_y` to the Y-axis.
776    #[inline]
777    pub const fn extend_dual_with_y(self, exclusion_y: Self) -> DualAxisExclusion {
778        DualAxisExclusion {
779            exclusion_x: self,
780            exclusion_y,
781        }
782    }
783}
784
785impl From<AxisExclusion> for DualAxisProcessor {
786    fn from(exclusion: AxisExclusion) -> Self {
787        Self::Exclusion(exclusion.extend_dual())
788    }
789}
790
791/// A scaled version of [`DualAxisExclusion`] with the bounds
792/// set to [`DualAxisBounds::symmetric_all(1.0)`](DualAxisBounds::default)
793/// that normalizes non-excluded input values into the "live zone",
794/// the remaining range within the bounds after dead zone exclusion.
795///
796/// Each axis is processed individually, resulting in a per-axis "snapping" effect,
797/// which enhances control precision for pure axial motion.
798///
799/// It is worth considering that this normalizer increases the magnitude of diagonal values.
800/// If that is not your goal, you might want to explore alternative normalizers.
801///
802/// In simple terms, this processor is just the dual-axis version of [`AxisDeadZone`].
803/// Helpers like [`AxisDeadZone::extend_dual()`] and its peers can be used to create a [`DualAxisDeadZone`].
804///
805/// ```rust
806/// use bevy::prelude::*;
807/// use leafwing_input_manager::prelude::*;
808///
809/// // Exclude X within [-0.2, 0.3] and Y within [-0.1, 0.4].
810/// let deadzone = DualAxisDeadZone::new((-0.2, 0.3), (-0.1, 0.4));
811/// assert_eq!(deadzone.deadzone_x().exclusion().min_max(), (-0.2, 0.3));
812/// assert_eq!(deadzone.deadzone_y().exclusion().min_max(), (-0.1, 0.4));
813///
814/// // Another way to create a DualAxisDeadZone.
815/// let deadzone_x = AxisDeadZone::new(-0.2, 0.3);
816/// let deadzone_y = AxisDeadZone::new(-0.1, 0.4);
817/// assert_eq!(deadzone_x.extend_dual_with_y(deadzone_y), deadzone);
818///
819/// for x in -300..300 {
820///     let x = x as f32 * 0.01;
821///     for y in -300..300 {
822///         let y = y as f32 * 0.01;
823///         let value = Vec2::new(x, y);
824///
825///         assert_eq!(deadzone.normalize(value).x, deadzone_x.normalize(x));
826///         assert_eq!(deadzone.normalize(value).y, deadzone_y.normalize(y));
827///     }
828/// }
829/// ```
830#[doc(alias("CrossDeadZone", "AxialDeadZone"))]
831#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
832#[must_use]
833pub struct DualAxisDeadZone {
834    /// The [`AxisDeadZone`] for the X-axis inputs.
835    pub deadzone_x: AxisDeadZone,
836
837    /// The [`AxisDeadZone`] for the Y-axis inputs.
838    pub deadzone_y: AxisDeadZone,
839}
840
841impl DualAxisDeadZone {
842    /// Zero-size [`DualAxisDeadZone`], only restricting values to the range `[-1.0, 1.0]` on both axes.
843    pub const ZERO: Self = AxisDeadZone::ZERO.extend_dual();
844
845    /// Creates a [`DualAxisDeadZone`] that excludes values within the range `[negative_max, positive_min]` on each axis.
846    ///
847    /// # Requirements
848    ///
849    /// - `negative_max` <= `0.0` <= `positive_min` on each axis.
850    ///
851    /// # Panics
852    ///
853    /// Panics if the requirements aren't met.
854    #[inline]
855    pub fn new(
856        (x_negative_max, x_positive_min): (f32, f32),
857        (y_negative_max, y_positive_min): (f32, f32),
858    ) -> Self {
859        Self {
860            deadzone_x: AxisDeadZone::new(x_negative_max, x_positive_min),
861            deadzone_y: AxisDeadZone::new(y_negative_max, y_positive_min),
862        }
863    }
864
865    /// Creates a [`DualAxisDeadZone`] that excludes values within the range `[negative_max, positive_min]` on both axes.
866    ///
867    /// # Requirements
868    ///
869    /// - `negative_max` <= `0.0` <= `positive_min`.
870    ///
871    /// # Panics
872    ///
873    /// Panics if the requirements aren't met.
874    #[inline]
875    pub fn all(negative_max: f32, positive_min: f32) -> Self {
876        AxisDeadZone::new(negative_max, positive_min).extend_dual()
877    }
878
879    /// Creates a [`DualAxisDeadZone`] that only excludes X values within the range `[negative_max, positive_min]`.
880    ///
881    /// # Requirements
882    ///
883    /// - `negative_max` <= `0.0` <= `positive_min`.
884    ///
885    /// # Panics
886    ///
887    /// Panics if the requirements aren't met.
888    #[inline]
889    pub fn only_x(negative_max: f32, positive_min: f32) -> Self {
890        Self {
891            deadzone_x: AxisDeadZone::new(negative_max, positive_min),
892            ..Self::ZERO
893        }
894    }
895
896    /// Creates a [`DualAxisDeadZone`] that only excludes Y values within the range `[negative_max, positive_min]`.
897    ///
898    /// # Requirements
899    ///
900    /// - `negative_max` <= `0.0` <= `positive_min`.
901    ///
902    /// # Panics
903    ///
904    /// Panics if the requirements aren't met.
905    #[inline]
906    pub fn only_y(negative_max: f32, positive_min: f32) -> Self {
907        Self {
908            deadzone_y: AxisDeadZone::new(negative_max, positive_min),
909            ..Self::ZERO
910        }
911    }
912
913    /// Creates a [`DualAxisDeadZone`] that excludes values within the range `[-threshold, threshold]` on each axis.
914    ///
915    /// # Requirements
916    ///
917    /// - `threshold` >= `0.0` on each axis.
918    ///
919    /// # Panics
920    ///
921    /// Panics if the requirements aren't met.
922    #[doc(alias = "magnitude")]
923    #[inline]
924    pub fn symmetric(threshold_x: f32, threshold_y: f32) -> Self {
925        Self {
926            deadzone_x: AxisDeadZone::symmetric(threshold_x),
927            deadzone_y: AxisDeadZone::symmetric(threshold_y),
928        }
929    }
930
931    /// Creates a [`DualAxisDeadZone`] that excludes values within the range `[-threshold, threshold]` on both axes.
932    ///
933    /// # Requirements
934    ///
935    /// - `threshold` >= `0.0`.
936    ///
937    /// # Panics
938    ///
939    /// Panics if the requirements aren't met.
940    #[doc(alias = "magnitude_all")]
941    #[inline]
942    pub fn symmetric_all(threshold: f32) -> Self {
943        AxisDeadZone::symmetric(threshold).extend_dual()
944    }
945
946    /// Creates a [`DualAxisDeadZone`] that only excludes X values within the range `[-threshold, threshold]`.
947    ///
948    /// # Requirements
949    ///
950    /// - `threshold` >= `0.0`.
951    ///
952    /// # Panics
953    ///
954    /// Panics if the requirements aren't met.
955    #[doc(alias = "magnitude_only_x")]
956    #[inline]
957    pub fn symmetric_only_x(threshold: f32) -> Self {
958        Self {
959            deadzone_x: AxisDeadZone::symmetric(threshold),
960            ..Self::ZERO
961        }
962    }
963
964    /// Creates a [`DualAxisDeadZone`] that only excludes Y values within the range `[-threshold, threshold]`.
965    ///
966    /// # Requirements
967    ///
968    /// - `threshold` >= `0.0`.
969    ///
970    /// # Panics
971    ///
972    /// Panics if the requirements aren't met.
973    #[doc(alias = "magnitude_only_y")]
974    #[inline]
975    pub fn symmetric_only_y(threshold: f32) -> Self {
976        Self {
977            deadzone_y: AxisDeadZone::symmetric(threshold),
978            ..Self::ZERO
979        }
980    }
981
982    /// Creates a [`DualAxisDeadZone`] that only passes positive values that greater than `positive_min` on each axis.
983    ///
984    /// # Requirements
985    ///
986    /// - `positive_min` >= `0.0` on each axis.
987    ///
988    /// # Panics
989    ///
990    /// Panics if the requirements aren't met.
991    #[inline]
992    pub fn only_positive(x_positive_min: f32, y_positive_min: f32) -> Self {
993        Self {
994            deadzone_x: AxisDeadZone::only_positive(x_positive_min),
995            deadzone_y: AxisDeadZone::only_positive(y_positive_min),
996        }
997    }
998
999    /// Creates a [`DualAxisDeadZone`] that only passes positive values that greater than `positive_min` on both axes.
1000    ///
1001    /// # Requirements
1002    ///
1003    /// - `positive_min` >= `0.0`.
1004    ///
1005    /// # Panics
1006    ///
1007    /// Panics if the requirements aren't met.
1008    #[inline]
1009    pub fn only_positive_all(positive_min: f32) -> Self {
1010        AxisDeadZone::only_positive(positive_min).extend_dual()
1011    }
1012
1013    /// Creates a [`DualAxisDeadZone`] that only excludes X values that less than or equal to `positive_min`.
1014    ///
1015    /// # Requirements
1016    ///
1017    /// - `positive_min` >= `0.0`.
1018    ///
1019    /// # Panics
1020    ///
1021    /// Panics if the requirements aren't met.
1022    #[inline]
1023    pub fn only_positive_x(positive_min: f32) -> Self {
1024        Self {
1025            deadzone_x: AxisDeadZone::only_positive(positive_min),
1026            ..Self::ZERO
1027        }
1028    }
1029
1030    /// Creates a [`DualAxisDeadZone`] that only excludes Y values that less than or equal to `positive_min`.
1031    ///
1032    /// # Requirements
1033    ///
1034    /// - `positive_min` >= `0.0`.
1035    ///
1036    /// # Panics
1037    ///
1038    /// Panics if the requirements aren't met.
1039    #[inline]
1040    pub fn only_positive_y(positive_min: f32) -> Self {
1041        Self {
1042            deadzone_y: AxisDeadZone::only_positive(positive_min),
1043            ..Self::ZERO
1044        }
1045    }
1046
1047    /// Creates a [`DualAxisDeadZone`] that only passes negative values that less than `negative_max` on each axis.
1048    ///
1049    /// # Requirements
1050    ///
1051    /// - `negative_max` <= `0.0` on each axis.
1052    ///
1053    /// # Panics
1054    ///
1055    /// Panics if the requirements aren't met.
1056    #[inline]
1057    pub fn only_negative(x_negative_max: f32, y_negative_max: f32) -> Self {
1058        Self {
1059            deadzone_x: AxisDeadZone::only_negative(x_negative_max),
1060            deadzone_y: AxisDeadZone::only_negative(y_negative_max),
1061        }
1062    }
1063
1064    /// Creates a [`DualAxisDeadZone`] that only passes negative values that less than `negative_max` on both axes.
1065    ///
1066    /// # Requirements
1067    ///
1068    /// - `negative_max` <= `0.0`.
1069    ///
1070    /// # Panics
1071    ///
1072    /// Panics if the requirements aren't met.
1073    #[inline]
1074    pub fn only_negative_all(negative_max: f32) -> Self {
1075        AxisDeadZone::only_negative(negative_max).extend_dual()
1076    }
1077
1078    /// Creates a [`DualAxisDeadZone`] that only excludes X values that greater than or equal to `negative_max`.
1079    ///
1080    /// # Requirements
1081    ///
1082    /// - `positive_min` >= `0.0`.
1083    ///
1084    /// # Panics
1085    ///
1086    /// Panics if the requirements aren't met.
1087    #[inline]
1088    pub fn only_negative_x(negative_max: f32) -> Self {
1089        Self {
1090            deadzone_x: AxisDeadZone::only_negative(negative_max),
1091            ..Self::ZERO
1092        }
1093    }
1094
1095    /// Creates a [`DualAxisDeadZone`] that only excludes Y values that greater than or equal to `negative_max`.
1096    ///
1097    /// # Requirements
1098    ///
1099    /// - `positive_min` >= `0.0`.
1100    ///
1101    /// # Panics
1102    ///
1103    /// Panics if the requirements aren't met.
1104    #[inline]
1105    pub fn only_negative_y(negative_max: f32) -> Self {
1106        Self {
1107            deadzone_y: AxisDeadZone::only_negative(negative_max),
1108            ..Self::ZERO
1109        }
1110    }
1111
1112    /// Returns the dead zones for inputs along each axis.
1113    #[inline]
1114    pub fn deadzones(&self) -> (AxisDeadZone, AxisDeadZone) {
1115        (self.deadzone_x, self.deadzone_y)
1116    }
1117
1118    /// Returns the dead zone for the X-axis inputs.
1119    #[inline]
1120    pub fn deadzone_x(&self) -> AxisDeadZone {
1121        self.deadzones().0
1122    }
1123
1124    /// Returns the dead zone for the Y-axis inputs.
1125    #[inline]
1126    pub fn deadzone_y(&self) -> AxisDeadZone {
1127        self.deadzones().1
1128    }
1129
1130    /// Returns the [`DualAxisExclusion`] used by this deadzone.
1131    #[inline]
1132    pub fn exclusion(&self) -> DualAxisExclusion {
1133        DualAxisExclusion {
1134            exclusion_x: self.deadzone_x.exclusion(),
1135            exclusion_y: self.deadzone_y.exclusion(),
1136        }
1137    }
1138
1139    /// Returns the [`DualAxisBounds`] used by this deadzone.
1140    #[inline]
1141    pub fn bounds(&self) -> DualAxisBounds {
1142        DualAxisBounds::default()
1143    }
1144
1145    /// Is the given `input_value` within the exclusion ranges?
1146    #[must_use]
1147    #[inline]
1148    pub fn within_exclusion(&self, input_value: Vec2) -> BVec2 {
1149        BVec2::new(
1150            self.deadzone_x.within_exclusion(input_value.x),
1151            self.deadzone_y.within_exclusion(input_value.y),
1152        )
1153    }
1154
1155    /// Is the given `input_value` within the bounds?
1156    #[must_use]
1157    #[inline]
1158    pub fn within_bounds(&self, input_value: Vec2) -> BVec2 {
1159        BVec2::new(
1160            self.deadzone_x.within_bounds(input_value.x),
1161            self.deadzone_y.within_bounds(input_value.y),
1162        )
1163    }
1164
1165    /// Is the given `input_value` within the lower live zone?
1166    #[must_use]
1167    #[inline]
1168    pub fn within_livezone_lower(&self, input_value: Vec2) -> BVec2 {
1169        BVec2::new(
1170            self.deadzone_x.within_livezone_lower(input_value.x),
1171            self.deadzone_y.within_livezone_lower(input_value.y),
1172        )
1173    }
1174
1175    /// Is the given `input_value` within the upper live zone?
1176    #[must_use]
1177    #[inline]
1178    pub fn within_livezone_upper(&self, input_value: Vec2) -> BVec2 {
1179        BVec2::new(
1180            self.deadzone_x.within_livezone_upper(input_value.x),
1181            self.deadzone_y.within_livezone_upper(input_value.y),
1182        )
1183    }
1184
1185    /// Normalizes input values into the live zone.
1186    #[must_use]
1187    #[inline]
1188    pub fn normalize(&self, input_value: Vec2) -> Vec2 {
1189        Vec2::new(
1190            self.deadzone_x.normalize(input_value.x),
1191            self.deadzone_y.normalize(input_value.y),
1192        )
1193    }
1194}
1195
1196impl Default for DualAxisDeadZone {
1197    /// Creates a [`DualAxisDeadZone`] that excludes input values within the deadzone `[-0.1, 0.1]` on both axes.
1198    fn default() -> Self {
1199        AxisDeadZone::default().extend_dual()
1200    }
1201}
1202
1203impl From<DualAxisDeadZone> for DualAxisProcessor {
1204    fn from(value: DualAxisDeadZone) -> Self {
1205        Self::DeadZone(value)
1206    }
1207}
1208
1209impl AxisDeadZone {
1210    /// Creates a [`DualAxisDeadZone`] using `self` for both axes.
1211    #[inline]
1212    pub const fn extend_dual(self) -> DualAxisDeadZone {
1213        DualAxisDeadZone {
1214            deadzone_x: self,
1215            deadzone_y: self,
1216        }
1217    }
1218
1219    /// Creates a [`DualAxisDeadZone`] only using `self` for the X-axis.
1220    #[inline]
1221    pub const fn extend_dual_only_x(self) -> DualAxisDeadZone {
1222        DualAxisDeadZone {
1223            deadzone_x: self,
1224            ..DualAxisDeadZone::ZERO
1225        }
1226    }
1227
1228    /// Creates a [`DualAxisDeadZone`] only using `self` to the Y-axis.
1229    #[inline]
1230    pub const fn extend_dual_only_y(self) -> DualAxisDeadZone {
1231        DualAxisDeadZone {
1232            deadzone_y: self,
1233            ..DualAxisDeadZone::ZERO
1234        }
1235    }
1236
1237    /// Creates a [`DualAxisDeadZone`] using `self` to the Y-axis with the given `bounds_x` to the X-axis.
1238    #[inline]
1239    pub const fn extend_dual_with_x(self, deadzone_x: Self) -> DualAxisDeadZone {
1240        DualAxisDeadZone {
1241            deadzone_x,
1242            deadzone_y: self,
1243        }
1244    }
1245
1246    /// Creates a [`DualAxisDeadZone`] using `self` to the X-axis with the given `bounds_y` to the Y-axis.
1247    #[inline]
1248    pub const fn extend_dual_with_y(self, deadzone_y: Self) -> DualAxisDeadZone {
1249        DualAxisDeadZone {
1250            deadzone_x: self,
1251            deadzone_y,
1252        }
1253    }
1254}
1255
1256impl From<AxisDeadZone> for DualAxisProcessor {
1257    fn from(deadzone: AxisDeadZone) -> Self {
1258        Self::DeadZone(deadzone.extend_dual())
1259    }
1260}
1261
1262impl From<DualAxisExclusion> for DualAxisDeadZone {
1263    fn from(exclusion: DualAxisExclusion) -> Self {
1264        Self::new(
1265            exclusion.exclusion_x.min_max(),
1266            exclusion.exclusion_y.min_max(),
1267        )
1268    }
1269}
1270
1271#[cfg(test)]
1272mod tests {
1273    use super::*;
1274
1275    #[test]
1276    fn test_dual_axis_value_bounds() {
1277        fn test_bounds(
1278            bounds: DualAxisBounds,
1279            (x_min, x_max): (f32, f32),
1280            (y_min, y_max): (f32, f32),
1281        ) {
1282            assert_eq!(bounds.bounds_x().min_max(), (x_min, x_max));
1283            assert_eq!(bounds.bounds_y().min_max(), (y_min, y_max));
1284
1285            let bounds_x = AxisBounds::new(x_min, x_max);
1286            let bounds_y = AxisBounds::new(y_min, y_max);
1287            assert_eq!(bounds_x.extend_dual_with_y(bounds_y), bounds);
1288            assert_eq!(bounds_y.extend_dual_with_x(bounds_x), bounds);
1289
1290            let (bx, by) = bounds.bounds();
1291            assert_eq!(bx, bounds_x);
1292            assert_eq!(by, bounds_y);
1293
1294            assert_eq!(
1295                DualAxisProcessor::from(bounds_x),
1296                DualAxisProcessor::ValueBounds(DualAxisBounds::all(x_min, x_max))
1297            );
1298
1299            let processor = DualAxisProcessor::ValueBounds(bounds);
1300            assert_eq!(DualAxisProcessor::from(bounds), processor);
1301
1302            for x in -300..300 {
1303                let x = x as f32 * 0.01;
1304                for y in -300..300 {
1305                    let y = y as f32 * 0.01;
1306                    let value = Vec2::new(x, y);
1307
1308                    assert_eq!(processor.process(value), bounds.clamp(value));
1309
1310                    let expected = BVec2::new(bounds_x.contains(x), bounds_y.contains(y));
1311                    assert_eq!(bounds.contains(value), expected);
1312
1313                    let expected = Vec2::new(bounds_x.clamp(x), bounds_y.clamp(y));
1314                    assert_eq!(bounds.clamp(value), expected);
1315                }
1316            }
1317        }
1318
1319        let full_range = (f32::MIN, f32::MAX);
1320
1321        let bounds = DualAxisBounds::FULL_RANGE;
1322        test_bounds(bounds, full_range, full_range);
1323
1324        let bounds = DualAxisBounds::default();
1325        test_bounds(bounds, (-1.0, 1.0), (-1.0, 1.0));
1326
1327        let bounds = DualAxisBounds::new((-2.0, 2.5), (-1.0, 1.5));
1328        test_bounds(bounds, (-2.0, 2.5), (-1.0, 1.5));
1329
1330        let bounds = DualAxisBounds::all(-2.0, 2.5);
1331        test_bounds(bounds, (-2.0, 2.5), (-2.0, 2.5));
1332
1333        let bounds = DualAxisBounds::only_x(-2.0, 2.5);
1334        test_bounds(bounds, (-2.0, 2.5), full_range);
1335
1336        let bounds = DualAxisBounds::only_y(-1.0, 1.5);
1337        test_bounds(bounds, full_range, (-1.0, 1.5));
1338
1339        let bounds = DualAxisBounds::symmetric(2.0, 2.5);
1340        test_bounds(bounds, (-2.0, 2.0), (-2.5, 2.5));
1341
1342        let bounds = DualAxisBounds::symmetric_all(2.5);
1343        test_bounds(bounds, (-2.5, 2.5), (-2.5, 2.5));
1344
1345        let bounds = DualAxisBounds::symmetric_only_x(2.5);
1346        test_bounds(bounds, (-2.5, 2.5), full_range);
1347
1348        let bounds = DualAxisBounds::symmetric_only_y(2.5);
1349        test_bounds(bounds, full_range, (-2.5, 2.5));
1350
1351        let bounds = DualAxisBounds::at_least(2.0, 2.5);
1352        test_bounds(bounds, (2.0, f32::MAX), (2.5, f32::MAX));
1353
1354        let bounds = DualAxisBounds::at_least_all(2.5);
1355        test_bounds(bounds, (2.5, f32::MAX), (2.5, f32::MAX));
1356
1357        let bounds = DualAxisBounds::at_least_only_x(2.5);
1358        test_bounds(bounds, (2.5, f32::MAX), full_range);
1359
1360        let bounds = DualAxisBounds::at_least_only_y(2.5);
1361        test_bounds(bounds, full_range, (2.5, f32::MAX));
1362
1363        let bounds = DualAxisBounds::at_most(2.0, 2.5);
1364        test_bounds(bounds, (f32::MIN, 2.0), (f32::MIN, 2.5));
1365
1366        let bounds = DualAxisBounds::at_most_all(2.5);
1367        test_bounds(bounds, (f32::MIN, 2.5), (f32::MIN, 2.5));
1368
1369        let bounds = DualAxisBounds::at_most_only_x(2.5);
1370        test_bounds(bounds, (f32::MIN, 2.5), full_range);
1371
1372        let bounds = DualAxisBounds::at_most_only_y(2.5);
1373        test_bounds(bounds, full_range, (f32::MIN, 2.5));
1374
1375        let bounds_x = AxisBounds::new(-2.0, 2.5);
1376        let bounds_y = AxisBounds::new(-1.0, 1.5);
1377
1378        test_bounds(bounds_x.extend_dual(), (-2.0, 2.5), (-2.0, 2.5));
1379
1380        test_bounds(bounds_y.extend_dual(), (-1.0, 1.5), (-1.0, 1.5));
1381
1382        test_bounds(bounds_x.extend_dual_only_x(), (-2.0, 2.5), full_range);
1383
1384        test_bounds(bounds_y.extend_dual_only_y(), full_range, (-1.0, 1.5));
1385
1386        test_bounds(
1387            bounds_x.extend_dual_with_y(bounds_y),
1388            (-2.0, 2.5),
1389            (-1.0, 1.5),
1390        );
1391
1392        test_bounds(
1393            bounds_y.extend_dual_with_x(bounds_x),
1394            (-2.0, 2.5),
1395            (-1.0, 1.5),
1396        );
1397    }
1398
1399    #[test]
1400    fn test_dual_axis_exclusion() {
1401        fn test_exclusion(
1402            exclusion: DualAxisExclusion,
1403            (x_negative_max, x_positive_min): (f32, f32),
1404            (y_negative_max, y_positive_min): (f32, f32),
1405        ) {
1406            assert_eq!(
1407                exclusion.exclusion_x.min_max(),
1408                (x_negative_max, x_positive_min)
1409            );
1410            assert_eq!(
1411                exclusion.exclusion_y.min_max(),
1412                (y_negative_max, y_positive_min)
1413            );
1414
1415            let exclusion_x = AxisExclusion::new(x_negative_max, x_positive_min);
1416            let exclusion_y = AxisExclusion::new(y_negative_max, y_positive_min);
1417            assert_eq!(exclusion_x.extend_dual_with_y(exclusion_y), exclusion);
1418
1419            let (ex, ey) = exclusion.exclusions();
1420            assert_eq!(ex, exclusion_x);
1421            assert_eq!(ey, exclusion_y);
1422
1423            assert_eq!(
1424                DualAxisProcessor::from(exclusion_x),
1425                DualAxisProcessor::Exclusion(DualAxisExclusion::all(
1426                    x_negative_max,
1427                    x_positive_min
1428                ))
1429            );
1430
1431            let processor = DualAxisProcessor::Exclusion(exclusion);
1432            assert_eq!(DualAxisProcessor::from(exclusion), processor);
1433
1434            for x in -300..300 {
1435                let x = x as f32 * 0.01;
1436                for y in -300..300 {
1437                    let y = y as f32 * 0.01;
1438                    let value = Vec2::new(x, y);
1439
1440                    assert_eq!(processor.process(value), exclusion.exclude(value));
1441
1442                    assert_eq!(
1443                        exclusion.contains(value),
1444                        BVec2::new(exclusion_x.contains(x), exclusion_y.contains(y))
1445                    );
1446
1447                    assert_eq!(
1448                        exclusion.exclude(value),
1449                        Vec2::new(exclusion_x.exclude(x), exclusion_y.exclude(y))
1450                    );
1451                }
1452            }
1453        }
1454
1455        let zero_size = (0.0, 0.0);
1456
1457        let exclusion = DualAxisExclusion::ZERO;
1458        test_exclusion(exclusion, zero_size, zero_size);
1459
1460        let exclusion = DualAxisExclusion::default();
1461        test_exclusion(exclusion, (-0.1, 0.1), (-0.1, 0.1));
1462
1463        let exclusion = DualAxisExclusion::new((-0.2, 0.3), (-0.1, 0.4));
1464        test_exclusion(exclusion, (-0.2, 0.3), (-0.1, 0.4));
1465
1466        let exclusion = DualAxisExclusion::all(-0.2, 0.3);
1467        test_exclusion(exclusion, (-0.2, 0.3), (-0.2, 0.3));
1468
1469        let exclusion = DualAxisExclusion::only_x(-0.2, 0.3);
1470        test_exclusion(exclusion, (-0.2, 0.3), zero_size);
1471
1472        let exclusion = DualAxisExclusion::only_y(-0.1, 0.4);
1473        test_exclusion(exclusion, zero_size, (-0.1, 0.4));
1474
1475        let exclusion = DualAxisExclusion::symmetric(0.2, 0.3);
1476        test_exclusion(exclusion, (-0.2, 0.2), (-0.3, 0.3));
1477
1478        let exclusion = DualAxisExclusion::symmetric_all(0.3);
1479        test_exclusion(exclusion, (-0.3, 0.3), (-0.3, 0.3));
1480
1481        let exclusion = DualAxisExclusion::symmetric_only_x(0.3);
1482        test_exclusion(exclusion, (-0.3, 0.3), zero_size);
1483
1484        let exclusion = DualAxisExclusion::symmetric_only_y(0.3);
1485        test_exclusion(exclusion, zero_size, (-0.3, 0.3));
1486
1487        let exclusion_x = AxisExclusion::new(-0.2, 0.3);
1488        let exclusion_y = AxisExclusion::new(-0.1, 0.4);
1489
1490        test_exclusion(exclusion_x.extend_dual(), (-0.2, 0.3), (-0.2, 0.3));
1491
1492        test_exclusion(exclusion_y.extend_dual(), (-0.1, 0.4), (-0.1, 0.4));
1493
1494        test_exclusion(exclusion_x.extend_dual_only_x(), (-0.2, 0.3), zero_size);
1495
1496        test_exclusion(exclusion_y.extend_dual_only_y(), zero_size, (-0.1, 0.4));
1497
1498        test_exclusion(
1499            exclusion_x.extend_dual_with_y(exclusion_y),
1500            (-0.2, 0.3),
1501            (-0.1, 0.4),
1502        );
1503
1504        test_exclusion(
1505            exclusion_y.extend_dual_with_x(exclusion_x),
1506            (-0.2, 0.3),
1507            (-0.1, 0.4),
1508        );
1509    }
1510
1511    #[test]
1512    fn test_dual_axis_deadzone() {
1513        fn test_deadzone(
1514            deadzone: DualAxisDeadZone,
1515            (x_negative_max, x_positive_min): (f32, f32),
1516            (y_negative_max, y_positive_min): (f32, f32),
1517        ) {
1518            assert_eq!(
1519                deadzone.deadzone_x.exclusion().min_max(),
1520                (x_negative_max, x_positive_min)
1521            );
1522            assert_eq!(
1523                deadzone.deadzone_y.exclusion().min_max(),
1524                (y_negative_max, y_positive_min)
1525            );
1526
1527            let deadzone_x = AxisDeadZone::new(x_negative_max, x_positive_min);
1528            let deadzone_y = AxisDeadZone::new(y_negative_max, y_positive_min);
1529            assert_eq!(deadzone_x.extend_dual_with_y(deadzone_y), deadzone);
1530
1531            let exclusion = DualAxisExclusion::new(
1532                (x_negative_max, x_positive_min),
1533                (y_negative_max, y_positive_min),
1534            );
1535            assert_eq!(exclusion.scaled(), deadzone);
1536
1537            let (dx, dy) = deadzone.deadzones();
1538            assert_eq!(dx, deadzone_x);
1539            assert_eq!(dy, deadzone_y);
1540
1541            assert_eq!(
1542                DualAxisProcessor::from(deadzone_x),
1543                DualAxisProcessor::DeadZone(DualAxisDeadZone::all(x_negative_max, x_positive_min))
1544            );
1545
1546            let processor = DualAxisProcessor::DeadZone(deadzone);
1547            assert_eq!(DualAxisProcessor::from(deadzone), processor);
1548
1549            for x in -300..300 {
1550                let x = x as f32 * 0.01;
1551                for y in -300..300 {
1552                    let y = y as f32 * 0.01;
1553                    let value = Vec2::new(x, y);
1554
1555                    assert_eq!(processor.process(value), deadzone.normalize(value));
1556
1557                    assert_eq!(
1558                        deadzone.within_exclusion(value),
1559                        BVec2::new(
1560                            deadzone_x.within_exclusion(x),
1561                            deadzone_y.within_exclusion(y)
1562                        )
1563                    );
1564
1565                    assert_eq!(
1566                        deadzone.within_bounds(value),
1567                        BVec2::new(deadzone_x.within_bounds(x), deadzone_y.within_bounds(y))
1568                    );
1569
1570                    assert_eq!(
1571                        deadzone.within_livezone_lower(value),
1572                        BVec2::new(
1573                            deadzone_x.within_livezone_lower(x),
1574                            deadzone_y.within_livezone_lower(y)
1575                        )
1576                    );
1577
1578                    assert_eq!(
1579                        deadzone.within_livezone_upper(value),
1580                        BVec2::new(
1581                            deadzone_x.within_livezone_upper(x),
1582                            deadzone_y.within_livezone_upper(y)
1583                        )
1584                    );
1585
1586                    assert_eq!(
1587                        deadzone.normalize(value),
1588                        Vec2::new(deadzone_x.normalize(x), deadzone_y.normalize(y))
1589                    );
1590                }
1591            }
1592        }
1593
1594        let zero_size = (0.0, 0.0);
1595
1596        let deadzone = DualAxisDeadZone::ZERO;
1597        test_deadzone(deadzone, zero_size, zero_size);
1598
1599        let deadzone = DualAxisDeadZone::default();
1600        test_deadzone(deadzone, (-0.1, 0.1), (-0.1, 0.1));
1601
1602        let deadzone = DualAxisDeadZone::new((-0.2, 0.3), (-0.1, 0.4));
1603        test_deadzone(deadzone, (-0.2, 0.3), (-0.1, 0.4));
1604
1605        let deadzone = DualAxisDeadZone::all(-0.2, 0.3);
1606        test_deadzone(deadzone, (-0.2, 0.3), (-0.2, 0.3));
1607
1608        let deadzone = DualAxisDeadZone::only_x(-0.2, 0.3);
1609        test_deadzone(deadzone, (-0.2, 0.3), zero_size);
1610
1611        let deadzone = DualAxisDeadZone::only_y(-0.1, 0.4);
1612        test_deadzone(deadzone, zero_size, (-0.1, 0.4));
1613
1614        let deadzone = DualAxisDeadZone::symmetric(0.2, 0.3);
1615        test_deadzone(deadzone, (-0.2, 0.2), (-0.3, 0.3));
1616
1617        let deadzone = DualAxisDeadZone::symmetric_all(0.3);
1618        test_deadzone(deadzone, (-0.3, 0.3), (-0.3, 0.3));
1619
1620        let deadzone = DualAxisDeadZone::symmetric_only_x(0.3);
1621        test_deadzone(deadzone, (-0.3, 0.3), zero_size);
1622
1623        let deadzone = DualAxisDeadZone::symmetric_only_y(0.3);
1624        test_deadzone(deadzone, zero_size, (-0.3, 0.3));
1625
1626        let deadzone_x = AxisDeadZone::new(-0.2, 0.3);
1627        let deadzone_y = AxisDeadZone::new(-0.1, 0.4);
1628
1629        test_deadzone(deadzone_x.extend_dual(), (-0.2, 0.3), (-0.2, 0.3));
1630
1631        test_deadzone(deadzone_y.extend_dual(), (-0.1, 0.4), (-0.1, 0.4));
1632
1633        test_deadzone(deadzone_x.extend_dual_only_x(), (-0.2, 0.3), zero_size);
1634
1635        test_deadzone(deadzone_y.extend_dual_only_y(), zero_size, (-0.1, 0.4));
1636
1637        test_deadzone(
1638            deadzone_x.extend_dual_with_y(deadzone_y),
1639            (-0.2, 0.3),
1640            (-0.1, 0.4),
1641        );
1642
1643        test_deadzone(
1644            deadzone_y.extend_dual_with_x(deadzone_x),
1645            (-0.2, 0.3),
1646            (-0.1, 0.4),
1647        );
1648    }
1649}