use std::fmt::Debug;
use std::hash::{Hash, Hasher};
use bevy::{
math::FloatOrd,
prelude::{Reflect, Vec2},
};
use serde::{Deserialize, Serialize};
use super::DualAxisProcessor;
#[doc(alias = "RadialBounds")]
#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
#[must_use]
pub struct CircleBounds {
pub(crate) radius: f32,
}
impl CircleBounds {
pub const FULL_RANGE: Self = Self { radius: f32::MAX };
#[doc(alias = "magnitude")]
#[doc(alias = "from_radius")]
#[inline]
pub fn new(max: f32) -> Self {
assert!(max >= 0.0);
Self { radius: max }
}
#[must_use]
#[inline]
pub fn radius(&self) -> f32 {
self.radius
}
#[must_use]
#[inline]
pub fn contains(&self, input_value: Vec2) -> bool {
input_value.length() <= self.radius
}
#[must_use]
#[inline]
pub fn clamp(&self, input_value: Vec2) -> Vec2 {
input_value.clamp_length_max(self.radius)
}
}
impl Default for CircleBounds {
#[inline]
fn default() -> Self {
Self::new(1.0)
}
}
impl From<CircleBounds> for DualAxisProcessor {
fn from(value: CircleBounds) -> Self {
Self::CircleBounds(value)
}
}
impl Eq for CircleBounds {}
impl Hash for CircleBounds {
fn hash<H: Hasher>(&self, state: &mut H) {
FloatOrd(self.radius).hash(state);
}
}
#[doc(alias = "RadialExclusion")]
#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
#[must_use]
pub struct CircleExclusion {
pub(crate) radius_squared: f32,
}
impl CircleExclusion {
pub const ZERO: Self = Self {
radius_squared: 0.0,
};
#[doc(alias = "magnitude")]
#[doc(alias = "from_radius")]
#[inline]
pub fn new(threshold: f32) -> Self {
assert!(threshold >= 0.0);
Self {
radius_squared: threshold.powi(2),
}
}
#[must_use]
#[inline]
pub fn radius(&self) -> f32 {
self.radius_squared.sqrt()
}
#[must_use]
#[inline]
pub fn contains(&self, input_value: Vec2) -> bool {
input_value.length_squared() <= self.radius_squared
}
#[inline]
pub fn scaled(self) -> CircleDeadZone {
CircleDeadZone::new(self.radius())
}
#[must_use]
#[inline]
pub fn exclude(&self, input_value: Vec2) -> Vec2 {
if self.contains(input_value) {
Vec2::ZERO
} else {
input_value
}
}
}
impl Default for CircleExclusion {
fn default() -> Self {
Self::new(0.1)
}
}
impl From<CircleExclusion> for DualAxisProcessor {
fn from(value: CircleExclusion) -> Self {
Self::CircleExclusion(value)
}
}
impl Eq for CircleExclusion {}
impl Hash for CircleExclusion {
fn hash<H: Hasher>(&self, state: &mut H) {
FloatOrd(self.radius_squared).hash(state);
}
}
#[doc(alias = "RadialDeadZone")]
#[derive(Debug, Clone, Copy, PartialEq, Reflect, Serialize, Deserialize)]
#[must_use]
pub struct CircleDeadZone {
pub(crate) radius: f32,
pub(crate) livezone_recip: f32,
}
impl CircleDeadZone {
pub const ZERO: Self = Self {
radius: 0.0,
livezone_recip: 1.0,
};
#[doc(alias = "magnitude")]
#[doc(alias = "from_radius")]
#[inline]
pub fn new(threshold: f32) -> Self {
let bounds = CircleBounds::default();
Self {
radius: threshold,
livezone_recip: (bounds.radius - threshold).recip(),
}
}
#[must_use]
#[inline]
pub fn radius(&self) -> f32 {
self.radius
}
#[inline]
pub fn exclusion(&self) -> CircleExclusion {
CircleExclusion::new(self.radius)
}
#[inline]
pub fn bounds(&self) -> CircleBounds {
CircleBounds::default()
}
#[must_use]
#[inline]
pub fn livezone_min_max(&self) -> (f32, f32) {
(self.radius, self.bounds().radius)
}
#[must_use]
#[inline]
pub fn within_exclusion(&self, input_value: Vec2) -> bool {
self.exclusion().contains(input_value)
}
#[must_use]
#[inline]
pub fn within_bounds(&self, input_value: Vec2) -> bool {
self.bounds().contains(input_value)
}
#[must_use]
#[inline]
pub fn within_livezone(&self, input_value: Vec2) -> bool {
let input_length = input_value.length();
let (min, max) = self.livezone_min_max();
min <= input_length && input_length <= max
}
#[must_use]
pub fn normalize(&self, input_value: Vec2) -> Vec2 {
let input_length = input_value.length();
if input_length == 0.0 {
return Vec2::ZERO;
}
let (deadzone, bound) = self.livezone_min_max();
let clamped_input_length = input_length.min(bound);
let offset_to_deadzone = (clamped_input_length - deadzone).max(0.0);
let magnitude_scale = (offset_to_deadzone * self.livezone_recip) / input_length;
input_value * magnitude_scale
}
}
impl Default for CircleDeadZone {
#[inline]
fn default() -> Self {
CircleDeadZone::new(0.1)
}
}
impl From<CircleDeadZone> for DualAxisProcessor {
fn from(value: CircleDeadZone) -> Self {
Self::CircleDeadZone(value)
}
}
impl Eq for CircleDeadZone {}
impl Hash for CircleDeadZone {
fn hash<H: Hasher>(&self, state: &mut H) {
FloatOrd(self.radius).hash(state);
}
}
#[cfg(test)]
mod tests {
use super::*;
use bevy::prelude::FloatExt;
#[test]
fn test_circle_value_bounds() {
fn test_bounds(bounds: CircleBounds, radius: f32) {
assert_eq!(bounds.radius(), radius);
let processor = DualAxisProcessor::CircleBounds(bounds);
assert_eq!(DualAxisProcessor::from(bounds), processor);
for x in -300..300 {
let x = x as f32 * 0.01;
for y in -300..300 {
let y = y as f32 * 0.01;
let value = Vec2::new(x, y);
assert_eq!(processor.process(value), bounds.clamp(value));
if value.length() <= radius {
assert!(bounds.contains(value));
} else {
assert!(!bounds.contains(value));
}
let expected = value.clamp_length_max(radius);
let delta = (bounds.clamp(value) - expected).abs();
assert!(delta.x <= f32::EPSILON);
assert!(delta.y <= f32::EPSILON);
}
}
}
let bounds = CircleBounds::FULL_RANGE;
test_bounds(bounds, f32::MAX);
let bounds = CircleBounds::default();
test_bounds(bounds, 1.0);
let bounds = CircleBounds::new(2.0);
test_bounds(bounds, 2.0);
}
#[test]
fn test_circle_exclusion() {
fn test_exclusion(exclusion: CircleExclusion, radius: f32) {
assert_eq!(exclusion.radius(), radius);
let processor = DualAxisProcessor::CircleExclusion(exclusion);
assert_eq!(DualAxisProcessor::from(exclusion), processor);
for x in -300..300 {
let x = x as f32 * 0.01;
for y in -300..300 {
let y = y as f32 * 0.01;
let value = Vec2::new(x, y);
assert_eq!(processor.process(value), exclusion.exclude(value));
if value.length() <= radius {
assert!(exclusion.contains(value));
assert_eq!(exclusion.exclude(value), Vec2::ZERO);
} else {
assert!(!exclusion.contains(value));
assert_eq!(exclusion.exclude(value), value);
}
}
}
}
let exclusion = CircleExclusion::ZERO;
test_exclusion(exclusion, 0.0);
let exclusion = CircleExclusion::default();
test_exclusion(exclusion, 0.1);
let exclusion = CircleExclusion::new(0.5);
test_exclusion(exclusion, 0.5);
}
#[test]
fn test_circle_deadzone() {
fn test_deadzone(deadzone: CircleDeadZone, radius: f32) {
assert_eq!(deadzone.radius(), radius);
let exclusion = CircleExclusion::new(radius);
assert_eq!(exclusion.scaled(), deadzone);
let processor = DualAxisProcessor::CircleDeadZone(deadzone);
assert_eq!(DualAxisProcessor::from(deadzone), processor);
for x in -300..300 {
let x = x as f32 * 0.01;
for y in -300..300 {
let y = y as f32 * 0.01;
let value = Vec2::new(x, y);
assert_eq!(processor.process(value), deadzone.normalize(value));
if value.length() <= radius {
assert!(deadzone.within_exclusion(value));
assert_eq!(deadzone.normalize(value), Vec2::ZERO);
}
else if value.length() <= 1.0 {
assert!(deadzone.within_livezone(value));
let expected_scale = f32::inverse_lerp(radius, 1.0, value.length());
let expected = value.normalize() * expected_scale;
let delta = (deadzone.normalize(value) - expected).abs();
assert!(delta.x <= 0.00001);
assert!(delta.y <= 0.00001);
}
else {
assert!(!deadzone.within_bounds(value));
let expected = value.clamp_length_max(1.0);
let delta = (deadzone.normalize(value) - expected).abs();
assert!(delta.x <= 0.00001);
assert!(delta.y <= 0.00001);
}
}
}
}
let deadzone = CircleDeadZone::ZERO;
test_deadzone(deadzone, 0.0);
let deadzone = CircleDeadZone::default();
test_deadzone(deadzone, 0.1);
let deadzone = CircleDeadZone::new(0.5);
test_deadzone(deadzone, 0.5);
}
}