From dd6ac74174c9873e377037c68f052f8a56ba57e1 Mon Sep 17 00:00:00 2001
From: Jerome Humbert <djeedai@gmail.com>
Date: Sun, 2 Oct 2022 13:32:49 +0100
Subject: [PATCH] Add `assert_approx_eq!()` testing macro (#66)

Clarify testing code and potential assertions message with the use of a
new `assert_approx_eq!()` macro for `f32` equality check with a
tolerance. The macro leverages `abs_diff_eq()` but produces a better
assertion message in case of failure. It also allows skipping the
tolerance parameter to use the default of `1e-5`, which is the
"standard" tolerance to use for progress and other small-ish values that
are expected to be equal but might be slightly off due to rounding
errors.

This change ignores the complications of testing for floating-point
equality in a generic way, which is too complex, and instead restrict
the usage to values like progress (range [0:1]) and other small position
values around the origin.
---
 examples/menu.rs  | 427 +++++++++++++++++++++++-----------------------
 src/lib.rs        |  55 +++---
 src/test_utils.rs |  51 ++++++
 src/tweenable.rs  | 114 ++++++-------
 4 files changed, 346 insertions(+), 301 deletions(-)
 create mode 100644 src/test_utils.rs

diff --git a/examples/menu.rs b/examples/menu.rs
index 731b49a..244a99e 100644
--- a/examples/menu.rs
+++ b/examples/menu.rs
@@ -1,213 +1,214 @@
-use bevy::prelude::*;
-use bevy_inspector_egui::WorldInspectorPlugin;
-use bevy_tweening::{lens::*, *};
-use std::time::Duration;
-
-const NORMAL_COLOR: Color = Color::rgba(162. / 255., 226. / 255., 95. / 255., 1.);
-const HOVER_COLOR: Color = Color::AZURE;
-const CLICK_COLOR: Color = Color::ALICE_BLUE;
-const TEXT_COLOR: Color = Color::rgba(83. / 255., 163. / 255., 130. / 255., 1.);
-const INIT_TRANSITION_DONE: u64 = 1;
-
-/// The menu in this example has two set of animations:
-/// one for appearance, one for interaction. Interaction animations
-/// are only enabled after appearance animations finished.
-///
-/// The logic is handled as:
-/// 1. Appearance animations send a `TweenComplete` event with `INIT_TRANSITION_DONE`
-/// 2. The `enable_interaction_after_initial_animation` system adds a label component
-/// `InitTransitionDone` to any button component which completed its appearance animation,
-/// to mark it as active.
-/// 3. The `interaction` system only queries buttons with a `InitTransitionDone` marker.
-fn main() {
-    App::default()
-        .insert_resource(WindowDescriptor {
-            title: "Menu".to_string(),
-            width: 800.,
-            height: 400.,
-            present_mode: bevy::window::PresentMode::Fifo, // vsync
-            ..default()
-        })
-        .add_plugins(DefaultPlugins)
-        .add_system(bevy::window::close_on_esc)
-        .add_system(interaction)
-        .add_system(enable_interaction_after_initial_animation)
-        .add_plugin(TweeningPlugin)
-        .add_plugin(WorldInspectorPlugin::new())
-        .add_startup_system(setup)
-        .run();
-}
-
-fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
-    commands.spawn_bundle(Camera2dBundle::default());
-
-    let font = asset_server.load("fonts/FiraMono-Regular.ttf");
-
-    commands
-        .spawn_bundle(NodeBundle {
-            style: Style {
-                position_type: PositionType::Absolute,
-                position: UiRect::all(Val::Px(0.)),
-                margin: UiRect::all(Val::Px(16.)),
-                padding: UiRect::all(Val::Px(16.)),
-                flex_direction: FlexDirection::ColumnReverse,
-                align_content: AlignContent::Center,
-                align_items: AlignItems::Center,
-                align_self: AlignSelf::Center,
-                justify_content: JustifyContent::Center,
-                ..default()
-            },
-            color: UiColor(Color::NONE),
-            ..default()
-        })
-        .insert(Name::new("menu"))
-        .with_children(|container| {
-            let mut start_time_ms = 0;
-            for (text, label) in [
-                ("Continue", ButtonLabel::Continue),
-                ("New Game", ButtonLabel::NewGame),
-                ("Settings", ButtonLabel::Settings),
-                ("Quit", ButtonLabel::Quit),
-            ] {
-                let tween_scale = Tween::new(
-                    EaseFunction::BounceOut,
-                    Duration::from_secs(2),
-                    TransformScaleLens {
-                        start: Vec3::splat(0.01),
-                        end: Vec3::ONE,
-                    },
-                )
-                .with_completed_event(INIT_TRANSITION_DONE);
-
-                let animator = if start_time_ms > 0 {
-                    let delay = Delay::new(Duration::from_millis(start_time_ms));
-                    Animator::new(delay.then(tween_scale))
-                } else {
-                    Animator::new(tween_scale)
-                };
-
-                start_time_ms += 500;
-                container
-                    .spawn_bundle(ButtonBundle {
-                        node: Node {
-                            size: Vec2::new(300., 80.),
-                        },
-                        style: Style {
-                            min_size: Size::new(Val::Px(300.), Val::Px(80.)),
-                            margin: UiRect::all(Val::Px(8.)),
-                            padding: UiRect::all(Val::Px(8.)),
-                            align_content: AlignContent::Center,
-                            align_items: AlignItems::Center,
-                            align_self: AlignSelf::Center,
-                            justify_content: JustifyContent::Center,
-                            ..default()
-                        },
-                        color: UiColor(NORMAL_COLOR),
-                        transform: Transform::from_scale(Vec3::splat(0.01)),
-                        ..default()
-                    })
-                    .insert(Name::new(format!("button:{}", text)))
-                    .insert(animator)
-                    .insert(label)
-                    .with_children(|parent| {
-                        parent.spawn_bundle(TextBundle {
-                            text: Text::from_section(
-                                text.to_string(),
-                                TextStyle {
-                                    font: font.clone(),
-                                    font_size: 48.0,
-                                    color: TEXT_COLOR,
-                                },
-                            )
-                            .with_alignment(TextAlignment {
-                                vertical: VerticalAlign::Center,
-                                horizontal: HorizontalAlign::Center,
-                            }),
-                            ..default()
-                        });
-                    });
-            }
-        });
-}
-
-fn enable_interaction_after_initial_animation(
-    mut commands: Commands,
-    mut reader: EventReader<TweenCompleted>,
-) {
-    for event in reader.iter() {
-        if event.user_data == INIT_TRANSITION_DONE {
-            commands.entity(event.entity).insert(InitTransitionDone);
-        }
-    }
-}
-
-#[derive(Component)]
-struct InitTransitionDone;
-
-#[derive(Component, Clone, Copy)]
-enum ButtonLabel {
-    Continue,
-    NewGame,
-    Settings,
-    Quit,
-}
-
-fn interaction(
-    mut interaction_query: Query<
-        (
-            &mut Animator<Transform>,
-            &Transform,
-            &Interaction,
-            &mut UiColor,
-            &ButtonLabel,
-        ),
-        (Changed<Interaction>, With<InitTransitionDone>),
-    >,
-) {
-    for (mut animator, transform, interaction, mut color, button_label) in &mut interaction_query {
-        match *interaction {
-            Interaction::Clicked => {
-                *color = CLICK_COLOR.into();
-
-                match button_label {
-                    ButtonLabel::Continue => {
-                        println!("Continue clicked");
-                    }
-                    ButtonLabel::NewGame => {
-                        println!("NewGame clicked");
-                    }
-                    ButtonLabel::Settings => {
-                        println!("Settings clicked");
-                    }
-                    ButtonLabel::Quit => {
-                        println!("Quit clicked");
-                    }
-                }
-            }
-            Interaction::Hovered => {
-                *color = HOVER_COLOR.into();
-                animator.set_tweenable(Tween::new(
-                    EaseFunction::QuadraticIn,
-                    Duration::from_millis(200),
-                    TransformScaleLens {
-                        start: Vec3::ONE,
-                        end: Vec3::splat(1.1),
-                    },
-                ));
-            }
-            Interaction::None => {
-                *color = NORMAL_COLOR.into();
-                let start_scale = transform.scale;
-
-                animator.set_tweenable(Tween::new(
-                    EaseFunction::QuadraticIn,
-                    Duration::from_millis(200),
-                    TransformScaleLens {
-                        start: start_scale,
-                        end: Vec3::ONE,
-                    },
-                ));
-            }
-        }
-    }
-}
+use bevy::prelude::*;
+use bevy_inspector_egui::WorldInspectorPlugin;
+use bevy_tweening::{lens::*, *};
+use std::time::Duration;
+
+const NORMAL_COLOR: Color = Color::rgba(162. / 255., 226. / 255., 95. / 255., 1.);
+const HOVER_COLOR: Color = Color::AZURE;
+const CLICK_COLOR: Color = Color::ALICE_BLUE;
+const TEXT_COLOR: Color = Color::rgba(83. / 255., 163. / 255., 130. / 255., 1.);
+const INIT_TRANSITION_DONE: u64 = 1;
+
+/// The menu in this example has two set of animations:
+/// one for appearance, one for interaction. Interaction animations
+/// are only enabled after appearance animations finished.
+///
+/// The logic is handled as:
+/// 1. Appearance animations send a `TweenComplete` event with
+/// `INIT_TRANSITION_DONE` 2. The `enable_interaction_after_initial_animation`
+/// system adds a label component `InitTransitionDone` to any button component
+/// which completed its appearance animation, to mark it as active.
+/// 3. The `interaction` system only queries buttons with a `InitTransitionDone`
+/// marker.
+fn main() {
+    App::default()
+        .insert_resource(WindowDescriptor {
+            title: "Menu".to_string(),
+            width: 800.,
+            height: 400.,
+            present_mode: bevy::window::PresentMode::Fifo, // vsync
+            ..default()
+        })
+        .add_plugins(DefaultPlugins)
+        .add_system(bevy::window::close_on_esc)
+        .add_system(interaction)
+        .add_system(enable_interaction_after_initial_animation)
+        .add_plugin(TweeningPlugin)
+        .add_plugin(WorldInspectorPlugin::new())
+        .add_startup_system(setup)
+        .run();
+}
+
+fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
+    commands.spawn_bundle(Camera2dBundle::default());
+
+    let font = asset_server.load("fonts/FiraMono-Regular.ttf");
+
+    commands
+        .spawn_bundle(NodeBundle {
+            style: Style {
+                position_type: PositionType::Absolute,
+                position: UiRect::all(Val::Px(0.)),
+                margin: UiRect::all(Val::Px(16.)),
+                padding: UiRect::all(Val::Px(16.)),
+                flex_direction: FlexDirection::ColumnReverse,
+                align_content: AlignContent::Center,
+                align_items: AlignItems::Center,
+                align_self: AlignSelf::Center,
+                justify_content: JustifyContent::Center,
+                ..default()
+            },
+            color: UiColor(Color::NONE),
+            ..default()
+        })
+        .insert(Name::new("menu"))
+        .with_children(|container| {
+            let mut start_time_ms = 0;
+            for (text, label) in [
+                ("Continue", ButtonLabel::Continue),
+                ("New Game", ButtonLabel::NewGame),
+                ("Settings", ButtonLabel::Settings),
+                ("Quit", ButtonLabel::Quit),
+            ] {
+                let tween_scale = Tween::new(
+                    EaseFunction::BounceOut,
+                    Duration::from_secs(2),
+                    TransformScaleLens {
+                        start: Vec3::splat(0.01),
+                        end: Vec3::ONE,
+                    },
+                )
+                .with_completed_event(INIT_TRANSITION_DONE);
+
+                let animator = if start_time_ms > 0 {
+                    let delay = Delay::new(Duration::from_millis(start_time_ms));
+                    Animator::new(delay.then(tween_scale))
+                } else {
+                    Animator::new(tween_scale)
+                };
+
+                start_time_ms += 500;
+                container
+                    .spawn_bundle(ButtonBundle {
+                        node: Node {
+                            size: Vec2::new(300., 80.),
+                        },
+                        style: Style {
+                            min_size: Size::new(Val::Px(300.), Val::Px(80.)),
+                            margin: UiRect::all(Val::Px(8.)),
+                            padding: UiRect::all(Val::Px(8.)),
+                            align_content: AlignContent::Center,
+                            align_items: AlignItems::Center,
+                            align_self: AlignSelf::Center,
+                            justify_content: JustifyContent::Center,
+                            ..default()
+                        },
+                        color: UiColor(NORMAL_COLOR),
+                        transform: Transform::from_scale(Vec3::splat(0.01)),
+                        ..default()
+                    })
+                    .insert(Name::new(format!("button:{}", text)))
+                    .insert(animator)
+                    .insert(label)
+                    .with_children(|parent| {
+                        parent.spawn_bundle(TextBundle {
+                            text: Text::from_section(
+                                text.to_string(),
+                                TextStyle {
+                                    font: font.clone(),
+                                    font_size: 48.0,
+                                    color: TEXT_COLOR,
+                                },
+                            )
+                            .with_alignment(TextAlignment {
+                                vertical: VerticalAlign::Center,
+                                horizontal: HorizontalAlign::Center,
+                            }),
+                            ..default()
+                        });
+                    });
+            }
+        });
+}
+
+fn enable_interaction_after_initial_animation(
+    mut commands: Commands,
+    mut reader: EventReader<TweenCompleted>,
+) {
+    for event in reader.iter() {
+        if event.user_data == INIT_TRANSITION_DONE {
+            commands.entity(event.entity).insert(InitTransitionDone);
+        }
+    }
+}
+
+#[derive(Component)]
+struct InitTransitionDone;
+
+#[derive(Component, Clone, Copy)]
+enum ButtonLabel {
+    Continue,
+    NewGame,
+    Settings,
+    Quit,
+}
+
+fn interaction(
+    mut interaction_query: Query<
+        (
+            &mut Animator<Transform>,
+            &Transform,
+            &Interaction,
+            &mut UiColor,
+            &ButtonLabel,
+        ),
+        (Changed<Interaction>, With<InitTransitionDone>),
+    >,
+) {
+    for (mut animator, transform, interaction, mut color, button_label) in &mut interaction_query {
+        match *interaction {
+            Interaction::Clicked => {
+                *color = CLICK_COLOR.into();
+
+                match button_label {
+                    ButtonLabel::Continue => {
+                        println!("Continue clicked");
+                    }
+                    ButtonLabel::NewGame => {
+                        println!("NewGame clicked");
+                    }
+                    ButtonLabel::Settings => {
+                        println!("Settings clicked");
+                    }
+                    ButtonLabel::Quit => {
+                        println!("Quit clicked");
+                    }
+                }
+            }
+            Interaction::Hovered => {
+                *color = HOVER_COLOR.into();
+                animator.set_tweenable(Tween::new(
+                    EaseFunction::QuadraticIn,
+                    Duration::from_millis(200),
+                    TransformScaleLens {
+                        start: Vec3::ONE,
+                        end: Vec3::splat(1.1),
+                    },
+                ));
+            }
+            Interaction::None => {
+                *color = NORMAL_COLOR.into();
+                let start_scale = transform.scale;
+
+                animator.set_tweenable(Tween::new(
+                    EaseFunction::QuadraticIn,
+                    Duration::from_millis(200),
+                    TransformScaleLens {
+                        start: start_scale,
+                        end: Vec3::ONE,
+                    },
+                ));
+            }
+        }
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 8be9eb2..f5cb2f1 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -171,6 +171,9 @@ pub mod lens;
 mod plugin;
 mod tweenable;
 
+#[cfg(test)]
+mod test_utils;
+
 /// How many times to repeat a tween animation. See also: [`RepeatStrategy`].
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum RepeatCount {
@@ -475,12 +478,8 @@ mod tests {
     #[cfg(feature = "bevy_asset")]
     use bevy::reflect::TypeUuid;
 
-    use super::{lens::*, *};
-
-    /// Utility to compare floating-point values with a tolerance.
-    fn abs_diff_eq(a: f32, b: f32, tol: f32) -> bool {
-        (a - b).abs() < tol
-    }
+    use super::*;
+    use crate::test_utils::*;
 
     struct DummyLens {
         start: f32,
@@ -511,7 +510,7 @@ mod tests {
         let mut l = DummyLens { start: 0., end: 1. };
         for r in [0_f32, 0.01, 0.3, 0.5, 0.9, 0.999, 1.] {
             l.lerp(&mut c, r);
-            assert!(abs_diff_eq(c.value, r, 1e-5));
+            assert_approx_eq!(c.value, r);
         }
     }
 
@@ -529,7 +528,7 @@ mod tests {
         let mut l = DummyLens { start: 0., end: 1. };
         for r in [0_f32, 0.01, 0.3, 0.5, 0.9, 0.999, 1.] {
             l.lerp(&mut a, r);
-            assert!(abs_diff_eq(a.value, r, 1e-5));
+            assert_approx_eq!(a.value, r);
         }
     }
 
@@ -628,32 +627,32 @@ mod tests {
         );
         let mut animator = Animator::new(tween);
         assert_eq!(animator.state, AnimatorState::Playing);
-        assert!(animator.tweenable().progress().abs() <= 1e-5);
+        assert_approx_eq!(animator.tweenable().progress(), 0.);
 
         animator.stop();
         assert_eq!(animator.state, AnimatorState::Paused);
-        assert!(animator.tweenable().progress().abs() <= 1e-5);
+        assert_approx_eq!(animator.tweenable().progress(), 0.);
 
         animator.tweenable_mut().set_progress(0.5);
         assert_eq!(animator.state, AnimatorState::Paused);
-        assert!((animator.tweenable().progress() - 0.5).abs() <= 1e-5);
+        assert_approx_eq!(animator.tweenable().progress(), 0.5);
 
         animator.tweenable_mut().rewind();
         assert_eq!(animator.state, AnimatorState::Paused);
-        assert!(animator.tweenable().progress().abs() <= 1e-5);
+        assert_approx_eq!(animator.tweenable().progress(), 0.);
 
         animator.tweenable_mut().set_progress(0.5);
         animator.state = AnimatorState::Playing;
         assert_eq!(animator.state, AnimatorState::Playing);
-        assert!((animator.tweenable().progress() - 0.5).abs() <= 1e-5);
+        assert_approx_eq!(animator.tweenable().progress(), 0.5);
 
         animator.tweenable_mut().rewind();
         assert_eq!(animator.state, AnimatorState::Playing);
-        assert!(animator.tweenable().progress().abs() <= 1e-5);
+        assert_approx_eq!(animator.tweenable().progress(), 0.);
 
         animator.stop();
         assert_eq!(animator.state, AnimatorState::Paused);
-        assert!(animator.tweenable().progress().abs() <= 1e-5);
+        assert_approx_eq!(animator.tweenable().progress(), 0.);
     }
 
     #[test]
@@ -665,10 +664,10 @@ mod tests {
         );
 
         let mut animator = Animator::new(tween);
-        assert!(abs_diff_eq(animator.speed(), 1., 1e-5)); // default speed
+        assert_approx_eq!(animator.speed(), 1.); // default speed
 
         animator.set_speed(2.4);
-        assert!(abs_diff_eq(animator.speed(), 2.4, 1e-5));
+        assert_approx_eq!(animator.speed(), 2.4);
 
         let tween = Tween::<DummyComponent>::new(
             EaseFunction::QuadraticInOut,
@@ -677,7 +676,7 @@ mod tests {
         );
 
         let animator = Animator::new(tween).with_speed(3.5);
-        assert!(abs_diff_eq(animator.speed(), 3.5, 1e-5));
+        assert_approx_eq!(animator.speed(), 3.5);
     }
 
     #[test]
@@ -746,32 +745,32 @@ mod tests {
         );
         let mut animator = AssetAnimator::new(Handle::<DummyAsset>::default(), tween);
         assert_eq!(animator.state, AnimatorState::Playing);
-        assert!(animator.tweenable().progress().abs() <= 1e-5);
+        assert_approx_eq!(animator.tweenable().progress(), 0.);
 
         animator.stop();
         assert_eq!(animator.state, AnimatorState::Paused);
-        assert!(animator.tweenable().progress().abs() <= 1e-5);
+        assert_approx_eq!(animator.tweenable().progress(), 0.);
 
         animator.tweenable_mut().set_progress(0.5);
         assert_eq!(animator.state, AnimatorState::Paused);
-        assert!((animator.tweenable().progress() - 0.5).abs() <= 1e-5);
+        assert_approx_eq!(animator.tweenable().progress(), 0.5);
 
         animator.tweenable_mut().rewind();
         assert_eq!(animator.state, AnimatorState::Paused);
-        assert!(animator.tweenable().progress().abs() <= 1e-5);
+        assert_approx_eq!(animator.tweenable().progress(), 0.);
 
         animator.tweenable_mut().set_progress(0.5);
         animator.state = AnimatorState::Playing;
         assert_eq!(animator.state, AnimatorState::Playing);
-        assert!((animator.tweenable().progress() - 0.5).abs() <= 1e-5);
+        assert_approx_eq!(animator.tweenable().progress(), 0.5);
 
         animator.tweenable_mut().rewind();
         assert_eq!(animator.state, AnimatorState::Playing);
-        assert!(animator.tweenable().progress().abs() <= 1e-5);
+        assert_approx_eq!(animator.tweenable().progress(), 0.);
 
         animator.stop();
         assert_eq!(animator.state, AnimatorState::Paused);
-        assert!(animator.tweenable().progress().abs() <= 1e-5);
+        assert_approx_eq!(animator.tweenable().progress(), 0.);
     }
 
     #[cfg(feature = "bevy_asset")]
@@ -784,10 +783,10 @@ mod tests {
         );
 
         let mut animator = AssetAnimator::new(Handle::<DummyAsset>::default(), tween);
-        assert!(abs_diff_eq(animator.speed(), 1., 1e-5)); // default speed
+        assert_approx_eq!(animator.speed(), 1.); // default speed
 
         animator.set_speed(2.4);
-        assert!(abs_diff_eq(animator.speed(), 2.4, 1e-5));
+        assert_approx_eq!(animator.speed(), 2.4);
 
         let tween = Tween::new(
             EaseFunction::QuadraticInOut,
@@ -796,7 +795,7 @@ mod tests {
         );
 
         let animator = AssetAnimator::new(Handle::<DummyAsset>::default(), tween).with_speed(3.5);
-        assert!(abs_diff_eq(animator.speed(), 3.5, 1e-5));
+        assert_approx_eq!(animator.speed(), 3.5);
     }
 
     #[cfg(feature = "bevy_asset")]
diff --git a/src/test_utils.rs b/src/test_utils.rs
new file mode 100644
index 0000000..f525fd3
--- /dev/null
+++ b/src/test_utils.rs
@@ -0,0 +1,51 @@
+/// Utility to compare floating-point values with a tolerance.
+pub(crate) fn abs_diff_eq(a: f32, b: f32, tol: f32) -> bool {
+    (a - b).abs() < tol
+}
+
+/// Assert that two floating-point quantities are approximately equal.
+///
+/// This macro asserts that the absolute difference between the two first
+/// arguments is strictly less than a tolerance factor, which can be explicitly
+/// passed as third argument or implicitly defaults to `1e-5`.
+///
+/// # Usage
+///
+/// ```
+/// let x = 3.500009;
+/// assert_approx_eq!(x, 3.5);       // default tolerance 1e-5
+///
+/// let x = 3.509;
+/// assert_approx_eq!(x, 3.5, 0.01); // explicit tolerance
+/// ```
+macro_rules! assert_approx_eq {
+    ($left:expr, $right:expr $(,)?) => {
+        match (&$left, &$right) {
+            (left_val, right_val) => {
+                assert!(
+                    abs_diff_eq(*left_val, *right_val, 1e-5),
+                    "assertion failed: expected={} actual={} delta={} tol=1e-5(default)",
+                    left_val,
+                    right_val,
+                    (left_val - right_val).abs(),
+                );
+            }
+        }
+    };
+    ($left:expr, $right:expr, $tol:expr $(,)?) => {
+        match (&$left, &$right, &$tol) {
+            (left_val, right_val, tol_val) => {
+                assert!(
+                    abs_diff_eq(*left_val, *right_val, *tol_val),
+                    "assertion failed: expected={} actual={} delta={} tol={}",
+                    left_val,
+                    right_val,
+                    (left_val - right_val).abs(),
+                    tol_val
+                );
+            }
+        }
+    };
+}
+
+pub(crate) use assert_approx_eq;
diff --git a/src/tweenable.rs b/src/tweenable.rs
index 5402dd2..d5ead02 100644
--- a/src/tweenable.rs
+++ b/src/tweenable.rs
@@ -301,7 +301,7 @@ impl<T: 'static> Tween<T> {
     /// # use std::time::Duration;
     /// let tween1 = Tween::new(
     ///     EaseFunction::QuadraticInOut,
-    ///     Duration::from_secs_f32(1.0),
+    ///     Duration::from_secs(1),
     ///     TransformPositionLens {
     ///         start: Vec3::ZERO,
     ///         end: Vec3::new(3.5, 0., 0.),
@@ -309,7 +309,7 @@ impl<T: 'static> Tween<T> {
     /// );
     /// let tween2 = Tween::new(
     ///     EaseFunction::QuadraticInOut,
-    ///     Duration::from_secs_f32(1.0),
+    ///     Duration::from_secs(1),
     ///     TransformRotationLens {
     ///         start: Quat::IDENTITY,
     ///         end: Quat::from_rotation_x(90.0_f32.to_radians()),
@@ -333,7 +333,7 @@ impl<T> Tween<T> {
     /// # use std::time::Duration;
     /// let tween = Tween::new(
     ///     EaseFunction::QuadraticInOut,
-    ///     Duration::from_secs_f32(1.0),
+    ///     Duration::from_secs(1),
     ///     TransformPositionLens {
     ///         start: Vec3::ZERO,
     ///         end: Vec3::new(3.5, 0., 0.),
@@ -369,7 +369,7 @@ impl<T> Tween<T> {
     /// let tween = Tween::new(
     ///     // [...]
     /// #    EaseFunction::QuadraticInOut,
-    /// #    Duration::from_secs_f32(1.0),
+    /// #    Duration::from_secs(1),
     /// #    TransformPositionLens {
     /// #        start: Vec3::ZERO,
     /// #        end: Vec3::new(3.5, 0., 0.),
@@ -887,14 +887,8 @@ mod tests {
 
     use bevy::ecs::{event::Events, system::SystemState};
 
-    use crate::lens::*;
-
     use super::*;
-
-    /// Utility to compare floating-point values with a tolerance.
-    fn abs_diff_eq(a: f32, b: f32, tol: f32) -> bool {
-        (a - b).abs() < tol
-    }
+    use crate::{lens::*, test_utils::*};
 
     #[derive(Default, Copy, Clone)]
     struct CallbackMonitor {
@@ -1005,7 +999,7 @@ mod tests {
                 assert_eq!(tween.event_data.unwrap(), USER_DATA);
 
                 // Loop over 2.2 seconds, so greater than one ping-pong loop
-                let tick_duration = Duration::from_secs_f32(0.2);
+                let tick_duration = Duration::from_millis(200);
                 for i in 1..=11 {
                     // Calculate expected values
                     let (progress, times_completed, mut direction, expected_state, just_completed) =
@@ -1136,7 +1130,7 @@ mod tests {
                     // Check actual values
                     assert_eq!(tween.direction(), direction);
                     assert_eq!(actual_state, expected_state);
-                    assert!(abs_diff_eq(tween.progress(), progress, 1e-5));
+                    assert_approx_eq!(tween.progress(), progress);
                     assert_eq!(tween.times_completed(), times_completed);
                     assert!(transform
                         .translation
@@ -1163,7 +1157,7 @@ mod tests {
                 // Rewind
                 tween.rewind();
                 assert_eq!(tween.direction(), *tweening_direction); // does not change
-                assert!(abs_diff_eq(tween.progress(), 0., 1e-5));
+                assert_approx_eq!(tween.progress(), 0.);
                 assert_eq!(tween.times_completed(), 0);
 
                 // Dummy tick to update target
@@ -1200,26 +1194,26 @@ mod tests {
 
         // Default
         assert_eq!(tween.direction(), TweeningDirection::Forward);
-        assert!(abs_diff_eq(tween.progress(), 0.0, 1e-5));
+        assert_approx_eq!(tween.progress(), 0.0);
 
         // no-op
         tween.set_direction(TweeningDirection::Forward);
         assert_eq!(tween.direction(), TweeningDirection::Forward);
-        assert!(abs_diff_eq(tween.progress(), 0.0, 1e-5));
+        assert_approx_eq!(tween.progress(), 0.0);
 
         // Backward
         tween.set_direction(TweeningDirection::Backward);
         assert_eq!(tween.direction(), TweeningDirection::Backward);
         // progress is independent of direction
-        assert!(abs_diff_eq(tween.progress(), 0.0, 1e-5));
+        assert_approx_eq!(tween.progress(), 0.0);
 
         // Progress-invariant
         tween.set_direction(TweeningDirection::Forward);
         tween.set_progress(0.3);
-        assert!(abs_diff_eq(tween.progress(), 0.3, 1e-5));
+        assert_approx_eq!(tween.progress(), 0.3);
         tween.set_direction(TweeningDirection::Backward);
         // progress is independent of direction
-        assert!(abs_diff_eq(tween.progress(), 0.3, 1e-5));
+        assert_approx_eq!(tween.progress(), 0.3);
 
         let (mut world, entity, mut transform) = make_test_env();
         let mut event_writer_system_state: SystemState<EventWriter<TweenCompleted>> =
@@ -1228,14 +1222,14 @@ mod tests {
 
         // Progress always increases alongside the current direction
         tween.set_direction(TweeningDirection::Backward);
-        assert!(abs_diff_eq(tween.progress(), 0.3, 1e-5));
+        assert_approx_eq!(tween.progress(), 0.3);
         tween.tick(
-            Duration::from_secs_f32(0.1),
+            Duration::from_millis(100),
             &mut transform,
             entity,
             &mut event_writer,
         );
-        assert!(abs_diff_eq(tween.progress(), 0.4, 1e-5));
+        assert_approx_eq!(tween.progress(), 0.4);
         assert!(transform.translation.abs_diff_eq(Vec3::splat(0.6), 1e-5));
     }
 
@@ -1244,7 +1238,7 @@ mod tests {
     fn seq_tick() {
         let tween1 = Tween::new(
             EaseMethod::Linear,
-            Duration::from_secs_f32(1.0),
+            Duration::from_secs(1),
             TransformPositionLens {
                 start: Vec3::ZERO,
                 end: Vec3::ONE,
@@ -1252,7 +1246,7 @@ mod tests {
         );
         let tween2 = Tween::new(
             EaseMethod::Linear,
-            Duration::from_secs_f32(1.0),
+            Duration::from_secs(1),
             TransformRotationLens {
                 start: Quat::IDENTITY,
                 end: Quat::from_rotation_x(90_f32.to_radians()),
@@ -1267,7 +1261,7 @@ mod tests {
 
         for i in 1..=16 {
             let state = seq.tick(
-                Duration::from_secs_f32(0.2),
+                Duration::from_millis(200),
                 &mut transform,
                 entity,
                 &mut event_writer,
@@ -1317,9 +1311,9 @@ mod tests {
         // - Finish the first tween
         // - Start and finish the second tween
         // - Start the third tween
-        for delta in [0.5, 2.0] {
+        for delta_ms in [500, 2000] {
             seq.tick(
-                Duration::from_secs_f32(delta),
+                Duration::from_millis(delta_ms),
                 &mut transform,
                 entity,
                 &mut event_writer,
@@ -1335,7 +1329,7 @@ mod tests {
         let mut seq = Sequence::new((1..5).map(|i| {
             Tween::new(
                 EaseMethod::Linear,
-                Duration::from_secs_f32(0.2 * i as f32),
+                Duration::from_millis(200 * i),
                 TransformPositionLens {
                     start: Vec3::ZERO,
                     end: Vec3::ONE,
@@ -1346,9 +1340,9 @@ mod tests {
         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));
+            assert_approx_eq!(seq.progress(), progress);
+            let duration = Duration::from_millis(200 * i as u64);
+            assert_eq!(seq.current().duration(), duration);
             progress += 0.25;
             seq.set_progress(progress);
             assert_eq!(seq.times_completed(), if i == 4 { 1 } else { 0 });
@@ -1395,7 +1389,7 @@ 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.duration(), Duration::from_secs(1)); // max(1., 0.8)
 
         let (mut world, entity, mut transform) = make_test_env();
         let mut system_state: SystemState<EventWriter<TweenCompleted>> =
@@ -1404,7 +1398,7 @@ mod tests {
 
         for i in 1..=6 {
             let state = tracks.tick(
-                Duration::from_secs_f32(0.2),
+                Duration::from_millis(200),
                 &mut transform,
                 entity,
                 &mut event_writer,
@@ -1413,7 +1407,7 @@ mod tests {
                 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);
+                assert_approx_eq!(tracks.progress(), r);
                 let alpha_deg = 22.5 * i as f32;
                 assert!(transform.translation.abs_diff_eq(Vec3::splat(r), 1e-5));
                 assert!(transform
@@ -1422,7 +1416,7 @@ mod tests {
             } else {
                 assert_eq!(state, TweenState::Completed);
                 assert_eq!(tracks.times_completed(), 1);
-                assert!((tracks.progress() - 1.).abs() < 1e-5);
+                assert_approx_eq!(tracks.progress(), 1.);
                 assert!(transform.translation.abs_diff_eq(Vec3::ONE, 1e-5));
                 assert!(transform
                     .rotation
@@ -1432,13 +1426,13 @@ mod tests {
 
         tracks.rewind();
         assert_eq!(tracks.times_completed(), 0);
-        assert!(tracks.progress().abs() < 1e-5);
+        assert_approx_eq!(tracks.progress(), 0.);
 
         tracks.set_progress(0.9);
-        assert!((tracks.progress() - 0.9).abs() < 1e-5);
+        assert_approx_eq!(tracks.progress(), 0.9);
         // tick to udpate state (set_progress() does not update state)
         let state = tracks.tick(
-            Duration::from_secs_f32(0.),
+            Duration::ZERO,
             &mut transform,
             Entity::from_raw(0),
             &mut event_writer,
@@ -1447,10 +1441,10 @@ mod tests {
         assert_eq!(tracks.times_completed(), 0);
 
         tracks.set_progress(3.2);
-        assert!((tracks.progress() - 1.).abs() < 1e-5);
+        assert_approx_eq!(tracks.progress(), 1.);
         // tick to udpate state (set_progress() does not update state)
         let state = tracks.tick(
-            Duration::from_secs_f32(0.),
+            Duration::ZERO,
             &mut transform,
             Entity::from_raw(0),
             &mut event_writer,
@@ -1459,10 +1453,10 @@ mod tests {
         assert_eq!(tracks.times_completed(), 1); // no looping
 
         tracks.set_progress(-0.5);
-        assert!(tracks.progress().abs() < 1e-5);
+        assert_approx_eq!(tracks.progress(), 0.);
         // tick to udpate state (set_progress() does not update state)
         let state = tracks.tick(
-            Duration::from_secs_f32(0.),
+            Duration::ZERO,
             &mut transform,
             Entity::from_raw(0),
             &mut event_writer,
@@ -1491,7 +1485,7 @@ mod tests {
         {
             let tweenable: &dyn Tweenable<Transform> = &delay;
             assert_eq!(tweenable.duration(), duration);
-            assert!(tweenable.progress().abs() < 1e-5);
+            assert_approx_eq!(tweenable.progress(), 0.);
         }
 
         // Dummy world and event writer
@@ -1513,11 +1507,11 @@ mod tests {
                     assert_eq!(state, TweenState::Active);
                     assert_eq!(tweenable.times_completed(), 0);
                     let r = i as f32 * 0.2;
-                    assert!((tweenable.progress() - r).abs() < 1e-5);
+                    assert_approx_eq!(tweenable.progress(), r);
                 } else {
                     assert_eq!(state, TweenState::Completed);
                     assert_eq!(tweenable.times_completed(), 1);
-                    assert!((tweenable.progress() - 1.).abs() < 1e-5);
+                    assert_approx_eq!(tweenable.progress(), 1.);
                 }
             }
         }
@@ -1526,16 +1520,16 @@ mod tests {
 
         tweenable.rewind();
         assert_eq!(tweenable.times_completed(), 0);
-        assert!(abs_diff_eq(tweenable.progress(), 0., 1e-5));
+        assert_approx_eq!(tweenable.progress(), 0.);
         let state = tweenable.tick(Duration::ZERO, &mut transform, entity, &mut event_writer);
         assert_eq!(state, TweenState::Active);
 
         tweenable.set_progress(0.3);
         assert_eq!(tweenable.times_completed(), 0);
-        assert!(abs_diff_eq(tweenable.progress(), 0.3, 1e-5));
+        assert_approx_eq!(tweenable.progress(), 0.3);
         tweenable.set_progress(1.);
         assert_eq!(tweenable.times_completed(), 1);
-        assert!(abs_diff_eq(tweenable.progress(), 1., 1e-5));
+        assert_approx_eq!(tweenable.progress(), 1.);
     }
 
     #[test]
@@ -1550,7 +1544,7 @@ mod tests {
             .with_repeat_count(RepeatCount::Finite(5))
             .with_repeat_strategy(RepeatStrategy::Repeat);
 
-        assert!(abs_diff_eq(tween.progress(), 0., 1e-5));
+        assert_approx_eq!(tween.progress(), 0.);
 
         let (mut world, entity, mut transform) = make_test_env();
         let mut event_writer_system_state: SystemState<EventWriter<TweenCompleted>> =
@@ -1566,7 +1560,7 @@ mod tests {
         );
         assert_eq!(TweenState::Active, state);
         assert_eq!(0, tween.times_completed());
-        assert!(abs_diff_eq(tween.progress(), 0.1, 1e-5));
+        assert_approx_eq!(tween.progress(), 0.1);
         assert!(transform.translation.abs_diff_eq(Vec3::splat(0.1), 1e-5));
 
         // 130%
@@ -1578,7 +1572,7 @@ mod tests {
         );
         assert_eq!(TweenState::Active, state);
         assert_eq!(1, tween.times_completed());
-        assert!(abs_diff_eq(tween.progress(), 0.3, 1e-5));
+        assert_approx_eq!(tween.progress(), 0.3);
         assert!(transform.translation.abs_diff_eq(Vec3::splat(0.3), 1e-5));
 
         // 480%
@@ -1590,7 +1584,7 @@ mod tests {
         );
         assert_eq!(TweenState::Active, state);
         assert_eq!(4, tween.times_completed());
-        assert!(abs_diff_eq(tween.progress(), 0.8, 1e-5));
+        assert_approx_eq!(tween.progress(), 0.8);
         assert!(transform.translation.abs_diff_eq(Vec3::splat(0.8), 1e-5));
 
         // 500% - done
@@ -1602,7 +1596,7 @@ mod tests {
         );
         assert_eq!(TweenState::Completed, state);
         assert_eq!(5, tween.times_completed());
-        assert!(abs_diff_eq(tween.progress(), 1.0, 1e-5));
+        assert_approx_eq!(tween.progress(), 1.0);
         assert!(transform.translation.abs_diff_eq(Vec3::ONE, 1e-5));
     }
 
@@ -1612,7 +1606,7 @@ mod tests {
             .with_repeat_count(RepeatCount::Finite(4))
             .with_repeat_strategy(RepeatStrategy::MirroredRepeat);
 
-        assert!(abs_diff_eq(tween.progress(), 0., 1e-5));
+        assert_approx_eq!(tween.progress(), 0.);
 
         let (mut world, entity, mut transform) = make_test_env();
         let mut event_writer_system_state: SystemState<EventWriter<TweenCompleted>> =
@@ -1629,14 +1623,14 @@ mod tests {
         assert_eq!(TweenState::Active, state);
         assert_eq!(TweeningDirection::Forward, tween.direction());
         assert_eq!(0, tween.times_completed());
-        assert!(abs_diff_eq(tween.progress(), 0.1, 1e-5));
+        assert_approx_eq!(tween.progress(), 0.1);
         assert!(transform.translation.abs_diff_eq(Vec3::splat(0.1), 1e-5));
 
         // rewind
         tween.rewind();
         assert_eq!(TweeningDirection::Forward, tween.direction());
         assert_eq!(0, tween.times_completed());
-        assert!(abs_diff_eq(tween.progress(), 0., 1e-5));
+        assert_approx_eq!(tween.progress(), 0.);
         assert!(transform.translation.abs_diff_eq(Vec3::splat(0.1), 1e-5)); // no-op, rewind doesn't apply Lens
 
         // 120% - mirror
@@ -1649,14 +1643,14 @@ mod tests {
         assert_eq!(TweeningDirection::Backward, tween.direction());
         assert_eq!(TweenState::Active, state);
         assert_eq!(1, tween.times_completed());
-        assert!(abs_diff_eq(tween.progress(), 0.2, 1e-5));
+        assert_approx_eq!(tween.progress(), 0.2);
         assert!(transform.translation.abs_diff_eq(Vec3::splat(0.8), 1e-5));
 
         // rewind
         tween.rewind();
         assert_eq!(TweeningDirection::Forward, tween.direction()); // restored
         assert_eq!(0, tween.times_completed());
-        assert!(abs_diff_eq(tween.progress(), 0., 1e-5));
+        assert_approx_eq!(tween.progress(), 0.);
         assert!(transform.translation.abs_diff_eq(Vec3::splat(0.8), 1e-5)); // no-op, rewind doesn't apply Lens
 
         // 400% - done mirror (because Completed freezes the state)
@@ -1669,14 +1663,14 @@ mod tests {
         assert_eq!(TweenState::Completed, state);
         assert_eq!(TweeningDirection::Backward, tween.direction()); // frozen from last loop
         assert_eq!(4, tween.times_completed());
-        assert!(abs_diff_eq(tween.progress(), 1., 1e-5)); // Completed
+        assert_approx_eq!(tween.progress(), 1.); // Completed
         assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5));
 
         // rewind
         tween.rewind();
         assert_eq!(TweeningDirection::Forward, tween.direction()); // restored
         assert_eq!(0, tween.times_completed());
-        assert!(abs_diff_eq(tween.progress(), 0., 1e-5));
+        assert_approx_eq!(tween.progress(), 0.);
         assert!(transform.translation.abs_diff_eq(Vec3::ZERO, 1e-5)); // no-op, rewind doesn't apply Lens
     }
 }
-- 
GitLab