From 43db44ca3a9b80fb1a48bbb1d1285de8af238e9a Mon Sep 17 00:00:00 2001 From: Benjamin Brienen Date: Fri, 3 Jan 2025 17:27:59 -0500 Subject: [PATCH] Scale input to account for deadzones (#17015) # Objective Fixes #3450 ## Solution Scale the input to account for the range ## Testing Updated unit tests ## Migration Guide `GamepadButtonChangedEvent.value` is now linearly rescaled to be from `0.0..=1.0` (instead of `low..=high`) and `GamepadAxisChangedEvent.value` is now linearly rescaled to be from `-1.0..=0.0`/`0.0..=1.0` (accounting for the deadzone). --- crates/bevy_input/src/gamepad.rs | 408 +++++++++++++++++++++---------- 1 file changed, 279 insertions(+), 129 deletions(-) diff --git a/crates/bevy_input/src/gamepad.rs b/crates/bevy_input/src/gamepad.rs index cf4ad9e315..be122d7320 100644 --- a/crates/bevy_input/src/gamepad.rs +++ b/crates/bevy_input/src/gamepad.rs @@ -1,5 +1,7 @@ //! The gamepad input functionality. +use core::ops::RangeInclusive; + use crate::{Axis, ButtonInput, ButtonState}; use alloc::string::String; #[cfg(feature = "bevy_reflect")] @@ -30,7 +32,7 @@ use thiserror::Error; /// [`GamepadButtonChangedEvent`] and [`GamepadAxisChangedEvent`] when /// the in-frame relative ordering of events is important. /// -/// This event is produced by `bevy_input` +/// This event is produced by `bevy_input`. #[derive(Event, Debug, Clone, PartialEq, From)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -70,7 +72,7 @@ pub enum RawGamepadEvent { Axis(RawGamepadAxisChangedEvent), } -/// [`GamepadButton`] changed event unfiltered by [`GamepadSettings`] +/// [`GamepadButton`] changed event unfiltered by [`GamepadSettings`]. #[derive(Event, Debug, Copy, Clone, PartialEq)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -98,7 +100,7 @@ impl RawGamepadButtonChangedEvent { } } -/// [`GamepadAxis`] changed event unfiltered by [`GamepadSettings`] +/// [`GamepadAxis`] changed event unfiltered by [`GamepadSettings`]. #[derive(Event, Debug, Copy, Clone, PartialEq)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -151,18 +153,18 @@ impl GamepadConnectionEvent { } } - /// Is the gamepad connected? + /// Whether the gamepad is connected. pub fn connected(&self) -> bool { matches!(self.connection, GamepadConnection::Connected { .. }) } - /// Is the gamepad disconnected? + /// Whether the gamepad is disconnected. pub fn disconnected(&self) -> bool { !self.connected() } } -/// [`GamepadButton`] event triggered by a digital state change +/// [`GamepadButton`] event triggered by a digital state change. #[derive(Event, Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -180,7 +182,7 @@ pub struct GamepadButtonStateChangedEvent { } impl GamepadButtonStateChangedEvent { - /// Creates a new [`GamepadButtonStateChangedEvent`] + /// Creates a new [`GamepadButtonStateChangedEvent`]. pub fn new(entity: Entity, button: GamepadButton, state: ButtonState) -> Self { Self { entity, @@ -190,7 +192,7 @@ impl GamepadButtonStateChangedEvent { } } -/// [`GamepadButton`] event triggered by an analog state change +/// [`GamepadButton`] event triggered by an analog state change. #[derive(Event, Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -205,12 +207,12 @@ pub struct GamepadButtonChangedEvent { pub button: GamepadButton, /// The pressed state of the button. pub state: ButtonState, - /// The analog value of the button. + /// The analog value of the button (rescaled to be in the 0.0..=1.0 range). pub value: f32, } impl GamepadButtonChangedEvent { - /// Creates a new [`GamepadButtonChangedEvent`] + /// Creates a new [`GamepadButtonChangedEvent`]. pub fn new(entity: Entity, button: GamepadButton, state: ButtonState, value: f32) -> Self { Self { entity, @@ -221,7 +223,7 @@ impl GamepadButtonChangedEvent { } } -/// [`GamepadAxis`] event triggered by an analog state change +/// [`GamepadAxis`] event triggered by an analog state change. #[derive(Event, Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] @@ -234,12 +236,12 @@ pub struct GamepadAxisChangedEvent { pub entity: Entity, /// The gamepad axis assigned to the event. pub axis: GamepadAxis, - /// The value of this axis. + /// The value of this axis (rescaled to account for axis settings). pub value: f32, } impl GamepadAxisChangedEvent { - /// Creates a new [`GamepadAxisChangedEvent`] + /// Creates a new [`GamepadAxisChangedEvent`]. pub fn new(entity: Entity, axis: GamepadAxis, value: f32) -> Self { Self { entity, @@ -339,12 +341,10 @@ pub struct Gamepad { /// The USB vendor ID as assigned by the USB-IF, if available. pub(crate) vendor_id: Option, - /// The USB product ID as assigned by the [vendor], if available. - /// - /// [vendor]: Self::vendor_id + /// The USB product ID as assigned by the [vendor][Self::vendor_id], if available. pub(crate) product_id: Option, - /// [`ButtonInput`] of [`GamepadButton`] representing their digital state + /// [`ButtonInput`] of [`GamepadButton`] representing their digital state. pub(crate) digital: ButtonInput, /// [`Axis`] of [`GamepadButton`] representing their analog state. @@ -378,7 +378,7 @@ impl Gamepad { self.analog.get_unclamped(input.into()) } - /// Returns the left stick as a [`Vec2`] + /// Returns the left stick as a [`Vec2`]. pub fn left_stick(&self) -> Vec2 { Vec2 { x: self.get(GamepadAxis::LeftStickX).unwrap_or(0.0), @@ -386,7 +386,7 @@ impl Gamepad { } } - /// Returns the right stick as a [`Vec2`] + /// Returns the right stick as a [`Vec2`]. pub fn right_stick(&self) -> Vec2 { Vec2 { x: self.get(GamepadAxis::RightStickX).unwrap_or(0.0), @@ -394,7 +394,7 @@ impl Gamepad { } } - /// Returns the directional pad as a [`Vec2`] + /// Returns the directional pad as a [`Vec2`]. pub fn dpad(&self) -> Vec2 { Vec2 { x: self.get(GamepadButton::DPadRight).unwrap_or(0.0) @@ -480,14 +480,12 @@ impl Gamepad { self.digital.get_just_released() } - /// Returns an iterator over all analog [axes]. - /// - /// [axes]: GamepadInput + /// Returns an iterator over all analog [axes][GamepadInput]. pub fn get_analog_axes(&self) -> impl Iterator { self.analog.all_axes() } - /// [`ButtonInput`] of [`GamepadButton`] representing their digital state + /// [`ButtonInput`] of [`GamepadButton`] representing their digital state. pub fn digital(&self) -> &ButtonInput { &self.digital } @@ -531,7 +529,7 @@ impl Default for Gamepad { /// /// ## Usage /// -/// This is used to determine which button has changed its value when receiving gamepad button events +/// This is used to determine which button has changed its value when receiving gamepad button events. /// It is also used in the [`Gamepad`] component. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[cfg_attr( @@ -593,7 +591,7 @@ pub enum GamepadButton { } impl GamepadButton { - /// Returns an array of all the standard [`GamepadButton`] + /// Returns an array of all the standard [`GamepadButton`]. pub const fn all() -> [GamepadButton; 19] { [ GamepadButton::South, @@ -619,7 +617,7 @@ impl GamepadButton { } } -/// Represents gamepad input types that are mapped in the range [-1.0, 1.0] +/// Represents gamepad input types that are mapped in the range [-1.0, 1.0]. /// /// ## Usage /// @@ -665,14 +663,14 @@ impl GamepadAxis { } } -/// Encapsulation over [`GamepadAxis`] and [`GamepadButton`] +/// Encapsulation over [`GamepadAxis`] and [`GamepadButton`]. // This is done so Gamepad can share a single Axis and simplifies the API by having only one get/get_unclamped method #[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, From)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] pub enum GamepadInput { - /// A [`GamepadAxis`] + /// A [`GamepadAxis`]. Axis(GamepadAxis), - /// A [`GamepadButton`] + /// A [`GamepadButton`]. Button(GamepadButton), } @@ -928,9 +926,9 @@ impl ButtonSettings { /// threshold for an axis. /// Values that are higher than `livezone_upperbound` will be rounded up to 1.0. /// Values that are lower than `livezone_lowerbound` will be rounded down to -1.0. -/// Values that are in-between `deadzone_lowerbound` and `deadzone_upperbound` will be rounded -/// to 0.0. -/// Otherwise, values will not be rounded. +/// Values that are in-between `deadzone_lowerbound` and `deadzone_upperbound` will be rounded to 0.0. +/// Otherwise, values will be linearly rescaled to fit into the sensitivity range. +/// For example, a value that is one fourth of the way from `deadzone_upperbound` to `livezone_upperbound` will be scaled to 0.25. /// /// The valid range is `[-1.0, 1.0]`. #[derive(Debug, Clone, PartialEq)] @@ -1041,7 +1039,7 @@ impl AxisSettings { /// /// # Errors /// - /// If the value passed is less than the dead zone upper bound, + /// If the value passed is less than the deadzone upper bound, /// returns `AxisSettingsError::DeadZoneUpperBoundGreaterThanLiveZoneUpperBound`. /// If the value passed is not in range [0.0..=1.0], returns `AxisSettingsError::LiveZoneUpperBoundOutOfRange`. pub fn try_set_livezone_upperbound(&mut self, value: f32) -> Result<(), AxisSettingsError> { @@ -1117,7 +1115,7 @@ impl AxisSettings { /// /// # Errors /// - /// If the value passed is less than the dead zone lower bound, + /// If the value passed is less than the deadzone lower bound, /// returns `AxisSettingsError::LiveZoneLowerBoundGreaterThanDeadZoneLowerBound`. /// If the value passed is not in range [-1.0..=0.0], returns `AxisSettingsError::LiveZoneLowerBoundOutOfRange`. pub fn try_set_livezone_lowerbound(&mut self, value: f32) -> Result<(), AxisSettingsError> { @@ -1213,39 +1211,135 @@ impl AxisSettings { } /// Clamps the `raw_value` according to the `AxisSettings`. - pub fn clamp(&self, new_value: f32) -> f32 { - if self.deadzone_lowerbound <= new_value && new_value <= self.deadzone_upperbound { + pub fn clamp(&self, raw_value: f32) -> f32 { + if self.deadzone_lowerbound <= raw_value && raw_value <= self.deadzone_upperbound { 0.0 - } else if new_value >= self.livezone_upperbound { + } else if raw_value >= self.livezone_upperbound { 1.0 - } else if new_value <= self.livezone_lowerbound { + } else if raw_value <= self.livezone_lowerbound { -1.0 } else { - new_value + raw_value } } - /// Determines whether the change from `old_value` to `new_value` should + /// Determines whether the change from `old_raw_value` to `new_raw_value` should /// be registered as a change, according to the [`AxisSettings`]. - fn should_register_change(&self, new_value: f32, old_value: Option) -> bool { - if old_value.is_none() { - return true; + fn should_register_change(&self, new_raw_value: f32, old_raw_value: Option) -> bool { + match old_raw_value { + None => true, + Some(old_raw_value) => ops::abs(new_raw_value - old_raw_value) >= self.threshold, } - - ops::abs(new_value - old_value.unwrap()) > self.threshold } - /// Filters the `new_value` based on the `old_value`, according to the [`AxisSettings`]. + /// Filters the `new_raw_value` based on the `old_raw_value`, according to the [`AxisSettings`]. /// - /// Returns the clamped `new_value` if the change exceeds the settings threshold, + /// Returns the clamped and scaled `new_raw_value` if the change exceeds the settings threshold, /// and `None` otherwise. - pub fn filter(&self, new_value: f32, old_value: Option) -> Option { - let new_value = self.clamp(new_value); - - if self.should_register_change(new_value, old_value) { - return Some(new_value); + fn filter( + &self, + new_raw_value: f32, + old_raw_value: Option, + ) -> Option { + let clamped_unscaled = self.clamp(new_raw_value); + match self.should_register_change(clamped_unscaled, old_raw_value) { + true => Some(FilteredAxisPosition { + scaled: self.get_axis_position_from_value(clamped_unscaled), + raw: new_raw_value, + }), + false => None, + } + } + + #[inline(always)] + fn get_axis_position_from_value(&self, value: f32) -> ScaledAxisWithDeadZonePosition { + if value < self.deadzone_upperbound && value > self.deadzone_lowerbound { + ScaledAxisWithDeadZonePosition::Dead + } else if value > self.livezone_upperbound { + ScaledAxisWithDeadZonePosition::AboveHigh + } else if value < self.livezone_lowerbound { + ScaledAxisWithDeadZonePosition::BelowLow + } else if value >= self.deadzone_upperbound { + ScaledAxisWithDeadZonePosition::High(linear_remapping( + value, + self.deadzone_upperbound..=self.livezone_upperbound, + 0.0..=1.0, + )) + } else if value <= self.deadzone_lowerbound { + ScaledAxisWithDeadZonePosition::Low(linear_remapping( + value, + self.livezone_lowerbound..=self.deadzone_lowerbound, + -1.0..=0.0, + )) + } else { + unreachable!(); + } + } +} + +/// A linear remapping of `value` from `old` to `new`. +fn linear_remapping(value: f32, old: RangeInclusive, new: RangeInclusive) -> f32 { + // https://stackoverflow.com/a/929104 + ((value - old.start()) / (old.end() - old.start())) * (new.end() - new.start()) + new.start() +} + +#[derive(Debug, Clone, Copy)] +/// Deadzone-aware axis position. +enum ScaledAxisWithDeadZonePosition { + /// The input clipped below the valid range of the axis. + BelowLow, + /// The input is lower than the deadzone. + Low(f32), + /// The input falls within the deadzone, meaning it is counted as 0. + Dead, + /// The input is higher than the deadzone. + High(f32), + /// The input clipped above the valid range of the axis. + AboveHigh, +} + +struct FilteredAxisPosition { + scaled: ScaledAxisWithDeadZonePosition, + raw: f32, +} + +impl ScaledAxisWithDeadZonePosition { + /// Converts the value into a float in the range [-1, 1]. + fn to_f32(self) -> f32 { + match self { + ScaledAxisWithDeadZonePosition::BelowLow => -1., + ScaledAxisWithDeadZonePosition::Low(scaled) + | ScaledAxisWithDeadZonePosition::High(scaled) => scaled, + ScaledAxisWithDeadZonePosition::Dead => 0., + ScaledAxisWithDeadZonePosition::AboveHigh => 1., + } + } +} + +#[derive(Debug, Clone, Copy)] +/// Low/High-aware axis position. +enum ScaledAxisPosition { + /// The input fell short of the "low" value. + ClampedLow, + /// The input was in the normal range. + Scaled(f32), + /// The input surpassed the "high" value. + ClampedHigh, +} + +struct FilteredButtonAxisPosition { + scaled: ScaledAxisPosition, + raw: f32, +} + +impl ScaledAxisPosition { + /// Converts the value into a float in the range [0, 1]. + fn to_f32(self) -> f32 { + match self { + ScaledAxisPosition::ClampedLow => 0., + ScaledAxisPosition::Scaled(scaled) => scaled, + ScaledAxisPosition::ClampedHigh => 1., } - None } } @@ -1300,27 +1394,48 @@ impl ButtonAxisSettings { raw_value } - /// Determines whether the change from an `old_value` to a `new_value` should + /// Determines whether the change from an `old_raw_value` to a `new_raw_value` should /// be registered as a change event, according to the specified settings. - fn should_register_change(&self, new_value: f32, old_value: Option) -> bool { - if old_value.is_none() { - return true; + fn should_register_change(&self, new_raw_value: f32, old_raw_value: Option) -> bool { + match old_raw_value { + None => true, + Some(old_raw_value) => ops::abs(new_raw_value - old_raw_value) >= self.threshold, } - - ops::abs(new_value - old_value.unwrap()) > self.threshold } - /// Filters the `new_value` based on the `old_value`, according to the [`ButtonAxisSettings`]. + /// Filters the `new_raw_value` based on the `old_raw_value`, according to the [`ButtonAxisSettings`]. /// - /// Returns the clamped `new_value`, according to the [`ButtonAxisSettings`], if the change + /// Returns the clamped and scaled `new_raw_value`, according to the [`ButtonAxisSettings`], if the change /// exceeds the settings threshold, and `None` otherwise. - pub fn filter(&self, new_value: f32, old_value: Option) -> Option { - let new_value = self.clamp(new_value); - - if self.should_register_change(new_value, old_value) { - return Some(new_value); + fn filter( + &self, + new_raw_value: f32, + old_raw_value: Option, + ) -> Option { + let clamped_unscaled = self.clamp(new_raw_value); + match self.should_register_change(clamped_unscaled, old_raw_value) { + true => Some(FilteredButtonAxisPosition { + scaled: self.get_axis_position_from_value(clamped_unscaled), + raw: new_raw_value, + }), + false => None, + } + } + + /// Clamps and scales the `value` according to the specified settings. + /// + /// If the `value` is: + /// - lower than or equal to `low` it will be rounded to 0.0. + /// - higher than or equal to `high` it will be rounded to 1.0. + /// - Otherwise, it will be scaled from (low, high) to (0, 1). + fn get_axis_position_from_value(&self, value: f32) -> ScaledAxisPosition { + if value <= self.low { + ScaledAxisPosition::ClampedLow + } else if value >= self.high { + ScaledAxisPosition::ClampedHigh + } else { + ScaledAxisPosition::Scaled(linear_remapping(value, self.low..=self.high, 0.0..=1.0)) } - None } } @@ -1328,7 +1443,7 @@ impl ButtonAxisSettings { /// /// On connection, adds the components representing a [`Gamepad`] to the entity. /// On disconnection, removes the [`Gamepad`] and other related components. -/// Entities are left alive and might leave components like [`GamepadSettings`] to preserve state in the case of a reconnection +/// Entities are left alive and might leave components like [`GamepadSettings`] to preserve state in the case of a reconnection. /// /// ## Note /// @@ -1441,9 +1556,9 @@ pub fn gamepad_event_processing_system( else { continue; }; - - gamepad_axis.analog.set(axis, filtered_value); - let send_event = GamepadAxisChangedEvent::new(gamepad, axis, filtered_value); + gamepad_axis.analog.set(axis, filtered_value.raw); + let send_event = + GamepadAxisChangedEvent::new(gamepad, axis, filtered_value.scaled.to_f32()); processed_axis_events.send(send_event); processed_events.send(GamepadEvent::from(send_event)); } @@ -1463,9 +1578,9 @@ pub fn gamepad_event_processing_system( continue; }; let button_settings = settings.get_button_settings(button); - gamepad_buttons.analog.set(button, filtered_value); + gamepad_buttons.analog.set(button, filtered_value.raw); - if button_settings.is_released(filtered_value) { + if button_settings.is_released(filtered_value.raw) { // Check if button was previously pressed if gamepad_buttons.pressed(button) { processed_digital_events.send(GamepadButtonStateChangedEvent::new( @@ -1477,7 +1592,7 @@ pub fn gamepad_event_processing_system( // We don't have to check if the button was previously pressed here // because that check is performed within Input::release() gamepad_buttons.digital.release(button); - } else if button_settings.is_pressed(filtered_value) { + } else if button_settings.is_pressed(filtered_value.raw) { // Check if button was previously not pressed if !gamepad_buttons.pressed(button) { processed_digital_events.send(GamepadButtonStateChangedEvent::new( @@ -1494,8 +1609,12 @@ pub fn gamepad_event_processing_system( } else { ButtonState::Released }; - let send_event = - GamepadButtonChangedEvent::new(gamepad, button, button_state, filtered_value); + let send_event = GamepadButtonChangedEvent::new( + gamepad, + button, + button_state, + filtered_value.scaled.to_f32(), + ); processed_analog_events.send(send_event); processed_events.send(GamepadEvent::from(send_event)); } @@ -1649,130 +1768,161 @@ mod tests { fn test_button_axis_settings_filter( settings: ButtonAxisSettings, - new_value: f32, - old_value: Option, + new_raw_value: f32, + old_raw_value: Option, expected: Option, ) { - let actual = settings.filter(new_value, old_value); + let actual = settings + .filter(new_raw_value, old_raw_value) + .map(|f| f.scaled.to_f32()); assert_eq!( expected, actual, - "Testing filtering for {settings:?} with new_value = {new_value:?}, old_value = {old_value:?}", + "Testing filtering for {settings:?} with new_raw_value = {new_raw_value:?}, old_raw_value = {old_raw_value:?}", ); } #[test] fn test_button_axis_settings_default_filter() { let cases = [ + // clamped (1.0, None, Some(1.0)), (0.99, None, Some(1.0)), (0.96, None, Some(1.0)), (0.95, None, Some(1.0)), - (0.9499, None, Some(0.9499)), - (0.84, None, Some(0.84)), - (0.43, None, Some(0.43)), - (0.05001, None, Some(0.05001)), + // linearly rescaled from 0.05..=0.95 to 0.0..=1.0 + (0.9499, None, Some(0.9998889)), + (0.84, None, Some(0.87777776)), + (0.43, None, Some(0.42222223)), + (0.05001, None, Some(0.000011109644)), + // clamped (0.05, None, Some(0.0)), (0.04, None, Some(0.0)), (0.01, None, Some(0.0)), (0.0, None, Some(0.0)), ]; - for (new_value, old_value, expected) in cases { + for (new_raw_value, old_raw_value, expected) in cases { let settings = ButtonAxisSettings::default(); - test_button_axis_settings_filter(settings, new_value, old_value, expected); + test_button_axis_settings_filter(settings, new_raw_value, old_raw_value, expected); } } #[test] - fn test_button_axis_settings_default_filter_with_old_value() { + fn test_button_axis_settings_default_filter_with_old_raw_value() { let cases = [ - (0.43, Some(0.44001), Some(0.43)), + // 0.43 gets rescaled to 0.42222223 (0.05..=0.95 -> 0.0..=1.0) + (0.43, Some(0.44001), Some(0.42222223)), (0.43, Some(0.44), None), (0.43, Some(0.43), None), - (0.43, Some(0.41999), Some(0.43)), - (0.43, Some(0.17), Some(0.43)), - (0.43, Some(0.84), Some(0.43)), + (0.43, Some(0.41999), Some(0.42222223)), + (0.43, Some(0.17), Some(0.42222223)), + (0.43, Some(0.84), Some(0.42222223)), (0.05, Some(0.055), Some(0.0)), (0.95, Some(0.945), Some(1.0)), ]; - for (new_value, old_value, expected) in cases { + for (new_raw_value, old_raw_value, expected) in cases { let settings = ButtonAxisSettings::default(); - test_button_axis_settings_filter(settings, new_value, old_value, expected); + test_button_axis_settings_filter(settings, new_raw_value, old_raw_value, expected); } } fn test_axis_settings_filter( settings: AxisSettings, - new_value: f32, - old_value: Option, + new_raw_value: f32, + old_raw_value: Option, expected: Option, ) { - let actual = settings.filter(new_value, old_value); + let actual = settings.filter(new_raw_value, old_raw_value); assert_eq!( - expected, actual, - "Testing filtering for {settings:?} with new_value = {new_value:?}, old_value = {old_value:?}", + expected, actual.map(|f| f.scaled.to_f32()), + "Testing filtering for {settings:?} with new_raw_value = {new_raw_value:?}, old_raw_value = {old_raw_value:?}", ); } #[test] fn test_axis_settings_default_filter() { + // new (raw), expected (rescaled linearly) let cases = [ + // high enough to round to 1.0 (1.0, Some(1.0)), (0.99, Some(1.0)), (0.96, Some(1.0)), (0.95, Some(1.0)), - (0.9499, Some(0.9499)), - (0.84, Some(0.84)), - (0.43, Some(0.43)), - (0.05001, Some(0.05001)), + // for the following, remember that 0.05 is the "low" value and 0.95 is the "high" value + // barely below the high value means barely below 1 after scaling + (0.9499, Some(0.9998889)), // scaled as: (0.9499 - 0.05) / (0.95 - 0.05) + (0.84, Some(0.87777776)), // scaled as: (0.84 - 0.05) / (0.95 - 0.05) + (0.43, Some(0.42222223)), // scaled as: (0.43 - 0.05) / (0.95 - 0.05) + // barely above the low value means barely above 0 after scaling + (0.05001, Some(0.000011109644)), // scaled as: (0.05001 - 0.05) / (0.95 - 0.05) + // low enough to be rounded to 0 (dead zone) (0.05, Some(0.0)), (0.04, Some(0.0)), (0.01, Some(0.0)), (0.0, Some(0.0)), + // same exact tests as above, but below 0 (bottom half of the dead zone and live zone) + // low enough to be rounded to -1 (-1.0, Some(-1.0)), (-0.99, Some(-1.0)), (-0.96, Some(-1.0)), (-0.95, Some(-1.0)), - (-0.9499, Some(-0.9499)), - (-0.84, Some(-0.84)), - (-0.43, Some(-0.43)), - (-0.05001, Some(-0.05001)), + // scaled inputs + (-0.9499, Some(-0.9998889)), // scaled as: (-0.9499 - -0.05) / (-0.95 - -0.05) + (-0.84, Some(-0.87777776)), // scaled as: (-0.84 - -0.05) / (-0.95 - -0.05) + (-0.43, Some(-0.42222226)), // scaled as: (-0.43 - -0.05) / (-0.95 - -0.05) + (-0.05001, Some(-0.000011146069)), // scaled as: (-0.05001 - -0.05) / (-0.95 - -0.05) + // high enough to be rounded to 0 (dead zone) (-0.05, Some(0.0)), (-0.04, Some(0.0)), (-0.01, Some(0.0)), ]; - for (new_value, expected) in cases { + for (new_raw_value, expected) in cases { let settings = AxisSettings::new(-0.95, -0.05, 0.05, 0.95, 0.01).unwrap(); - test_axis_settings_filter(settings, new_value, None, expected); + test_axis_settings_filter(settings, new_raw_value, None, expected); } } #[test] - fn test_axis_settings_default_filter_with_old_values() { + fn test_axis_settings_default_filter_with_old_raw_values() { + let threshold = 0.01; + // expected values are hardcoded to be rescaled to from 0.05..=0.95 to 0.0..=1.0 + // new (raw), old (raw), expected let cases = [ - (0.43, Some(0.44001), Some(0.43)), - (0.43, Some(0.44), None), - (0.43, Some(0.43), None), - (0.43, Some(0.41999), Some(0.43)), - (0.43, Some(0.17), Some(0.43)), - (0.43, Some(0.84), Some(0.43)), - (0.05, Some(0.055), Some(0.0)), - (0.95, Some(0.945), Some(1.0)), - (-0.43, Some(-0.44001), Some(-0.43)), - (-0.43, Some(-0.44), None), - (-0.43, Some(-0.43), None), - (-0.43, Some(-0.41999), Some(-0.43)), - (-0.43, Some(-0.17), Some(-0.43)), - (-0.43, Some(-0.84), Some(-0.43)), - (-0.05, Some(-0.055), Some(0.0)), - (-0.95, Some(-0.945), Some(-1.0)), + // enough increase to change + (0.43, Some(0.43 + threshold * 1.1), Some(0.42222223)), + // enough decrease to change + (0.43, Some(0.43 - threshold * 1.1), Some(0.42222223)), + // not enough increase to change + (0.43, Some(0.43 + threshold * 0.9), None), + // not enough decrease to change + (0.43, Some(0.43 - threshold * 0.9), None), + // enough increase to change + (-0.43, Some(-0.43 + threshold * 1.1), Some(-0.42222226)), + // enough decrease to change + (-0.43, Some(-0.43 - threshold * 1.1), Some(-0.42222226)), + // not enough increase to change + (-0.43, Some(-0.43 + threshold * 0.9), None), + // not enough decrease to change + (-0.43, Some(-0.43 - threshold * 0.9), None), + // test upper deadzone logic + (0.05, Some(0.0), None), + (0.06, Some(0.0), Some(0.0111111095)), + // test lower deadzone logic + (-0.05, Some(0.0), None), + (-0.06, Some(0.0), Some(-0.011111081)), + // test upper livezone logic + (0.95, Some(1.0), None), + (0.94, Some(1.0), Some(0.9888889)), + // test lower livezone logic + (-0.95, Some(-1.0), None), + (-0.94, Some(-1.0), Some(-0.9888889)), ]; - for (new_value, old_value, expected) in cases { - let settings = AxisSettings::new(-0.95, -0.05, 0.05, 0.95, 0.01).unwrap(); - test_axis_settings_filter(settings, new_value, old_value, expected); + for (new_raw_value, old_raw_value, expected) in cases { + let settings = AxisSettings::new(-0.95, -0.05, 0.05, 0.95, threshold).unwrap(); + test_axis_settings_filter(settings, new_raw_value, old_raw_value, expected); } }