leafwing_input_manager/input_processing/single_axis/
mod.rs

1//! Processors for single-axis input values
2
3use std::hash::{Hash, Hasher};
4
5use bevy::{math::FloatOrd, prelude::Reflect};
6use serde::{Deserialize, Serialize};
7
8pub use self::custom::*;
9pub use self::range::*;
10
11mod custom;
12mod range;
13
14/// A processor for single-axis input values,
15/// accepting a `f32` input and producing a `f32` output.
16#[must_use]
17#[non_exhaustive]
18#[derive(Debug, Clone, PartialEq, Reflect, Serialize, Deserialize)]
19pub enum AxisProcessor {
20    /// Converts input values into three discrete values,
21    /// similar to [`f32::signum()`] but returning `0.0` for zero values.
22    ///
23    /// ```rust
24    /// use leafwing_input_manager::prelude::*;
25    ///
26    /// // 1.0 for positive values
27    /// assert_eq!(AxisProcessor::Digital.process(2.5), 1.0);
28    /// assert_eq!(AxisProcessor::Digital.process(0.5), 1.0);
29    ///
30    /// // 0.0 for zero values
31    /// assert_eq!(AxisProcessor::Digital.process(0.0), 0.0);
32    /// assert_eq!(AxisProcessor::Digital.process(-0.0), 0.0);
33    ///
34    /// // -1.0 for negative values
35    /// assert_eq!(AxisProcessor::Digital.process(-0.5), -1.0);
36    /// assert_eq!(AxisProcessor::Digital.process(-2.5), -1.0);
37    /// ```
38    Digital,
39
40    /// Flips the sign of input values, resulting in a directional reversal of control.
41    ///
42    /// ```rust
43    /// use leafwing_input_manager::prelude::*;
44    ///
45    /// assert_eq!(AxisProcessor::Inverted.process(2.5), -2.5);
46    /// assert_eq!(AxisProcessor::Inverted.process(-2.5), 2.5);
47    /// ```
48    Inverted,
49
50    /// Scales input values using a specified multiplier to fine-tune the responsiveness of control.
51    ///
52    /// ```rust
53    /// use leafwing_input_manager::prelude::*;
54    ///
55    /// // Doubled!
56    /// assert_eq!(AxisProcessor::Sensitivity(2.0).process(2.0), 4.0);
57    ///
58    /// // Halved!
59    /// assert_eq!(AxisProcessor::Sensitivity(0.5).process(2.0), 1.0);
60    ///
61    /// // Negated and halved!
62    /// assert_eq!(AxisProcessor::Sensitivity(-0.5).process(2.0), -1.0);
63    /// ```
64    Sensitivity(f32),
65
66    /// A wrapper around [`AxisBounds`] to represent value bounds.
67    ValueBounds(AxisBounds),
68
69    /// A wrapper around [`AxisExclusion`] to represent unscaled deadzone.
70    Exclusion(AxisExclusion),
71
72    /// A wrapper around [`AxisDeadZone`] to represent scaled deadzone.
73    DeadZone(AxisDeadZone),
74
75    /// A user-defined processor that implements [`CustomAxisProcessor`].
76    Custom(Box<dyn CustomAxisProcessor>),
77}
78
79impl AxisProcessor {
80    /// Computes the result by processing the `input_value`.
81    #[must_use]
82    #[inline]
83    pub fn process(&self, input_value: f32) -> f32 {
84        match self {
85            Self::Digital => {
86                if input_value == 0.0 {
87                    0.0
88                } else {
89                    input_value.signum()
90                }
91            }
92            Self::Inverted => -input_value,
93            Self::Sensitivity(sensitivity) => sensitivity * input_value,
94            Self::ValueBounds(bounds) => bounds.clamp(input_value),
95            Self::Exclusion(exclusion) => exclusion.exclude(input_value),
96            Self::DeadZone(deadzone) => deadzone.normalize(input_value),
97            Self::Custom(processor) => processor.process(input_value),
98        }
99    }
100}
101
102impl Eq for AxisProcessor {}
103
104impl Hash for AxisProcessor {
105    fn hash<H: Hasher>(&self, state: &mut H) {
106        std::mem::discriminant(self).hash(state);
107        match self {
108            Self::Digital => {}
109            Self::Inverted => {}
110            Self::Sensitivity(sensitivity) => FloatOrd(*sensitivity).hash(state),
111            Self::ValueBounds(bounds) => bounds.hash(state),
112            Self::Exclusion(exclusion) => exclusion.hash(state),
113            Self::DeadZone(deadzone) => deadzone.hash(state),
114            Self::Custom(processor) => processor.hash(state),
115        }
116    }
117}
118
119/// Provides methods for configuring and manipulating the processing pipeline for single-axis input.
120pub trait WithAxisProcessingPipelineExt: Sized {
121    /// Resets the processing pipeline, removing any currently applied processors.
122    fn reset_processing_pipeline(self) -> Self;
123
124    /// Replaces the current processing pipeline with the given [`AxisProcessor`]s.
125    fn replace_processing_pipeline(
126        self,
127        processors: impl IntoIterator<Item = AxisProcessor>,
128    ) -> Self;
129
130    /// Appends the given [`AxisProcessor`] as the next processing step.
131    fn with_processor(self, processor: impl Into<AxisProcessor>) -> Self;
132
133    /// Appends an [`AxisProcessor::Digital`] processor as the next processing step,
134    /// similar to [`f32::signum`] but returning `0.0` for zero values.
135    #[inline]
136    fn digital(self) -> Self {
137        self.with_processor(AxisProcessor::Digital)
138    }
139
140    /// Appends an [`AxisProcessor::Inverted`] processor as the next processing step,
141    /// flipping the sign of values on the axis.
142    #[inline]
143    fn inverted(self) -> Self {
144        self.with_processor(AxisProcessor::Inverted)
145    }
146
147    /// Appends an [`AxisProcessor::Sensitivity`] processor as the next processing step,
148    /// multiplying values on the axis with the given sensitivity factor.
149    #[inline]
150    fn sensitivity(self, sensitivity: f32) -> Self {
151        self.with_processor(AxisProcessor::Sensitivity(sensitivity))
152    }
153
154    /// Appends an [`AxisBounds`] processor as the next processing step,
155    /// restricting values within the range `[min, max]` on the axis.
156    #[inline]
157    fn with_bounds(self, min: f32, max: f32) -> Self {
158        self.with_processor(AxisBounds::new(min, max))
159    }
160
161    /// Appends an [`AxisBounds`] processor as the next processing step,
162    /// restricting values within the range `[-threshold, threshold]`.
163    #[inline]
164    fn with_bounds_symmetric(self, threshold: f32) -> Self {
165        self.with_processor(AxisBounds::symmetric(threshold))
166    }
167
168    /// Appends an [`AxisBounds`] processor as the next processing step,
169    /// restricting values to a minimum value.
170    #[inline]
171    fn at_least(self, min: f32) -> Self {
172        self.with_processor(AxisBounds::at_least(min))
173    }
174
175    /// Appends an [`AxisBounds`] processor as the next processing step,
176    /// restricting values to a maximum value.
177    #[inline]
178    fn at_most(self, max: f32) -> Self {
179        self.with_processor(AxisBounds::at_most(max))
180    }
181
182    /// Appends an [`AxisDeadZone`] processor as the next processing step,
183    /// excluding values within the dead zone range `[negative_max, positive_min]` on the axis,
184    /// treating them as zeros, then normalizing non-excluded input values into the "live zone",
185    /// the remaining range within the [`AxisBounds::magnitude(1.0)`](AxisBounds::default)
186    /// after dead zone exclusion.
187    ///
188    /// # Requirements
189    ///
190    /// - `negative_max` <= `0.0` <= `positive_min`.
191    ///
192    /// # Panics
193    ///
194    /// Panics if the requirements aren't met.
195    #[inline]
196    fn with_deadzone(self, negative_max: f32, positive_min: f32) -> Self {
197        self.with_processor(AxisDeadZone::new(negative_max, positive_min))
198    }
199
200    /// Appends an [`AxisDeadZone`] processor as the next processing step,
201    /// excluding values within the dead zone range `[-threshold, threshold]` on the axis,
202    /// treating them as zeros, then normalizing non-excluded input values into the "live zone",
203    /// the remaining range within the [`AxisBounds::magnitude(1.0)`](AxisBounds::default)
204    /// after dead zone exclusion.
205    ///
206    /// # Requirements
207    ///
208    /// - `threshold` >= `0.0`.
209    ///
210    /// # Panics
211    ///
212    /// Panics if the requirements aren't met.
213    #[inline]
214    fn with_deadzone_symmetric(self, threshold: f32) -> Self {
215        self.with_processor(AxisDeadZone::symmetric(threshold))
216    }
217
218    /// Appends an [`AxisDeadZone`] processor as the next processing step,
219    /// only passing positive values that greater than `positive_min`
220    /// and then normalizing them into the "live zone" range `[positive_min, 1.0]`.
221    ///
222    /// # Requirements
223    ///
224    /// - `positive_min` >= `0.0`.
225    ///
226    /// # Panics
227    ///
228    /// Panics if the requirements aren't met.
229    #[inline]
230    fn only_positive(self, positive_min: f32) -> Self {
231        self.with_processor(AxisDeadZone::only_positive(positive_min))
232    }
233
234    /// Appends an [`AxisDeadZone`] processor as the next processing step,
235    /// only passing negative values that less than `negative_max`
236    /// and then normalizing them into the "live zone" range `[-1.0, negative_max]`.
237    ///
238    /// # Requirements
239    ///
240    /// - `negative_max` <= `0.0`.
241    ///
242    /// # Panics
243    ///
244    /// Panics if the requirements aren't met.
245    #[inline]
246    fn only_negative(self, negative_max: f32) -> Self {
247        self.with_processor(AxisDeadZone::only_negative(negative_max))
248    }
249
250    /// Appends an [`AxisExclusion`] processor as the next processing step,
251    /// ignoring values within the dead zone range `[negative_max, positive_min]` on the axis,
252    /// treating them as zeros.
253    ///
254    /// # Requirements
255    ///
256    /// - `negative_max` <= `0.0` <= `positive_min`.
257    ///
258    /// # Panics
259    ///
260    /// Panics if the requirements aren't met.
261    #[inline]
262    fn with_deadzone_unscaled(self, negative_max: f32, positive_min: f32) -> Self {
263        self.with_processor(AxisExclusion::new(negative_max, positive_min))
264    }
265
266    /// Appends an [`AxisExclusion`] processor as the next processing step,
267    /// ignoring values within the dead zone range `[-threshold, threshold]` on the axis,
268    /// treating them as zeros.
269    ///
270    /// # Requirements
271    ///
272    /// - `threshold` >= `0.0`.
273    ///
274    /// # Panics
275    ///
276    /// Panics if the requirements aren't met.
277    #[inline]
278    fn with_deadzone_symmetric_unscaled(self, threshold: f32) -> Self {
279        self.with_processor(AxisExclusion::symmetric(threshold))
280    }
281
282    /// Appends an [`AxisExclusion`] processor as the next processing step,
283    /// only passing positive values that greater than `positive_min`.
284    ///
285    /// # Requirements
286    ///
287    /// - `positive_min` >= `0.0`.
288    ///
289    /// # Panics
290    ///
291    /// Panics if the requirements aren't met.
292    #[inline]
293    fn only_positive_unscaled(self, positive_min: f32) -> Self {
294        self.with_processor(AxisExclusion::only_positive(positive_min))
295    }
296
297    /// Appends an [`AxisExclusion`] processor as the next processing step,
298    /// only passing negative values that less than `negative_max`.
299    ///
300    /// # Requirements
301    ///
302    /// - `negative_max` <= `0.0`.
303    ///
304    /// # Panics
305    ///
306    /// Panics if the requirements aren't met.
307    #[inline]
308    fn only_negative_unscaled(self, negative_max: f32) -> Self {
309        self.with_processor(AxisExclusion::only_negative(negative_max))
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn test_axis_inversion_processor() {
319        for value in -300..300 {
320            let value = value as f32 * 0.01;
321
322            assert_eq!(AxisProcessor::Inverted.process(value), -value);
323            assert_eq!(AxisProcessor::Inverted.process(-value), value);
324        }
325    }
326
327    #[test]
328    fn test_axis_sensitivity_processor() {
329        for value in -300..300 {
330            let value = value as f32 * 0.01;
331
332            for sensitivity in -300..300 {
333                let sensitivity = sensitivity as f32 * 0.01;
334
335                let processor = AxisProcessor::Sensitivity(sensitivity);
336                assert_eq!(processor.process(value), sensitivity * value);
337            }
338        }
339    }
340}