diff --git a/turtle-lib-macroquad/src/execution.rs b/turtle-lib-macroquad/src/execution.rs index a494c7c..492445b 100644 --- a/turtle-lib-macroquad/src/execution.rs +++ b/turtle-lib-macroquad/src/execution.rs @@ -9,8 +9,135 @@ 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) +pub fn execute_command_side_effects( + command: &TurtleCommand, + state: &mut TurtleState, + commands: &mut Vec, +) -> bool { + match command { + TurtleCommand::BeginFill => { + if state.filling.is_some() { + tracing::warn!("begin_fill() called while already filling"); + } + let fill_color = state.fill_color.unwrap_or_else(|| { + tracing::warn!("No fill_color set, using black"); + BLACK + }); + state.begin_fill(fill_color); + true + } + + TurtleCommand::EndFill => { + if let Some(mut fill_state) = 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", contours = fill_state.contours.len()); + let _enter = span.enter(); + + for (i, contour) in fill_state.contours.iter().enumerate() { + tracing::debug!(contour_idx = i, vertices = contour.len(), "Contour info"); + } + + if !fill_state.contours.is_empty() { + if let Ok(mesh_data) = tessellation::tessellate_multi_contour( + &fill_state.contours, + fill_state.fill_color, + ) { + tracing::debug!( + contours = fill_state.contours.len(), + "Successfully tessellated contours" + ); + commands.push(DrawCommand::Mesh(mesh_data)); + } else { + tracing::error!("Failed to tessellate contours"); + } + } + } else { + tracing::warn!("end_fill() called without begin_fill()"); + } + true + } + + TurtleCommand::PenUp => { + state.pen_down = false; + if state.filling.is_some() { + tracing::debug!("PenUp: Closing current contour"); + } + state.close_fill_contour(); + true + } + + TurtleCommand::PenDown => { + state.pen_down = true; + if state.filling.is_some() { + tracing::debug!( + x = state.position.x, + y = state.position.y, + "PenDown: Starting new contour" + ); + } + state.start_fill_contour(); + true + } + + _ => false, // Not a side-effect-only command + } +} + +/// Record fill vertices after movement commands have updated state +pub fn record_fill_vertices_after_movement( + command: &TurtleCommand, + start_state: &TurtleState, + state: &mut TurtleState, +) { + if state.filling.is_none() { + return; + } + + match command { + TurtleCommand::Circle { + radius, + angle, + steps, + direction, + } => { + let geom = CircleGeometry::new( + start_state.position, + start_state.heading, + *radius, + *direction, + ); + state.record_fill_vertices_for_arc( + geom.center, + *radius, + geom.start_angle_from_center, + angle.to_radians(), + *direction, + *steps as u32, + ); + } + TurtleCommand::Move(_) | TurtleCommand::Goto(_) => { + state.record_fill_vertex(); + } + _ => {} + } +} + /// Execute a single turtle command, updating state and adding draw commands pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world: &mut TurtleWorld) { + // Try to execute as side-effect-only command first + if execute_command_side_effects(command, state, &mut world.commands) { + return; // Command fully handled + } + + // Store start state for fill vertex recording + let start_state = state.clone(); + + // Execute movement and appearance commands match command { TurtleCommand::Move(distance) => { let start = state.position; @@ -18,9 +145,6 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world: let dy = distance * state.heading.sin(); state.position = vec2(state.position.x + dx, state.position.y + dy); - // Record vertex for fill if filling - state.record_fill_vertex(); - if state.pen_down { // Draw line segment with round caps (caps handled by tessellate_stroke) if let Ok(mesh_data) = tessellation::tessellate_stroke( @@ -70,67 +194,12 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world: CircleDirection::Left => start_heading - angle.to_radians(), CircleDirection::Right => start_heading + angle.to_radians(), }; - - // Record vertices along arc for fill if filling - state.record_fill_vertices_for_arc( - geom.center, - *radius, - geom.start_angle_from_center, - angle.to_radians(), - *direction, - *steps as u32, - ); - } - - TurtleCommand::PenUp => { - state.pen_down = false; - // Close current contour if filling - if state.filling.is_some() { - tracing::debug!("PenUp: Closing current contour"); - } - state.close_fill_contour(); - } - - TurtleCommand::PenDown => { - state.pen_down = true; - // Start new contour if filling - if state.filling.is_some() { - tracing::debug!( - x = state.position.x, - y = state.position.y, - "PenDown: Starting new contour" - ); - } - state.start_fill_contour(); - } - - TurtleCommand::SetColor(color) => { - state.color = *color; - } - - TurtleCommand::SetFillColor(color) => { - state.fill_color = *color; - } - - TurtleCommand::SetPenWidth(width) => { - state.pen_width = *width; - } - - TurtleCommand::SetSpeed(speed) => { - state.set_speed(*speed); - } - - TurtleCommand::SetShape(shape) => { - state.shape = shape.clone(); } TurtleCommand::Goto(coord) => { let start = state.position; state.position = *coord; - // Record vertex for fill if filling - state.record_fill_vertex(); - if state.pen_down { // Draw line segment with round caps if let Ok(mesh_data) = tessellation::tessellate_stroke( @@ -144,66 +213,21 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world: } } - TurtleCommand::SetHeading(heading) => { - state.heading = *heading; - } + // Appearance commands + TurtleCommand::SetColor(color) => state.color = *color, + TurtleCommand::SetFillColor(color) => state.fill_color = *color, + TurtleCommand::SetPenWidth(width) => state.pen_width = *width, + TurtleCommand::SetSpeed(speed) => state.set_speed(*speed), + TurtleCommand::SetShape(shape) => state.shape = shape.clone(), + TurtleCommand::SetHeading(heading) => state.heading = *heading, + TurtleCommand::ShowTurtle => state.visible = true, + TurtleCommand::HideTurtle => state.visible = false, - TurtleCommand::ShowTurtle => { - state.visible = true; - } - - TurtleCommand::HideTurtle => { - state.visible = false; - } - - TurtleCommand::BeginFill => { - if state.filling.is_some() { - tracing::warn!("begin_fill() called while already filling"); - } - - let fill_color = state.fill_color.unwrap_or_else(|| { - tracing::warn!("No fill_color set, using black"); - BLACK - }); - - state.begin_fill(fill_color); - } - - TurtleCommand::EndFill => { - if let Some(mut fill_state) = state.filling.take() { - // Close final contour if it has vertices - if !fill_state.current_contour.is_empty() { - fill_state.contours.push(fill_state.current_contour); - } - - // Debug output - let span = tracing::debug_span!("end_fill", contours = fill_state.contours.len()); - let _enter = span.enter(); - - for (i, contour) in fill_state.contours.iter().enumerate() { - tracing::debug!(contour_idx = i, vertices = contour.len(), "Contour info"); - } - - // Create fill command - Lyon will handle EvenOdd automatically with multiple contours - if !fill_state.contours.is_empty() { - if let Ok(mesh_data) = tessellation::tessellate_multi_contour( - &fill_state.contours, - fill_state.fill_color, - ) { - tracing::debug!( - contours = fill_state.contours.len(), - "Successfully tessellated contours" - ); - world.add_command(DrawCommand::Mesh(mesh_data)); - } else { - tracing::error!("Failed to tessellate contours"); - } - } - } else { - tracing::warn!("end_fill() called without begin_fill()"); - } - } + _ => {} // Already handled by execute_command_side_effects } + + // Record fill vertices AFTER movement + record_fill_vertices_after_movement(command, &start_state, state); } /// Add drawing command for a completed tween (state transition already occurred) diff --git a/turtle-lib-macroquad/src/tweening.rs b/turtle-lib-macroquad/src/tweening.rs index 5bf100c..a0b1800 100644 --- a/turtle-lib-macroquad/src/tweening.rs +++ b/turtle-lib-macroquad/src/tweening.rs @@ -3,8 +3,7 @@ use crate::circle_geometry::{CircleDirection, CircleGeometry}; use crate::commands::{CommandQueue, TurtleCommand}; use crate::general::AnimationSpeed; -use crate::state::{DrawCommand, TurtleState}; -use crate::tessellation; +use crate::state::TurtleState; use macroquad::prelude::*; use tween::{CubicInOut, TweenValue, Tweener}; @@ -94,107 +93,41 @@ impl TweenController { 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 switched to animated mode, exit instant mode processing if matches!(self.speed, AnimationSpeed::Animated(_)) { break; } continue; } - // For commands with side effects (fill operations), handle specially - match &command { - TurtleCommand::BeginFill => { - let fill_color = state.fill_color.unwrap_or(macroquad::prelude::BLACK); - state.begin_fill(fill_color); - continue; - } - TurtleCommand::PenUp => { - state.pen_down = false; - // Close current contour if filling - state.close_fill_contour(); - continue; - } - TurtleCommand::PenDown => { - state.pen_down = true; - // Start new contour if filling - state.start_fill_contour(); - continue; - } - TurtleCommand::EndFill => { - if let Some(mut fill_state) = state.filling.take() { - // Close final contour if it has vertices - if !fill_state.current_contour.is_empty() { - fill_state.contours.push(fill_state.current_contour); - } - // Create fill command - Lyon will handle EvenOdd automatically - if !fill_state.contours.is_empty() { - if let Ok(mesh_data) = tessellation::tessellate_multi_contour( - &fill_state.contours, - fill_state.fill_color, - ) { - commands.push(DrawCommand::Mesh(mesh_data)); - } - } - } - continue; - } - _ => {} + // Execute side-effect-only commands using centralized helper + if crate::execution::execute_command_side_effects(&command, state, commands) { + continue; // Command fully handled } - // Execute command immediately + // Execute movement commands let target_state = self.calculate_target_state(state, &command); *state = target_state.clone(); - // Record vertices after position update if filling - match &command { - TurtleCommand::Circle { - radius, - angle, - steps, - direction, - } => { - // For circles, record multiple vertices along the arc - if state.filling.is_some() { - use crate::circle_geometry::CircleGeometry; - let geom = CircleGeometry::new( - start_state.position, - start_state.heading, - *radius, - *direction, - ); - state.record_fill_vertices_for_arc( - geom.center, - *radius, - geom.start_angle_from_center, - angle.to_radians(), - *direction, - *steps as u32, - ); - } - } - TurtleCommand::Move(_) | TurtleCommand::Goto(_) => { - state.record_fill_vertex(); - } - _ => {} - } + // Record fill vertices AFTER movement using centralized helper + crate::execution::record_fill_vertices_after_movement( + &command, + &start_state, + state, + ); - // Capture end state AFTER executing this command let end_state = state.clone(); - // Collect drawable commands with their individual start and end states - // Only create line drawing if pen is down + // Collect drawable commands if Self::command_creates_drawing(&command) && start_state.pen_down { 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; } @@ -269,97 +202,34 @@ impl TweenController { // Check if tween is finished (use heading_tweener as it's used by all commands) if tween.heading_tweener.is_finished() { - // 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/end states let completed_command = tween.command.clone(); self.current_tween = None; - // Handle fill commands that have side effects - match &completed_command { - TurtleCommand::BeginFill => { - let fill_color = state.fill_color.unwrap_or(macroquad::prelude::BLACK); - state.begin_fill(fill_color); - // Don't return, continue to next command - return self.update(state, commands); - } - TurtleCommand::EndFill => { - if let Some(mut fill_state) = state.filling.take() { - // Close final contour if it has vertices - if !fill_state.current_contour.is_empty() { - fill_state.contours.push(fill_state.current_contour); - } - // Create fill command - Lyon will handle EvenOdd automatically - if !fill_state.contours.is_empty() { - if let Ok(mesh_data) = tessellation::tessellate_multi_contour( - &fill_state.contours, - fill_state.fill_color, - ) { - commands.push(DrawCommand::Mesh(mesh_data)); - } - } - } - // Don't return, continue to next command - return self.update(state, commands); - } - TurtleCommand::Circle { - radius, - angle, - steps, - direction, - } => { - // For circles, record multiple vertices along the arc - if state.filling.is_some() { - use crate::circle_geometry::CircleGeometry; - let geom = CircleGeometry::new( - start_state.position, - start_state.heading, - *radius, - *direction, - ); - state.record_fill_vertices_for_arc( - geom.center, - *radius, - geom.start_angle_from_center, - angle.to_radians(), - *direction, - *steps as u32, - ); - } + // Execute side-effect-only commands using centralized helper + if crate::execution::execute_command_side_effects( + &completed_command, + state, + commands, + ) { + return self.update(state, commands); // Continue to next command + } - if Self::command_creates_drawing(&completed_command) && start_state.pen_down - { - return vec![(completed_command, start_state, end_state)]; - } else { - // Movement but no drawing (pen up) - continue - return self.update(state, commands); - } - } - TurtleCommand::Move(_) | TurtleCommand::Goto(_) => { - // Movement commands: record vertex if filling - state.record_fill_vertex(); + // Record fill vertices for movement commands using centralized helper + crate::execution::record_fill_vertices_after_movement( + &completed_command, + &start_state, + state, + ); - if Self::command_creates_drawing(&completed_command) && start_state.pen_down - { - return vec![(completed_command, start_state, end_state)]; - } else { - // Movement but no drawing (pen up) - continue - return self.update(state, commands); - } - } - _ if Self::command_creates_drawing(&completed_command) - && start_state.pen_down => - { - // Return drawable commands - return vec![(completed_command, start_state, end_state)]; - } - _ => { - // Non-drawable, non-fill commands - continue to next - return self.update(state, commands); - } + // Return drawable commands + if Self::command_creates_drawing(&completed_command) && start_state.pen_down { + return vec![(completed_command, start_state, end_state)]; + } else { + return self.update(state, commands); // Continue to next command } } @@ -375,47 +245,21 @@ impl TweenController { TurtleCommand::SetSpeed(new_speed) => { state.set_speed(*new_speed); self.speed = *new_speed; - // If switched to instant mode, process commands immediately if matches!(self.speed, AnimationSpeed::Instant(_)) { - return self.update(state, commands); // Recursively process in instant mode - } - // For animated mode speed changes, continue to next command - return self.update(state, commands); - } - TurtleCommand::PenUp => { - state.pen_down = false; - state.close_fill_contour(); - return self.update(state, commands); - } - TurtleCommand::PenDown => { - state.pen_down = true; - state.start_fill_contour(); - return self.update(state, commands); - } - TurtleCommand::BeginFill => { - let fill_color = state.fill_color.unwrap_or(macroquad::prelude::BLACK); - state.begin_fill(fill_color); - return self.update(state, commands); - } - TurtleCommand::EndFill => { - if let Some(mut fill_state) = state.filling.take() { - // Close final contour if it has vertices - if !fill_state.current_contour.is_empty() { - fill_state.contours.push(fill_state.current_contour); - } - // Create fill command - Lyon will handle EvenOdd automatically - if !fill_state.contours.is_empty() { - if let Ok(mesh_data) = tessellation::tessellate_multi_contour( - &fill_state.contours, - fill_state.fill_color, - ) { - commands.push(DrawCommand::Mesh(mesh_data)); - } - } + return self.update(state, commands); } return self.update(state, commands); } - _ => {} + _ => { + // Use centralized helper for side effects + if crate::execution::execute_command_side_effects( + &command_clone, + state, + commands, + ) { + return self.update(state, commands); + } + } } let speed = state.speed; // Extract speed before borrowing self