Fix Timing Clocks Divergence
Fixed the timing clock divergence between [tweening.rs](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/tweening.rs) and [drawing.rs](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/drawing.rs) by delegating animation progress evaluation exclusively to `TweenController`. `current_time()` in `tweening.rs` was using a monotonic `Instant` epoch (to support headless execution and unit tests without panicking on missing Macroquad context), while `drawing.rs` was measuring progress via `macroquad::time::get_time() - tween.start_time`. Because these two clocks operate on completely different epochs, subtracting them caused negative elapsed times, premature clamping, jitter, or animation lockup. In [`turtle-lib/src/tweening.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/tweening.rs): - Added `pub(crate) progress: f32` to [`CommandTween`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/tweening.rs#L55-L68) to track the current evaluated, eased progress in `[0.0, 1.0]`. - Made `start_time: f64` and `duration: f64` private to `tweening.rs` so outside modules cannot accidentally perform desynchronized clock arithmetic. - Initialized `progress: 0.0` when constructing a new `CommandTween`. - In `TweenController::update`, evaluated and clamped `tween.progress = tween.heading_tweener.move_to(elapsed).clamp(0.0, 1.0)`. In [`turtle-lib/src/drawing.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/drawing.rs): - Removed `use tween::CubicInOut;` and duplicate easing calculations. - Replaced `get_time() - tween.start_time` and `CubicInOut.tween(...)` in in-flight fill preview with `tween.progress`. - Replaced `get_time() - tween.start_time` and `CubicInOut.tween(...)` in `draw_tween_arc` with `tween.progress`. - Ensured `drawing.rs` has zero clock queries and strictly renders the state provided by `TweenController`. In [`turtle-lib/src/tweening.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/tweening.rs#L540-L579): - Updated `test_animated_mode_pops_to_current_tween` to assert that `progress` initializes to `0.0`. - Added `test_animated_mode_progress_advances_and_clamps` to verify that `progress` advances monotonically across frames and stays within `[0.0, 1.0]`. --- ```bash cargo test --package turtle-lib --features svg ``` Output: ```text running 17 tests test circle_geometry::tests::test_circle_left_geometry ... ok test circle_geometry::tests::test_circle_right_geometry ... ok test command_behavior::tests::test_goto_duration_cartesian_inversion ... ok test command_behavior::tests::test_set_heading_degrees_and_instant_duration ... ok test execution::tests::test_forward_left_forward ... ok test general::angle::tests::degrees_to_radians_roundtrip ... ok test general::angle::tests::from_f64 ... ok test general::angle::tests::from_integer ... ok test general::angle::tests::negation ... ok test general::fontsize::tests::font_size_conversions ... ok test general::length::tests::test_length_conversions_and_negation ... ok test general::tests::animation_speed_conversions ... ok test tweening::tests::test_animated_mode_pops_to_current_tween ... ok test tweening::tests::test_instant_mode_drains_queue ... ok test tweening::tests::test_instant_mode_respects_batch_limit_and_retains_pending ... ok test tweening::tests::test_streaming_append_commands_does_not_accumulate ... ok test tweening::tests::test_animated_mode_progress_advances_and_clamps ... ok test result: ok. 17 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s Doc-tests turtle_lib: 34 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.26s ``` ```bash cargo clippy --package turtle-lib --features svg -- -Wclippy::pedantic \ -Aclippy::cast_precision_loss -Aclippy::cast_sign_loss -Aclippy::cast_possible_truncation ``` Output: ```text Finished `dev` profile [optimized + debuginfo] target(s) in 0.82s ``` **0 warnings.** ```bash cargo run --example yinyang --features svg -- --export-svg /tmp/yinyang_test.svg ``` Output: ```text SVG exported successfully to: /tmp/yinyang_test.svg ``` ```bash cargo build --package turtle-lib --examples ``` Output: ```text Finished `dev` profile [optimized + debuginfo] target(s) in 6.74s ``` **All 30 examples compiled cleanly with 0 errors and 0 warnings.**
This commit is contained in:
@@ -10,4 +10,4 @@ proc-macro = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
proc-macro2 = "1.0"
|
proc-macro2 = "1.0"
|
||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
syn = { version = "2.0", features = ["full"] }
|
syn = { version = "3.0", features = ["full"] }
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ crossbeam = "0.8"
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
# For examples and testing
|
# For examples and testing
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||||
dialog = "*"
|
dialog = "0.3"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -5,12 +5,7 @@ use crate::state::{DrawCommand, TurtleParams, TurtleWorld};
|
|||||||
use crate::tessellation;
|
use crate::tessellation;
|
||||||
use macroquad::prelude::*;
|
use macroquad::prelude::*;
|
||||||
|
|
||||||
// Import the easing function from the tween crate
|
|
||||||
// To change the easing, change both this import and the usage in the draw_tween_arc function below
|
|
||||||
// Available options: Linear, SineInOut, QuadInOut, CubicInOut, QuartInOut, QuintInOut,
|
|
||||||
// ExpoInOut, CircInOut, BackInOut, ElasticInOut, BounceInOut, etc.
|
|
||||||
// See https://easings.net/ for visual demonstrations
|
|
||||||
use tween::CubicInOut;
|
|
||||||
|
|
||||||
/// Render the turtle world with active tween visualization.
|
/// Render the turtle world with active tween visualization.
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
@@ -136,15 +131,11 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) {
|
|||||||
radius.value(),
|
radius.value(),
|
||||||
*direction,
|
*direction,
|
||||||
);
|
);
|
||||||
let elapsed = get_time() - tween.start_time;
|
|
||||||
let progress = (elapsed / tween.duration).min(1.0);
|
|
||||||
let eased_progress = CubicInOut.tween(1.0, progress as f32);
|
|
||||||
|
|
||||||
// Delegate to the shared arc_points function — same sampling
|
// Delegate to the shared arc_points function — same sampling
|
||||||
// strategy as tessellate_arc, eliminating the divergence.
|
// strategy as tessellate_arc, using tween.progress directly.
|
||||||
let samples_to_draw =
|
let samples_to_draw =
|
||||||
(((*steps).max(1) as f32 * eased_progress) as usize).max(1);
|
(((*steps).max(1) as f32 * tween.progress) as usize).max(1);
|
||||||
let sweep_so_far = angle.as_radians().value() * eased_progress;
|
let sweep_so_far = angle.as_radians().value() * tween.progress;
|
||||||
for pt in arc_points(
|
for pt in arc_points(
|
||||||
geom.center,
|
geom.center,
|
||||||
radius.value(),
|
radius.value(),
|
||||||
@@ -309,21 +300,15 @@ fn draw_tween_arc(
|
|||||||
draw_mesh(&mesh);
|
draw_mesh(&mesh);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate how much of the arc we've traveled based on tween progress
|
// Draw the partial arc traveled based on tween progress
|
||||||
// Use the same eased progress as the turtle position for synchronized animation
|
|
||||||
let elapsed = get_time() - tween.start_time;
|
|
||||||
let t = (elapsed / tween.duration).min(1.0);
|
|
||||||
let progress = CubicInOut.tween(1.0, t as f32); // tween from 0 to 1
|
|
||||||
|
|
||||||
// Use Lyon to tessellate and draw the partial arc
|
|
||||||
if let Ok(mesh) = crate::tessellation::tessellate_arc(
|
if let Ok(mesh) = crate::tessellation::tessellate_arc(
|
||||||
geom.center,
|
geom.center,
|
||||||
radius,
|
radius,
|
||||||
geom.start_angle_from_center.to_degrees(),
|
geom.start_angle_from_center.to_degrees(),
|
||||||
total_angle.value() * progress,
|
total_angle.value() * tween.progress,
|
||||||
tween.start_params.color,
|
tween.start_params.color,
|
||||||
tween.start_params.pen_width,
|
tween.start_params.pen_width,
|
||||||
((steps as f32 * progress).ceil() as usize).max(1),
|
((steps as f32 * tween.progress).ceil() as usize).max(1),
|
||||||
direction,
|
direction,
|
||||||
) {
|
) {
|
||||||
draw_mesh(&mesh);
|
draw_mesh(&mesh);
|
||||||
|
|||||||
@@ -55,12 +55,13 @@ pub(crate) struct TweenController {
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct CommandTween {
|
pub(crate) struct CommandTween {
|
||||||
pub(crate) command: TurtleCommand,
|
pub(crate) command: TurtleCommand,
|
||||||
pub(crate) start_time: f64,
|
pub(crate) progress: f32,
|
||||||
pub(crate) duration: f64,
|
|
||||||
pub(crate) start_params: TurtleParams,
|
pub(crate) start_params: TurtleParams,
|
||||||
pub(crate) target_params: TurtleParams,
|
pub(crate) target_params: TurtleParams,
|
||||||
pub(crate) current_position: Vec2,
|
pub(crate) current_position: Vec2,
|
||||||
pub(crate) current_heading: f32,
|
pub(crate) current_heading: f32,
|
||||||
|
start_time: f64,
|
||||||
|
duration: f64,
|
||||||
position_tweener: Tweener<TweenVec2, f64, CubicInOut>,
|
position_tweener: Tweener<TweenVec2, f64, CubicInOut>,
|
||||||
heading_tweener: Tweener<f32, f64, CubicInOut>,
|
heading_tweener: Tweener<f32, f64, CubicInOut>,
|
||||||
pen_width_tweener: Tweener<f32, f64, CubicInOut>,
|
pen_width_tweener: Tweener<f32, f64, CubicInOut>,
|
||||||
@@ -166,7 +167,8 @@ impl TweenController {
|
|||||||
|
|
||||||
// Use tweeners to calculate current values
|
// Use tweeners to calculate current values
|
||||||
// For circles, calculate position along the arc instead of straight line
|
// For circles, calculate position along the arc instead of straight line
|
||||||
let progress = tween.heading_tweener.move_to(elapsed);
|
let progress = tween.heading_tweener.move_to(elapsed).clamp(0.0, 1.0);
|
||||||
|
tween.progress = progress;
|
||||||
|
|
||||||
let current_position = match &tween.command {
|
let current_position = match &tween.command {
|
||||||
TurtleCommand::Circle {
|
TurtleCommand::Circle {
|
||||||
@@ -316,6 +318,7 @@ impl TweenController {
|
|||||||
|
|
||||||
self.current_tween = Some(CommandTween {
|
self.current_tween = Some(CommandTween {
|
||||||
command,
|
command,
|
||||||
|
progress: 0.0,
|
||||||
start_time: current_time(),
|
start_time: current_time(),
|
||||||
duration,
|
duration,
|
||||||
start_params: params.clone(),
|
start_params: params.clone(),
|
||||||
@@ -537,7 +540,42 @@ mod tests {
|
|||||||
controller.update(0, &mut params, &mut filling, &mut commands, &mut svg_log);
|
controller.update(0, &mut params, &mut filling, &mut commands, &mut svg_log);
|
||||||
|
|
||||||
assert_eq!(controller.queue.len(), 1, "First command must be popped into current_tween");
|
assert_eq!(controller.queue.len(), 1, "First command must be popped into current_tween");
|
||||||
assert!(controller.current_tween().is_some());
|
let active = controller.current_tween().expect("Must have active tween");
|
||||||
|
assert_eq!(active.progress, 0.0, "New tween must initialize progress to 0.0");
|
||||||
assert!(!controller.is_complete());
|
assert!(!controller.is_complete());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_animated_mode_progress_advances_and_clamps() {
|
||||||
|
let mut queue = CommandQueue::new();
|
||||||
|
// Circle command with speed 10.0 and radius 100 => duration ~62.8s
|
||||||
|
queue.push(TurtleCommand::Circle {
|
||||||
|
radius: Length::new(100.0),
|
||||||
|
angle: Degrees::new(360.0),
|
||||||
|
steps: 36,
|
||||||
|
direction: CircleDirection::Right,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut controller = TweenController::new(
|
||||||
|
queue,
|
||||||
|
AnimationSpeed::Animated(10.0),
|
||||||
|
);
|
||||||
|
let mut params = make_test_params();
|
||||||
|
let mut filling = None;
|
||||||
|
let mut commands = Vec::new();
|
||||||
|
let mut svg_log = crate::state::SvgLog::default();
|
||||||
|
|
||||||
|
// Frame 0: pops into current_tween with progress = 0.0
|
||||||
|
controller.update(0, &mut params, &mut filling, &mut commands, &mut svg_log);
|
||||||
|
let initial_progress = controller.current_tween().unwrap().progress;
|
||||||
|
assert_eq!(initial_progress, 0.0);
|
||||||
|
|
||||||
|
// Advance time by sleeping briefly
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(15));
|
||||||
|
controller.update(0, &mut params, &mut filling, &mut commands, &mut svg_log);
|
||||||
|
if let Some(tween) = controller.current_tween() {
|
||||||
|
assert!(tween.progress >= 0.0 && tween.progress <= 1.0);
|
||||||
|
assert!(tween.progress >= initial_progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user