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