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).
This commit is contained in:
Benjamin Brienen 2025-01-03 17:27:59 -05:00 committed by GitHub
parent 120b733ab5
commit 43db44ca3a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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<u16>,
/// 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<u16>,
/// [`ButtonInput`] of [`GamepadButton`] representing their digital state
/// [`ButtonInput`] of [`GamepadButton`] representing their digital state.
pub(crate) digital: ButtonInput<GamepadButton>,
/// [`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<Item = &GamepadInput> {
self.analog.all_axes()
}
/// [`ButtonInput`] of [`GamepadButton`] representing their digital state
/// [`ButtonInput`] of [`GamepadButton`] representing their digital state.
pub fn digital(&self) -> &ButtonInput<GamepadButton> {
&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<T> 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<f32>) -> bool {
if old_value.is_none() {
return true;
fn should_register_change(&self, new_raw_value: f32, old_raw_value: Option<f32>) -> 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<f32>) -> Option<f32> {
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<f32>,
) -> Option<FilteredAxisPosition> {
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<f32>, new: RangeInclusive<f32>) -> 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<f32>) -> bool {
if old_value.is_none() {
return true;
fn should_register_change(&self, new_raw_value: f32, old_raw_value: Option<f32>) -> 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<f32>) -> Option<f32> {
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<f32>,
) -> Option<FilteredButtonAxisPosition> {
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<T>::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<f32>,
new_raw_value: f32,
old_raw_value: Option<f32>,
expected: Option<f32>,
) {
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<f32>,
new_raw_value: f32,
old_raw_value: Option<f32>,
expected: Option<f32>,
) {
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);
}
}