Skip to main content

animato_physics/
drag.rs

1//! Pointer drag tracking with constraints and velocity estimation.
2
3#[cfg(any(feature = "std", feature = "alloc"))]
4use crate::inertia::{InertiaBounds, InertiaConfig, InertiaN};
5
6/// Pointer sample used by drag and gesture systems.
7#[derive(Clone, Copy, Debug, PartialEq)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9pub struct PointerData {
10    /// Pointer x coordinate.
11    pub x: f32,
12    /// Pointer y coordinate.
13    pub y: f32,
14    /// Pointer pressure, usually in `[0.0, 1.0]`.
15    pub pressure: f32,
16    /// Stable pointer identifier supplied by the input backend.
17    pub pointer_id: u64,
18}
19
20impl PointerData {
21    /// Create a pointer sample with default pressure `1.0`.
22    pub fn new(x: f32, y: f32, pointer_id: u64) -> Self {
23        Self {
24            x,
25            y,
26            pressure: 1.0,
27            pointer_id,
28        }
29    }
30
31    /// Return the pointer position as `[x, y]`.
32    pub fn position(&self) -> [f32; 2] {
33        [self.x, self.y]
34    }
35}
36
37/// Axis filter for drag movement.
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub enum DragAxis {
41    /// Drag on both x and y axes.
42    Both,
43    /// Drag only on the x axis.
44    X,
45    /// Drag only on the y axis.
46    Y,
47}
48
49/// Constraints applied to drag position.
50#[derive(Clone, Copy, Debug, PartialEq)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52pub struct DragConstraints {
53    /// Optional minimum x position.
54    pub min_x: Option<f32>,
55    /// Optional maximum x position.
56    pub max_x: Option<f32>,
57    /// Optional minimum y position.
58    pub min_y: Option<f32>,
59    /// Optional maximum y position.
60    pub max_y: Option<f32>,
61    /// Optional grid size used to snap both axes.
62    pub grid_snap: Option<f32>,
63}
64
65impl Default for DragConstraints {
66    fn default() -> Self {
67        Self::unbounded()
68    }
69}
70
71impl DragConstraints {
72    /// Create unconstrained drag bounds.
73    pub fn unbounded() -> Self {
74        Self {
75            min_x: None,
76            max_x: None,
77            min_y: None,
78            max_y: None,
79            grid_snap: None,
80        }
81    }
82
83    /// Create rectangular drag bounds.
84    pub fn bounded(min_x: f32, max_x: f32, min_y: f32, max_y: f32) -> Self {
85        Self {
86            min_x: Some(min_x),
87            max_x: Some(max_x),
88            min_y: Some(min_y),
89            max_y: Some(max_y),
90            grid_snap: None,
91        }
92    }
93
94    /// Add grid snapping.
95    pub fn with_grid_snap(mut self, grid: f32) -> Self {
96        self.grid_snap = if grid.is_finite() && grid > 0.0 {
97            Some(grid)
98        } else {
99            None
100        };
101        self
102    }
103
104    /// Apply axis filtering, bounds, and grid snapping.
105    pub fn constrain(
106        &self,
107        position: [f32; 2],
108        axis: DragAxis,
109        locked_origin: [f32; 2],
110    ) -> [f32; 2] {
111        let mut x = finite_or_zero(position[0]);
112        let mut y = finite_or_zero(position[1]);
113
114        match axis {
115            DragAxis::Both => {}
116            DragAxis::X => y = locked_origin[1],
117            DragAxis::Y => x = locked_origin[0],
118        }
119
120        x = clamp_optional(x, self.min_x, self.max_x);
121        y = clamp_optional(y, self.min_y, self.max_y);
122
123        if let Some(grid) = self.grid_snap {
124            x = snap(x, grid);
125            y = snap(y, grid);
126            x = clamp_optional(x, self.min_x, self.max_x);
127            y = clamp_optional(y, self.min_y, self.max_y);
128        }
129
130        [x, y]
131    }
132}
133
134/// Stateful drag tracker.
135///
136/// Requires the `alloc` or `std` feature because releasing a drag can create
137/// an [`InertiaN<[f32; 2]>`].
138#[cfg(any(feature = "std", feature = "alloc"))]
139#[derive(Clone, Debug)]
140#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
141pub struct DragState {
142    position: [f32; 2],
143    start_position: [f32; 2],
144    start_pointer: [f32; 2],
145    last_position: [f32; 2],
146    velocity: [f32; 2],
147    active_pointer_id: Option<u64>,
148    axis: DragAxis,
149    constraints: DragConstraints,
150    inertia_config: InertiaConfig<[f32; 2]>,
151    velocity_smoothing: f32,
152}
153
154#[cfg(any(feature = "std", feature = "alloc"))]
155impl DragState {
156    /// Create a drag tracker at an initial position.
157    pub fn new(position: [f32; 2]) -> Self {
158        Self {
159            position,
160            start_position: position,
161            start_pointer: [0.0, 0.0],
162            last_position: position,
163            velocity: [0.0, 0.0],
164            active_pointer_id: None,
165            axis: DragAxis::Both,
166            constraints: DragConstraints::unbounded(),
167            inertia_config: InertiaConfig::new(1400.0, 2.0),
168            velocity_smoothing: 0.35,
169        }
170    }
171
172    /// Set the drag axis filter.
173    pub fn axis(mut self, axis: DragAxis) -> Self {
174        self.axis = axis;
175        self
176    }
177
178    /// Set drag constraints.
179    pub fn constraints(mut self, constraints: DragConstraints) -> Self {
180        self.constraints = constraints;
181        self
182    }
183
184    /// Set the inertia configuration used on pointer release.
185    pub fn inertia_config(mut self, config: InertiaConfig<[f32; 2]>) -> Self {
186        self.inertia_config = config;
187        self
188    }
189
190    /// Set velocity EMA smoothing in `[0.0, 1.0]`.
191    pub fn velocity_smoothing(mut self, smoothing: f32) -> Self {
192        self.velocity_smoothing = smoothing.clamp(0.0, 1.0);
193        self
194    }
195
196    /// Current constrained drag position.
197    pub fn position(&self) -> [f32; 2] {
198        self.position
199    }
200
201    /// Current estimated drag velocity.
202    pub fn velocity(&self) -> [f32; 2] {
203        self.velocity
204    }
205
206    /// `true` while a pointer is captured.
207    pub fn is_dragging(&self) -> bool {
208        self.active_pointer_id.is_some()
209    }
210
211    /// Captured pointer id, if any.
212    pub fn active_pointer_id(&self) -> Option<u64> {
213        self.active_pointer_id
214    }
215
216    /// Capture a pointer and start tracking movement.
217    pub fn on_pointer_down(&mut self, data: PointerData) {
218        self.active_pointer_id = Some(data.pointer_id);
219        self.start_pointer = data.position();
220        self.start_position = self.position;
221        self.last_position = self.position;
222        self.velocity = [0.0, 0.0];
223    }
224
225    /// Update position and velocity from a pointer move.
226    pub fn on_pointer_move(&mut self, data: PointerData, dt: f32) {
227        if self.active_pointer_id != Some(data.pointer_id) {
228            return;
229        }
230
231        let delta = [
232            data.x - self.start_pointer[0],
233            data.y - self.start_pointer[1],
234        ];
235        let raw_position = [
236            self.start_position[0] + delta[0],
237            self.start_position[1] + delta[1],
238        ];
239        let constrained = self
240            .constraints
241            .constrain(raw_position, self.axis, self.start_position);
242
243        let dt = dt.max(0.0);
244        if dt > 0.0 {
245            let instant = [
246                (constrained[0] - self.last_position[0]) / dt,
247                (constrained[1] - self.last_position[1]) / dt,
248            ];
249            let alpha = self.velocity_smoothing;
250            self.velocity = [
251                self.velocity[0] * (1.0 - alpha) + instant[0] * alpha,
252                self.velocity[1] * (1.0 - alpha) + instant[1] * alpha,
253            ];
254        }
255
256        self.position = constrained;
257        self.last_position = constrained;
258    }
259
260    /// Release the captured pointer and create inertia if velocity is high enough.
261    pub fn on_pointer_up(&mut self, data: PointerData) -> Option<InertiaN<[f32; 2]>> {
262        if self.active_pointer_id != Some(data.pointer_id) {
263            return None;
264        }
265
266        self.active_pointer_id = None;
267        let velocity = match self.axis {
268            DragAxis::Both => self.velocity,
269            DragAxis::X => [self.velocity[0], 0.0],
270            DragAxis::Y => [0.0, self.velocity[1]],
271        };
272
273        if velocity[0].abs() <= self.inertia_config.min_velocity
274            && velocity[1].abs() <= self.inertia_config.min_velocity
275        {
276            self.velocity = [0.0, 0.0];
277            return None;
278        }
279
280        let mut config = self.inertia_config.clone();
281        config.bounds = self.bounds_for_inertia();
282        let mut inertia = InertiaN::new(config, self.position);
283        inertia.kick(velocity);
284        Some(inertia)
285    }
286
287    fn bounds_for_inertia(&self) -> Option<InertiaBounds<[f32; 2]>> {
288        match (
289            self.constraints.min_x,
290            self.constraints.max_x,
291            self.constraints.min_y,
292            self.constraints.max_y,
293        ) {
294            (Some(min_x), Some(max_x), Some(min_y), Some(max_y)) => {
295                Some(InertiaBounds::new([min_x, min_y], [max_x, max_y]))
296            }
297            _ => self.inertia_config.bounds.clone(),
298        }
299    }
300}
301
302#[inline]
303fn finite_or_zero(value: f32) -> f32 {
304    if value.is_finite() { value } else { 0.0 }
305}
306
307#[inline]
308fn clamp_optional(value: f32, min: Option<f32>, max: Option<f32>) -> f32 {
309    match (min, max) {
310        (Some(a), Some(b)) => value.clamp(a.min(b), a.max(b)),
311        (Some(min), None) => value.max(min),
312        (None, Some(max)) => value.min(max),
313        (None, None) => value,
314    }
315}
316
317#[inline]
318fn snap(value: f32, grid: f32) -> f32 {
319    if grid > 0.0 {
320        libm::roundf(value / grid) * grid
321    } else {
322        value
323    }
324}
325
326#[cfg(all(test, any(feature = "std", feature = "alloc")))]
327mod tests {
328    use super::*;
329
330    #[cfg(any(feature = "std", feature = "alloc"))]
331    #[test]
332    fn drag_respects_axis_and_constraints() {
333        let mut drag = DragState::new([0.0, 5.0])
334            .axis(DragAxis::X)
335            .constraints(DragConstraints::bounded(-10.0, 10.0, -10.0, 10.0));
336
337        drag.on_pointer_down(PointerData::new(0.0, 0.0, 1));
338        drag.on_pointer_move(PointerData::new(30.0, 40.0, 1), 0.016);
339
340        assert_eq!(drag.position(), [10.0, 5.0]);
341    }
342
343    #[cfg(any(feature = "std", feature = "alloc"))]
344    #[test]
345    fn drag_ignores_wrong_pointer_id() {
346        let mut drag = DragState::new([0.0, 0.0]);
347        drag.on_pointer_down(PointerData::new(0.0, 0.0, 7));
348        drag.on_pointer_move(PointerData::new(20.0, 0.0, 8), 0.016);
349        assert_eq!(drag.position(), [0.0, 0.0]);
350    }
351
352    #[cfg(any(feature = "std", feature = "alloc"))]
353    #[test]
354    fn drag_estimates_velocity_with_ema() {
355        let mut drag = DragState::new([0.0, 0.0]).velocity_smoothing(1.0);
356        drag.on_pointer_down(PointerData::new(0.0, 0.0, 1));
357        drag.on_pointer_move(PointerData::new(16.0, 0.0, 1), 0.016);
358        assert!((drag.velocity()[0] - 1000.0).abs() < 0.01);
359        assert_eq!(drag.velocity()[1], 0.0);
360    }
361
362    #[cfg(any(feature = "std", feature = "alloc"))]
363    #[test]
364    fn grid_snap_applies_to_position() {
365        let mut drag = DragState::new([0.0, 0.0])
366            .constraints(DragConstraints::unbounded().with_grid_snap(10.0));
367        drag.on_pointer_down(PointerData::new(0.0, 0.0, 1));
368        drag.on_pointer_move(PointerData::new(16.0, 24.0, 1), 0.016);
369        assert_eq!(drag.position(), [20.0, 20.0]);
370    }
371}