Skip to main content

i_slint_core/animations/
physics_simulation.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4// cSpell: ignore signum underdamped
5//! This module contains various physics simulations which can be used as animation (internally only yet).
6//! Currently it is used in the flickable to animate the viewport position
7//!
8//! Currently it contains two simulations:
9//! - `ConstantDeceleration`
10//! - `ConstantDecelerationSpringDamper` with spring damper simulation when reaching the limit
11
12use crate::{Coord, animations::Instant};
13#[cfg(not(feature = "std"))]
14use num_traits::Float;
15
16/// The direction the simulation is running
17#[derive(Debug)]
18enum Direction {
19    /// The start value is smaller than the limit value
20    Increasing,
21    /// The start value is larger than the limit value
22    Decreasing,
23}
24
25/// Common simulation trait
26/// All simulations must implement this trait
27pub trait Simulation {
28    fn step(&mut self, current: &mut f32, new_tick: Instant) -> bool;
29}
30
31/// Trait to convert parameter objects into a simulation
32/// All parameter objects must implement this trait!
33pub trait Parameter {
34    type Output;
35    fn simulation(
36        self,
37        start_value: f32,
38        limit_value: core::pin::Pin<alloc::boxed::Box<crate::Property<f32>>>,
39    ) -> Self::Output;
40}
41
42/// Input parameters for the `ConstantDeceleration` simulation
43#[derive(Debug, Clone)]
44pub struct ConstantDecelerationParameters {
45    pub initial_velocity: f32,
46    pub deceleration: f32,
47}
48
49impl ConstantDecelerationParameters {
50    pub fn new(initial_velocity: f32, deceleration: f32) -> Self {
51        Self { initial_velocity, deceleration }
52    }
53
54    /// Creates a new `ConstantDecelerationParameters` parameter object based on the distance
55    /// to travel and duration of the animation.
56    /// The deceleration is chosen such that the animation covers the given distance at the end of
57    /// the animation and the velocity becomes zero at the same time (after duration_secs).
58    ///
59    // * `distance` - the distance to cover with this animation
60    // * `duration_secs` - the duration of the animation in seconds
61    pub fn new_with_distance(distance: f32, duration_secs: f32) -> Self {
62        debug_assert!(duration_secs > 0., "Duration must be greater than zero");
63
64        // The initial velocity and deceleration are calculated based on the distance and duration to cover the given distance in the given time.
65        //
66        // The calculation is based on the equations of motion for constant acceleration:
67        //      => v0 * t + 0.5 * a * t^2 = d
68        //
69        //
70        // Where t = duration_secs, d = distance, v0 = initial_velocity and a = -deceleration
71        // Warning! a is acceleration, not deceleration, so we need to flip the sign at the end
72        //
73        // We want to reach the limit value at the end of the animation, and the velocity should become zero at the same time, so we can determine `a` based on:
74        //          v0 + a * t = 0
75        //
76        //      => a = -v0 / t
77        //
78        // Then we can solve for `v0` and `a`:
79        //
80        //     v0 * t + 0.5 * -(v0 / t) * t^2 = d
81        //     v0 * t + 0.5 * -v0 * t = d
82        //     v0 * (t + -0.5 * t) = d
83        //     v0 * (0.5 * t) = d
84        //     => v0 = d / (0.5 * t)
85        //
86        let d = distance;
87        let t = duration_secs;
88        let v0 = d / (0.5 * t);
89        let a = -(v0 / t);
90        // deceleration: therefore -a
91        Self::new(v0, -a)
92    }
93
94    /// Calculates the remaining distance to the limit value at a given time based on the initial velocity and deceleration.
95    pub fn remaining_distance(&self, time_elapsed: core::time::Duration) -> Coord {
96        debug_assert!(self.deceleration != 0., "deceleration must not be zero");
97        debug_assert!(
98            self.deceleration.signum() == self.initial_velocity.signum(),
99            "deceleration must actually decelerate the velocity"
100        );
101
102        // The animation stops if the velocity becomes zero.
103        // Therefore we can calculate the animation duration based on the initial velocity and deceleration:
104        //          v0 + a * t = 0
105        //          => t = -v0 / a
106        // Note: our deceleration is `-a` negated
107        let total_duration = self.initial_velocity / self.deceleration;
108
109        if time_elapsed.as_secs_f32() < total_duration {
110            // Based on the equations of motion for constant acceleration we can calculate the remaining distance at a given time:
111            (0.5 * (-self.deceleration)
112                * (total_duration.powi(2) - time_elapsed.as_secs_f32().powi(2))
113                + self.initial_velocity * (total_duration - time_elapsed.as_secs_f32()))
114                as Coord
115        } else {
116            Coord::default()
117        }
118    }
119}
120
121impl Parameter for ConstantDecelerationParameters {
122    type Output = ConstantDeceleration;
123    fn simulation(
124        self,
125        start_value: f32,
126        limit_value: core::pin::Pin<alloc::boxed::Box<crate::Property<f32>>>,
127    ) -> Self::Output {
128        ConstantDeceleration::new(start_value, limit_value, self)
129    }
130}
131
132/// This simulation simulates a constant deceleration of a point starting at position `start_value` with
133/// an initial velocity of `initial_velocity`. When the point reaches the limit value `limit_value` it stops there
134#[derive(Debug)]
135pub struct ConstantDeceleration {
136    /// If the limit is not reached, it is also fine. Also exceeding the limit can be ok,
137    /// but at the end of the animation the limit shall not be exceeded
138    limit_value: core::pin::Pin<alloc::boxed::Box<crate::Property<f32>>>,
139    velocity: f32,
140    data: ConstantDecelerationParameters,
141    direction: Direction,
142    start_time: Instant,
143}
144
145impl ConstantDeceleration {
146    /// Create a new ConstantDeceleration simulation
147    ///
148    /// * `start_value` - start position
149    /// * `limit_value` - value at which the simulation ends if the velocity did not get zero before
150    /// * `initial_velocity` - the initial velocity of the point
151    /// * `data` - the properties of this simulation
152    pub fn new(
153        start_value: f32,
154        limit_value: core::pin::Pin<alloc::boxed::Box<crate::Property<f32>>>,
155        data: ConstantDecelerationParameters,
156    ) -> Self {
157        Self::new_internal(start_value, limit_value, data, crate::animations::current_tick())
158    }
159
160    fn new_internal(
161        start_value: f32,
162        limit_value: core::pin::Pin<alloc::boxed::Box<crate::Property<f32>>>,
163        mut data: ConstantDecelerationParameters,
164        start_time: Instant,
165    ) -> Self {
166        let mut initial_velocity = data.initial_velocity;
167        let direction = if start_value == limit_value.as_ref().get() {
168            if initial_velocity >= 0. {
169                data.deceleration = f32::abs(data.deceleration);
170                Direction::Increasing
171            } else {
172                data.deceleration = -f32::abs(data.deceleration);
173                Direction::Decreasing
174            }
175        } else if start_value < limit_value.as_ref().get() {
176            data.deceleration = f32::abs(data.deceleration);
177            assert!(initial_velocity >= 0.); // Makes no sense yet that the velocity goes into the other direction
178            initial_velocity = f32::abs(initial_velocity);
179            Direction::Increasing
180        } else {
181            data.deceleration = -f32::abs(data.deceleration);
182            initial_velocity = -f32::abs(initial_velocity);
183            assert!(initial_velocity <= 0.);
184            Direction::Decreasing
185        };
186
187        Self { limit_value, velocity: initial_velocity, data, direction, start_time }
188    }
189
190    fn step_internal(&mut self, current: &mut f32, new_tick: Instant) -> bool {
191        let limit_value = self.limit_value.as_ref().get();
192
193        // We have to prevent go go beyond the limit where velocity gets zero
194        let duration = f32::min(
195            new_tick.duration_since(self.start_time).as_secs_f32(),
196            f32::abs(self.velocity / self.data.deceleration),
197        );
198
199        self.start_time = new_tick;
200
201        let new_velocity = self.velocity - duration * self.data.deceleration;
202
203        *current += duration * (self.velocity + new_velocity) / 2.; // Trapezoidal integration
204        self.velocity = new_velocity;
205
206        match self.direction {
207            Direction::Increasing => {
208                if *current >= limit_value {
209                    *current = limit_value;
210                    self.velocity = 0.;
211                    return true;
212                } else if self.velocity <= 0. {
213                    return true;
214                }
215            }
216            Direction::Decreasing => {
217                if *current <= limit_value {
218                    *current = limit_value;
219                    self.velocity = 0.;
220                    return true;
221                } else if self.velocity >= 0. {
222                    return true;
223                }
224            }
225        }
226        false
227    }
228}
229
230impl Simulation for ConstantDeceleration {
231    fn step(&mut self, current: &mut f32, new_tick: Instant) -> bool {
232        self.step_internal(current, new_tick)
233    }
234}
235
236#[cfg(test)]
237macro_rules! assert_approx_eq {
238    ($a:expr, $b:expr) => {
239        assert!(($a - $b).abs() < 1e-4, "{} != {}", $a, $b);
240    };
241}
242#[cfg(test)]
243fn test_limit_property(value: f32) -> core::pin::Pin<alloc::boxed::Box<crate::Property<f32>>> {
244    alloc::boxed::Box::pin(crate::Property::new(value))
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use core::time::Duration;
251
252    #[test]
253    fn constant_deceleration_start_eq_limit() {
254        const START_VALUE: f32 = 10.;
255        const LIMIT_VALUE: f32 = 10.;
256        const INITIAL_VELOCITY: f32 = 50.;
257        const DECELERATION: f32 = 20.;
258        let parameters = ConstantDecelerationParameters::new(INITIAL_VELOCITY, DECELERATION);
259
260        let time = Instant::now();
261        let mut simulation = ConstantDeceleration::new_internal(
262            START_VALUE,
263            test_limit_property(LIMIT_VALUE),
264            parameters,
265            time,
266        );
267
268        let mut current = START_VALUE;
269        let finished = simulation.step(&mut current, time + Duration::from_hours(10));
270        assert_eq!(finished, true);
271        assert_eq!(current, START_VALUE);
272    }
273
274    /// The velocity becomes zero before we are reaching the limit
275    /// start_value < limit_value
276    #[test]
277    fn constant_deceleration_increasing_limit_not_reached() {
278        const START_VALUE: f32 = 10.;
279        const LIMIT_VALUE: f32 = 2000.;
280        const INITIAL_VELOCITY: f32 = 50.;
281        const DECELERATION: f32 = 20.;
282        let parameters = ConstantDecelerationParameters::new(INITIAL_VELOCITY, DECELERATION);
283
284        let mut time = Instant::now();
285        let mut simulation = ConstantDeceleration::new_internal(
286            START_VALUE,
287            test_limit_property(LIMIT_VALUE),
288            parameters,
289            time,
290        );
291        let mut current = START_VALUE;
292
293        // Velocity does not become zero
294        let mut duration = Duration::from_secs(1);
295        assert!(DECELERATION * duration.as_secs_f32() < INITIAL_VELOCITY);
296        time += duration;
297        let finished = simulation.step(&mut current, time);
298        assert_eq!(finished, false);
299        assert_approx_eq!(
300            current,
301            START_VALUE + INITIAL_VELOCITY * duration.as_secs_f32()
302                - 0.5 * DECELERATION * duration.as_secs_f32().powi(2)
303        );
304
305        // Now the velocity becomes zero and we don't do any further calculations
306        duration = Duration::from_hours(10);
307        assert!(Duration::from_secs((INITIAL_VELOCITY / DECELERATION) as u64) < duration);
308        time += duration;
309        let finished = simulation.step(&mut current, time);
310        assert_eq!(finished, true);
311        assert_approx_eq!(
312            current,
313            START_VALUE + INITIAL_VELOCITY * INITIAL_VELOCITY / DECELERATION
314                - 0.5 * DECELERATION * (INITIAL_VELOCITY / DECELERATION).powi(2)
315        );
316
317        assert!(current < LIMIT_VALUE); // We reached velocity zero before we reached the position limit
318    }
319
320    /// We reach the position limit before the velocity got zero
321    #[test]
322    fn constant_deceleration_increasing_limit_reached() {
323        const START_VALUE: f32 = 10.;
324        const LIMIT_VALUE: f32 = 20.;
325        const INITIAL_VELOCITY: f32 = 50.;
326        const DECELERATION: f32 = 20.;
327        let parameters = ConstantDecelerationParameters::new(INITIAL_VELOCITY, DECELERATION);
328
329        let mut time = Instant::now();
330        let mut simulation = ConstantDeceleration::new_internal(
331            START_VALUE,
332            test_limit_property(LIMIT_VALUE),
333            parameters,
334            time,
335        );
336        let mut current = START_VALUE;
337
338        let duration = Duration::from_secs(1);
339        assert!(f32::abs(DECELERATION * duration.as_secs_f32()) < f32::abs(INITIAL_VELOCITY)); // We don't reach the limit where the velocity gets zero
340        time += duration;
341        let finished = simulation.step(&mut current, time);
342        assert_eq!(finished, true);
343        assert_eq!(current, LIMIT_VALUE); // Limit reached
344    }
345
346    /// We don't reach the position limit. Before the velocity gets zero
347    /// start_value > limit_value
348    #[test]
349    fn constant_deceleration_decreasing_limit_not_reached() {
350        const START_VALUE: f32 = 2000.;
351        const LIMIT_VALUE: f32 = 10.;
352        const INITIAL_VELOCITY: f32 = -50.;
353        const DECELERATION: f32 = 20.;
354
355        let parameters = ConstantDecelerationParameters::new(INITIAL_VELOCITY, DECELERATION);
356
357        let mut time = Instant::now();
358        let mut simulation = ConstantDeceleration::new_internal(
359            START_VALUE,
360            test_limit_property(LIMIT_VALUE),
361            parameters,
362            time,
363        );
364        let mut current = START_VALUE;
365
366        let mut duration = Duration::from_secs(1);
367        assert!(f32::abs(DECELERATION * duration.as_secs_f32()) < f32::abs(INITIAL_VELOCITY));
368        time += duration;
369        let finished = simulation.step(&mut current, time);
370        assert_eq!(finished, false);
371        assert_eq!(
372            current,
373            START_VALUE + INITIAL_VELOCITY * duration.as_secs_f32()
374                - INITIAL_VELOCITY.signum() * 0.5 * DECELERATION * duration.as_secs_f32().powi(2)
375        );
376
377        duration = Duration::from_hours(10);
378        assert!(Duration::from_secs((INITIAL_VELOCITY / DECELERATION) as u64) < duration);
379        time += duration;
380        let finished = simulation.step(&mut current, time);
381        assert_eq!(finished, true);
382        assert_eq!(
383            current,
384            START_VALUE + INITIAL_VELOCITY * f32::abs(INITIAL_VELOCITY / DECELERATION)
385                - 0.5
386                    * INITIAL_VELOCITY.signum()
387                    * DECELERATION
388                    * (INITIAL_VELOCITY / DECELERATION).powi(2)
389        );
390
391        assert!(current > LIMIT_VALUE); // We reached velocity zero before we reached the position limit
392    }
393
394    /// We reach the position limit before the velocity got zero
395    /// start_value > limit_value
396    #[test]
397    fn constant_deceleration_decreasing_limit_reached() {
398        const START_VALUE: f32 = 20.;
399        const LIMIT_VALUE: f32 = 10.;
400        const INITIAL_VELOCITY: f32 = -50.;
401        const DECELERATION: f32 = 20.;
402        let parameters = ConstantDecelerationParameters::new(INITIAL_VELOCITY, DECELERATION);
403
404        let mut time = Instant::now();
405        let mut simulation = ConstantDeceleration::new_internal(
406            START_VALUE,
407            test_limit_property(LIMIT_VALUE),
408            parameters,
409            time,
410        );
411        let mut current = START_VALUE;
412
413        let duration = Duration::from_secs(3);
414        assert!(f32::abs(DECELERATION * duration.as_secs_f32()) > f32::abs(INITIAL_VELOCITY)); // We don't reach the limit where the velocity gets zero
415        time += duration;
416        let finished = simulation.step(&mut current, time);
417        assert_eq!(finished, true);
418        assert_eq!(current, LIMIT_VALUE); // Limit reached
419    }
420}
421
422/// Input parameters for the `ConstantDecelerationSpringDamper` simulation
423/// [1] https://www.maplesoft.com/content/EngineeringFundamentals/6/MapleDocument_32/Free%20Response%20Part%202.pdf
424#[cfg(test)]
425#[derive(Debug, Clone)]
426pub struct ConstantDecelerationSpringDamperParameters {
427    pub initial_velocity: f32,
428    pub deceleration: f32,
429    pub mass: f32,                // [1] parameter m
430    pub spring_constant: f32,     // [1] parameter k
431    pub damping_coefficient: f32, // [1] parameter c
432}
433
434#[cfg(test)]
435impl ConstantDecelerationSpringDamperParameters {
436    /// Creates a new `ConstantDecelerationSpringDamperParameters` parameter object
437    /// It is more comfortable to use than specifying the parameters manually because here the parameter calculation
438    /// is done based on the `half_period_time` parameter
439    ///
440    /// * `initial_velocity` - the initial velocity of the point
441    /// * `deceleration` - the constant deceleration of the point
442    /// * `half_period_time` - the time of the simulation when the limit value got exceeded to return back to it
443    pub fn new(initial_velocity: f32, deceleration: f32, half_period_time: f32) -> Self {
444        let (mass, spring_constant, damping_coefficient) =
445            Self::calculate_parameters(half_period_time);
446
447        Self { initial_velocity, deceleration, mass, spring_constant, damping_coefficient }
448    }
449
450    fn calculate_parameters(half_period_time: f32) -> (f32, f32, f32) {
451        // [1] eq 13
452        const MASS: f32 = 1.;
453        const DAMPING_COEFFICIENT: f32 = 1.;
454        let w_d = 2. * core::f32::consts::PI * 1. / (2. * half_period_time);
455        let spring_constant = w_d.powi(2) + DAMPING_COEFFICIENT.powi(2) / (4. * MASS.powi(2));
456
457        (MASS, spring_constant, DAMPING_COEFFICIENT)
458    }
459}
460
461#[cfg(test)]
462impl Parameter for ConstantDecelerationSpringDamperParameters {
463    type Output = ConstantDecelerationSpringDamper;
464    fn simulation(
465        self,
466        start_value: f32,
467        limit_value: core::pin::Pin<alloc::boxed::Box<crate::Property<f32>>>,
468    ) -> Self::Output {
469        ConstantDecelerationSpringDamper::new(start_value, limit_value, self)
470    }
471}
472
473#[cfg(test)]
474#[derive(Debug, PartialEq)]
475enum State {
476    Deceleration,
477    SpringDamper,
478    Done,
479}
480
481/// This simulation simulates a constant deceleration of a point starting at position `start_value` with
482/// an initial velocity of `initial_velocity`. When the point reaches the limit value `limit_value` before
483/// the velocity reaches zero, the system simulates a spring damper system to go shortly beyond the limit
484/// value and returning then back
485#[cfg(test)]
486#[derive(Debug)]
487pub struct ConstantDecelerationSpringDamper {
488    /// If the limit is not reached, it is also fine. Also exceeding the limit can be ok,
489    /// but at the end of the animation the limit shall not be exceeded
490    limit_value: core::pin::Pin<alloc::boxed::Box<crate::Property<f32>>>,
491    curr_val_zeroed: f32,
492    velocity: f32,
493    data: ConstantDecelerationSpringDamperParameters,
494    direction: Direction,
495    start_time: Instant,
496    state: State,
497    damping_ratio: f32,
498    /// Undamped natural frequency
499    w_n: f32,
500    /// Damped natural frequency
501    w_d: f32,
502    constant_a: f32,
503    constant_phi: f32,
504}
505
506#[cfg(test)]
507impl ConstantDecelerationSpringDamper {
508    pub fn new(
509        start_value: f32,
510        limit_value: core::pin::Pin<alloc::boxed::Box<crate::Property<f32>>>,
511        data: ConstantDecelerationSpringDamperParameters,
512    ) -> Self {
513        Self::new_internal(start_value, limit_value, data, crate::animations::current_tick())
514    }
515
516    fn new_internal(
517        start_value: f32,
518        limit_value: core::pin::Pin<alloc::boxed::Box<crate::Property<f32>>>,
519        mut data: ConstantDecelerationSpringDamperParameters,
520        start_time: Instant,
521    ) -> Self {
522        let mut initial_velocity = data.initial_velocity;
523        let mut state = State::Deceleration;
524        let direction = if start_value == limit_value.as_ref().get() {
525            state = State::Done;
526            if initial_velocity >= 0. {
527                data.deceleration = f32::abs(data.deceleration);
528                Direction::Increasing
529            } else {
530                data.deceleration = -f32::abs(data.deceleration);
531                Direction::Decreasing
532            }
533        } else if start_value < limit_value.as_ref().get() {
534            data.deceleration = f32::abs(data.deceleration);
535            assert!(initial_velocity >= 0.); // Makes no sense yet that the velocity goes into the other direction
536            initial_velocity = f32::abs(initial_velocity);
537            Direction::Increasing
538        } else {
539            data.deceleration = -f32::abs(data.deceleration);
540            initial_velocity = -f32::abs(initial_velocity);
541            assert!(initial_velocity <= 0.);
542            Direction::Decreasing
543        };
544
545        assert!(data.mass > 0.);
546        assert!(data.spring_constant >= 0.);
547
548        let c_cr = 2. * f32::sqrt(data.mass * data.spring_constant); // Critical damping coefficient
549        let damping_ratio = data.damping_coefficient / c_cr;
550        assert!(damping_ratio > 0.);
551        assert!(damping_ratio < 1.); // Currently we support only the underdamped motion, because we wanna return to the `limit_value`
552
553        let w_n = c_cr / (2. * data.mass);
554        let w_d = w_n * f32::sqrt(1. - damping_ratio.powi(2));
555
556        Self {
557            limit_value,
558            curr_val_zeroed: 0.,
559            velocity: initial_velocity,
560            data,
561            direction,
562            start_time,
563            state,
564            damping_ratio,
565            w_n,
566            w_d,
567            constant_a: 0., // Calculated when transitioning to the damper spring state
568            constant_phi: 0., // Calculated when transitioning to the damper spring state
569        }
570    }
571
572    fn new_value(&self) -> f32 {
573        self.limit_value.as_ref().get() + self.curr_val_zeroed
574    }
575
576    fn step_internal(&mut self, current: &mut f32, new_tick: Instant) -> bool {
577        match self.state {
578            State::Deceleration => self.state_deceleration(current, new_tick),
579            State::SpringDamper => self.state_spring_damper(current, new_tick),
580            State::Done => {
581                *current = self.new_value();
582                true
583            }
584        }
585    }
586
587    fn state_deceleration(&mut self, current: &mut f32, new_tick: Instant) -> bool {
588        let limit_value = self.limit_value.as_ref().get();
589        let duration_unlimited = new_tick.duration_since(self.start_time);
590        // We have to prevent go go beyond the limit where velocity gets zero
591        let duration = f32::min(
592            duration_unlimited.as_secs_f32(),
593            f32::abs(self.velocity / self.data.deceleration),
594        );
595
596        self.start_time = new_tick;
597
598        let new_velocity = self.velocity - (duration * self.data.deceleration);
599        let new_val = *current + (duration * (self.velocity + new_velocity) / 2.); // Trapezoidal integration
600
601        enum S {
602            LimitReached,
603            VelocityZero,
604            None,
605        }
606
607        let s = match self.direction {
608            Direction::Increasing if new_val > limit_value => S::LimitReached,
609            Direction::Increasing if new_velocity <= 0. => S::VelocityZero,
610            Direction::Decreasing if new_val < limit_value => S::LimitReached,
611            Direction::Decreasing if new_velocity >= 0. => S::VelocityZero,
612            _ => S::None,
613        };
614        match s {
615            S::LimitReached => {
616                self.state = State::SpringDamper;
617
618                // time when reaching the limit
619                // solving p_limit = p_old + v_old * dt - 0.5 * a * dt^2
620                let root = f32::sqrt(
621                    self.velocity.powi(2) - self.data.deceleration * (limit_value - *current),
622                );
623                // The smaller is the relevant. The larger is when the initial velocity got zero and due to the constant acceleration we turn
624                let dt = f32::min(
625                    (self.velocity - root) / self.data.deceleration,
626                    (self.velocity + root) / self.data.deceleration,
627                );
628
629                self.velocity -= dt * self.data.deceleration; // Velocity at limit value point. Solved `new_val` equation for new_velocity
630                self.curr_val_zeroed = 0.;
631                *current = limit_value;
632
633                const X0: f32 = 0.; // Relative point
634                self.constant_a = self.velocity.signum()
635                    * f32::sqrt(
636                        (self.w_d.powi(2) * X0.powi(2)
637                            + (self.velocity + self.damping_ratio * self.w_n * 0.).powi(2))
638                            / self.w_d.powi(2),
639                    );
640                self.constant_phi =
641                    f32::atan(self.w_d * X0 / (self.velocity + self.damping_ratio * self.w_n * X0));
642                self.state_spring_damper(
643                    current,
644                    new_tick
645                        + (duration_unlimited
646                            - core::time::Duration::from_millis((dt * 1000.) as u64)),
647                )
648            }
649            S::VelocityZero => {
650                self.velocity = 0.;
651                *current = new_val;
652                true
653            }
654            S::None => {
655                self.velocity = new_velocity;
656                *current = new_val;
657                false
658            }
659        }
660    }
661
662    fn state_spring_damper(&mut self, current: &mut f32, new_tick: Instant) -> bool {
663        // Here we use absolute time because it simplifies the equation
664        let t = (new_tick - self.start_time).as_secs_f32();
665        // Underdamped spring damper equation
666        assert!(self.damping_ratio < 1.);
667        let new_val = self.constant_a
668            * f32::exp(-self.damping_ratio * self.w_n * t)
669            * f32::sin(self.w_d * t + self.constant_phi);
670        self.curr_val_zeroed = new_val; // relative value
671
672        let limit_value = self.limit_value.as_ref().get();
673        let max_time = 2. * core::f32::consts::PI / self.w_d;
674        let current_val = self.new_value();
675        *current = current_val;
676        let finished = match self.direction {
677            Direction::Increasing => {
678                // We are coming back from a value higher than the limit
679                current_val < limit_value || t > max_time
680            }
681            Direction::Decreasing => {
682                // We are coming back from a value lower than the limit
683                current_val > limit_value || t > max_time
684            }
685        };
686        if finished {
687            self.velocity = 0.;
688            *current = limit_value;
689            self.curr_val_zeroed = 0.;
690            self.state = State::Done;
691        }
692        finished
693    }
694}
695
696#[cfg(test)]
697impl Simulation for ConstantDecelerationSpringDamper {
698    fn step(&mut self, current: &mut f32, new_tick: Instant) -> bool {
699        self.step_internal(current, new_tick)
700    }
701}
702
703#[cfg(test)]
704mod tests_spring_damper {
705    use super::*;
706    use core::{f32::consts::PI, time::Duration};
707
708    #[test]
709    fn calculate_parameters() {
710        const INITIAL_VELOCITY: f32 = 50.;
711        const DECELERATION: f32 = 20.;
712        const HALF_PERIOD_TIME: f32 = 100e-3;
713        let res = super::ConstantDecelerationSpringDamperParameters::new(
714            INITIAL_VELOCITY,
715            DECELERATION,
716            HALF_PERIOD_TIME,
717        );
718
719        let w_n = f32::sqrt(res.spring_constant * res.mass) / res.mass;
720        let damping_ratio = res.damping_coefficient / (2. * res.mass * w_n);
721        let w_d = w_n * f32::sqrt(1. - damping_ratio.powi(2));
722        assert_approx_eq!(w_d, 2. * PI * 1. / (2. * HALF_PERIOD_TIME));
723    }
724
725    #[test]
726    fn constant_deceleration_start_eq_limit() {
727        const START_VALUE: f32 = 10.;
728        const LIMIT_VALUE: f32 = 10.;
729        const INITIAL_VELOCITY: f32 = 50.;
730        const DECELERATION: f32 = 20.;
731        const HALF_PERIOD_TIME: f32 = 100e-3;
732        let parameters = ConstantDecelerationSpringDamperParameters::new(
733            INITIAL_VELOCITY,
734            DECELERATION,
735            HALF_PERIOD_TIME,
736        );
737
738        assert_eq!(START_VALUE, LIMIT_VALUE);
739        let time = Instant::now();
740        let mut simulation = ConstantDecelerationSpringDamper::new_internal(
741            START_VALUE,
742            test_limit_property(LIMIT_VALUE),
743            parameters,
744            time,
745        );
746        let mut current = START_VALUE;
747        let finished = simulation.step(&mut current, time);
748        assert_eq!(current, START_VALUE);
749        assert_eq!(finished, true);
750        assert_eq!(simulation.state, State::Done);
751    }
752
753    /// The velocity becomes zero before we are reaching the limit
754    /// start_value < limit_value
755    #[test]
756    fn constant_deceleration_increasing_limit_not_reached() {
757        const INITIAL_VELOCITY: f32 = 50.;
758        const DECELERATION: f32 = 20.;
759        const HALF_PERIOD_TIME: f32 = 100e-3;
760        let parameters = ConstantDecelerationSpringDamperParameters::new(
761            INITIAL_VELOCITY,
762            DECELERATION,
763            HALF_PERIOD_TIME,
764        );
765
766        let mut time = Instant::now();
767        let mut simulation = ConstantDecelerationSpringDamper::new_internal(
768            10.,
769            test_limit_property(2000.),
770            parameters,
771            time,
772        );
773        let mut current = 10.;
774
775        // Velocity does not become zero
776        let mut duration = Duration::from_secs(1);
777        assert!(DECELERATION * duration.as_secs_f32() < INITIAL_VELOCITY);
778        time += duration;
779        let finished = simulation.step(&mut current, time);
780        assert_eq!(finished, false);
781        assert_approx_eq!(
782            current,
783            10. + 50. * duration.as_secs_f32()
784                - 0.5 * DECELERATION * duration.as_secs_f32().powi(2)
785        );
786
787        // Now the velocity becomes zero and we don't do any further calculations
788        duration = Duration::from_hours(10);
789        assert!(Duration::from_secs((INITIAL_VELOCITY / DECELERATION) as u64) < duration);
790        time += duration;
791        let finished = simulation.step(&mut current, time);
792        assert_eq!(finished, true);
793        assert_approx_eq!(
794            current,
795            10. + 50. * INITIAL_VELOCITY / DECELERATION
796                - 0.5 * DECELERATION * (INITIAL_VELOCITY / DECELERATION).powi(2)
797        );
798
799        assert!(current < 2000.); // We reached velocity zero before we reached the position limit
800    }
801
802    /// We don't reach the position limit. Before the velocity gets zero
803    /// start_value > limit_value
804    #[test]
805    fn constant_deceleration_decreasing_limit_not_reached() {
806        const START_VALUE: f32 = 2000.;
807        const LIMIT_VALUE: f32 = 10.;
808        const INITIAL_VELOCITY: f32 = -50.;
809        const DECELERATION: f32 = 20.;
810        const HALF_PERIOD_TIME: f32 = 100e-3;
811
812        let parameters = ConstantDecelerationSpringDamperParameters::new(
813            INITIAL_VELOCITY,
814            DECELERATION,
815            HALF_PERIOD_TIME,
816        );
817
818        let mut time = Instant::now();
819        let mut simulation = ConstantDecelerationSpringDamper::new_internal(
820            START_VALUE,
821            test_limit_property(LIMIT_VALUE),
822            parameters,
823            time,
824        );
825        let mut current = START_VALUE;
826
827        let mut duration = Duration::from_secs(1);
828        assert!(f32::abs(DECELERATION * duration.as_secs_f32()) < f32::abs(INITIAL_VELOCITY));
829        time += duration;
830        let finished = simulation.step(&mut current, time);
831        assert_eq!(finished, false);
832        assert_eq!(
833            current,
834            START_VALUE + INITIAL_VELOCITY * duration.as_secs_f32()
835                - INITIAL_VELOCITY.signum() * 0.5 * DECELERATION * duration.as_secs_f32().powi(2)
836        );
837
838        duration = Duration::from_hours(10);
839        assert!(Duration::from_secs((INITIAL_VELOCITY / DECELERATION) as u64) < duration);
840        time += duration;
841        let finished = simulation.step(&mut current, time);
842        assert_eq!(finished, true);
843        assert_eq!(
844            current,
845            START_VALUE + INITIAL_VELOCITY * f32::abs(INITIAL_VELOCITY / DECELERATION)
846                - 0.5
847                    * INITIAL_VELOCITY.signum()
848                    * DECELERATION
849                    * (INITIAL_VELOCITY / DECELERATION).powi(2)
850        );
851
852        assert!(current > LIMIT_VALUE); // We reached velocity zero before we reached the position limit
853    }
854
855    /// We reach the position limit before the velocity got zero and so we run into the spring damper system
856    /// Increasing case: start_value < limit_value
857    #[test]
858    fn constant_deceleration_spring_damper_increasing_limit_reached() {
859        const INITIAL_VELOCITY: f32 = 50.;
860        const DECELERATION: f32 = 20.;
861        const HALF_PERIOD_TIME: f32 = 10.;
862        const START_VALUE: f32 = 10.;
863        const LIMIT_VALUE: f32 = 70.;
864        let parameters = super::ConstantDecelerationSpringDamperParameters::new(
865            INITIAL_VELOCITY,
866            DECELERATION,
867            HALF_PERIOD_TIME,
868        );
869
870        let mut time = Instant::now();
871        let mut simulation = ConstantDecelerationSpringDamper::new_internal(
872            START_VALUE,
873            test_limit_property(LIMIT_VALUE),
874            parameters,
875            time,
876        );
877        let mut current = START_VALUE;
878
879        let duration = Duration::from_secs(1);
880        assert!(f32::abs(DECELERATION) * duration.as_secs_f32() < f32::abs(INITIAL_VELOCITY)); // We don't reach the limit where the velocity gets zero
881        time += duration;
882        let finished = simulation.step(&mut current, time);
883        assert_eq!(finished, false);
884        assert_eq!(simulation.state, State::Deceleration);
885        assert!(current < LIMIT_VALUE); // We are still in the constant deceleration state
886
887        time += Duration::from_secs((HALF_PERIOD_TIME / 2.) as u64);
888        let finished = simulation.step(&mut current, time);
889        assert_eq!(finished, false);
890        assert_eq!(simulation.state, State::SpringDamper);
891        assert!(current > LIMIT_VALUE);
892
893        time += Duration::from_hours(10);
894        let finished = simulation.step(&mut current, time);
895        assert_eq!(finished, true);
896        assert_eq!(simulation.state, State::Done);
897        assert_eq!(current, LIMIT_VALUE);
898    }
899
900    /// We reach the position limit before the velocity got zero and so we run into the spring damper system
901    /// Decreasing case. limit_value < start_value
902    #[test]
903    fn constant_deceleration_spring_damper_decreasing_limit_reached() {
904        const INITIAL_VELOCITY: f32 = -50.;
905        const DECELERATION: f32 = 20.;
906        const HALF_PERIOD_TIME: f32 = 10.;
907        const START_VALUE: f32 = 70.;
908        const LIMIT_VALUE: f32 = 10.;
909        let parameters = super::ConstantDecelerationSpringDamperParameters::new(
910            INITIAL_VELOCITY,
911            DECELERATION,
912            HALF_PERIOD_TIME,
913        );
914
915        let mut time = Instant::now();
916        let mut simulation = ConstantDecelerationSpringDamper::new_internal(
917            START_VALUE,
918            test_limit_property(LIMIT_VALUE),
919            parameters,
920            time,
921        );
922        let mut current = START_VALUE;
923
924        let duration = Duration::from_secs(1);
925        assert!(f32::abs(DECELERATION) * duration.as_secs_f32() < f32::abs(INITIAL_VELOCITY)); // We don't reach the limit where the velocity gets zero
926        time += duration;
927        let finished = simulation.step(&mut current, time);
928        assert_eq!(finished, false);
929        assert_eq!(simulation.state, State::Deceleration);
930        assert!(current > LIMIT_VALUE); // We are still in the constant deceleration state
931
932        time += Duration::from_secs((HALF_PERIOD_TIME / 2.) as u64);
933        let finished = simulation.step(&mut current, time);
934        assert_eq!(finished, false);
935        assert_eq!(simulation.state, State::SpringDamper);
936        assert!(current < LIMIT_VALUE);
937
938        time += Duration::from_hours(10);
939        let finished = simulation.step(&mut current, time);
940        assert_eq!(finished, true);
941        assert_eq!(simulation.state, State::Done);
942        assert_eq!(current, LIMIT_VALUE);
943    }
944}