unify draws and state updates from instant and animated drawing
This commit is contained in:
parent
c96d66247e
commit
033a1982fc
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user