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)]
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
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)

View File

@ -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