1#[cfg(any(feature = "std", feature = "alloc"))]
4use crate::decompose::Decompose;
5#[cfg(any(feature = "std", feature = "alloc"))]
6use alloc::vec::Vec;
7use animato_core::Update;
8#[cfg(any(feature = "std", feature = "alloc"))]
9use core::marker::PhantomData;
10
11#[derive(Clone, Debug, PartialEq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub struct InertiaBounds<T = f32> {
19 pub min: T,
21 pub max: T,
23}
24
25impl<T> InertiaBounds<T> {
26 pub fn new(min: T, max: T) -> Self {
28 Self { min, max }
29 }
30}
31
32#[derive(Clone, Debug, PartialEq)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39pub struct InertiaConfig<T = f32> {
40 pub friction: f32,
42 pub min_velocity: f32,
44 pub bounds: Option<InertiaBounds<T>>,
46}
47
48impl<T> InertiaConfig<T> {
49 pub fn new(friction: f32, min_velocity: f32) -> Self {
51 Self {
52 friction,
53 min_velocity,
54 bounds: None,
55 }
56 }
57
58 pub fn with_bounds(mut self, bounds: InertiaBounds<T>) -> Self {
60 self.bounds = Some(bounds);
61 self
62 }
63
64 #[inline]
65 fn friction(&self) -> f32 {
66 if self.friction.is_finite() {
67 self.friction.max(0.0)
68 } else {
69 0.0
70 }
71 }
72
73 #[inline]
74 fn min_velocity(&self) -> f32 {
75 if self.min_velocity.is_finite() {
76 self.min_velocity.max(0.0)
77 } else {
78 0.0
79 }
80 }
81}
82
83impl Default for InertiaConfig<f32> {
84 fn default() -> Self {
85 Self::smooth()
86 }
87}
88
89impl InertiaConfig<f32> {
90 pub fn smooth() -> Self {
92 Self {
93 friction: 1400.0,
94 min_velocity: 2.0,
95 bounds: None,
96 }
97 }
98
99 pub fn snappy() -> Self {
101 Self {
102 friction: 3600.0,
103 min_velocity: 4.0,
104 bounds: None,
105 }
106 }
107
108 pub fn heavy() -> Self {
110 Self {
111 friction: 800.0,
112 min_velocity: 1.0,
113 bounds: None,
114 }
115 }
116}
117
118#[derive(Clone, Debug)]
124#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
125pub struct Inertia {
126 pub config: InertiaConfig<f32>,
128 position: f32,
129 velocity: f32,
130}
131
132impl Inertia {
133 pub fn new(config: InertiaConfig<f32>) -> Self {
135 Self::with_position(config, 0.0)
136 }
137
138 pub fn with_position(config: InertiaConfig<f32>, position: f32) -> Self {
140 let mut this = Self {
141 config,
142 position: finite_or_zero(position),
143 velocity: 0.0,
144 };
145 this.apply_bounds();
146 this
147 }
148
149 pub fn kick(&mut self, velocity: f32) {
151 let velocity = finite_or_zero(velocity);
152 self.velocity = if velocity.abs() <= self.config.min_velocity() {
153 0.0
154 } else {
155 velocity
156 };
157 }
158
159 pub fn position(&self) -> f32 {
161 self.position
162 }
163
164 pub fn velocity(&self) -> f32 {
166 self.velocity
167 }
168
169 pub fn snap_to(&mut self, position: f32) {
171 self.position = finite_or_zero(position);
172 self.velocity = 0.0;
173 self.apply_bounds();
174 }
175
176 pub fn is_settled(&self) -> bool {
178 self.velocity.abs() <= self.config.min_velocity()
179 }
180
181 #[inline]
182 fn apply_bounds(&mut self) -> bool {
183 if let Some(bounds) = &self.config.bounds {
184 let min = bounds.min.min(bounds.max);
185 let max = bounds.min.max(bounds.max);
186 if self.position < min {
187 self.position = min;
188 self.velocity = 0.0;
189 return true;
190 }
191 if self.position > max {
192 self.position = max;
193 self.velocity = 0.0;
194 return true;
195 }
196 }
197 false
198 }
199}
200
201impl Update for Inertia {
202 fn update(&mut self, dt: f32) -> bool {
207 let dt = dt.max(0.0);
208 if dt == 0.0 || self.is_settled() {
209 if self.is_settled() {
210 self.velocity = 0.0;
211 }
212 return !self.is_settled();
213 }
214
215 let friction = self.config.friction();
216 if friction <= 0.0 {
217 self.velocity = 0.0;
218 return false;
219 }
220
221 let sign = self.velocity.signum();
222 let speed = self.velocity.abs();
223 let stop_time = speed / friction;
224 let step = dt.min(stop_time);
225
226 self.position += self.velocity * step - 0.5 * sign * friction * step * step;
227
228 let next_speed = speed - friction * step;
229 self.velocity = if step >= stop_time || next_speed <= self.config.min_velocity() {
230 0.0
231 } else {
232 sign * next_speed
233 };
234
235 if self.apply_bounds() {
236 return false;
237 }
238
239 !self.is_settled()
240 }
241}
242
243#[cfg(any(feature = "std", feature = "alloc"))]
247#[derive(Clone, Debug)]
248#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
249pub struct InertiaN<T: Decompose> {
250 components: Vec<Inertia>,
251 _marker: PhantomData<T>,
252}
253
254#[cfg(any(feature = "std", feature = "alloc"))]
255impl<T: Decompose> InertiaN<T> {
256 pub fn new(config: InertiaConfig<T>, initial: T) -> Self {
258 let count = T::component_count();
259 let mut initial_components = alloc::vec![0.0; count];
260 initial.write_components(&mut initial_components);
261
262 let mut min_components = alloc::vec![0.0; count];
263 let mut max_components = alloc::vec![0.0; count];
264 let has_bounds = if let Some(bounds) = &config.bounds {
265 bounds.min.write_components(&mut min_components);
266 bounds.max.write_components(&mut max_components);
267 true
268 } else {
269 false
270 };
271
272 let mut components = Vec::with_capacity(count);
273 for index in 0..count {
274 let mut component_config = InertiaConfig::new(config.friction, config.min_velocity);
275 if has_bounds {
276 component_config = component_config.with_bounds(InertiaBounds::new(
277 min_components[index],
278 max_components[index],
279 ));
280 }
281 components.push(Inertia::with_position(
282 component_config,
283 initial_components[index],
284 ));
285 }
286
287 Self {
288 components,
289 _marker: PhantomData,
290 }
291 }
292
293 #[allow(clippy::useless_conversion)]
295 pub fn kick(&mut self, velocity: T) {
296 let count = T::component_count();
297 let mut velocity_components = alloc::vec![0.0; count];
298 velocity.write_components(&mut velocity_components);
299 for (component, velocity) in self
300 .components
301 .iter_mut()
302 .zip(velocity_components.into_iter())
303 {
304 component.kick(velocity);
305 }
306 }
307
308 pub fn position(&self) -> T {
310 let values: Vec<f32> = self
311 .components
312 .iter()
313 .map(|component| component.position())
314 .collect();
315 T::from_components(&values)
316 }
317
318 pub fn velocity(&self) -> T {
320 let values: Vec<f32> = self
321 .components
322 .iter()
323 .map(|component| component.velocity())
324 .collect();
325 T::from_components(&values)
326 }
327
328 #[allow(clippy::useless_conversion)]
330 pub fn snap_to(&mut self, position: T) {
331 let count = T::component_count();
332 let mut position_components = alloc::vec![0.0; count];
333 position.write_components(&mut position_components);
334 for (component, position) in self
335 .components
336 .iter_mut()
337 .zip(position_components.into_iter())
338 {
339 component.snap_to(position);
340 }
341 }
342
343 pub fn is_settled(&self) -> bool {
345 self.components
346 .iter()
347 .all(|component| component.is_settled())
348 }
349}
350
351#[cfg(any(feature = "std", feature = "alloc"))]
352impl<T: Decompose> Update for InertiaN<T> {
353 fn update(&mut self, dt: f32) -> bool {
354 if self.is_settled() {
355 return false;
356 }
357 for component in self.components.iter_mut() {
358 component.update(dt);
359 }
360 !self.is_settled()
361 }
362}
363
364#[inline]
365fn finite_or_zero(value: f32) -> f32 {
366 if value.is_finite() { value } else { 0.0 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 const DT: f32 = 1.0 / 60.0;
374
375 #[test]
376 fn inertia_settles_from_kick() {
377 let mut inertia = Inertia::new(InertiaConfig::smooth());
378 inertia.kick(600.0);
379 for _ in 0..10_000 {
380 if !inertia.update(DT) {
381 break;
382 }
383 }
384 assert!(inertia.is_settled());
385 assert_eq!(inertia.velocity(), 0.0);
386 assert!(inertia.position() > 0.0);
387 }
388
389 #[test]
390 fn negative_dt_is_noop() {
391 let mut inertia = Inertia::new(InertiaConfig::smooth());
392 inertia.kick(100.0);
393 inertia.update(-1.0);
394 assert_eq!(inertia.position(), 0.0);
395 assert_eq!(inertia.velocity(), 100.0);
396 }
397
398 #[test]
399 fn bounds_clamp_and_stop() {
400 let config = InertiaConfig::smooth().with_bounds(InertiaBounds::new(0.0, 10.0));
401 let mut inertia = Inertia::with_position(config, 5.0);
402 inertia.kick(1000.0);
403 for _ in 0..60 {
404 if !inertia.update(DT) {
405 break;
406 }
407 }
408 assert_eq!(inertia.position(), 10.0);
409 assert_eq!(inertia.velocity(), 0.0);
410 assert!(inertia.is_settled());
411 }
412
413 #[test]
414 fn snap_to_respects_bounds() {
415 let config = InertiaConfig::smooth().with_bounds(InertiaBounds::new(-5.0, 5.0));
416 let mut inertia = Inertia::new(config);
417 inertia.snap_to(20.0);
418 assert_eq!(inertia.position(), 5.0);
419 }
420
421 #[cfg(any(feature = "std", feature = "alloc"))]
422 #[test]
423 fn inertia_n_updates_independent_axes() {
424 let config = InertiaConfig::new(1000.0, 1.0)
425 .with_bounds(InertiaBounds::new([-100.0, -100.0], [100.0, 100.0]));
426 let mut inertia: InertiaN<[f32; 2]> = InertiaN::new(config, [0.0, 0.0]);
427 inertia.kick([400.0, -200.0]);
428 inertia.update(DT);
429 let position = inertia.position();
430 assert!(position[0] > 0.0);
431 assert!(position[1] < 0.0);
432 }
433
434 #[test]
435 fn presets_and_bounds_are_constructible() {
436 let bounds = InertiaBounds::new(-10.0, 10.0);
437 let config = InertiaConfig::snappy().with_bounds(bounds.clone());
438
439 assert_eq!(bounds.min, -10.0);
440 assert_eq!(bounds.max, 10.0);
441 assert_eq!(config.bounds, Some(bounds));
442 assert!(InertiaConfig::heavy().friction < InertiaConfig::snappy().friction);
443 assert_eq!(InertiaConfig::<f32>::default(), InertiaConfig::smooth());
444 }
445
446 #[test]
447 fn invalid_config_values_settle_immediately() {
448 let mut inertia = Inertia::with_position(InertiaConfig::new(f32::NAN, f32::NAN), f32::NAN);
449
450 inertia.kick(f32::INFINITY);
451
452 assert_eq!(inertia.position(), 0.0);
453 assert_eq!(inertia.velocity(), 0.0);
454 assert!(!inertia.update(DT));
455 }
456
457 #[test]
458 fn zero_friction_stops_on_first_update() {
459 let mut inertia = Inertia::new(InertiaConfig::new(0.0, 0.0));
460
461 inertia.kick(100.0);
462
463 assert!(!inertia.update(DT));
464 assert_eq!(inertia.velocity(), 0.0);
465 }
466
467 #[test]
468 fn reversed_bounds_are_normalized_when_applied() {
469 let config = InertiaConfig::new(1000.0, 1.0).with_bounds(InertiaBounds::new(10.0, -10.0));
470 let mut inertia = Inertia::with_position(config, 100.0);
471
472 assert_eq!(inertia.position(), 10.0);
473 inertia.snap_to(-100.0);
474 assert_eq!(inertia.position(), -10.0);
475 }
476
477 #[cfg(any(feature = "std", feature = "alloc"))]
478 #[test]
479 fn inertia_n_vec4_bounds_velocity_and_snap() {
480 let config = InertiaConfig::new(1000.0, 0.5).with_bounds(InertiaBounds::new(
481 [-10.0, -10.0, -10.0, -10.0],
482 [10.0, 10.0, 10.0, 10.0],
483 ));
484 let mut inertia: InertiaN<[f32; 4]> = InertiaN::new(config, [0.0, 0.0, 0.0, 0.0]);
485
486 inertia.kick([100.0, -50.0, 25.0, -10.0]);
487 assert_eq!(inertia.velocity(), [100.0, -50.0, 25.0, -10.0]);
488 assert!(inertia.update(DT));
489 inertia.snap_to([20.0, -20.0, 5.0, -5.0]);
490
491 assert_eq!(inertia.position(), [10.0, -10.0, 5.0, -5.0]);
492 assert_eq!(inertia.velocity(), [0.0, 0.0, 0.0, 0.0]);
493 }
494}