1use std::collections::VecDeque;
6
7pub const TOUCH_SLOP: f64 = 18.0;
9
10pub const MOUSE_SLOP: f64 = 3.0;
13
14pub const WHEEL_LINE_PX: f64 = 40.0;
16
17pub const FLING_DECAY: f64 = 0.998;
19
20pub const FLING_STOP: f64 = 30.0;
22
23pub const VELOCITY_WINDOW_MS: f64 = 100.0;
25
26pub fn fling_decay(v0: f64, elapsed_ms: f64) -> f64 {
28 v0 * FLING_DECAY.powf(elapsed_ms)
29}
30
31pub fn fling_displacement(v0: f64, elapsed_ms: f64) -> f64 {
34 (v0 / 1000.0) * (FLING_DECAY.powf(elapsed_ms) - 1.0) / FLING_DECAY.ln()
35}
36
37#[derive(Clone, Debug, Default)]
40pub struct VelocityTracker {
41 samples: VecDeque<(f64, f64)>,
42}
43
44impl VelocityTracker {
45 pub fn new() -> Self {
46 Self {
47 samples: VecDeque::new(),
48 }
49 }
50
51 pub fn clear(&mut self) {
53 self.samples.clear();
54 }
55
56 pub fn record(&mut self, time_ms: f64, position: f64) {
58 self.samples.push_back((time_ms, position));
59 let cutoff = time_ms - VELOCITY_WINDOW_MS;
60 while let Some(&(t, _)) = self.samples.front() {
61 if t < cutoff {
62 self.samples.pop_front();
63 } else {
64 break;
65 }
66 }
67 }
68
69 pub fn velocity(&self) -> f64 {
71 if self.samples.len() < 2 {
72 return 0.0;
73 }
74 let (t0, p0) = *self.samples.front().unwrap();
75 let (t1, p1) = *self.samples.back().unwrap();
76 let dt = t1 - t0;
77 if dt <= 0.0 {
78 return 0.0;
79 }
80 (p1 - p0) / dt * 1000.0
81 }
82
83 pub fn len(&self) -> usize {
84 self.samples.len()
85 }
86
87 pub fn is_empty(&self) -> bool {
88 self.samples.is_empty()
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn fling_decay_halves_in_about_347ms() {
98 let v = fling_decay(1000.0, 347.0);
99 assert!((v - 500.0).abs() < 1.0, "expected ~500, got {v}");
100 assert_eq!(fling_decay(1000.0, 0.0), 1000.0);
102 }
103
104 #[test]
105 fn fling_displacement_matches_numeric_integration() {
106 let v0 = 800.0;
107 let total_ms = 500.0;
108 let mut numeric = 0.0;
110 let step = 1.0;
111 let mut t = 0.0;
112 while t < total_ms {
113 numeric += fling_decay(v0, t) / 1000.0 * step;
114 t += step;
115 }
116 let closed = fling_displacement(v0, total_ms);
117 let rel_err = (closed - numeric).abs() / numeric.abs();
118 assert!(rel_err < 0.01, "closed {closed} vs numeric {numeric}");
119 assert!(closed > 0.0);
120 }
121
122 #[test]
123 fn fling_displacement_sign_follows_velocity() {
124 assert!(fling_displacement(500.0, 100.0) > 0.0);
125 assert!(fling_displacement(-500.0, 100.0) < 0.0);
126 assert_eq!(fling_displacement(0.0, 100.0), 0.0);
127 }
128
129 #[test]
130 fn velocity_is_delta_over_window() {
131 let mut vt = VelocityTracker::new();
132 vt.record(0.0, 100.0);
133 vt.record(50.0, 50.0); assert!((vt.velocity() - (-1000.0)).abs() < 1e-9);
135 }
136
137 #[test]
138 fn velocity_empty_or_single_sample_is_zero() {
139 let mut vt = VelocityTracker::new();
140 assert_eq!(vt.velocity(), 0.0);
141 vt.record(0.0, 10.0);
142 assert_eq!(vt.velocity(), 0.0);
143 assert!(!vt.is_empty());
144 }
145
146 #[test]
147 fn old_samples_are_pruned_from_the_window() {
148 let mut vt = VelocityTracker::new();
149 vt.record(0.0, 0.0);
150 vt.record(50.0, 10.0);
151 vt.record(200.0, 40.0);
153 assert_eq!(vt.len(), 1);
156 assert_eq!(vt.velocity(), 0.0); }
158
159 #[test]
160 fn clear_resets_the_tracker() {
161 let mut vt = VelocityTracker::new();
162 vt.record(0.0, 1.0);
163 vt.record(10.0, 2.0);
164 vt.clear();
165 assert!(vt.is_empty());
166 assert_eq!(vt.velocity(), 0.0);
167 }
168}