From ce5abc761f6cc19c75f8f6c13c9eb8a0752fba97 Mon Sep 17 00:00:00 2001
From: Jerome Humbert <djeedai@gmail.com>
Date: Thu, 24 Feb 2022 20:25:55 +0000
Subject: [PATCH] Fix bugs in `set_progress()` and `times_completed()`

---
 src/lib.rs       | 132 ++++++++++++++++++++
 src/tweenable.rs | 315 +++++++++++++++++++++++++++++++++++++----------
 2 files changed, 380 insertions(+), 67 deletions(-)

diff --git a/src/lib.rs b/src/lib.rs
index 8453259..38ed925 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -526,6 +526,54 @@ impl<T: Asset> AssetAnimator<T> {
 mod tests {
     use super::{lens::*, *};
 
+    #[test]
+    fn tweening_type() {
+        let tweening_type = TweeningType::default();
+        assert_eq!(tweening_type, TweeningType::Once);
+    }
+
+    #[test]
+    fn tweening_direction() {
+        let tweening_direction = TweeningDirection::default();
+        assert_eq!(tweening_direction, TweeningDirection::Forward);
+    }
+
+    #[test]
+    fn animator_state() {
+        let mut state = AnimatorState::default();
+        assert_eq!(state, AnimatorState::Playing);
+        state = !state;
+        assert_eq!(state, AnimatorState::Paused);
+        state = !state;
+        assert_eq!(state, AnimatorState::Playing);
+    }
+
+    #[test]
+    fn ease_method() {
+        let ease = EaseMethod::default();
+        assert!(matches!(ease, EaseMethod::Linear));
+
+        let ease = EaseMethod::EaseFunction(EaseFunction::QuadraticIn);
+        assert_eq!(0., ease.sample(0.));
+        assert_eq!(0.25, ease.sample(0.5));
+        assert_eq!(1., ease.sample(1.));
+
+        let ease = EaseMethod::Linear;
+        assert_eq!(0., ease.sample(0.));
+        assert_eq!(0.5, ease.sample(0.5));
+        assert_eq!(1., ease.sample(1.));
+
+        let ease = EaseMethod::Discrete(0.3);
+        assert_eq!(0., ease.sample(0.));
+        assert_eq!(1., ease.sample(0.5));
+        assert_eq!(1., ease.sample(1.));
+
+        let ease = EaseMethod::CustomFunction(|f| 1. - f);
+        assert_eq!(0., ease.sample(1.));
+        assert_eq!(0.5, ease.sample(0.5));
+        assert_eq!(1., ease.sample(0.));
+    }
+
     /// Animator::new()
     #[test]
     fn animator_new() {
@@ -583,6 +631,48 @@ mod tests {
         assert!(animator.tweenable_mut().is_some());
     }
 
+    /// Animator control playback
+    #[test]
+    fn animator_controls() {
+        let tween = Tween::new(
+            EaseFunction::QuadraticInOut,
+            TweeningType::PingPong,
+            std::time::Duration::from_secs(1),
+            TransformRotationLens {
+                start: Quat::IDENTITY,
+                end: Quat::from_axis_angle(Vec3::Z, std::f32::consts::PI / 2.),
+            },
+        );
+        let mut animator = Animator::new(tween);
+        assert_eq!(animator.state, AnimatorState::Playing);
+        assert!(animator.progress().abs() <= 1e-5);
+
+        animator.stop();
+        assert_eq!(animator.state, AnimatorState::Paused);
+        assert!(animator.progress().abs() <= 1e-5);
+
+        animator.set_progress(0.5);
+        assert_eq!(animator.state, AnimatorState::Paused);
+        assert!((animator.progress() - 0.5).abs() <= 1e-5);
+
+        animator.rewind();
+        assert_eq!(animator.state, AnimatorState::Paused);
+        assert!(animator.progress().abs() <= 1e-5);
+
+        animator.set_progress(0.5);
+        animator.state = AnimatorState::Playing;
+        assert_eq!(animator.state, AnimatorState::Playing);
+        assert!((animator.progress() - 0.5).abs() <= 1e-5);
+
+        animator.rewind();
+        assert_eq!(animator.state, AnimatorState::Playing);
+        assert!(animator.progress().abs() <= 1e-5);
+
+        animator.stop();
+        assert_eq!(animator.state, AnimatorState::Paused);
+        assert!(animator.progress().abs() <= 1e-5);
+    }
+
     /// AssetAnimator::new()
     #[test]
     fn asset_animator_new() {
@@ -642,4 +732,46 @@ mod tests {
         assert!(animator.tweenable_mut().is_some());
         assert_eq!(animator.handle(), Handle::<ColorMaterial>::default());
     }
+
+    /// AssetAnimator control playback
+    #[test]
+    fn asset_animator_controls() {
+        let tween = Tween::new(
+            EaseFunction::QuadraticInOut,
+            TweeningType::PingPong,
+            std::time::Duration::from_secs(1),
+            ColorMaterialColorLens {
+                start: Color::RED,
+                end: Color::BLUE,
+            },
+        );
+        let mut animator = AssetAnimator::new(Handle::<ColorMaterial>::default(), tween);
+        assert_eq!(animator.state, AnimatorState::Playing);
+        assert!(animator.progress().abs() <= 1e-5);
+
+        animator.stop();
+        assert_eq!(animator.state, AnimatorState::Paused);
+        assert!(animator.progress().abs() <= 1e-5);
+
+        animator.set_progress(0.5);
+        assert_eq!(animator.state, AnimatorState::Paused);
+        assert!((animator.progress() - 0.5).abs() <= 1e-5);
+
+        animator.rewind();
+        assert_eq!(animator.state, AnimatorState::Paused);
+        assert!(animator.progress().abs() <= 1e-5);
+
+        animator.set_progress(0.5);
+        animator.state = AnimatorState::Playing;
+        assert_eq!(animator.state, AnimatorState::Playing);
+        assert!((animator.progress() - 0.5).abs() <= 1e-5);
+
+        animator.rewind();
+        assert_eq!(animator.state, AnimatorState::Playing);
+        assert!(animator.progress().abs() <= 1e-5);
+
+        animator.stop();
+        assert_eq!(animator.state, AnimatorState::Paused);
+        assert!(animator.progress().abs() <= 1e-5);
+    }
 }
diff --git a/src/tweenable.rs b/src/tweenable.rs
index e5a3247..a0ddd28 100644
--- a/src/tweenable.rs
+++ b/src/tweenable.rs
@@ -315,6 +315,8 @@ impl<T> Tweenable<T> for Tween<T> {
     }
 
     fn set_progress(&mut self, progress: f32) {
+        // need to reset() to clear finished() unfortunately
+        self.timer.reset();
         self.timer.set_elapsed(Duration::from_secs_f64(
             self.timer.duration().as_secs_f64() * progress as f64,
         ));
@@ -456,15 +458,9 @@ impl<T> Tweenable<T> for Sequence<T> {
     }
 
     fn set_progress(&mut self, progress: f32) {
-        let progress = progress.max(0.);
-        self.times_completed = progress as u32;
-        let progress = if self.is_looping() {
-            progress.fract()
-        } else {
-            progress.min(1.)
-        };
-
-        // Set the total sequence progress
+        self.times_completed = if progress >= 1. { 1 } else { 0 };
+        let progress = progress.clamp(0., 1.); // not looping
+                                               // Set the total sequence progress
         let total_elapsed_secs = self.duration().as_secs_f64() * progress as f64;
         self.time = Duration::from_secs_f64(total_elapsed_secs);
 
@@ -568,10 +564,14 @@ impl<T> Tweenable<T> for Tracks<T> {
     }
 
     fn set_progress(&mut self, progress: f32) {
-        let progress = progress.max(0.);
-        self.times_completed = progress as u32;
-        let progress = progress.fract();
-        self.time = Duration::from_secs_f64(self.duration().as_secs_f64() * progress as f64);
+        self.times_completed = if progress >= 1. { 1 } else { 0 }; // not looping
+        let progress = progress.clamp(0., 1.); // not looping
+        let time_secs = self.duration.as_secs_f64() * progress as f64;
+        self.time = Duration::from_secs_f64(time_secs);
+        for tweenable in &mut self.tracks {
+            let progress = time_secs / tweenable.duration().as_secs_f64();
+            tweenable.set_progress(progress as f32);
+        }
     }
 
     fn progress(&self) -> f32 {
@@ -594,6 +594,7 @@ impl<T> Tweenable<T> for Tracks<T> {
         if any_active {
             TweenState::Active
         } else {
+            self.times_completed = 1;
             TweenState::Completed
         }
     }
@@ -644,6 +645,8 @@ impl<T> Tweenable<T> for Delay {
     }
 
     fn set_progress(&mut self, progress: f32) {
+        // need to reset() to clear finished() unfortunately
+        self.timer.reset();
         self.timer.set_elapsed(Duration::from_secs_f64(
             self.timer.duration().as_secs_f64() * progress as f64,
         ));
@@ -722,6 +725,8 @@ mod tests {
                     end: Vec3::ONE,
                 },
             );
+            assert!(tween.on_completed.is_none());
+            assert!(tween.event_data.is_none());
 
             let dummy_entity = Entity::from_raw(42);
 
@@ -734,69 +739,104 @@ mod tests {
                 cb_mon.invoke_count += 1;
                 cb_mon.last_reported_count = tween.times_completed();
             });
+            assert!(tween.on_completed.is_some());
+            assert!(tween.event_data.is_none());
             assert_eq!(callback_monitor.lock().unwrap().invoke_count, 0);
 
+            // Activate event sending
+            const USER_DATA: u64 = 54789; // dummy
+            tween.set_completed_event(true, USER_DATA);
+            assert!(tween.event_data.is_some());
+            assert_eq!(tween.event_data.unwrap(), USER_DATA);
+
             // Dummy world and event writer
             let mut world = World::new();
             world.insert_resource(Events::<TweenCompleted>::default());
-            let mut system_state: SystemState<EventWriter<TweenCompleted>> =
+            let mut event_writer_system_state: SystemState<EventWriter<TweenCompleted>> =
+                SystemState::new(&mut world);
+            let mut event_reader_system_state: SystemState<EventReader<TweenCompleted>> =
                 SystemState::new(&mut world);
-            let mut event_writer = system_state.get_mut(&mut world);
 
             // Loop over 2.2 seconds, so greater than one ping-pong loop
             let mut transform = Transform::default();
             let tick_duration = Duration::from_secs_f32(0.2);
             for i in 1..=11 {
                 // Calculate expected values
-                let (progress, times_completed, direction, expected_state) = match tweening_type {
-                    TweeningType::Once => {
-                        let progress = (i as f32 * 0.2).min(1.0);
-                        let times_completed = if i >= 5 { 1 } else { 0 };
-                        let state = if i < 5 {
-                            TweenState::Active
-                        } else {
-                            TweenState::Completed
-                        };
-                        (progress, times_completed, TweeningDirection::Forward, state)
-                    }
-                    TweeningType::Loop => {
-                        let progress = (i as f32 * 0.2).fract();
-                        let times_completed = i / 5;
-                        (
-                            progress,
-                            times_completed,
-                            TweeningDirection::Forward,
-                            TweenState::Active,
-                        )
-                    }
-                    TweeningType::PingPong => {
-                        let i10 = i % 10;
-                        let progress = if i10 >= 5 {
-                            (10 - i10) as f32 * 0.2
-                        } else {
-                            i10 as f32 * 0.2
-                        };
-                        let times_completed = i / 5;
-                        let direction = if i10 >= 5 {
-                            TweeningDirection::Backward
-                        } else {
-                            TweeningDirection::Forward
-                        };
-                        (progress, times_completed, direction, TweenState::Active)
-                    }
-                };
+                let (progress, times_completed, direction, expected_state, just_completed) =
+                    match tweening_type {
+                        TweeningType::Once => {
+                            let progress = (i as f32 * 0.2).min(1.0);
+                            let times_completed = if i >= 5 { 1 } else { 0 };
+                            let state = if i < 5 {
+                                TweenState::Active
+                            } else {
+                                TweenState::Completed
+                            };
+                            let just_completed = i == 5;
+                            (
+                                progress,
+                                times_completed,
+                                TweeningDirection::Forward,
+                                state,
+                                just_completed,
+                            )
+                        }
+                        TweeningType::Loop => {
+                            let progress = (i as f32 * 0.2).fract();
+                            let times_completed = i / 5;
+                            let just_completed = i % 5 == 0;
+                            (
+                                progress,
+                                times_completed,
+                                TweeningDirection::Forward,
+                                TweenState::Active,
+                                just_completed,
+                            )
+                        }
+                        TweeningType::PingPong => {
+                            let i10 = i % 10;
+                            let progress = if i10 >= 5 {
+                                (10 - i10) as f32 * 0.2
+                            } else {
+                                i10 as f32 * 0.2
+                            };
+                            let times_completed = i / 5;
+                            let direction = if i10 >= 5 {
+                                TweeningDirection::Backward
+                            } else {
+                                TweeningDirection::Forward
+                            };
+                            let just_completed = i % 5 == 0;
+                            (
+                                progress,
+                                times_completed,
+                                direction,
+                                TweenState::Active,
+                                just_completed,
+                            )
+                        }
+                    };
                 println!(
-                    "Expected: progress={} times_completed={} direction={:?} state={:?}",
-                    progress, times_completed, direction, expected_state
+                    "Expected: progress={} times_completed={} direction={:?} state={:?} just_completed={}",
+                    progress, times_completed, direction, expected_state, just_completed
                 );
 
                 // Tick the tween
-                let actual_state = tween.tick(
-                    tick_duration,
-                    &mut transform,
-                    dummy_entity,
-                    &mut event_writer,
-                );
+                let actual_state = {
+                    let mut event_writer = event_writer_system_state.get_mut(&mut world);
+                    tween.tick(
+                        tick_duration,
+                        &mut transform,
+                        dummy_entity,
+                        &mut event_writer,
+                    )
+                };
+
+                // Propagate events
+                {
+                    let mut events = world.get_resource_mut::<Events<TweenCompleted>>().unwrap();
+                    events.update();
+                }
 
                 // Check actual values
                 assert_eq!(tween.direction(), direction);
@@ -811,6 +851,19 @@ mod tests {
                 let cb_mon = callback_monitor.lock().unwrap();
                 assert_eq!(cb_mon.invoke_count, times_completed as u64);
                 assert_eq!(cb_mon.last_reported_count, times_completed);
+                {
+                    let mut event_reader = event_reader_system_state.get_mut(&mut world);
+                    let event = event_reader.iter().next();
+                    if just_completed {
+                        assert!(event.is_some());
+                        if let Some(event) = event {
+                            assert_eq!(event.entity, dummy_entity);
+                            assert_eq!(event.user_data, USER_DATA);
+                        }
+                    } else {
+                        assert!(event.is_none());
+                    }
+                }
             }
 
             // Rewind
@@ -821,15 +874,22 @@ mod tests {
             assert_eq!(tween.times_completed(), 0);
 
             // Dummy tick to update target
-            let actual_state = tween.tick(
-                Duration::ZERO,
-                &mut transform,
-                Entity::from_raw(0),
-                &mut event_writer,
-            );
+            let actual_state = {
+                let mut event_writer = event_writer_system_state.get_mut(&mut world);
+                tween.tick(
+                    Duration::ZERO,
+                    &mut transform,
+                    Entity::from_raw(0),
+                    &mut event_writer,
+                )
+            };
             assert_eq!(actual_state, TweenState::Active);
             assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5));
             assert!(transform.rotation.abs_diff_eq(Quat::IDENTITY, 1e-5));
+
+            // Clear callback
+            tween.clear_completed();
+            assert!(tween.on_completed.is_none());
         }
     }
 
@@ -892,13 +952,45 @@ mod tests {
         }
     }
 
+    /// Sequence::new() and various Sequence-specific methods
+    #[test]
+    fn seq_iter() {
+        let mut seq = Sequence::new((1..5).map(|i| {
+            Tween::new(
+                EaseMethod::Linear,
+                TweeningType::Once,
+                Duration::from_secs_f32(0.2 * i as f32),
+                TransformPositionLens {
+                    start: Vec3::ZERO,
+                    end: Vec3::ONE,
+                },
+            )
+        }));
+        assert!(!seq.is_looping());
+
+        let mut progress = 0.;
+        for i in 1..5 {
+            assert_eq!(seq.index(), i - 1);
+            assert!((seq.progress() - progress).abs() < 1e-5);
+            let secs = 0.2 * i as f32;
+            assert_eq!(seq.current().duration(), Duration::from_secs_f32(secs));
+            progress += 0.25;
+            seq.set_progress(progress);
+            assert_eq!(seq.times_completed(), if i == 4 { 1 } else { 0 });
+        }
+
+        seq.rewind();
+        assert_eq!(seq.progress(), 0.);
+        assert_eq!(seq.times_completed(), 0);
+    }
+
     /// Test ticking parallel tracks of tweens.
     #[test]
     fn tracks_tick() {
         let tween1 = Tween::new(
             EaseMethod::Linear,
             TweeningType::Once,
-            Duration::from_secs_f32(1.0),
+            Duration::from_secs_f32(1.),
             TransformPositionLens {
                 start: Vec3::ZERO,
                 end: Vec3::ONE,
@@ -914,6 +1006,9 @@ mod tests {
             },
         );
         let mut tracks = Tracks::new([tween1, tween2]);
+        assert_eq!(tracks.duration(), Duration::from_secs_f32(1.)); // max(1., 0.8)
+        assert_eq!(tracks.is_looping(), false);
+
         let mut transform = Transform::default();
 
         // Dummy world and event writer
@@ -932,7 +1027,9 @@ mod tests {
             );
             if i < 5 {
                 assert_eq!(state, TweenState::Active);
+                assert_eq!(tracks.times_completed(), 0);
                 let r = i as f32 * 0.2;
+                assert!((tracks.progress() - r).abs() < 1e-5);
                 let alpha_deg = (45 * i) as f32;
                 assert!(transform.translation.abs_diff_eq(Vec3::splat(r), 1e-5));
                 assert!(transform
@@ -940,11 +1037,95 @@ mod tests {
                     .abs_diff_eq(Quat::from_rotation_x(alpha_deg.to_radians()), 1e-5));
             } else {
                 assert_eq!(state, TweenState::Completed);
+                assert_eq!(tracks.times_completed(), 1);
+                assert!((tracks.progress() - 1.).abs() < 1e-5);
                 assert!(transform.translation.abs_diff_eq(Vec3::splat(1.), 1e-5));
                 assert!(transform
                     .rotation
                     .abs_diff_eq(Quat::from_rotation_x(180_f32.to_radians()), 1e-5));
             }
         }
+
+        tracks.rewind();
+        assert_eq!(tracks.times_completed(), 0);
+        assert!(tracks.progress().abs() < 1e-5);
+
+        tracks.set_progress(0.9);
+        assert!((tracks.progress() - 0.9).abs() < 1e-5);
+        // tick to udpate state (set_progress() does not update state)
+        let state = tracks.tick(
+            Duration::from_secs_f32(0.),
+            &mut transform,
+            Entity::from_raw(0),
+            &mut event_writer,
+        );
+        assert_eq!(state, TweenState::Active);
+        assert_eq!(tracks.times_completed(), 0);
+
+        tracks.set_progress(3.2);
+        assert!((tracks.progress() - 1.).abs() < 1e-5);
+        // tick to udpate state (set_progress() does not update state)
+        let state = tracks.tick(
+            Duration::from_secs_f32(0.),
+            &mut transform,
+            Entity::from_raw(0),
+            &mut event_writer,
+        );
+        assert_eq!(state, TweenState::Completed);
+        assert_eq!(tracks.times_completed(), 1); // no looping
+
+        tracks.set_progress(-0.5);
+        assert!(tracks.progress().abs() < 1e-5);
+        // tick to udpate state (set_progress() does not update state)
+        let state = tracks.tick(
+            Duration::from_secs_f32(0.),
+            &mut transform,
+            Entity::from_raw(0),
+            &mut event_writer,
+        );
+        assert_eq!(state, TweenState::Active);
+        assert_eq!(tracks.times_completed(), 0); // no looping
+    }
+
+    /// Test ticking a delay.
+    #[test]
+    fn delay_tick() {
+        let duration = Duration::from_secs_f32(1.0);
+        let mut delay = Delay::new(duration);
+        {
+            let tweenable: &dyn Tweenable<Transform> = &delay;
+            assert_eq!(tweenable.duration(), duration);
+            assert_eq!(tweenable.is_looping(), false);
+            assert!(tweenable.progress().abs() < 1e-5);
+        }
+
+        let mut transform = Transform::default();
+
+        // Dummy world and event writer
+        let mut world = World::new();
+        world.insert_resource(Events::<TweenCompleted>::default());
+        let mut system_state: SystemState<EventWriter<TweenCompleted>> =
+            SystemState::new(&mut world);
+        let mut event_writer = system_state.get_mut(&mut world);
+
+        for i in 1..=6 {
+            let state = delay.tick(
+                Duration::from_secs_f32(0.2),
+                &mut transform,
+                Entity::from_raw(0),
+                &mut event_writer,
+            );
+            {
+                let tweenable: &dyn Tweenable<Transform> = &delay;
+                if i < 5 {
+                    assert_eq!(state, TweenState::Active);
+                    let r = i as f32 * 0.2;
+                    assert!((tweenable.progress() - r).abs() < 1e-5);
+                } else {
+                    assert_eq!(state, TweenState::Completed);
+                    assert!((tweenable.progress() - 1.).abs() < 1e-5);
+                }
+            }
+        }
     }
 }
-- 
GitLab