use std::hash::{Hash, Hasher};
use bevy::{math::FloatOrd, prelude::Reflect};
use serde::{Deserialize, Serialize};
use super::AxisProcessor;
#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
#[must_use]
pub struct AxisBounds {
pub(crate) min: f32,
pub(crate) max: f32,
}
impl AxisBounds {
pub const FULL_RANGE: Self = Self {
min: f32::MIN,
max: f32::MAX,
};
#[inline]
pub fn new(min: f32, max: f32) -> Self {
assert!(min <= max);
Self { min, max }
}
#[doc(alias = "magnitude")]
#[inline]
pub fn symmetric(threshold: f32) -> Self {
Self::new(-threshold, threshold)
}
#[inline]
pub const fn at_least(min: f32) -> Self {
Self {
min,
..Self::FULL_RANGE
}
}
#[inline]
pub const fn at_most(max: f32) -> Self {
Self {
max,
..Self::FULL_RANGE
}
}
#[must_use]
#[inline]
pub fn min_max(&self) -> (f32, f32) {
(self.min(), self.max())
}
#[must_use]
#[inline]
pub fn min(&self) -> f32 {
self.min
}
#[must_use]
#[inline]
pub fn max(&self) -> f32 {
self.max
}
#[must_use]
#[inline]
pub fn contains(&self, input_value: f32) -> bool {
self.min <= input_value && input_value <= self.max
}
#[must_use]
#[inline]
pub fn clamp(&self, input_value: f32) -> f32 {
input_value.min(self.max).max(self.min)
}
}
impl Default for AxisBounds {
#[inline]
fn default() -> Self {
Self {
min: -1.0,
max: 1.0,
}
}
}
impl From<AxisBounds> for AxisProcessor {
fn from(value: AxisBounds) -> Self {
Self::ValueBounds(value)
}
}
impl Eq for AxisBounds {}
impl Hash for AxisBounds {
fn hash<H: Hasher>(&self, state: &mut H) {
FloatOrd(self.min).hash(state);
FloatOrd(self.max).hash(state);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
#[must_use]
pub struct AxisExclusion {
pub(crate) negative_max: f32,
pub(crate) positive_min: f32,
}
impl AxisExclusion {
pub const ZERO: Self = Self {
negative_max: 0.0,
positive_min: 0.0,
};
#[inline]
pub fn new(negative_max: f32, positive_min: f32) -> Self {
assert!(negative_max <= 0.0);
assert!(positive_min >= 0.0);
Self {
negative_max,
positive_min,
}
}
#[doc(alias = "magnitude")]
#[inline]
pub fn symmetric(threshold: f32) -> Self {
Self::new(-threshold, threshold)
}
#[inline]
pub fn only_positive(positive_min: f32) -> Self {
Self::new(f32::NEG_INFINITY, positive_min)
}
#[inline]
pub fn only_negative(negative_max: f32) -> Self {
Self::new(negative_max, f32::INFINITY)
}
#[must_use]
#[inline]
pub fn min_max(&self) -> (f32, f32) {
(self.negative_max, self.positive_min)
}
#[must_use]
#[inline]
pub fn min(&self) -> f32 {
self.negative_max
}
#[must_use]
#[inline]
pub fn max(&self) -> f32 {
self.positive_min
}
#[must_use]
#[inline]
pub fn contains(&self, input_value: f32) -> bool {
self.negative_max <= input_value && input_value <= self.positive_min
}
#[must_use]
#[inline]
pub fn exclude(&self, input_value: f32) -> f32 {
if self.contains(input_value) {
0.0
} else {
input_value
}
}
#[inline]
pub fn scaled(self) -> AxisDeadZone {
AxisDeadZone::new(self.negative_max, self.positive_min)
}
}
impl Default for AxisExclusion {
#[inline]
fn default() -> Self {
Self {
negative_max: -0.1,
positive_min: 0.1,
}
}
}
impl From<AxisExclusion> for AxisProcessor {
fn from(value: AxisExclusion) -> Self {
Self::Exclusion(value)
}
}
impl Eq for AxisExclusion {}
impl Hash for AxisExclusion {
fn hash<H: Hasher>(&self, state: &mut H) {
FloatOrd(self.negative_max).hash(state);
FloatOrd(self.positive_min).hash(state);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
#[must_use]
pub struct AxisDeadZone {
pub(crate) exclusion: AxisExclusion,
pub(crate) livezone_lower_recip: f32,
pub(crate) livezone_upper_recip: f32,
}
impl AxisDeadZone {
pub const ZERO: Self = Self {
exclusion: AxisExclusion::ZERO,
livezone_lower_recip: 1.0,
livezone_upper_recip: 1.0,
};
#[inline]
pub fn new(negative_max: f32, positive_min: f32) -> Self {
let (bound_min, bound_max) = AxisBounds::default().min_max();
Self {
exclusion: AxisExclusion::new(negative_max, positive_min),
livezone_lower_recip: (negative_max - bound_min).recip(),
livezone_upper_recip: (bound_max - positive_min).recip(),
}
}
#[doc(alias = "magnitude")]
#[inline]
pub fn symmetric(threshold: f32) -> Self {
Self::new(-threshold, threshold)
}
#[inline]
pub fn only_positive(positive_min: f32) -> Self {
Self::new(f32::NEG_INFINITY, positive_min)
}
#[inline]
pub fn only_negative(negative_max: f32) -> Self {
Self::new(negative_max, f32::INFINITY)
}
#[inline]
pub fn exclusion(&self) -> AxisExclusion {
self.exclusion
}
#[inline]
pub fn bounds(&self) -> AxisBounds {
AxisBounds::default()
}
#[must_use]
#[inline]
pub fn livezone_lower_min_max(&self) -> (f32, f32) {
(self.bounds().min(), self.exclusion.min())
}
#[must_use]
#[inline]
pub fn livezone_upper_min_max(&self) -> (f32, f32) {
(self.exclusion.max(), self.bounds().max())
}
#[must_use]
#[inline]
pub fn within_exclusion(&self, input_value: f32) -> bool {
self.exclusion.contains(input_value)
}
#[must_use]
#[inline]
pub fn within_bounds(&self, input_value: f32) -> bool {
self.bounds().contains(input_value)
}
#[must_use]
#[inline]
pub fn within_livezone_lower(&self, input_value: f32) -> bool {
let (min, max) = self.livezone_lower_min_max();
min <= input_value && input_value <= max
}
#[must_use]
#[inline]
pub fn within_livezone_upper(&self, input_value: f32) -> bool {
let (min, max) = self.livezone_upper_min_max();
min <= input_value && input_value <= max
}
#[must_use]
pub fn normalize(&self, input_value: f32) -> f32 {
if input_value <= 0.0 {
let (bound, deadzone) = self.livezone_lower_min_max();
let clamped_input = input_value.max(bound);
let distance_to_deadzone = (clamped_input - deadzone).min(0.0);
distance_to_deadzone * self.livezone_lower_recip
} else {
let (deadzone, bound) = self.livezone_upper_min_max();
let clamped_input = input_value.min(bound);
let distance_to_deadzone = (clamped_input - deadzone).max(0.0);
distance_to_deadzone * self.livezone_upper_recip
}
}
}
impl Default for AxisDeadZone {
#[inline]
fn default() -> Self {
AxisDeadZone::new(-0.1, 0.1)
}
}
impl From<AxisDeadZone> for AxisProcessor {
fn from(value: AxisDeadZone) -> Self {
Self::DeadZone(value)
}
}
impl Eq for AxisDeadZone {}
impl Hash for AxisDeadZone {
fn hash<H: Hasher>(&self, state: &mut H) {
self.exclusion.hash(state);
}
}
#[cfg(test)]
mod tests {
use super::*;
use bevy::prelude::FloatExt;
#[test]
fn test_axis_value_bounds() {
fn test_bounds(bounds: AxisBounds, min: f32, max: f32) {
assert_eq!(bounds.min(), min);
assert_eq!(bounds.max(), max);
assert_eq!(bounds.min_max(), (min, max));
let processor = AxisProcessor::ValueBounds(bounds);
assert_eq!(AxisProcessor::from(bounds), processor);
for value in -300..300 {
let value = value as f32 * 0.01;
assert_eq!(bounds.clamp(value), processor.process(value));
if min <= value && value <= max {
assert!(bounds.contains(value));
} else {
assert!(!bounds.contains(value));
}
assert_eq!(bounds.clamp(value), value.clamp(min, max));
}
}
let bounds = AxisBounds::FULL_RANGE;
test_bounds(bounds, f32::MIN, f32::MAX);
let bounds = AxisBounds::default();
test_bounds(bounds, -1.0, 1.0);
let bounds = AxisBounds::new(-2.0, 2.5);
test_bounds(bounds, -2.0, 2.5);
let bounds = AxisBounds::symmetric(2.0);
test_bounds(bounds, -2.0, 2.0);
let bounds = AxisBounds::at_least(-1.0);
test_bounds(bounds, -1.0, f32::MAX);
let bounds = AxisBounds::at_most(1.5);
test_bounds(bounds, f32::MIN, 1.5);
}
#[test]
fn test_axis_exclusion() {
fn test_exclusion(exclusion: AxisExclusion, min: f32, max: f32) {
assert_eq!(exclusion.min(), min);
assert_eq!(exclusion.max(), max);
assert_eq!(exclusion.min_max(), (min, max));
let processor = AxisProcessor::Exclusion(exclusion);
assert_eq!(AxisProcessor::from(exclusion), processor);
for value in -300..300 {
let value = value as f32 * 0.01;
assert_eq!(exclusion.exclude(value), processor.process(value));
if min <= value && value <= max {
assert!(exclusion.contains(value));
assert_eq!(exclusion.exclude(value), 0.0);
} else {
assert!(!exclusion.contains(value));
assert_eq!(exclusion.exclude(value), value);
}
}
}
let exclusion = AxisExclusion::ZERO;
test_exclusion(exclusion, 0.0, 0.0);
let exclusion = AxisExclusion::default();
test_exclusion(exclusion, -0.1, 0.1);
let exclusion = AxisExclusion::new(-2.0, 2.5);
test_exclusion(exclusion, -2.0, 2.5);
let exclusion = AxisExclusion::symmetric(1.5);
test_exclusion(exclusion, -1.5, 1.5);
}
#[test]
fn test_axis_deadzone() {
fn test_deadzone(deadzone: AxisDeadZone, min: f32, max: f32) {
let exclusion = deadzone.exclusion();
assert_eq!(exclusion.min_max(), (min, max));
assert_eq!(deadzone.livezone_lower_min_max(), (-1.0, min));
let width_recip = (min + 1.0).recip();
assert!((deadzone.livezone_lower_recip - width_recip).abs() <= f32::EPSILON);
assert_eq!(deadzone.livezone_upper_min_max(), (max, 1.0));
let width_recip = (1.0 - max).recip();
assert!((deadzone.livezone_upper_recip - width_recip).abs() <= f32::EPSILON);
assert_eq!(AxisExclusion::new(min, max).scaled(), deadzone);
let processor = AxisProcessor::DeadZone(deadzone);
assert_eq!(AxisProcessor::from(deadzone), processor);
for value in -300..300 {
let value = value as f32 * 0.01;
assert_eq!(deadzone.normalize(value), processor.process(value));
if min <= value && value <= max {
assert!(deadzone.within_exclusion(value));
assert_eq!(deadzone.normalize(value), 0.0);
}
else if -1.0 <= value && value < min {
assert!(deadzone.within_livezone_lower(value));
let expected = f32::inverse_lerp(-1.0, min, value) - 1.0;
let delta = (deadzone.normalize(value) - expected).abs();
assert!(delta <= f32::EPSILON);
} else if max < value && value <= 1.0 {
assert!(deadzone.within_livezone_upper(value));
let expected = f32::inverse_lerp(max, 1.0, value);
let delta = (deadzone.normalize(value) - expected).abs();
assert!(delta <= f32::EPSILON);
}
else {
assert!(!deadzone.within_bounds(value));
assert_eq!(deadzone.normalize(value), value.clamp(-1.0, 1.0));
}
}
}
let deadzone = AxisDeadZone::ZERO;
test_deadzone(deadzone, 0.0, 0.0);
let deadzone = AxisDeadZone::default();
test_deadzone(deadzone, -0.1, 0.1);
let deadzone = AxisDeadZone::new(-0.2, 0.3);
test_deadzone(deadzone, -0.2, 0.3);
let deadzone = AxisDeadZone::symmetric(0.4);
test_deadzone(deadzone, -0.4, 0.4);
}
}