improve command handling
This commit is contained in:
parent
44046abe12
commit
3c076fdd03
@ -53,11 +53,14 @@ pub enum TurtleCommand {
|
|||||||
Reset,
|
Reset,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Queue of turtle commands with execution state
|
/// A pure-data sequence of turtle commands.
|
||||||
|
///
|
||||||
|
/// `CommandQueue` is intentionally *not* an `Iterator` — it carries no cursor
|
||||||
|
/// state. Execution state ("which command are we on?") belongs to the
|
||||||
|
/// consumer; `TweenController` owns the cursor that walks this queue.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct CommandQueue {
|
pub struct CommandQueue {
|
||||||
commands: Vec<TurtleCommand>,
|
commands: Vec<TurtleCommand>,
|
||||||
current_index: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandQueue {
|
impl CommandQueue {
|
||||||
@ -65,14 +68,12 @@ impl CommandQueue {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
commands: Vec::new(),
|
commands: Vec::new(),
|
||||||
current_index: 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_capacity(capacity: usize) -> Self {
|
pub fn with_capacity(capacity: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
commands: Vec::with_capacity(capacity),
|
commands: Vec::with_capacity(capacity),
|
||||||
current_index: 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,26 +84,22 @@ impl CommandQueue {
|
|||||||
pub fn extend(&mut self, commands: impl IntoIterator<Item = TurtleCommand>) {
|
pub fn extend(&mut self, commands: impl IntoIterator<Item = TurtleCommand>) {
|
||||||
self.commands.extend(commands);
|
self.commands.extend(commands);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return a reference to the command at `index`, or `None` if out of range.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn is_complete(&self) -> bool {
|
pub fn get(&self, index: usize) -> Option<&TurtleCommand> {
|
||||||
self.current_index >= self.commands.len()
|
self.commands.get(index)
|
||||||
}
|
|
||||||
pub fn reset(&mut self) {
|
|
||||||
self.current_index = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.commands.len()
|
self.commands.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.commands.is_empty()
|
self.commands.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn remaining(&self) -> usize {
|
|
||||||
self.commands.len().saturating_sub(self.current_index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CommandQueue {
|
impl Default for CommandQueue {
|
||||||
@ -111,16 +108,16 @@ impl Default for CommandQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Iterator for CommandQueue {
|
/// Consuming iteration — yields every command in order.
|
||||||
|
///
|
||||||
|
/// This is used by `CommandQueue::extend` and `TweenController::append_commands`
|
||||||
|
/// to drain one queue into another. It does *not* imply that `CommandQueue`
|
||||||
|
/// itself is stateful; the cursor always lives in the consumer.
|
||||||
|
impl IntoIterator for CommandQueue {
|
||||||
type Item = TurtleCommand;
|
type Item = TurtleCommand;
|
||||||
|
type IntoIter = std::vec::IntoIter<TurtleCommand>;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
if self.current_index < self.commands.len() {
|
self.commands.into_iter()
|
||||||
let cmd = self.commands[self.current_index].clone();
|
|
||||||
self.current_index += 1;
|
|
||||||
Some(cmd)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,52 +1,120 @@
|
|||||||
//! Command execution logic
|
//! Command execution logic
|
||||||
|
|
||||||
use crate::circle_geometry::CircleGeometry;
|
use crate::circle_geometry::{CircleDirection, CircleGeometry};
|
||||||
use crate::commands::TurtleCommand;
|
use crate::commands::TurtleCommand;
|
||||||
use crate::state::{DrawCommand, Turtle, TurtleParams, TurtleWorld};
|
use crate::general::Coordinate;
|
||||||
|
use crate::state::{DrawCommand, FillState, Turtle, TurtleParams, TurtleWorld};
|
||||||
use crate::tessellation;
|
use crate::tessellation;
|
||||||
use macroquad::prelude::*;
|
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
|
/// Close the current open fill contour (factored out of `Turtle::close_fill_contour`).
|
||||||
/// Returns true if the command was handled (caller should skip movement processing)
|
fn close_fill_contour(turtle_id: usize, filling: &mut Option<FillState>) {
|
||||||
|
if let Some(ref mut fill_state) = filling {
|
||||||
|
tracing::debug!(
|
||||||
|
turtle_id,
|
||||||
|
vertices = fill_state.current_contour.len(),
|
||||||
|
"close_fill_contour called"
|
||||||
|
);
|
||||||
|
if fill_state.current_contour.len() >= 2 {
|
||||||
|
tracing::debug!(
|
||||||
|
turtle_id,
|
||||||
|
vertices = fill_state.current_contour.len(),
|
||||||
|
first_x = fill_state.current_contour[0].x,
|
||||||
|
first_y = fill_state.current_contour[0].y,
|
||||||
|
last_x = fill_state.current_contour[fill_state.current_contour.len() - 1].x,
|
||||||
|
last_y = fill_state.current_contour[fill_state.current_contour.len() - 1].y,
|
||||||
|
"Closing contour"
|
||||||
|
);
|
||||||
|
let contour = std::mem::take(&mut fill_state.current_contour);
|
||||||
|
fill_state.contours.push(contour);
|
||||||
|
tracing::debug!(
|
||||||
|
turtle_id,
|
||||||
|
completed_contours = fill_state.contours.len(),
|
||||||
|
"Contour moved to completed list"
|
||||||
|
);
|
||||||
|
} else if !fill_state.current_contour.is_empty() {
|
||||||
|
tracing::warn!(
|
||||||
|
turtle_id,
|
||||||
|
vertices = fill_state.current_contour.len(),
|
||||||
|
"Current contour has insufficient vertices, not closing"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tracing::warn!(turtle_id, "Current contour is empty, nothing to close");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::warn!(
|
||||||
|
turtle_id,
|
||||||
|
"close_fill_contour called but no active fill state"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Begin a new fill contour at `position` (factored out of `Turtle::start_fill_contour`).
|
||||||
|
fn start_fill_contour(turtle_id: usize, position: Coordinate, filling: &mut Option<FillState>) {
|
||||||
|
if let Some(ref mut fill_state) = filling {
|
||||||
|
tracing::debug!(
|
||||||
|
x = position.x,
|
||||||
|
y = position.y,
|
||||||
|
completed_contours = fill_state.contours.len(),
|
||||||
|
turtle_id,
|
||||||
|
"Starting new contour"
|
||||||
|
);
|
||||||
|
fill_state.current_contour = vec![position];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute side effects for commands that don't involve movement.
|
||||||
|
///
|
||||||
|
/// Returns `true` if the command was fully handled; the caller should skip
|
||||||
|
/// params-update and tessellation when this returns `true`.
|
||||||
|
///
|
||||||
|
/// Accepts the three logically-separate pieces of turtle state as disjoint
|
||||||
|
/// mutable borrows so that this function can be called from
|
||||||
|
/// `TweenController::update(&mut self, …)` without requiring a `&mut Turtle`.
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
pub(crate) fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle) -> bool {
|
pub(crate) fn execute_command_side_effects(
|
||||||
|
command: &TurtleCommand,
|
||||||
|
turtle_id: usize,
|
||||||
|
params: &mut TurtleParams,
|
||||||
|
filling: &mut Option<FillState>,
|
||||||
|
commands: &mut Vec<DrawCommand>,
|
||||||
|
) -> bool {
|
||||||
match command {
|
match command {
|
||||||
TurtleCommand::BeginFill => {
|
TurtleCommand::BeginFill => {
|
||||||
if state.filling.is_some() {
|
if filling.is_some() {
|
||||||
tracing::warn!(
|
tracing::warn!(turtle_id, "begin_fill() called while already filling");
|
||||||
turtle_id = state.turtle_id,
|
|
||||||
"begin_fill() called while already filling"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
let fill_color = state.params.fill_color.unwrap_or_else(|| {
|
let fill_color = params.fill_color.unwrap_or_else(|| {
|
||||||
tracing::warn!(
|
tracing::warn!(turtle_id, "No fill_color set, using black");
|
||||||
turtle_id = state.turtle_id,
|
|
||||||
"No fill_color set, using black"
|
|
||||||
);
|
|
||||||
BLACK
|
BLACK
|
||||||
});
|
});
|
||||||
state.begin_fill(fill_color);
|
*filling = Some(FillState {
|
||||||
|
start_position: params.position,
|
||||||
|
contours: Vec::new(),
|
||||||
|
current_contour: vec![params.position],
|
||||||
|
fill_color,
|
||||||
|
});
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
TurtleCommand::EndFill => {
|
TurtleCommand::EndFill => {
|
||||||
if let Some(mut fill_state) = state.filling.take() {
|
if let Some(mut fill_state) = filling.take() {
|
||||||
if !fill_state.current_contour.is_empty() {
|
if !fill_state.current_contour.is_empty() {
|
||||||
fill_state.contours.push(fill_state.current_contour);
|
fill_state.contours.push(fill_state.current_contour);
|
||||||
}
|
}
|
||||||
|
|
||||||
let span = tracing::debug_span!(
|
let span = tracing::debug_span!(
|
||||||
"end_fill",
|
"end_fill",
|
||||||
turtle_id = state.turtle_id,
|
turtle_id,
|
||||||
contours = fill_state.contours.len()
|
contours = fill_state.contours.len()
|
||||||
);
|
);
|
||||||
let _enter = span.enter();
|
let _enter = span.enter();
|
||||||
|
|
||||||
for (i, contour) in fill_state.contours.iter().enumerate() {
|
for (i, contour) in fill_state.contours.iter().enumerate() {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
turtle_id = state.turtle_id,
|
turtle_id,
|
||||||
contour_idx = i,
|
contour_idx = i,
|
||||||
vertices = contour.len(),
|
vertices = contour.len(),
|
||||||
"Contour info"
|
"Contour info"
|
||||||
@ -59,83 +127,76 @@ pub(crate) fn execute_command_side_effects(command: &TurtleCommand, state: &mut
|
|||||||
fill_state.fill_color,
|
fill_state.fill_color,
|
||||||
) {
|
) {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
turtle_id = state.turtle_id,
|
turtle_id,
|
||||||
contours = fill_state.contours.len(),
|
contours = fill_state.contours.len(),
|
||||||
"Successfully created fill mesh - persisting to commands"
|
"Successfully created fill mesh - persisting to commands"
|
||||||
);
|
);
|
||||||
state.commands.push(DrawCommand::Mesh {
|
commands.push(DrawCommand::Mesh {
|
||||||
data: mesh_data,
|
data: mesh_data,
|
||||||
source: crate::state::TurtleSource {
|
source: crate::state::TurtleSource {
|
||||||
command: crate::commands::TurtleCommand::EndFill,
|
command: crate::commands::TurtleCommand::EndFill,
|
||||||
color: state.params.color,
|
color: params.color,
|
||||||
fill_color: fill_state.fill_color,
|
fill_color: fill_state.fill_color,
|
||||||
pen_width: state.params.pen_width,
|
pen_width: params.pen_width,
|
||||||
start_position: fill_state.start_position,
|
start_position: fill_state.start_position,
|
||||||
end_position: fill_state.start_position,
|
end_position: fill_state.start_position,
|
||||||
start_heading: state.params.heading,
|
start_heading: params.heading,
|
||||||
contours: Some(fill_state.contours.clone()),
|
contours: Some(fill_state.contours.clone()),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
tracing::error!(
|
tracing::error!(turtle_id, "Failed to tessellate contours");
|
||||||
turtle_id = state.turtle_id,
|
|
||||||
"Failed to tessellate contours"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!(
|
tracing::warn!(turtle_id, "end_fill() called without begin_fill()");
|
||||||
turtle_id = state.turtle_id,
|
|
||||||
"end_fill() called without begin_fill()"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
TurtleCommand::PenUp => {
|
TurtleCommand::PenUp => {
|
||||||
state.params.pen_down = false;
|
params.pen_down = false;
|
||||||
if state.filling.is_some() {
|
if filling.is_some() {
|
||||||
tracing::debug!(
|
tracing::debug!(turtle_id, "PenUp: Closing current contour");
|
||||||
turtle_id = state.turtle_id,
|
|
||||||
"PenUp: Closing current contour"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
state.close_fill_contour();
|
close_fill_contour(turtle_id, filling);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
TurtleCommand::PenDown => {
|
TurtleCommand::PenDown => {
|
||||||
state.params.pen_down = true;
|
params.pen_down = true;
|
||||||
if state.filling.is_some() {
|
if filling.is_some() {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
turtle_id = state.turtle_id,
|
turtle_id,
|
||||||
x = state.params.position.x,
|
x = params.position.x,
|
||||||
y = state.params.position.y,
|
y = params.position.y,
|
||||||
"PenDown: Starting new contour"
|
"PenDown: Starting new contour"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
state.start_fill_contour();
|
start_fill_contour(turtle_id, params.position, filling);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
TurtleCommand::Reset => {
|
TurtleCommand::Reset => {
|
||||||
state.reset();
|
commands.clear();
|
||||||
|
*filling = None;
|
||||||
|
*params = TurtleParams::default();
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
TurtleCommand::WriteText { text, font_size } => {
|
TurtleCommand::WriteText { text, font_size } => {
|
||||||
state.commands.push(DrawCommand::Text {
|
commands.push(DrawCommand::Text {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
position: state.params.position,
|
position: params.position,
|
||||||
heading: state.params.heading,
|
heading: params.heading,
|
||||||
font_size: *font_size,
|
font_size: *font_size,
|
||||||
color: state.params.color,
|
color: params.color,
|
||||||
source: crate::state::TurtleSource {
|
source: crate::state::TurtleSource {
|
||||||
command: command.clone(),
|
command: command.clone(),
|
||||||
color: state.params.color,
|
color: params.color,
|
||||||
fill_color: state.params.fill_color.unwrap_or(BLACK),
|
fill_color: params.fill_color.unwrap_or(BLACK),
|
||||||
pen_width: state.params.pen_width,
|
pen_width: params.pen_width,
|
||||||
start_position: state.params.position,
|
start_position: params.position,
|
||||||
end_position: state.params.position,
|
end_position: params.position,
|
||||||
start_heading: state.params.heading,
|
start_heading: params.heading,
|
||||||
contours: None,
|
contours: None,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -157,14 +218,23 @@ pub(crate) fn execute_command_side_effects(command: &TurtleCommand, state: &mut
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Record fill vertices after movement commands have updated state
|
/// Record fill vertices after movement commands have updated state.
|
||||||
#[tracing::instrument]
|
///
|
||||||
|
/// `start_state` is the params snapshot taken **before** the command ran.
|
||||||
|
/// `params` is the current (post-movement) state — `params.position` is the
|
||||||
|
/// endpoint that gets pushed into the active fill contour.
|
||||||
|
///
|
||||||
|
/// Accepts disjoint borrows so it can be called from `TweenController::update`
|
||||||
|
/// without needing a `&mut Turtle`.
|
||||||
|
#[tracing::instrument(skip(params, filling))]
|
||||||
pub(crate) fn record_fill_vertices_after_movement(
|
pub(crate) fn record_fill_vertices_after_movement(
|
||||||
command: &TurtleCommand,
|
command: &TurtleCommand,
|
||||||
start_state: &TurtleParams,
|
start_state: &TurtleParams,
|
||||||
state: &mut Turtle,
|
turtle_id: usize,
|
||||||
|
params: &TurtleParams,
|
||||||
|
filling: &mut Option<FillState>,
|
||||||
) {
|
) {
|
||||||
if state.filling.is_none() {
|
if filling.is_none() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,17 +251,60 @@ pub(crate) fn record_fill_vertices_after_movement(
|
|||||||
*radius,
|
*radius,
|
||||||
*direction,
|
*direction,
|
||||||
);
|
);
|
||||||
state.record_fill_vertices_for_arc(
|
if let Some(ref mut fill_state) = filling {
|
||||||
geom.center,
|
if params.pen_down {
|
||||||
*radius,
|
let num_samples = (*steps as u32).max(1);
|
||||||
geom.start_angle_from_center,
|
tracing::trace!(
|
||||||
angle.to_radians(),
|
turtle_id,
|
||||||
*direction,
|
center_x = geom.center.x,
|
||||||
*steps as u32,
|
center_y = geom.center.y,
|
||||||
);
|
radius,
|
||||||
|
steps,
|
||||||
|
num_samples,
|
||||||
|
"Recording arc vertices"
|
||||||
|
);
|
||||||
|
for i in 1..=num_samples {
|
||||||
|
let progress = i as f32 / num_samples as f32;
|
||||||
|
let current_angle = match direction {
|
||||||
|
CircleDirection::Left => {
|
||||||
|
geom.start_angle_from_center - angle.to_radians() * progress
|
||||||
|
}
|
||||||
|
CircleDirection::Right => {
|
||||||
|
geom.start_angle_from_center + angle.to_radians() * progress
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let vertex = Coordinate::new(
|
||||||
|
geom.center.x + radius * current_angle.cos(),
|
||||||
|
geom.center.y + radius * current_angle.sin(),
|
||||||
|
);
|
||||||
|
tracing::trace!(
|
||||||
|
turtle_id,
|
||||||
|
vertex_idx = i,
|
||||||
|
x = vertex.x,
|
||||||
|
y = vertex.y,
|
||||||
|
angle_degrees = current_angle.to_degrees(),
|
||||||
|
"Arc vertex"
|
||||||
|
);
|
||||||
|
fill_state.current_contour.push(vertex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
TurtleCommand::Move(_) | TurtleCommand::Goto(_) => {
|
TurtleCommand::Move(_) | TurtleCommand::Goto(_) => {
|
||||||
state.record_fill_vertex();
|
if let Some(ref mut fill_state) = filling {
|
||||||
|
if params.pen_down {
|
||||||
|
tracing::trace!(
|
||||||
|
turtle_id,
|
||||||
|
x = params.position.x,
|
||||||
|
y = params.position.y,
|
||||||
|
vertices = fill_state.current_contour.len() + 1,
|
||||||
|
"Adding vertex to current contour"
|
||||||
|
);
|
||||||
|
fill_state.current_contour.push(params.position);
|
||||||
|
} else {
|
||||||
|
tracing::trace!(turtle_id, "Skipping vertex (pen is up)");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@ -283,12 +396,18 @@ pub(crate) fn tessellate_command(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute a single turtle command, updating state and adding draw commands
|
/// Execute a single turtle command, updating state and adding draw commands.
|
||||||
#[tracing::instrument]
|
#[tracing::instrument(skip(state))]
|
||||||
pub(crate) fn execute_command(command: &TurtleCommand, state: &mut Turtle) {
|
pub(crate) fn execute_command(command: &TurtleCommand, state: &mut Turtle) {
|
||||||
// Phase 1: side effects (fills, pen contours, reset, text).
|
// Phase 1: side effects (fills, pen contours, reset, text).
|
||||||
// Returns true if the command is fully handled — no params update or tessellation needed.
|
// Returns true if the command is fully handled — no params update or tessellation needed.
|
||||||
if execute_command_side_effects(command, state) {
|
if execute_command_side_effects(
|
||||||
|
command,
|
||||||
|
state.turtle_id,
|
||||||
|
&mut state.params,
|
||||||
|
&mut state.filling,
|
||||||
|
&mut state.commands,
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,7 +416,13 @@ pub(crate) fn execute_command(command: &TurtleCommand, state: &mut Turtle) {
|
|||||||
command.apply_to_params(&mut state.params);
|
command.apply_to_params(&mut state.params);
|
||||||
|
|
||||||
// Phase 3: record fill vertices after movement (must follow params update)
|
// Phase 3: record fill vertices after movement (must follow params update)
|
||||||
record_fill_vertices_after_movement(command, &start_params, state);
|
record_fill_vertices_after_movement(
|
||||||
|
command,
|
||||||
|
&start_params,
|
||||||
|
state.turtle_id,
|
||||||
|
&state.params,
|
||||||
|
&mut state.filling,
|
||||||
|
);
|
||||||
|
|
||||||
// Phase 4: tessellate and persist the committed drawing
|
// Phase 4: tessellate and persist the committed drawing
|
||||||
if let Some(draw_cmd) = tessellate_command(command, &start_params, state.params.position) {
|
if let Some(draw_cmd) = tessellate_command(command, &start_params, state.params.position) {
|
||||||
@ -305,20 +430,18 @@ pub(crate) fn execute_command(command: &TurtleCommand, state: &mut Turtle) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute command on a specific turtle by ID
|
/// Execute command on a specific turtle by ID.
|
||||||
|
///
|
||||||
|
/// There is no ownership conflict here: `execute_command` only needs `&mut Turtle`
|
||||||
|
/// and never touches `TurtleWorld`, so we can obtain the mutable reference directly
|
||||||
|
/// from `get_turtle_mut` without any intermediate clone.
|
||||||
pub(crate) fn execute_command_with_id(
|
pub(crate) fn execute_command_with_id(
|
||||||
command: &TurtleCommand,
|
command: &TurtleCommand,
|
||||||
turtle_id: usize,
|
turtle_id: usize,
|
||||||
world: &mut TurtleWorld,
|
world: &mut TurtleWorld,
|
||||||
) {
|
) {
|
||||||
// Clone turtle state to avoid borrow checker issues
|
if let Some(turtle) = world.get_turtle_mut(turtle_id) {
|
||||||
if let Some(turtle) = world.get_turtle(turtle_id) {
|
execute_command(command, turtle);
|
||||||
let mut state = turtle.clone();
|
|
||||||
execute_command(command, &mut state);
|
|
||||||
// Update the turtle state back
|
|
||||||
if let Some(turtle_mut) = world.get_turtle_mut(turtle_id) {
|
|
||||||
*turtle_mut = state;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,7 +450,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::commands::TurtleCommand;
|
use crate::commands::TurtleCommand;
|
||||||
use crate::shapes::TurtleShape;
|
use crate::shapes::TurtleShape;
|
||||||
use crate::TweenController;
|
use crate::tweening::TweenController;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_forward_left_forward() {
|
fn test_forward_left_forward() {
|
||||||
|
|||||||
@ -80,7 +80,6 @@ pub use macroquad::prelude::{
|
|||||||
|
|
||||||
use crate::commands_channel::TurtleCommandReceiver;
|
use crate::commands_channel::TurtleCommandReceiver;
|
||||||
use crate::state::TurtleWorld;
|
use crate::state::TurtleWorld;
|
||||||
use crate::tweening::TweenController;
|
|
||||||
use macroquad::prelude::*;
|
use macroquad::prelude::*;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@ -292,10 +291,10 @@ impl TurtleApp {
|
|||||||
|
|
||||||
// Update all turtles' tween controllers
|
// Update all turtles' tween controllers
|
||||||
for turtle in &mut self.world.turtles {
|
for turtle in &mut self.world.turtles {
|
||||||
// Extract draw_commands and controller temporarily to avoid borrow conflicts
|
// Drive this turtle's animation controller for one frame.
|
||||||
|
// `update_tweens` splits &mut Turtle into disjoint field borrows so
|
||||||
// Update the controller
|
// TweenController::update can be a proper &mut self method.
|
||||||
let completed_commands = TweenController::update(turtle);
|
let completed_commands = turtle.update_tweens();
|
||||||
|
|
||||||
// Process all completed commands and add to the turtle's commands
|
// Process all completed commands and add to the turtle's commands
|
||||||
for (completed_cmd, tween_start, end_state) in completed_commands {
|
for (completed_cmd, tween_start, end_state) in completed_commands {
|
||||||
|
|||||||
@ -106,6 +106,26 @@ impl Turtle {
|
|||||||
// Keep turtle_id and tween_controller (preserves queued commands)
|
// Keep turtle_id and tween_controller (preserves queued commands)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drive the animation controller for one frame.
|
||||||
|
///
|
||||||
|
/// Returns `(command, start_params, end_params)` for every command that
|
||||||
|
/// completed this frame and whose stroke needs to be tessellated by the
|
||||||
|
/// caller into a `DrawCommand`.
|
||||||
|
///
|
||||||
|
/// This method performs the correct disjoint field-borrow split so that
|
||||||
|
/// `TweenController::update` can be a proper `&mut self` method instead
|
||||||
|
/// of the old static-method borrow-checker workaround.
|
||||||
|
pub fn update_tweens(
|
||||||
|
&mut self,
|
||||||
|
) -> Vec<(crate::commands::TurtleCommand, TurtleParams, TurtleParams)> {
|
||||||
|
self.tween_controller.update(
|
||||||
|
self.turtle_id,
|
||||||
|
&mut self.params,
|
||||||
|
&mut self.filling,
|
||||||
|
&mut self.commands,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Start recording fill vertices
|
/// Start recording fill vertices
|
||||||
pub fn begin_fill(&mut self, fill_color: Color) {
|
pub fn begin_fill(&mut self, fill_color: Color) {
|
||||||
self.filling = Some(FillState {
|
self.filling = Some(FillState {
|
||||||
|
|||||||
@ -3,7 +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::{Turtle, TurtleParams};
|
use crate::state::{DrawCommand, FillState, TurtleParams};
|
||||||
use macroquad::prelude::*;
|
use macroquad::prelude::*;
|
||||||
use tween::{CubicInOut, TweenValue, Tweener};
|
use tween::{CubicInOut, TweenValue, Tweener};
|
||||||
|
|
||||||
@ -47,6 +47,10 @@ impl From<TweenVec2> for Vec2 {
|
|||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub(crate) struct TweenController {
|
pub(crate) struct TweenController {
|
||||||
queue: CommandQueue,
|
queue: CommandQueue,
|
||||||
|
/// Cursor into `queue` — tracks which command executes next.
|
||||||
|
/// Lives here, not in `CommandQueue`, so that cloning or appending to the
|
||||||
|
/// queue never silently resets or mid-stream-shifts the execution position.
|
||||||
|
cursor: usize,
|
||||||
current_tween: Option<CommandTween>,
|
current_tween: Option<CommandTween>,
|
||||||
speed: AnimationSpeed,
|
speed: AnimationSpeed,
|
||||||
}
|
}
|
||||||
@ -71,6 +75,7 @@ impl TweenController {
|
|||||||
pub fn new(queue: CommandQueue, speed: AnimationSpeed) -> Self {
|
pub fn new(queue: CommandQueue, speed: AnimationSpeed) -> Self {
|
||||||
Self {
|
Self {
|
||||||
queue,
|
queue,
|
||||||
|
cursor: 0,
|
||||||
current_tween: None,
|
current_tween: None,
|
||||||
speed,
|
speed,
|
||||||
}
|
}
|
||||||
@ -80,51 +85,71 @@ impl TweenController {
|
|||||||
self.speed = speed;
|
self.speed = speed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Append commands to the queue
|
/// Append commands to the queue.
|
||||||
|
///
|
||||||
|
/// The cursor is **not** reset — commands already consumed remain consumed,
|
||||||
|
/// and the new commands are picked up naturally as the cursor advances.
|
||||||
pub fn append_commands(&mut self, new_queue: CommandQueue) {
|
pub fn append_commands(&mut self, new_queue: CommandQueue) {
|
||||||
self.queue.extend(new_queue);
|
self.queue.extend(new_queue);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the tween, returns `Vec` of (`command`, `start_state`, `end_state`) for all completed commands this frame
|
/// Drive the animation controller for one frame.
|
||||||
/// Also takes commands vec to handle side effects like fill operations
|
///
|
||||||
/// Each `command` has its own `start_state` and `end_state` pair
|
/// Returns `(command, start_params, end_params)` for every command that
|
||||||
|
/// completed this frame and whose stroke needs to be tessellated by the
|
||||||
|
/// caller.
|
||||||
|
///
|
||||||
|
/// By accepting `params`, `filling`, and `commands` as separate mutable
|
||||||
|
/// borrows the caller can split `&mut Turtle` into disjoint field borrows,
|
||||||
|
/// eliminating the old static-method borrow-checker workaround.
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
pub fn update(state: &mut Turtle) -> Vec<(TurtleCommand, TurtleParams, TurtleParams)> {
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
turtle_id: usize,
|
||||||
|
params: &mut TurtleParams,
|
||||||
|
filling: &mut Option<FillState>,
|
||||||
|
commands: &mut Vec<DrawCommand>,
|
||||||
|
) -> Vec<(TurtleCommand, TurtleParams, TurtleParams)> {
|
||||||
// In instant mode, execute commands up to the draw calls per frame limit
|
// In instant mode, execute commands up to the draw calls per frame limit
|
||||||
if let AnimationSpeed::Instant(max_draw_calls) = state.tween_controller.speed {
|
if let AnimationSpeed::Instant(max_draw_calls) = self.speed {
|
||||||
let mut completed_commands: Vec<(TurtleCommand, TurtleParams, TurtleParams)> =
|
let mut completed_commands: Vec<(TurtleCommand, TurtleParams, TurtleParams)> =
|
||||||
Vec::new();
|
Vec::new();
|
||||||
let mut draw_call_count = 0;
|
let mut draw_call_count = 0;
|
||||||
|
|
||||||
// Consume commands from the real queue so the current_index advances
|
// Advance cursor through the queue for each command consumed
|
||||||
while let Some(command) = state.tween_controller.queue.next() {
|
while let Some(command) = self.queue.get(self.cursor).cloned() {
|
||||||
|
self.cursor += 1;
|
||||||
// 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.params.speed = *new_speed;
|
params.speed = *new_speed;
|
||||||
state.tween_controller.speed = *new_speed;
|
self.speed = *new_speed;
|
||||||
if matches!(state.tween_controller.speed, AnimationSpeed::Animated(_)) {
|
if matches!(self.speed, AnimationSpeed::Animated(_)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute side-effect-only commands using centralized helper
|
// Execute side-effect-only commands using centralized helper
|
||||||
if crate::execution::execute_command_side_effects(&command, state) {
|
if crate::execution::execute_command_side_effects(
|
||||||
|
&command, turtle_id, params, filling, commands,
|
||||||
|
) {
|
||||||
continue; // Command fully handled
|
continue; // Command fully handled
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save start state and compute target state
|
// Save start state and compute target state
|
||||||
let start_params = state.params.clone();
|
let start_params = params.clone();
|
||||||
let target_params = Self::calculate_target_state(&start_params, &command);
|
let target_params = Self::calculate_target_state(&start_params, &command);
|
||||||
|
|
||||||
// Update state to the target (instant execution)
|
// Update state to the target (instant execution)
|
||||||
state.params = target_params.clone();
|
*params = target_params.clone();
|
||||||
|
|
||||||
// Record fill vertices AFTER movement
|
// Record fill vertices AFTER movement
|
||||||
crate::execution::record_fill_vertices_after_movement(
|
crate::execution::record_fill_vertices_after_movement(
|
||||||
&command,
|
&command,
|
||||||
&start_params,
|
&start_params,
|
||||||
state,
|
turtle_id,
|
||||||
|
params,
|
||||||
|
filling,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Collect drawable commands (return start and target so caller can create draw meshes)
|
// Collect drawable commands (return start and target so caller can create draw meshes)
|
||||||
@ -141,7 +166,7 @@ impl TweenController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process current tween
|
// Process current tween
|
||||||
if let Some(ref mut tween) = state.tween_controller.current_tween {
|
if let Some(ref mut tween) = self.current_tween {
|
||||||
let elapsed = get_time() - tween.start_time;
|
let elapsed = get_time() - tween.start_time;
|
||||||
|
|
||||||
// Use tweeners to calculate current values
|
// Use tweeners to calculate current values
|
||||||
@ -170,7 +195,7 @@ impl TweenController {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
state.params.position = current_position;
|
params.position = current_position;
|
||||||
tween.current_position = current_position;
|
tween.current_position = current_position;
|
||||||
|
|
||||||
// Heading changes proportionally with progress for all commands
|
// Heading changes proportionally with progress for all commands
|
||||||
@ -195,18 +220,18 @@ impl TweenController {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
state.params.heading = current_heading;
|
params.heading = current_heading;
|
||||||
tween.current_heading = current_heading;
|
tween.current_heading = current_heading;
|
||||||
state.params.pen_width = tween.pen_width_tweener.move_to(elapsed);
|
params.pen_width = tween.pen_width_tweener.move_to(elapsed);
|
||||||
|
|
||||||
// Discrete properties (switch at 50% progress)
|
// Discrete properties (switch at 50% progress)
|
||||||
let progress = (elapsed / tween.duration).min(1.0);
|
let progress = (elapsed / tween.duration).min(1.0);
|
||||||
if progress >= 0.5 {
|
if progress >= 0.5 {
|
||||||
state.params.pen_down = tween.target_params.pen_down;
|
params.pen_down = tween.target_params.pen_down;
|
||||||
state.params.color = tween.target_params.color;
|
params.color = tween.target_params.color;
|
||||||
state.params.fill_color = tween.target_params.fill_color;
|
params.fill_color = tween.target_params.fill_color;
|
||||||
state.params.visible = tween.target_params.visible;
|
params.visible = tween.target_params.visible;
|
||||||
state.params.shape = tween.target_params.shape.clone();
|
params.shape = tween.target_params.shape.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
@ -215,20 +240,24 @@ impl TweenController {
|
|||||||
let target_params = tween.target_params.clone();
|
let target_params = tween.target_params.clone();
|
||||||
let command = tween.command.clone();
|
let command = tween.command.clone();
|
||||||
|
|
||||||
// Drop the mutable borrow of tween before mutably borrowing state
|
// tween borrow ends here (NLL) — safe to reassign self.current_tween below
|
||||||
state.params = target_params.clone();
|
*params = target_params.clone();
|
||||||
|
|
||||||
crate::execution::record_fill_vertices_after_movement(
|
crate::execution::record_fill_vertices_after_movement(
|
||||||
&command,
|
&command,
|
||||||
&start_params,
|
&start_params,
|
||||||
state,
|
turtle_id,
|
||||||
|
params,
|
||||||
|
filling,
|
||||||
);
|
);
|
||||||
|
|
||||||
state.tween_controller.current_tween = None;
|
self.current_tween = None;
|
||||||
|
|
||||||
// Execute side-effect-only commands using centralized helper
|
// Execute side-effect-only commands using centralized helper
|
||||||
if crate::execution::execute_command_side_effects(&command, state) {
|
if crate::execution::execute_command_side_effects(
|
||||||
return Self::update(state); // Continue to next command
|
&command, turtle_id, params, filling, commands,
|
||||||
|
) {
|
||||||
|
return self.update(turtle_id, params, filling, commands);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return drawable commands using the original start and target params
|
// Return drawable commands using the original start and target params
|
||||||
@ -236,43 +265,45 @@ impl TweenController {
|
|||||||
return vec![(command, start_params.clone(), target_params.clone())];
|
return vec![(command, start_params.clone(), target_params.clone())];
|
||||||
}
|
}
|
||||||
|
|
||||||
return Self::update(state); // Continue to next command
|
return self.update(turtle_id, params, filling, commands);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start next tween
|
// Start next tween
|
||||||
if let Some(command) = state.tween_controller.queue.next() {
|
if let Some(command) = self.queue.get(self.cursor).cloned() {
|
||||||
let command_clone = command.clone();
|
self.cursor += 1;
|
||||||
|
|
||||||
// Handle commands that should execute immediately (no animation)
|
// Handle commands that should execute immediately (no animation)
|
||||||
match &command_clone {
|
match &command {
|
||||||
TurtleCommand::SetSpeed(new_speed) => {
|
TurtleCommand::SetSpeed(new_speed) => {
|
||||||
state.set_speed(*new_speed);
|
params.speed = *new_speed;
|
||||||
state.tween_controller.speed = *new_speed;
|
self.speed = *new_speed;
|
||||||
if matches!(state.tween_controller.speed, AnimationSpeed::Instant(_)) {
|
if matches!(self.speed, AnimationSpeed::Instant(_)) {
|
||||||
return Self::update(state);
|
return self.update(turtle_id, params, filling, commands);
|
||||||
}
|
}
|
||||||
return Self::update(state);
|
return self.update(turtle_id, params, filling, commands);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Use centralized helper for side effects
|
// Use centralized helper for side effects
|
||||||
if crate::execution::execute_command_side_effects(&command_clone, state) {
|
if crate::execution::execute_command_side_effects(
|
||||||
return Self::update(state);
|
&command, turtle_id, params, filling, commands,
|
||||||
|
) {
|
||||||
|
return self.update(turtle_id, params, filling, commands);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let speed = state.tween_controller.speed; // Extract speed before borrowing self
|
let speed = self.speed;
|
||||||
let duration = Self::calculate_duration_with_state(&command_clone, state, speed);
|
let duration = Self::calculate_duration_with_state(&command, params, speed);
|
||||||
|
|
||||||
// Calculate target state
|
// Calculate target state
|
||||||
let target_state = Self::calculate_target_state(&state.params, &command_clone);
|
let target_state = Self::calculate_target_state(params, &command);
|
||||||
|
|
||||||
// Create tweeners for smooth animation
|
// Create tweeners for smooth animation
|
||||||
let position_tweener = Tweener::new(
|
let position_tweener = Tweener::new(
|
||||||
TweenVec2::from(state.params.position),
|
TweenVec2::from(params.position),
|
||||||
TweenVec2::from(target_state.position),
|
TweenVec2::from(target_state.position),
|
||||||
duration,
|
duration,
|
||||||
CubicInOut,
|
CubicInOut,
|
||||||
@ -284,21 +315,21 @@ impl TweenController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let pen_width_tweener = Tweener::new(
|
let pen_width_tweener = Tweener::new(
|
||||||
state.params.pen_width,
|
params.pen_width,
|
||||||
target_state.pen_width,
|
target_state.pen_width,
|
||||||
duration,
|
duration,
|
||||||
CubicInOut,
|
CubicInOut,
|
||||||
);
|
);
|
||||||
|
|
||||||
state.tween_controller.current_tween = Some(CommandTween {
|
self.current_tween = Some(CommandTween {
|
||||||
turtle_id: state.turtle_id,
|
turtle_id,
|
||||||
command: command_clone,
|
command,
|
||||||
start_time: get_time(),
|
start_time: get_time(),
|
||||||
duration,
|
duration,
|
||||||
start_params: state.params.clone(),
|
start_params: params.clone(),
|
||||||
target_params: target_state.clone(),
|
target_params: target_state.clone(),
|
||||||
current_position: state.params.position,
|
current_position: params.position,
|
||||||
current_heading: state.params.heading,
|
current_heading: params.heading,
|
||||||
position_tweener,
|
position_tweener,
|
||||||
heading_tweener,
|
heading_tweener,
|
||||||
pen_width_tweener,
|
pen_width_tweener,
|
||||||
@ -310,7 +341,7 @@ impl TweenController {
|
|||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn is_complete(&self) -> bool {
|
pub fn is_complete(&self) -> bool {
|
||||||
self.current_tween.is_none() && self.queue.is_complete()
|
self.current_tween.is_none() && self.cursor >= self.queue.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current active tween if one is in progress
|
/// Get the current active tween if one is in progress
|
||||||
@ -324,10 +355,10 @@ impl TweenController {
|
|||||||
|
|
||||||
fn calculate_duration_with_state(
|
fn calculate_duration_with_state(
|
||||||
command: &TurtleCommand,
|
command: &TurtleCommand,
|
||||||
current: &Turtle,
|
params: &TurtleParams,
|
||||||
speed: AnimationSpeed,
|
speed: AnimationSpeed,
|
||||||
) -> f64 {
|
) -> f64 {
|
||||||
command.animation_duration(¤t.params, speed)
|
command.animation_duration(params, speed)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_target_state(current: &TurtleParams, command: &TurtleCommand) -> TurtleParams {
|
fn calculate_target_state(current: &TurtleParams, command: &TurtleCommand) -> TurtleParams {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user