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}