diff --git a/turtle-lib-macroquad/examples/koch.rs b/turtle-lib-macroquad/examples/koch.rs index ea9263a..bd47dbc 100644 --- a/turtle-lib-macroquad/examples/koch.rs +++ b/turtle-lib-macroquad/examples/koch.rs @@ -23,17 +23,17 @@ async fn main() { let mut plan = create_turtle(); // Position turtle + plan.set_speed(1001); plan.pen_up(); plan.backward(150.0); plan.pen_down(); - plan.set_speed(10000); // Draw Koch snowflake (triangle of Koch curves) for _ in 0..3 { koch(4, &mut plan, 300.0); plan.right(120.0); - plan.set_speed(1000); + plan.set_speed(1200); } plan.hide(); // Hide turtle when done diff --git a/turtle-lib-macroquad/examples/stern.rs b/turtle-lib-macroquad/examples/stern.rs index d1ca8f7..975f1e0 100644 --- a/turtle-lib-macroquad/examples/stern.rs +++ b/turtle-lib-macroquad/examples/stern.rs @@ -8,10 +8,11 @@ async fn main() { // Create a turtle plan let mut plan = create_turtle(); plan.shape(ShapeType::Turtle); - plan.set_speed(800); + plan.set_speed(1500); + plan.set_pen_width(0.5); - // Draw a square - for _ in 0..5 { + // Draw a 5-pointed star pattern repeatedly + for _i in 0..50000 { plan.forward(200.0); plan.circle_left(10.0, 72.0, 1000); plan.circle_right(5.0, 360.0, 1000); diff --git a/turtle-lib-macroquad/src/builders.rs b/turtle-lib-macroquad/src/builders.rs index 9af3459..b96633c 100644 --- a/turtle-lib-macroquad/src/builders.rs +++ b/turtle-lib-macroquad/src/builders.rs @@ -124,6 +124,11 @@ impl TurtlePlan { self } + pub fn set_heading(&mut self, heading: Precision) -> &mut Self { + self.queue.push(TurtleCommand::SetHeading(heading)); + self + } + pub fn pen_up(&mut self) -> &mut Self { self.queue.push(TurtleCommand::PenUp); self diff --git a/turtle-lib-macroquad/src/general.rs b/turtle-lib-macroquad/src/general.rs index 3294d21..6fc85bb 100644 --- a/turtle-lib-macroquad/src/general.rs +++ b/turtle-lib-macroquad/src/general.rs @@ -18,32 +18,35 @@ pub type Coordinate = Vec2; pub type Visibility = bool; /// Execution speed setting -/// - Instant: No animation, commands execute immediately +/// - Instant(draw_calls): Fast execution with limited draw calls per frame (speed - 1000, minimum 1) /// - Animated(speed): Smooth animation at specified pixels/second #[derive(Clone, Copy, Debug, PartialEq)] pub enum AnimationSpeed { - Instant, + Instant(u32), // Number of draw calls per frame (minimum 1) Animated(f32), // pixels per second } impl AnimationSpeed { /// Check if this is instant mode - pub fn is_instant(&self) -> bool { - matches!(self, AnimationSpeed::Instant) + pub fn is_animating(&self) -> bool { + matches!(self, AnimationSpeed::Animated(_)) } - /// Get the speed value (returns a high value for Instant) + /// Get the speed value (returns encoded value for Instant) pub fn value(&self) -> f32 { match self { - AnimationSpeed::Instant => 9999.0, + AnimationSpeed::Instant(calls) => 1000.0 + *calls as f32, AnimationSpeed::Animated(speed) => *speed, } } - /// Create from a raw speed value (>= 999 becomes Instant) + /// Create from a raw speed value + /// - speed >= 1000 becomes Instant with max(1, speed - 1000) draw calls per frame + /// - speed < 1000 becomes Animated pub fn from_value(speed: f32) -> Self { - if speed >= 999.0 { - AnimationSpeed::Instant + if speed >= 1000.0 { + let draw_calls = (speed - 1000.0).max(1.0) as u32; // Ensure at least 1 + AnimationSpeed::Instant(draw_calls) } else { AnimationSpeed::Animated(speed.max(1.0)) } diff --git a/turtle-lib-macroquad/src/lib.rs b/turtle-lib-macroquad/src/lib.rs index f75fcbd..0986500 100644 --- a/turtle-lib-macroquad/src/lib.rs +++ b/turtle-lib-macroquad/src/lib.rs @@ -91,10 +91,10 @@ impl TurtleApp { self.handle_mouse_zoom(); if let Some(ref mut controller) = self.tween_controller { - if let Some((completed_cmd, start_state)) = controller.update(&mut self.world.turtle) { - // Copy end state before we borrow world mutably - let end_state = self.world.turtle.clone(); + let completed_commands = controller.update(&mut self.world.turtle); + // Process all completed commands (multiple in instant mode, 0-1 in animated mode) + for (completed_cmd, start_state, end_state) in completed_commands { // Add draw commands for the completed tween execution::add_draw_for_completed_tween( &completed_cmd, @@ -105,7 +105,6 @@ impl TurtleApp { } } } - /// Handle mouse click and drag for panning fn handle_mouse_panning(&mut self) { let mouse_pos = mouse_position(); diff --git a/turtle-lib-macroquad/src/tweening.rs b/turtle-lib-macroquad/src/tweening.rs index 89edece..5fdf24f 100644 --- a/turtle-lib-macroquad/src/tweening.rs +++ b/turtle-lib-macroquad/src/tweening.rs @@ -74,25 +74,33 @@ impl TweenController { self.speed = speed; } - /// Update the tween, returns (command, start_state) if command completed - pub fn update(&mut self, state: &mut TurtleState) -> Option<(TurtleCommand, TurtleState)> { - // In immediate mode, execute all remaining commands instantly - if self.speed.is_instant() { + /// Update the tween, returns Vec of (command, start_state, end_state) for all completed commands this frame + /// Each command has its own start_state and end_state pair + pub fn update( + &mut self, + state: &mut TurtleState, + ) -> Vec<(TurtleCommand, TurtleState, TurtleState)> { + // In instant mode, execute commands up to the draw calls per frame limit + if let AnimationSpeed::Instant(max_draw_calls) = self.speed { + let mut completed_commands = Vec::new(); + let mut draw_call_count = 0; + loop { let command = match self.queue.next() { Some(cmd) => cmd.clone(), - None => return None, + None => break, }; + // Capture start state BEFORE executing this command let start_state = state.clone(); // Handle SetSpeed command to potentially switch modes if let TurtleCommand::SetSpeed(new_speed) = &command { state.set_speed(*new_speed); self.speed = *new_speed; - // If speed dropped below instant, switch to animated mode - if !self.speed.is_instant() { - return None; + // If speed switched to animated mode, exit instant mode processing + if matches!(self.speed, AnimationSpeed::Animated(_)) { + break; } continue; } @@ -101,11 +109,22 @@ impl TweenController { let target_state = self.calculate_target_state(state, &command); *state = target_state.clone(); - // Return drawable commands for rendering + // Capture end state AFTER executing this command + let end_state = state.clone(); + + // Collect drawable commands with their individual start and end states if Self::command_creates_drawing(&command) { - return Some((command, start_state)); + completed_commands.push((command, start_state, end_state)); + draw_call_count += 1; + + // Stop if we've reached the draw call limit for this frame + if draw_call_count >= max_draw_calls { + break; + } } } + + return completed_commands; } // Process current tween @@ -139,7 +158,7 @@ impl TweenController { }; // Heading changes proportionally with progress for all commands - state.heading = match &tween.command { + state.heading = normalize_angle(match &tween.command { TurtleCommand::Circle { angle, direction, .. } => match direction { @@ -158,7 +177,7 @@ impl TweenController { let heading_diff = tween.target_state.heading - tween.start_state.heading; tween.start_state.heading + heading_diff * progress } - }; + }); state.pen_width = tween.pen_width_tweener.move_to(elapsed); // Discrete properties (switch at 50% progress) @@ -176,18 +195,19 @@ impl TweenController { // Tween complete, finalize state let start_state = tween.start_state.clone(); *state = tween.target_state.clone(); + let end_state = state.clone(); - // Return the completed command and start state to add draw commands + // Return the completed command and start/end states to add draw commands let completed_command = tween.command.clone(); self.current_tween = None; // Only return command if it creates drawable elements if Self::command_creates_drawing(&completed_command) { - return Some((completed_command, start_state)); + return vec![(completed_command, start_state, end_state)]; } } - return None; + return Vec::new(); } // Start next tween @@ -198,9 +218,9 @@ impl TweenController { if let TurtleCommand::SetSpeed(new_speed) = &command_clone { state.set_speed(*new_speed); self.speed = *new_speed; - // If switched to immediate mode, process immediately - if self.speed.is_instant() { - return self.update(state); // Recursively process in immediate mode + // If switched to instant mode, process commands immediately + if matches!(self.speed, AnimationSpeed::Instant(_)) { + return self.update(state); // Recursively process in instant mode } // For animated mode speed changes, continue to next command return self.update(state); @@ -246,7 +266,7 @@ impl TweenController { }); } - None + Vec::new() } pub fn is_complete(&self) -> bool { @@ -301,7 +321,7 @@ impl TweenController { target.position = vec2(current.position.x + dx, current.position.y + dy); } TurtleCommand::Turn(angle) => { - target.heading += angle.to_radians(); + target.heading = normalize_angle(current.heading + angle.to_radians()); } TurtleCommand::Circle { radius, @@ -317,16 +337,16 @@ impl TweenController { angle.to_radians(), *direction, ); - target.heading = match direction { + target.heading = normalize_angle(match direction { CircleDirection::Left => current.heading - angle.to_radians(), CircleDirection::Right => current.heading + angle.to_radians(), - }; + }); } TurtleCommand::Goto(coord) => { target.position = *coord; } TurtleCommand::SetHeading(heading) => { - target.heading = *heading; + target.heading = normalize_angle(*heading); } TurtleCommand::SetColor(color) => { target.color = *color; @@ -372,3 +392,18 @@ fn calculate_circle_position( let geom = CircleGeometry::new(start_pos, start_heading, radius, direction); geom.position_at_angle(angle_traveled) } + +/// Normalize angle to range [-PI, PI] to prevent floating-point drift +fn normalize_angle(angle: f32) -> f32 { + let two_pi = std::f32::consts::PI * 2.0; + let mut normalized = angle % two_pi; + + // Ensure result is in [-PI, PI] + if normalized > std::f32::consts::PI { + normalized -= two_pi; + } else if normalized < -std::f32::consts::PI { + normalized += two_pi; + } + + normalized +}