From 3c076fdd03587704a758726d54156602809caac2 Mon Sep 17 00:00:00 2001 From: Franz Dietrich Date: Sun, 17 May 2026 07:23:13 +0200 Subject: [PATCH] improve command handling --- turtle-lib/src/commands.rs | 43 +++--- turtle-lib/src/execution.rs | 289 +++++++++++++++++++++++++----------- turtle-lib/src/lib.rs | 9 +- turtle-lib/src/state.rs | 20 +++ turtle-lib/src/tweening.rs | 143 +++++++++++------- 5 files changed, 337 insertions(+), 167 deletions(-) diff --git a/turtle-lib/src/commands.rs b/turtle-lib/src/commands.rs index b6b3e8d..6750079 100644 --- a/turtle-lib/src/commands.rs +++ b/turtle-lib/src/commands.rs @@ -53,11 +53,14 @@ pub enum TurtleCommand { Reset, } -/// Queue of turtle commands with execution state +/// A pure-data sequence of turtle commands. +/// +/// `CommandQueue` is intentionally *not* an `Iterator` — it carries no cursor +/// state. Execution state ("which command are we on?") belongs to the +/// consumer; `TweenController` owns the cursor that walks this queue. #[derive(Clone, Debug)] pub struct CommandQueue { commands: Vec, - current_index: usize, } impl CommandQueue { @@ -65,14 +68,12 @@ impl CommandQueue { pub fn new() -> Self { Self { commands: Vec::new(), - current_index: 0, } } #[must_use] pub fn with_capacity(capacity: usize) -> Self { Self { commands: Vec::with_capacity(capacity), - current_index: 0, } } @@ -83,26 +84,22 @@ impl CommandQueue { pub fn extend(&mut self, commands: impl IntoIterator) { self.commands.extend(commands); } + + /// Return a reference to the command at `index`, or `None` if out of range. #[must_use] - pub fn is_complete(&self) -> bool { - self.current_index >= self.commands.len() - } - pub fn reset(&mut self) { - self.current_index = 0; + pub fn get(&self, index: usize) -> Option<&TurtleCommand> { + self.commands.get(index) } + #[must_use] pub fn len(&self) -> usize { self.commands.len() } + #[must_use] pub fn is_empty(&self) -> bool { self.commands.is_empty() } - - #[must_use] - pub fn remaining(&self) -> usize { - self.commands.len().saturating_sub(self.current_index) - } } impl Default for CommandQueue { @@ -111,16 +108,16 @@ impl Default for CommandQueue { } } -impl Iterator for CommandQueue { +/// Consuming iteration — yields every command in order. +/// +/// This is used by `CommandQueue::extend` and `TweenController::append_commands` +/// to drain one queue into another. It does *not* imply that `CommandQueue` +/// itself is stateful; the cursor always lives in the consumer. +impl IntoIterator for CommandQueue { type Item = TurtleCommand; + type IntoIter = std::vec::IntoIter; - fn next(&mut self) -> Option { - if self.current_index < self.commands.len() { - let cmd = self.commands[self.current_index].clone(); - self.current_index += 1; - Some(cmd) - } else { - None - } + fn into_iter(self) -> Self::IntoIter { + self.commands.into_iter() } } diff --git a/turtle-lib/src/execution.rs b/turtle-lib/src/execution.rs index 6a34e7e..c3d1851 100644 --- a/turtle-lib/src/execution.rs +++ b/turtle-lib/src/execution.rs @@ -1,52 +1,120 @@ //! Command execution logic -use crate::circle_geometry::CircleGeometry; +use crate::circle_geometry::{CircleDirection, CircleGeometry}; use crate::commands::TurtleCommand; -use crate::state::{DrawCommand, Turtle, TurtleParams, TurtleWorld}; +use crate::general::Coordinate; +use crate::state::{DrawCommand, FillState, Turtle, TurtleParams, TurtleWorld}; use crate::tessellation; use macroquad::prelude::*; #[cfg(test)] use crate::general::AnimationSpeed; -/// Execute side effects for commands that don't involve movement -/// Returns true if the command was handled (caller should skip movement processing) +/// Close the current open fill contour (factored out of `Turtle::close_fill_contour`). +fn close_fill_contour(turtle_id: usize, filling: &mut Option) { + if let Some(ref mut fill_state) = filling { + tracing::debug!( + turtle_id, + vertices = fill_state.current_contour.len(), + "close_fill_contour called" + ); + if fill_state.current_contour.len() >= 2 { + tracing::debug!( + turtle_id, + vertices = fill_state.current_contour.len(), + first_x = fill_state.current_contour[0].x, + first_y = fill_state.current_contour[0].y, + last_x = fill_state.current_contour[fill_state.current_contour.len() - 1].x, + last_y = fill_state.current_contour[fill_state.current_contour.len() - 1].y, + "Closing contour" + ); + let contour = std::mem::take(&mut fill_state.current_contour); + fill_state.contours.push(contour); + tracing::debug!( + turtle_id, + completed_contours = fill_state.contours.len(), + "Contour moved to completed list" + ); + } else if !fill_state.current_contour.is_empty() { + tracing::warn!( + turtle_id, + vertices = fill_state.current_contour.len(), + "Current contour has insufficient vertices, not closing" + ); + } else { + tracing::warn!(turtle_id, "Current contour is empty, nothing to close"); + } + } else { + tracing::warn!( + turtle_id, + "close_fill_contour called but no active fill state" + ); + } +} + +/// Begin a new fill contour at `position` (factored out of `Turtle::start_fill_contour`). +fn start_fill_contour(turtle_id: usize, position: Coordinate, filling: &mut Option) { + if let Some(ref mut fill_state) = filling { + tracing::debug!( + x = position.x, + y = position.y, + completed_contours = fill_state.contours.len(), + turtle_id, + "Starting new contour" + ); + fill_state.current_contour = vec![position]; + } +} + +/// Execute side effects for commands that don't involve movement. +/// +/// Returns `true` if the command was fully handled; the caller should skip +/// params-update and tessellation when this returns `true`. +/// +/// Accepts the three logically-separate pieces of turtle state as disjoint +/// mutable borrows so that this function can be called from +/// `TweenController::update(&mut self, …)` without requiring a `&mut Turtle`. #[allow(clippy::too_many_lines)] -pub(crate) fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle) -> bool { +pub(crate) fn execute_command_side_effects( + command: &TurtleCommand, + turtle_id: usize, + params: &mut TurtleParams, + filling: &mut Option, + commands: &mut Vec, +) -> bool { match command { TurtleCommand::BeginFill => { - if state.filling.is_some() { - tracing::warn!( - turtle_id = state.turtle_id, - "begin_fill() called while already filling" - ); + if filling.is_some() { + tracing::warn!(turtle_id, "begin_fill() called while already filling"); } - let fill_color = state.params.fill_color.unwrap_or_else(|| { - tracing::warn!( - turtle_id = state.turtle_id, - "No fill_color set, using black" - ); + let fill_color = params.fill_color.unwrap_or_else(|| { + tracing::warn!(turtle_id, "No fill_color set, using black"); BLACK }); - state.begin_fill(fill_color); + *filling = Some(FillState { + start_position: params.position, + contours: Vec::new(), + current_contour: vec![params.position], + fill_color, + }); true } TurtleCommand::EndFill => { - if let Some(mut fill_state) = state.filling.take() { + if let Some(mut fill_state) = filling.take() { if !fill_state.current_contour.is_empty() { fill_state.contours.push(fill_state.current_contour); } let span = tracing::debug_span!( "end_fill", - turtle_id = state.turtle_id, + turtle_id, contours = fill_state.contours.len() ); let _enter = span.enter(); for (i, contour) in fill_state.contours.iter().enumerate() { tracing::debug!( - turtle_id = state.turtle_id, + turtle_id, contour_idx = i, vertices = contour.len(), "Contour info" @@ -59,83 +127,76 @@ pub(crate) fn execute_command_side_effects(command: &TurtleCommand, state: &mut fill_state.fill_color, ) { tracing::debug!( - turtle_id = state.turtle_id, + turtle_id, contours = fill_state.contours.len(), "Successfully created fill mesh - persisting to commands" ); - state.commands.push(DrawCommand::Mesh { + commands.push(DrawCommand::Mesh { data: mesh_data, source: crate::state::TurtleSource { command: crate::commands::TurtleCommand::EndFill, - color: state.params.color, + color: params.color, fill_color: fill_state.fill_color, - pen_width: state.params.pen_width, + pen_width: params.pen_width, start_position: fill_state.start_position, end_position: fill_state.start_position, - start_heading: state.params.heading, + start_heading: params.heading, contours: Some(fill_state.contours.clone()), }, }); } else { - tracing::error!( - turtle_id = state.turtle_id, - "Failed to tessellate contours" - ); + tracing::error!(turtle_id, "Failed to tessellate contours"); } } } else { - tracing::warn!( - turtle_id = state.turtle_id, - "end_fill() called without begin_fill()" - ); + tracing::warn!(turtle_id, "end_fill() called without begin_fill()"); } true } TurtleCommand::PenUp => { - state.params.pen_down = false; - if state.filling.is_some() { - tracing::debug!( - turtle_id = state.turtle_id, - "PenUp: Closing current contour" - ); + params.pen_down = false; + if filling.is_some() { + tracing::debug!(turtle_id, "PenUp: Closing current contour"); } - state.close_fill_contour(); + close_fill_contour(turtle_id, filling); true } TurtleCommand::PenDown => { - state.params.pen_down = true; - if state.filling.is_some() { + params.pen_down = true; + if filling.is_some() { tracing::debug!( - turtle_id = state.turtle_id, - x = state.params.position.x, - y = state.params.position.y, + turtle_id, + x = params.position.x, + y = params.position.y, "PenDown: Starting new contour" ); } - state.start_fill_contour(); + start_fill_contour(turtle_id, params.position, filling); true } TurtleCommand::Reset => { - state.reset(); + commands.clear(); + *filling = None; + *params = TurtleParams::default(); true } TurtleCommand::WriteText { text, font_size } => { - state.commands.push(DrawCommand::Text { + commands.push(DrawCommand::Text { text: text.clone(), - position: state.params.position, - heading: state.params.heading, + position: params.position, + heading: params.heading, font_size: *font_size, - color: state.params.color, + color: params.color, source: crate::state::TurtleSource { command: command.clone(), - color: state.params.color, - fill_color: state.params.fill_color.unwrap_or(BLACK), - pen_width: state.params.pen_width, - start_position: state.params.position, - end_position: state.params.position, - start_heading: state.params.heading, + color: params.color, + fill_color: params.fill_color.unwrap_or(BLACK), + pen_width: params.pen_width, + start_position: params.position, + end_position: params.position, + start_heading: params.heading, contours: None, }, }); @@ -157,14 +218,23 @@ pub(crate) fn execute_command_side_effects(command: &TurtleCommand, state: &mut } } -/// Record fill vertices after movement commands have updated state -#[tracing::instrument] +/// Record fill vertices after movement commands have updated state. +/// +/// `start_state` is the params snapshot taken **before** the command ran. +/// `params` is the current (post-movement) state — `params.position` is the +/// endpoint that gets pushed into the active fill contour. +/// +/// Accepts disjoint borrows so it can be called from `TweenController::update` +/// without needing a `&mut Turtle`. +#[tracing::instrument(skip(params, filling))] pub(crate) fn record_fill_vertices_after_movement( command: &TurtleCommand, start_state: &TurtleParams, - state: &mut Turtle, + turtle_id: usize, + params: &TurtleParams, + filling: &mut Option, ) { - if state.filling.is_none() { + if filling.is_none() { return; } @@ -181,17 +251,60 @@ pub(crate) fn record_fill_vertices_after_movement( *radius, *direction, ); - state.record_fill_vertices_for_arc( - geom.center, - *radius, - geom.start_angle_from_center, - angle.to_radians(), - *direction, - *steps as u32, - ); + if let Some(ref mut fill_state) = filling { + if params.pen_down { + let num_samples = (*steps as u32).max(1); + tracing::trace!( + turtle_id, + center_x = geom.center.x, + center_y = geom.center.y, + radius, + steps, + num_samples, + "Recording arc vertices" + ); + for i in 1..=num_samples { + let progress = i as f32 / num_samples as f32; + let current_angle = match direction { + CircleDirection::Left => { + geom.start_angle_from_center - angle.to_radians() * progress + } + CircleDirection::Right => { + geom.start_angle_from_center + angle.to_radians() * progress + } + }; + let vertex = Coordinate::new( + geom.center.x + radius * current_angle.cos(), + geom.center.y + radius * current_angle.sin(), + ); + tracing::trace!( + turtle_id, + vertex_idx = i, + x = vertex.x, + y = vertex.y, + angle_degrees = current_angle.to_degrees(), + "Arc vertex" + ); + fill_state.current_contour.push(vertex); + } + } + } } TurtleCommand::Move(_) | TurtleCommand::Goto(_) => { - state.record_fill_vertex(); + if let Some(ref mut fill_state) = filling { + if params.pen_down { + tracing::trace!( + turtle_id, + x = params.position.x, + y = params.position.y, + vertices = fill_state.current_contour.len() + 1, + "Adding vertex to current contour" + ); + fill_state.current_contour.push(params.position); + } else { + tracing::trace!(turtle_id, "Skipping vertex (pen is up)"); + } + } } _ => {} } @@ -283,12 +396,18 @@ pub(crate) fn tessellate_command( } } -/// Execute a single turtle command, updating state and adding draw commands -#[tracing::instrument] +/// Execute a single turtle command, updating state and adding draw commands. +#[tracing::instrument(skip(state))] pub(crate) fn execute_command(command: &TurtleCommand, state: &mut Turtle) { // Phase 1: side effects (fills, pen contours, reset, text). // Returns true if the command is fully handled — no params update or tessellation needed. - if execute_command_side_effects(command, state) { + if execute_command_side_effects( + command, + state.turtle_id, + &mut state.params, + &mut state.filling, + &mut state.commands, + ) { return; } @@ -297,7 +416,13 @@ pub(crate) fn execute_command(command: &TurtleCommand, state: &mut Turtle) { command.apply_to_params(&mut state.params); // Phase 3: record fill vertices after movement (must follow params update) - record_fill_vertices_after_movement(command, &start_params, state); + record_fill_vertices_after_movement( + command, + &start_params, + state.turtle_id, + &state.params, + &mut state.filling, + ); // Phase 4: tessellate and persist the committed drawing if let Some(draw_cmd) = tessellate_command(command, &start_params, state.params.position) { @@ -305,20 +430,18 @@ pub(crate) fn execute_command(command: &TurtleCommand, state: &mut Turtle) { } } -/// Execute command on a specific turtle by ID +/// Execute command on a specific turtle by ID. +/// +/// There is no ownership conflict here: `execute_command` only needs `&mut Turtle` +/// and never touches `TurtleWorld`, so we can obtain the mutable reference directly +/// from `get_turtle_mut` without any intermediate clone. pub(crate) fn execute_command_with_id( command: &TurtleCommand, turtle_id: usize, world: &mut TurtleWorld, ) { - // Clone turtle state to avoid borrow checker issues - if let Some(turtle) = world.get_turtle(turtle_id) { - let mut state = turtle.clone(); - execute_command(command, &mut state); - // Update the turtle state back - if let Some(turtle_mut) = world.get_turtle_mut(turtle_id) { - *turtle_mut = state; - } + if let Some(turtle) = world.get_turtle_mut(turtle_id) { + execute_command(command, turtle); } } @@ -327,7 +450,7 @@ mod tests { use super::*; use crate::commands::TurtleCommand; use crate::shapes::TurtleShape; - use crate::TweenController; + use crate::tweening::TweenController; #[test] fn test_forward_left_forward() { diff --git a/turtle-lib/src/lib.rs b/turtle-lib/src/lib.rs index 13a82dc..a1228b3 100644 --- a/turtle-lib/src/lib.rs +++ b/turtle-lib/src/lib.rs @@ -80,7 +80,6 @@ pub use macroquad::prelude::{ use crate::commands_channel::TurtleCommandReceiver; use crate::state::TurtleWorld; -use crate::tweening::TweenController; use macroquad::prelude::*; use std::collections::HashMap; @@ -292,10 +291,10 @@ impl TurtleApp { // Update all turtles' tween controllers for turtle in &mut self.world.turtles { - // Extract draw_commands and controller temporarily to avoid borrow conflicts - - // Update the controller - let completed_commands = TweenController::update(turtle); + // Drive this turtle's animation controller for one frame. + // `update_tweens` splits &mut Turtle into disjoint field borrows so + // TweenController::update can be a proper &mut self method. + let completed_commands = turtle.update_tweens(); // Process all completed commands and add to the turtle's commands for (completed_cmd, tween_start, end_state) in completed_commands { diff --git a/turtle-lib/src/state.rs b/turtle-lib/src/state.rs index fcd1ab8..6bd594e 100644 --- a/turtle-lib/src/state.rs +++ b/turtle-lib/src/state.rs @@ -106,6 +106,26 @@ impl Turtle { // Keep turtle_id and tween_controller (preserves queued commands) } + /// Drive the animation controller for one frame. + /// + /// Returns `(command, start_params, end_params)` for every command that + /// completed this frame and whose stroke needs to be tessellated by the + /// caller into a `DrawCommand`. + /// + /// This method performs the correct disjoint field-borrow split so that + /// `TweenController::update` can be a proper `&mut self` method instead + /// of the old static-method borrow-checker workaround. + pub fn update_tweens( + &mut self, + ) -> Vec<(crate::commands::TurtleCommand, TurtleParams, TurtleParams)> { + self.tween_controller.update( + self.turtle_id, + &mut self.params, + &mut self.filling, + &mut self.commands, + ) + } + /// Start recording fill vertices pub fn begin_fill(&mut self, fill_color: Color) { self.filling = Some(FillState { diff --git a/turtle-lib/src/tweening.rs b/turtle-lib/src/tweening.rs index 7ed6726..c1c6f90 100644 --- a/turtle-lib/src/tweening.rs +++ b/turtle-lib/src/tweening.rs @@ -3,7 +3,7 @@ use crate::circle_geometry::{CircleDirection, CircleGeometry}; use crate::commands::{CommandQueue, TurtleCommand}; use crate::general::AnimationSpeed; -use crate::state::{Turtle, TurtleParams}; +use crate::state::{DrawCommand, FillState, TurtleParams}; use macroquad::prelude::*; use tween::{CubicInOut, TweenValue, Tweener}; @@ -47,6 +47,10 @@ impl From for Vec2 { #[derive(Clone, Debug, Default)] pub(crate) struct TweenController { queue: CommandQueue, + /// Cursor into `queue` — tracks which command executes next. + /// Lives here, not in `CommandQueue`, so that cloning or appending to the + /// queue never silently resets or mid-stream-shifts the execution position. + cursor: usize, current_tween: Option, speed: AnimationSpeed, } @@ -71,6 +75,7 @@ impl TweenController { pub fn new(queue: CommandQueue, speed: AnimationSpeed) -> Self { Self { queue, + cursor: 0, current_tween: None, speed, } @@ -80,51 +85,71 @@ impl TweenController { self.speed = speed; } - /// Append commands to the queue + /// Append commands to the queue. + /// + /// The cursor is **not** reset — commands already consumed remain consumed, + /// and the new commands are picked up naturally as the cursor advances. pub fn append_commands(&mut self, new_queue: CommandQueue) { self.queue.extend(new_queue); } - /// Update the tween, returns `Vec` of (`command`, `start_state`, `end_state`) for all completed commands this frame - /// Also takes commands vec to handle side effects like fill operations - /// Each `command` has its own `start_state` and `end_state` pair + /// Drive the animation controller for one frame. + /// + /// Returns `(command, start_params, end_params)` for every command that + /// completed this frame and whose stroke needs to be tessellated by the + /// caller. + /// + /// By accepting `params`, `filling`, and `commands` as separate mutable + /// borrows the caller can split `&mut Turtle` into disjoint field borrows, + /// eliminating the old static-method borrow-checker workaround. #[allow(clippy::too_many_lines)] - pub fn update(state: &mut Turtle) -> Vec<(TurtleCommand, TurtleParams, TurtleParams)> { + pub fn update( + &mut self, + turtle_id: usize, + params: &mut TurtleParams, + filling: &mut Option, + commands: &mut Vec, + ) -> Vec<(TurtleCommand, TurtleParams, TurtleParams)> { // In instant mode, execute commands up to the draw calls per frame limit - if let AnimationSpeed::Instant(max_draw_calls) = state.tween_controller.speed { + if let AnimationSpeed::Instant(max_draw_calls) = self.speed { let mut completed_commands: Vec<(TurtleCommand, TurtleParams, TurtleParams)> = Vec::new(); let mut draw_call_count = 0; - // Consume commands from the real queue so the current_index advances - while let Some(command) = state.tween_controller.queue.next() { + // Advance cursor through the queue for each command consumed + while let Some(command) = self.queue.get(self.cursor).cloned() { + self.cursor += 1; // Handle SetSpeed command to potentially switch modes if let TurtleCommand::SetSpeed(new_speed) = &command { - state.params.speed = *new_speed; - state.tween_controller.speed = *new_speed; - if matches!(state.tween_controller.speed, AnimationSpeed::Animated(_)) { + params.speed = *new_speed; + self.speed = *new_speed; + if matches!(self.speed, AnimationSpeed::Animated(_)) { break; } continue; } // Execute side-effect-only commands using centralized helper - if crate::execution::execute_command_side_effects(&command, state) { + if crate::execution::execute_command_side_effects( + &command, turtle_id, params, filling, commands, + ) { continue; // Command fully handled } // Save start state and compute target state - let start_params = state.params.clone(); + let start_params = params.clone(); let target_params = Self::calculate_target_state(&start_params, &command); // Update state to the target (instant execution) - state.params = target_params.clone(); + *params = target_params.clone(); // Record fill vertices AFTER movement crate::execution::record_fill_vertices_after_movement( &command, &start_params, - state, + turtle_id, + params, + filling, ); // Collect drawable commands (return start and target so caller can create draw meshes) @@ -141,7 +166,7 @@ impl TweenController { } // Process current tween - if let Some(ref mut tween) = state.tween_controller.current_tween { + if let Some(ref mut tween) = self.current_tween { let elapsed = get_time() - tween.start_time; // Use tweeners to calculate current values @@ -170,7 +195,7 @@ impl TweenController { } }; - state.params.position = current_position; + params.position = current_position; tween.current_position = current_position; // Heading changes proportionally with progress for all commands @@ -195,18 +220,18 @@ impl TweenController { } }); - state.params.heading = current_heading; + params.heading = current_heading; tween.current_heading = current_heading; - state.params.pen_width = tween.pen_width_tweener.move_to(elapsed); + params.pen_width = tween.pen_width_tweener.move_to(elapsed); // Discrete properties (switch at 50% progress) let progress = (elapsed / tween.duration).min(1.0); if progress >= 0.5 { - state.params.pen_down = tween.target_params.pen_down; - state.params.color = tween.target_params.color; - state.params.fill_color = tween.target_params.fill_color; - state.params.visible = tween.target_params.visible; - state.params.shape = tween.target_params.shape.clone(); + params.pen_down = tween.target_params.pen_down; + params.color = tween.target_params.color; + params.fill_color = tween.target_params.fill_color; + params.visible = tween.target_params.visible; + params.shape = tween.target_params.shape.clone(); } // Check if tween is finished (use heading_tweener as it's used by all commands) @@ -215,20 +240,24 @@ impl TweenController { let target_params = tween.target_params.clone(); let command = tween.command.clone(); - // Drop the mutable borrow of tween before mutably borrowing state - state.params = target_params.clone(); + // tween borrow ends here (NLL) — safe to reassign self.current_tween below + *params = target_params.clone(); crate::execution::record_fill_vertices_after_movement( &command, &start_params, - state, + turtle_id, + params, + filling, ); - state.tween_controller.current_tween = None; + self.current_tween = None; // Execute side-effect-only commands using centralized helper - if crate::execution::execute_command_side_effects(&command, state) { - return Self::update(state); // Continue to next command + if crate::execution::execute_command_side_effects( + &command, turtle_id, params, filling, commands, + ) { + return self.update(turtle_id, params, filling, commands); } // Return drawable commands using the original start and target params @@ -236,43 +265,45 @@ impl TweenController { return vec![(command, start_params.clone(), target_params.clone())]; } - return Self::update(state); // Continue to next command + return self.update(turtle_id, params, filling, commands); } return Vec::new(); } // Start next tween - if let Some(command) = state.tween_controller.queue.next() { - let command_clone = command.clone(); + if let Some(command) = self.queue.get(self.cursor).cloned() { + self.cursor += 1; // Handle commands that should execute immediately (no animation) - match &command_clone { + match &command { TurtleCommand::SetSpeed(new_speed) => { - state.set_speed(*new_speed); - state.tween_controller.speed = *new_speed; - if matches!(state.tween_controller.speed, AnimationSpeed::Instant(_)) { - return Self::update(state); + params.speed = *new_speed; + self.speed = *new_speed; + if matches!(self.speed, AnimationSpeed::Instant(_)) { + return self.update(turtle_id, params, filling, commands); } - return Self::update(state); + return self.update(turtle_id, params, filling, commands); } _ => { // Use centralized helper for side effects - if crate::execution::execute_command_side_effects(&command_clone, state) { - return Self::update(state); + if crate::execution::execute_command_side_effects( + &command, turtle_id, params, filling, commands, + ) { + return self.update(turtle_id, params, filling, commands); } } } - let speed = state.tween_controller.speed; // Extract speed before borrowing self - let duration = Self::calculate_duration_with_state(&command_clone, state, speed); + let speed = self.speed; + let duration = Self::calculate_duration_with_state(&command, params, speed); // Calculate target state - let target_state = Self::calculate_target_state(&state.params, &command_clone); + let target_state = Self::calculate_target_state(params, &command); // Create tweeners for smooth animation let position_tweener = Tweener::new( - TweenVec2::from(state.params.position), + TweenVec2::from(params.position), TweenVec2::from(target_state.position), duration, CubicInOut, @@ -284,21 +315,21 @@ impl TweenController { ); let pen_width_tweener = Tweener::new( - state.params.pen_width, + params.pen_width, target_state.pen_width, duration, CubicInOut, ); - state.tween_controller.current_tween = Some(CommandTween { - turtle_id: state.turtle_id, - command: command_clone, + self.current_tween = Some(CommandTween { + turtle_id, + command, start_time: get_time(), duration, - start_params: state.params.clone(), + start_params: params.clone(), target_params: target_state.clone(), - current_position: state.params.position, - current_heading: state.params.heading, + current_position: params.position, + current_heading: params.heading, position_tweener, heading_tweener, pen_width_tweener, @@ -310,7 +341,7 @@ impl TweenController { #[must_use] pub fn is_complete(&self) -> bool { - self.current_tween.is_none() && self.queue.is_complete() + self.current_tween.is_none() && self.cursor >= self.queue.len() } /// Get the current active tween if one is in progress @@ -324,10 +355,10 @@ impl TweenController { fn calculate_duration_with_state( command: &TurtleCommand, - current: &Turtle, + params: &TurtleParams, speed: AnimationSpeed, ) -> f64 { - command.animation_duration(¤t.params, speed) + command.animation_duration(params, speed) } fn calculate_target_state(current: &TurtleParams, command: &TurtleCommand) -> TurtleParams {