unify draws and state updates from instant and animated drawing

This commit is contained in:
Franz Dietrich 2025-10-12 13:46:11 +02:00
parent c96d66247e
commit 033a1982fc
2 changed files with 182 additions and 314 deletions

View File

@ -9,8 +9,135 @@ use macroquad::prelude::*;
#[cfg(test)] #[cfg(test)]
use crate::general::AnimationSpeed; 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<DrawCommand>,
) -> 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 /// Execute a single turtle command, updating state and adding draw commands
pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world: &mut TurtleWorld) { 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 { match command {
TurtleCommand::Move(distance) => { TurtleCommand::Move(distance) => {
let start = state.position; let start = state.position;
@ -18,9 +145,6 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
let dy = distance * state.heading.sin(); let dy = distance * state.heading.sin();
state.position = vec2(state.position.x + dx, state.position.y + dy); 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 { if state.pen_down {
// Draw line segment with round caps (caps handled by tessellate_stroke) // Draw line segment with round caps (caps handled by tessellate_stroke)
if let Ok(mesh_data) = tessellation::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::Left => start_heading - angle.to_radians(),
CircleDirection::Right => 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) => { TurtleCommand::Goto(coord) => {
let start = state.position; let start = state.position;
state.position = *coord; state.position = *coord;
// Record vertex for fill if filling
state.record_fill_vertex();
if state.pen_down { if state.pen_down {
// Draw line segment with round caps // Draw line segment with round caps
if let Ok(mesh_data) = tessellation::tessellate_stroke( 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) => { // Appearance commands
state.heading = *heading; 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,
_ => {} // Already handled by execute_command_side_effects
} }
TurtleCommand::ShowTurtle => { // Record fill vertices AFTER movement
state.visible = true; record_fill_vertices_after_movement(command, &start_state, state);
}
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()");
}
}
}
} }
/// Add drawing command for a completed tween (state transition already occurred) /// Add drawing command for a completed tween (state transition already occurred)

View File

@ -3,8 +3,7 @@
use crate::circle_geometry::{CircleDirection, CircleGeometry}; use crate::circle_geometry::{CircleDirection, CircleGeometry};
use crate::commands::{CommandQueue, TurtleCommand}; use crate::commands::{CommandQueue, TurtleCommand};
use crate::general::AnimationSpeed; use crate::general::AnimationSpeed;
use crate::state::{DrawCommand, TurtleState}; use crate::state::TurtleState;
use crate::tessellation;
use macroquad::prelude::*; use macroquad::prelude::*;
use tween::{CubicInOut, TweenValue, Tweener}; use tween::{CubicInOut, TweenValue, Tweener};
@ -94,107 +93,41 @@ impl TweenController {
None => break, None => break,
}; };
// Capture start state BEFORE executing this command
let start_state = state.clone(); let start_state = state.clone();
// Handle SetSpeed command to potentially switch modes // Handle SetSpeed command to potentially switch modes
if let TurtleCommand::SetSpeed(new_speed) = &command { if let TurtleCommand::SetSpeed(new_speed) = &command {
state.set_speed(*new_speed); state.set_speed(*new_speed);
self.speed = *new_speed; self.speed = *new_speed;
// If speed switched to animated mode, exit instant mode processing
if matches!(self.speed, AnimationSpeed::Animated(_)) { if matches!(self.speed, AnimationSpeed::Animated(_)) {
break; break;
} }
continue; continue;
} }
// For commands with side effects (fill operations), handle specially // Execute side-effect-only commands using centralized helper
match &command { if crate::execution::execute_command_side_effects(&command, state, commands) {
TurtleCommand::BeginFill => { continue; // Command fully handled
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 command immediately // Execute movement commands
let target_state = self.calculate_target_state(state, &command); let target_state = self.calculate_target_state(state, &command);
*state = target_state.clone(); *state = target_state.clone();
// Record vertices after position update if filling // Record fill vertices AFTER movement using centralized helper
match &command { crate::execution::record_fill_vertices_after_movement(
TurtleCommand::Circle { &command,
radius, &start_state,
angle, state,
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();
}
_ => {}
}
// Capture end state AFTER executing this command
let end_state = state.clone(); let end_state = state.clone();
// Collect drawable commands with their individual start and end states // Collect drawable commands
// Only create line drawing if pen is down
if Self::command_creates_drawing(&command) && start_state.pen_down { if Self::command_creates_drawing(&command) && start_state.pen_down {
completed_commands.push((command, start_state, end_state)); completed_commands.push((command, start_state, end_state));
draw_call_count += 1; draw_call_count += 1;
// Stop if we've reached the draw call limit for this frame
if draw_call_count >= max_draw_calls { if draw_call_count >= max_draw_calls {
break; break;
} }
@ -269,97 +202,34 @@ impl TweenController {
// Check if tween is finished (use heading_tweener as it's used by all commands) // Check if tween is finished (use heading_tweener as it's used by all commands)
if tween.heading_tweener.is_finished() { if tween.heading_tweener.is_finished() {
// Tween complete, finalize state
let start_state = tween.start_state.clone(); let start_state = tween.start_state.clone();
*state = tween.target_state.clone(); *state = tween.target_state.clone();
let end_state = state.clone(); let end_state = state.clone();
// Return the completed command and start/end states
let completed_command = tween.command.clone(); let completed_command = tween.command.clone();
self.current_tween = None; self.current_tween = None;
// Handle fill commands that have side effects // Execute side-effect-only commands using centralized helper
match &completed_command { if crate::execution::execute_command_side_effects(
TurtleCommand::BeginFill => { &completed_command,
let fill_color = state.fill_color.unwrap_or(macroquad::prelude::BLACK); state,
state.begin_fill(fill_color); commands,
// 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)); return self.update(state, commands); // Continue to next command
}
}
}
// 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,
);
} }
if Self::command_creates_drawing(&completed_command) && start_state.pen_down // Record fill vertices for movement commands using centralized helper
{ crate::execution::record_fill_vertices_after_movement(
return vec![(completed_command, start_state, end_state)]; &completed_command,
} else { &start_state,
// Movement but no drawing (pen up) - continue state,
return self.update(state, commands); );
}
}
TurtleCommand::Move(_) | TurtleCommand::Goto(_) => {
// Movement commands: record vertex if filling
state.record_fill_vertex();
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 drawable commands
if Self::command_creates_drawing(&completed_command) && start_state.pen_down {
return vec![(completed_command, start_state, end_state)]; return vec![(completed_command, start_state, end_state)];
} } else {
_ => { return self.update(state, commands); // Continue to next command
// Non-drawable, non-fill commands - continue to next
return self.update(state, commands);
}
} }
} }
@ -375,47 +245,21 @@ impl TweenController {
TurtleCommand::SetSpeed(new_speed) => { TurtleCommand::SetSpeed(new_speed) => {
state.set_speed(*new_speed); state.set_speed(*new_speed);
self.speed = *new_speed; self.speed = *new_speed;
// If switched to instant mode, process commands immediately
if matches!(self.speed, AnimationSpeed::Instant(_)) { 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); return self.update(state, commands);
} }
TurtleCommand::PenUp => {
state.pen_down = false;
state.close_fill_contour();
return self.update(state, commands); return self.update(state, commands);
} }
TurtleCommand::PenDown => { _ => {
state.pen_down = true; // Use centralized helper for side effects
state.start_fill_contour(); if crate::execution::execute_command_side_effects(
return self.update(state, commands); &command_clone,
} state,
TurtleCommand::BeginFill => { commands,
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);
} }
_ => {} }
} }
let speed = state.speed; // Extract speed before borrowing self let speed = state.speed; // Extract speed before borrowing self