Skip to main content

dreamwell_engine/input/
axis.rs

1// Axis — float axis abstraction with dead zones.
2// Handles analog sticks, triggers, scroll wheels, and composite digital axes (WASD → [-1, 1]).
3
4/// A single-axis float value with dead zone filtering.
5#[derive(Debug, Clone, Copy)]
6pub struct AxisValue {
7    raw: f32,
8    dead_zone: f32,
9}
10
11impl AxisValue {
12    pub fn new(dead_zone: f32) -> Self {
13        Self {
14            raw: 0.0,
15            dead_zone: dead_zone.abs(),
16        }
17    }
18
19    /// Set the raw axis value.
20    pub fn set(&mut self, value: f32) {
21        self.raw = value;
22    }
23
24    /// Get the filtered value (0.0 if within dead zone).
25    pub fn value(&self) -> f32 {
26        if self.raw.abs() <= self.dead_zone {
27            0.0
28        } else {
29            self.raw
30        }
31    }
32
33    /// Get the raw unfiltered value.
34    pub fn raw(&self) -> f32 {
35        self.raw
36    }
37
38    /// Whether the axis is outside its dead zone.
39    pub fn active(&self) -> bool {
40        self.raw.abs() > self.dead_zone
41    }
42
43    /// Reset to zero.
44    pub fn reset(&mut self) {
45        self.raw = 0.0;
46    }
47}
48
49impl Default for AxisValue {
50    fn default() -> Self {
51        Self::new(0.0)
52    }
53}
54
55/// A 2D axis pair (e.g., left stick, mouse delta, WASD composite).
56#[derive(Debug, Clone, Copy)]
57pub struct AxisPair {
58    pub x: AxisValue,
59    pub y: AxisValue,
60}
61
62impl AxisPair {
63    pub fn new(dead_zone: f32) -> Self {
64        Self {
65            x: AxisValue::new(dead_zone),
66            y: AxisValue::new(dead_zone),
67        }
68    }
69
70    /// Set both axes.
71    pub fn set(&mut self, x: f32, y: f32) {
72        self.x.set(x);
73        self.y.set(y);
74    }
75
76    /// Get filtered values as [x, y].
77    pub fn value(&self) -> [f32; 2] {
78        [self.x.value(), self.y.value()]
79    }
80
81    /// Whether either axis is outside its dead zone.
82    pub fn active(&self) -> bool {
83        self.x.active() || self.y.active()
84    }
85
86    /// Squared magnitude (avoids sqrt).
87    pub fn magnitude_sq(&self) -> f32 {
88        let [x, y] = self.value();
89        x * x + y * y
90    }
91
92    /// Normalized direction (unit vector). Returns [0, 0] if magnitude is zero.
93    pub fn normalized(&self) -> [f32; 2] {
94        let [x, y] = self.value();
95        let mag_sq = x * x + y * y;
96        if mag_sq < 1e-10 {
97            [0.0, 0.0]
98        } else {
99            let inv_mag = 1.0 / mag_sq.sqrt();
100            [x * inv_mag, y * inv_mag]
101        }
102    }
103
104    /// Reset both axes to zero.
105    pub fn reset(&mut self) {
106        self.x.reset();
107        self.y.reset();
108    }
109}
110
111impl Default for AxisPair {
112    fn default() -> Self {
113        Self::new(0.0)
114    }
115}
116
117/// Build a composite axis value from two digital buttons (e.g., A/D → x axis).
118/// Returns -1.0, 0.0, or 1.0.
119pub fn digital_axis(negative: bool, positive: bool) -> f32 {
120    match (negative, positive) {
121        (true, false) => -1.0,
122        (false, true) => 1.0,
123        _ => 0.0,
124    }
125}
126
127/// Build a composite axis pair from four digital buttons (WASD pattern).
128pub fn digital_axis_pair(left: bool, right: bool, down: bool, up: bool) -> [f32; 2] {
129    [digital_axis(left, right), digital_axis(down, up)]
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn axis_value_dead_zone() {
138        let mut axis = AxisValue::new(0.1);
139        axis.set(0.05);
140        assert_eq!(axis.value(), 0.0);
141        assert!(!axis.active());
142
143        axis.set(0.5);
144        assert_eq!(axis.value(), 0.5);
145        assert!(axis.active());
146    }
147
148    #[test]
149    fn axis_value_negative() {
150        let mut axis = AxisValue::new(0.1);
151        axis.set(-0.8);
152        assert_eq!(axis.value(), -0.8);
153        assert!(axis.active());
154    }
155
156    #[test]
157    fn axis_value_reset() {
158        let mut axis = AxisValue::new(0.0);
159        axis.set(1.0);
160        axis.reset();
161        assert_eq!(axis.value(), 0.0);
162    }
163
164    #[test]
165    fn axis_pair_basic() {
166        let mut pair = AxisPair::new(0.0);
167        pair.set(0.5, -0.3);
168        assert_eq!(pair.value(), [0.5, -0.3]);
169        assert!(pair.active());
170    }
171
172    #[test]
173    fn axis_pair_normalized() {
174        let mut pair = AxisPair::new(0.0);
175        pair.set(3.0, 4.0);
176        let [nx, ny] = pair.normalized();
177        assert!((nx - 0.6).abs() < 1e-5);
178        assert!((ny - 0.8).abs() < 1e-5);
179    }
180
181    #[test]
182    fn axis_pair_zero_normalized() {
183        let pair = AxisPair::new(0.0);
184        assert_eq!(pair.normalized(), [0.0, 0.0]);
185    }
186
187    #[test]
188    fn digital_axis_values() {
189        assert_eq!(digital_axis(false, false), 0.0);
190        assert_eq!(digital_axis(true, false), -1.0);
191        assert_eq!(digital_axis(false, true), 1.0);
192        assert_eq!(digital_axis(true, true), 0.0); // cancel out
193    }
194
195    #[test]
196    fn digital_axis_pair_wasd() {
197        // W=up, A=left, S=down, D=right
198        let [x, y] = digital_axis_pair(true, false, false, true); // A + W
199        assert_eq!(x, -1.0);
200        assert_eq!(y, 1.0);
201    }
202}