initial multi-turtle support

This commit is contained in:
Franz Dietrich 2025-10-13 09:42:34 +02:00
parent 1366f5e77f
commit bbb9348497
11 changed files with 408 additions and 198 deletions

View File

@ -61,7 +61,7 @@ use syn::{parse_macro_input, ItemFn};
/// turtle.right(90.0);
/// turtle.forward(100.0);
///
/// let mut app = TurtleApp::new().with_commands(turtle.build());
/// let mut app = TurtleApp::new().with_commands(0, turtle.build());
///
/// loop {
/// clear_background(WHITE);
@ -108,7 +108,7 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream {
#fn_name(&mut turtle);
let mut app = turtle_lib::TurtleApp::new()
.with_commands(turtle.build());
.with_commands(0, turtle.build());
loop {
macroquad::prelude::clear_background(macroquad::prelude::WHITE);
@ -145,7 +145,7 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream {
#fn_block
let mut app = turtle_lib::TurtleApp::new()
.with_commands(turtle.build());
.with_commands(0, turtle.build());
loop {
macroquad::prelude::clear_background(macroquad::prelude::WHITE);

View File

@ -71,7 +71,7 @@ async fn main() {
println!("Building and executing turtle plan...");
// Execute the plan
let mut app = TurtleApp::new().with_commands(turtle.build());
let mut app = TurtleApp::new().with_commands(0, turtle.build());
loop {
clear_background(Color::new(0.95, 0.95, 0.98, 1.0));

View File

@ -111,7 +111,7 @@ async fn main() {
// Set animation speed
t.set_speed(500);
let mut app = TurtleApp::new().with_commands(t.build());
let mut app = TurtleApp::new().with_commands(0, t.build());
let target_fps = 1.0; // 1 frame per second for debugging
let frame_time = 1.0 / target_fps;

View File

@ -75,7 +75,7 @@ async fn main() {
t.set_speed(100); // Slow animation to see the logs in real-time
// Create turtle app
let mut app = TurtleApp::new().with_commands(t.build());
let mut app = TurtleApp::new().with_commands(0, t.build());
// Main loop
loop {

View File

@ -4,7 +4,7 @@ use turtle_lib::*;
#[turtle_main("Yin-Yang")]
fn draw(turtle: &mut TurtlePlan) {
turtle.set_speed(200);
turtle.set_speed(100);
turtle.circle_left(90.0, 180.0, 36);
turtle.begin_fill();

View File

@ -3,6 +3,7 @@
use crate::circle_geometry::{CircleDirection, CircleGeometry};
use crate::state::{DrawCommand, TurtleState, TurtleWorld};
use crate::tessellation;
use crate::tweening::CommandTween;
use macroquad::prelude::*;
// Import the easing function from the tween crate
@ -27,15 +28,17 @@ pub fn render_world(world: &TurtleWorld) {
// Draw all accumulated commands
for cmd in &world.commands {
match cmd {
DrawCommand::Mesh(mesh_data) => {
draw_mesh(&mesh_data.to_mesh());
DrawCommand::Mesh { data, .. } => {
draw_mesh(&data.to_mesh());
}
}
}
// Draw turtle if visible
if world.turtle.visible {
draw_turtle(&world.turtle);
// Draw all visible turtles
for turtle in &world.turtles {
if turtle.visible {
draw_turtle(turtle);
}
}
// Reset to default camera
@ -44,9 +47,9 @@ pub fn render_world(world: &TurtleWorld) {
/// Render the turtle world with active tween visualization
#[allow(clippy::too_many_lines)]
pub(crate) fn render_world_with_tween(
pub fn render_world_with_tween(
world: &TurtleWorld,
active_tween: Option<&crate::tweening::CommandTween>,
active_tween: Option<&CommandTween>,
zoom_level: f32,
) {
// Update camera zoom based on current screen size to prevent stretching
@ -66,13 +69,16 @@ pub(crate) fn render_world_with_tween(
// Draw all accumulated commands
for cmd in &world.commands {
match cmd {
DrawCommand::Mesh(mesh_data) => {
draw_mesh(&mesh_data.to_mesh());
DrawCommand::Mesh { data, .. } => {
draw_mesh(&data.to_mesh());
}
}
}
// Draw in-progress tween line if pen is down
// Extract turtle_id from active tween (default to 0 if no active tween)
let active_turtle_id = active_tween.map_or(0, |tween| tween.turtle_id);
if let Some(tween) = active_tween {
if tween.start_state.pen_down {
match &tween.command {
@ -86,22 +92,24 @@ pub(crate) fn render_world_with_tween(
draw_tween_arc(tween, *radius, *angle, *steps, *direction);
}
_ if should_draw_tween_line(&tween.command) => {
// Draw straight line for other movement commands
draw_line(
tween.start_state.position.x,
tween.start_state.position.y,
world.turtle.position.x,
world.turtle.position.y,
tween.start_state.pen_width,
tween.start_state.color,
);
// Add circle at current position for smooth line joins
draw_circle(
world.turtle.position.x,
world.turtle.position.y,
tween.start_state.pen_width / 2.0,
tween.start_state.color,
);
// Draw straight line for other movement commands (use active turtle)
if let Some(turtle) = world.turtles.get(active_turtle_id) {
draw_line(
tween.start_state.position.x,
tween.start_state.position.y,
turtle.position.x,
turtle.position.y,
tween.start_state.pen_width,
tween.start_state.color,
);
// Add circle at current position for smooth line joins
draw_circle(
turtle.position.x,
turtle.position.y,
tween.start_state.pen_width / 2.0,
tween.start_state.color,
);
}
}
_ => {}
}
@ -109,139 +117,146 @@ pub(crate) fn render_world_with_tween(
}
// Draw live fill preview if currently filling (always show, not just during tweens)
if let Some(ref fill_state) = world.turtle.filling {
// Build all contours: completed contours + current contour with animation
let mut all_contours: Vec<Vec<Vec2>> = Vec::new();
// Use the active turtle if available, otherwise default to turtle 0
if let Some(turtle) = world.turtles.get(active_turtle_id) {
if let Some(ref fill_state) = turtle.filling {
// Build all contours: completed contours + current contour with animation
let mut all_contours: Vec<Vec<Vec2>> = Vec::new();
// Add all completed contours
for completed_contour in &fill_state.contours {
let contour_vec2: Vec<Vec2> = completed_contour
// Add all completed contours
for completed_contour in &fill_state.contours {
let contour_vec2: Vec<Vec2> = completed_contour
.iter()
.map(|c| Vec2::new(c.x, c.y))
.collect();
all_contours.push(contour_vec2);
}
// Build current contour with animation
let mut current_preview: Vec<Vec2> = fill_state
.current_contour
.iter()
.map(|c| Vec2::new(c.x, c.y))
.collect();
all_contours.push(contour_vec2);
}
// Build current contour with animation
let mut current_preview: Vec<Vec2> = fill_state
.current_contour
.iter()
.map(|c| Vec2::new(c.x, c.y))
.collect();
// If we have an active tween, add progressive vertices
if let Some(tween) = active_tween {
// If we're animating a circle command with pen down, add arc vertices
if tween.start_state.pen_down {
if let crate::commands::TurtleCommand::Circle {
radius,
angle,
steps,
direction,
} = &tween.command
{
// Calculate partial arc vertices based on current progress
use crate::circle_geometry::CircleGeometry;
let geom = CircleGeometry::new(
tween.start_state.position,
tween.start_state.heading,
*radius,
*direction,
);
// Calculate progress
let elapsed = get_time() - tween.start_time;
let progress = (elapsed / tween.duration).min(1.0);
let eased_progress = CubicInOut.tween(1.0, progress as f32);
// Generate arc vertices for the partial arc
let num_samples = *steps.max(&1);
let samples_to_draw = ((num_samples as f32 * eased_progress) as usize).max(1);
for i in 1..=samples_to_draw {
let sample_progress = i as f32 / num_samples as f32;
let current_angle = match direction {
crate::circle_geometry::CircleDirection::Left => {
geom.start_angle_from_center - angle.to_radians() * sample_progress
}
crate::circle_geometry::CircleDirection::Right => {
geom.start_angle_from_center + angle.to_radians() * sample_progress
}
};
let vertex = Vec2::new(
geom.center.x + radius * current_angle.cos(),
geom.center.y + radius * current_angle.sin(),
// If we have an active tween, add progressive vertices
if let Some(tween) = active_tween {
// If we're animating a circle command with pen down, add arc vertices
if tween.start_state.pen_down {
if let crate::commands::TurtleCommand::Circle {
radius,
angle,
steps,
direction,
} = &tween.command
{
// Calculate partial arc vertices based on current progress
use crate::circle_geometry::CircleGeometry;
let geom = CircleGeometry::new(
tween.start_state.position,
tween.start_state.heading,
*radius,
*direction,
);
current_preview.push(vertex);
// Calculate progress
let elapsed = get_time() - tween.start_time;
let progress = (elapsed / tween.duration).min(1.0);
let eased_progress = CubicInOut.tween(1.0, progress as f32);
// Generate arc vertices for the partial arc
let num_samples = *steps.max(&1);
let samples_to_draw =
((num_samples as f32 * eased_progress) as usize).max(1);
for i in 1..=samples_to_draw {
let sample_progress = i as f32 / num_samples as f32;
let current_angle = match direction {
crate::circle_geometry::CircleDirection::Left => {
geom.start_angle_from_center
- angle.to_radians() * sample_progress
}
crate::circle_geometry::CircleDirection::Right => {
geom.start_angle_from_center
+ angle.to_radians() * sample_progress
}
};
let vertex = Vec2::new(
geom.center.x + radius * current_angle.cos(),
geom.center.y + radius * current_angle.sin(),
);
current_preview.push(vertex);
}
} else if matches!(
&tween.command,
crate::commands::TurtleCommand::Move(_)
| crate::commands::TurtleCommand::Goto(_)
) {
// For Move/Goto commands, just add the current position
current_preview.push(Vec2::new(turtle.position.x, turtle.position.y));
}
} else if matches!(
&tween.command,
crate::commands::TurtleCommand::Move(_)
| crate::commands::TurtleCommand::Goto(_)
) {
// For Move/Goto commands, just add the current position
current_preview
.push(Vec2::new(world.turtle.position.x, world.turtle.position.y));
// For Move/Goto with pen up during filling, still add current position for preview
current_preview.push(Vec2::new(turtle.position.x, turtle.position.y));
}
} else if matches!(
&tween.command,
crate::commands::TurtleCommand::Move(_) | crate::commands::TurtleCommand::Goto(_)
) {
// For Move/Goto with pen up during filling, still add current position for preview
current_preview.push(Vec2::new(world.turtle.position.x, world.turtle.position.y));
}
// Add current turtle position if not already included
if let Some(last) = current_preview.last() {
let current_pos = world.turtle.position;
// Use a larger threshold to reduce flickering from tiny movements
if (last.x - current_pos.x).abs() > 0.1 || (last.y - current_pos.y).abs() > 0.1 {
current_preview.push(Vec2::new(current_pos.x, current_pos.y));
}
} else if !current_preview.is_empty() {
current_preview.push(Vec2::new(world.turtle.position.x, world.turtle.position.y));
}
} else {
// No active tween - just show current state
if !current_preview.is_empty() {
// Add current turtle position if not already included
if let Some(last) = current_preview.last() {
let current_pos = world.turtle.position;
let current_pos = turtle.position;
// Use a larger threshold to reduce flickering from tiny movements
if (last.x - current_pos.x).abs() > 0.1 || (last.y - current_pos.y).abs() > 0.1
{
current_preview.push(Vec2::new(current_pos.x, current_pos.y));
}
} else if !current_preview.is_empty() {
current_preview.push(Vec2::new(turtle.position.x, turtle.position.y));
}
} else {
// No active tween - just show current state
if !current_preview.is_empty() {
if let Some(last) = current_preview.last() {
let current_pos = turtle.position;
if (last.x - current_pos.x).abs() > 0.1
|| (last.y - current_pos.y).abs() > 0.1
{
current_preview.push(Vec2::new(current_pos.x, current_pos.y));
}
}
}
}
}
// Add current contour to all contours if it has enough vertices
if current_preview.len() >= 3 {
all_contours.push(current_preview);
}
// Add current contour to all contours if it has enough vertices
if current_preview.len() >= 3 {
all_contours.push(current_preview);
}
// Tessellate and draw all contours together using multi-contour tessellation
if !all_contours.is_empty() {
match crate::tessellation::tessellate_multi_contour(
&all_contours,
fill_state.fill_color,
) {
Ok(mesh_data) => {
draw_mesh(&mesh_data.to_mesh());
}
Err(e) => {
tracing::error!(
error = ?e,
"Lyon multi-contour tessellation error for fill preview"
);
// Tessellate and draw all contours together using multi-contour tessellation
if !all_contours.is_empty() {
match crate::tessellation::tessellate_multi_contour(
&all_contours,
fill_state.fill_color,
) {
Ok(mesh_data) => {
draw_mesh(&mesh_data.to_mesh());
}
Err(e) => {
tracing::error!("Failed to tessellate fill preview: {:?}", e);
}
}
}
}
}
// Draw turtle if visible
if world.turtle.visible {
draw_turtle(&world.turtle);
// Draw all visible turtles
for turtle in &world.turtles {
if turtle.visible {
draw_turtle(turtle);
}
}
// Reset to default camera
@ -279,18 +294,17 @@ fn draw_tween_arc(
let elapsed = get_time() - tween.start_time;
let t = (elapsed / tween.duration).min(1.0);
let progress = CubicInOut.tween(1.0, t as f32); // tween from 0 to 1
let angle_traveled = total_angle.to_radians() * progress;
let (rotation_degrees, arc_degrees) = geom.draw_arc_params_partial(angle_traveled);
// Use Lyon to tessellate and draw the partial arc
if let Ok(mesh_data) = crate::tessellation::tessellate_arc(
geom.center,
radius,
rotation_degrees,
arc_degrees,
geom.start_angle_from_center.to_degrees(),
total_angle * progress,
tween.start_state.color,
tween.start_state.pen_width,
steps,
((steps as f32 * progress).ceil() as usize).max(1),
direction,
) {
draw_mesh(&mesh_data.to_mesh());
}

View File

@ -51,7 +51,10 @@ pub fn execute_command_side_effects(
contours = fill_state.contours.len(),
"Successfully tessellated contours"
);
commands.push(DrawCommand::Mesh(mesh_data));
commands.push(DrawCommand::Mesh {
turtle_id: 0,
data: mesh_data,
});
} else {
tracing::error!("Failed to tessellate contours");
}
@ -153,7 +156,10 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
state.pen_width,
false, // not closed
) {
world.add_command(DrawCommand::Mesh(mesh_data));
world.add_command(DrawCommand::Mesh {
turtle_id: 0,
data: mesh_data,
});
}
}
}
@ -172,19 +178,21 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
let geom = CircleGeometry::new(state.position, start_heading, *radius, *direction);
if state.pen_down {
let (rotation_degrees, arc_degrees) = geom.draw_arc_params(*angle);
// Use Lyon to tessellate the arc
if let Ok(mesh_data) = tessellation::tessellate_arc(
geom.center,
*radius,
rotation_degrees,
arc_degrees,
geom.start_angle_from_center.to_degrees(),
*angle,
state.color,
state.pen_width,
*steps,
*direction,
) {
world.add_command(DrawCommand::Mesh(mesh_data));
world.add_command(DrawCommand::Mesh {
turtle_id: 0,
data: mesh_data,
});
}
}
@ -208,7 +216,10 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
state.pen_width,
false, // not closed
) {
world.add_command(DrawCommand::Mesh(mesh_data));
world.add_command(DrawCommand::Mesh {
turtle_id: 0,
data: mesh_data,
});
}
}
}
@ -230,24 +241,40 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
record_fill_vertices_after_movement(command, &start_state, state);
}
/// Add drawing command for a completed tween (state transition already occurred)
pub fn add_draw_for_completed_tween(
/// Execute command on a specific turtle by ID
pub fn execute_command_with_id(command: &TurtleCommand, turtle_id: usize, world: &mut TurtleWorld) {
// Clone turtle state to avoid borrow checker issues
if let Some(turtle) = world.get_turtle(turtle_id) {
let mut state = turtle.clone();
execute_command(command, &mut state, world);
// Update the turtle state back
if let Some(turtle_mut) = world.get_turtle_mut(turtle_id) {
*turtle_mut = state;
}
}
}
/// Add drawing command for a completed tween with turtle_id tracking
pub fn add_draw_for_completed_tween_with_id(
command: &TurtleCommand,
start_state: &TurtleState,
end_state: &TurtleState,
world: &mut TurtleWorld,
turtle_id: usize,
) {
match command {
TurtleCommand::Move(_) | TurtleCommand::Goto(_) => {
if start_state.pen_down {
// Draw line segment with round caps
if let Ok(mesh_data) = tessellation::tessellate_stroke(
&[start_state.position, end_state.position],
start_state.color,
start_state.pen_width,
false, // not closed
false,
) {
world.add_command(DrawCommand::Mesh(mesh_data));
world.add_command(DrawCommand::Mesh {
turtle_id,
data: mesh_data,
});
}
}
}
@ -264,19 +291,79 @@ pub fn add_draw_for_completed_tween(
*radius,
*direction,
);
let (rotation_degrees, arc_degrees) = geom.draw_arc_params(*angle);
// Use Lyon to tessellate the arc
if let Ok(mesh_data) = tessellation::tessellate_arc(
geom.center,
*radius,
rotation_degrees,
arc_degrees,
geom.start_angle_from_center.to_degrees(),
*angle,
start_state.color,
start_state.pen_width,
*steps,
*direction,
) {
world.add_command(DrawCommand::Mesh(mesh_data));
world.add_command(DrawCommand::Mesh {
turtle_id,
data: mesh_data,
});
}
}
}
_ => {}
}
}
/// Add drawing command for a completed tween (state transition already occurred)
pub fn add_draw_for_completed_tween(
command: &TurtleCommand,
start_state: &TurtleState,
end_state: &TurtleState,
world: &mut TurtleWorld,
) {
match command {
TurtleCommand::Move(_) | TurtleCommand::Goto(_) => {
if start_state.pen_down {
// Draw line segment with round caps
if let Ok(mesh_data) = tessellation::tessellate_stroke(
&[start_state.position, end_state.position],
start_state.color,
start_state.pen_width,
false,
) {
world.add_command(DrawCommand::Mesh {
turtle_id: 0,
data: mesh_data,
});
}
}
}
TurtleCommand::Circle {
radius,
angle,
steps,
direction,
} => {
if start_state.pen_down {
let geom = CircleGeometry::new(
start_state.position,
start_state.heading,
*radius,
*direction,
);
if let Ok(mesh_data) = tessellation::tessellate_arc(
geom.center,
*radius,
geom.start_angle_from_center.to_degrees(),
*angle,
start_state.color,
start_state.pen_width,
*steps,
*direction,
) {
world.add_command(DrawCommand::Mesh {
turtle_id: 0,
data: mesh_data,
});
}
}
}
@ -311,7 +398,7 @@ mod tests {
// We'll use a dummy world but won't actually call drawing commands
let mut world = TurtleWorld {
turtle: state.clone(),
turtles: vec![state.clone()],
commands: Vec::new(),
camera: macroquad::camera::Camera2D {
zoom: vec2(1.0, 1.0),
@ -323,6 +410,7 @@ mod tests {
},
background_color: Color::new(1.0, 1.0, 1.0, 1.0),
};
let mut state = world.turtles[0].clone();
// Initial state: position (0, 0), heading 0 (east)
assert_eq!(state.position.x, 0.0);

View File

@ -78,7 +78,8 @@ use macroquad::prelude::*;
/// Main turtle application struct
pub struct TurtleApp {
world: TurtleWorld,
tween_controller: Option<TweenController>,
/// One tween controller per turtle (indexed by turtle ID)
tween_controllers: Vec<TweenController>,
speed: AnimationSpeed,
// Mouse panning state
is_dragging: bool,
@ -93,7 +94,7 @@ impl TurtleApp {
pub fn new() -> Self {
Self {
world: TurtleWorld::new(),
tween_controller: None,
tween_controllers: Vec::new(),
speed: AnimationSpeed::default(),
is_dragging: false,
last_mouse_pos: None,
@ -101,40 +102,89 @@ impl TurtleApp {
}
}
/// Add commands to the turtle
/// Add a new turtle and return its ID
pub fn add_turtle(&mut self) -> usize {
let id = self.world.add_turtle();
let speed = self.speed;
self.tween_controllers
.push(TweenController::new(id, CommandQueue::new(), speed));
id
}
/// Add commands to a specific turtle
///
/// Speed is controlled by `SetSpeed` commands in the queue.
/// Use `set_speed()` on the turtle plan to set animation speed.
/// Speed >= 999 = instant mode, speed < 999 = animated mode.
///
/// # Arguments
/// * `turtle_id` - The ID of the turtle to control
/// * `queue` - The command queue to execute
#[must_use]
pub fn with_commands(mut self, queue: CommandQueue) -> Self {
// The `TweenController` will switch between instant and animated mode
// based on `SetSpeed` commands encountered
self.tween_controller = Some(TweenController::new(queue, self.speed));
pub fn with_commands(mut self, turtle_id: usize, queue: CommandQueue) -> Self {
// Ensure we have a controller for this turtle
while self.tween_controllers.len() <= turtle_id {
let id = self.tween_controllers.len();
let speed = self.speed;
self.tween_controllers
.push(TweenController::new(id, CommandQueue::new(), speed));
}
// Append commands to the controller
self.tween_controllers[turtle_id].append_commands(queue);
self
}
/// Execute a plan immediately on a specific turtle (no animation)
pub fn execute_immediate(&mut self, turtle_id: usize, plan: TurtlePlan) {
for ref cmd in plan.build() {
execution::execute_command_with_id(cmd, turtle_id, &mut self.world);
}
}
/// Append commands to a turtle's animation queue
pub fn append_to_queue(&mut self, turtle_id: usize, plan: TurtlePlan) {
// Ensure we have a controller for this turtle
while self.tween_controllers.len() <= turtle_id {
let id = self.tween_controllers.len();
let speed = AnimationSpeed::default();
self.tween_controllers
.push(TweenController::new(id, CommandQueue::new(), speed));
}
self.tween_controllers[turtle_id].append_commands(plan.build());
}
/// Update animation state (call every frame)
pub fn update(&mut self) {
// Handle mouse panning and zoom
self.handle_mouse_panning();
self.handle_mouse_zoom();
if let Some(ref mut controller) = self.tween_controller {
let completed_commands =
controller.update(&mut self.world.turtle, &mut self.world.commands);
// Update all active tween controllers
// Process each turtle separately to avoid borrow conflicts
let turtle_count = self.tween_controllers.len();
for turtle_id in 0..turtle_count {
// Extract commands temporarily to avoid double mutable borrow
let mut commands = std::mem::take(&mut self.world.commands);
// Process all completed commands (multiple in instant mode, 0-1 in animated mode)
let completed_commands = if let Some(turtle) = self.world.get_turtle_mut(turtle_id) {
self.tween_controllers[turtle_id].update(turtle, &mut commands)
} else {
Vec::new()
};
// Put commands back
self.world.commands = commands;
// Process all completed commands
for (completed_cmd, start_state, end_state) in completed_commands {
// Add draw commands for the completed tween
execution::add_draw_for_completed_tween(
execution::add_draw_for_completed_tween_with_id(
&completed_cmd,
&start_state,
&end_state,
&mut self.world,
turtle_id,
);
}
}
@ -187,20 +237,21 @@ impl TurtleApp {
/// Render the turtle world (call every frame)
pub fn render(&self) {
// Get active tween if in animated mode
// Find the first active tween (turtle_id is now stored in the tween itself)
let active_tween = self
.tween_controller
.as_ref()
.and_then(|c| c.current_tween());
.tween_controllers
.iter()
.find_map(|controller| controller.current_tween());
drawing::render_world_with_tween(&self.world, active_tween, self.zoom_level);
}
/// Check if all commands have been executed
#[must_use]
pub fn is_complete(&self) -> bool {
self.tween_controller
.as_ref()
.is_none_or(TweenController::is_complete)
self.tween_controllers
.iter()
.all(TweenController::is_complete)
}
/// Get reference to the world state

View File

@ -65,6 +65,11 @@ impl TurtleState {
Angle::radians(self.heading)
}
/// Reset turtle to default state
pub fn reset(&mut self) {
*self = Self::default();
}
/// Start recording fill vertices
pub fn begin_fill(&mut self, fill_color: Color) {
self.filling = Some(FillState {
@ -225,12 +230,15 @@ impl MeshData {
#[derive(Clone, Debug)]
pub enum DrawCommand {
/// Pre-tessellated mesh data (lines, arcs, circles, polygons - all use this)
Mesh(MeshData),
/// Includes the turtle ID that created this command
Mesh { turtle_id: usize, data: MeshData },
}
/// The complete turtle world containing all drawing state
pub struct TurtleWorld {
pub turtle: TurtleState,
/// All turtles in the world (indexed by turtle ID)
pub turtles: Vec<TurtleState>,
/// All drawing commands from all turtles
pub commands: Vec<DrawCommand>,
pub camera: Camera2D,
pub background_color: Color,
@ -240,7 +248,7 @@ impl TurtleWorld {
#[must_use]
pub fn new() -> Self {
Self {
turtle: TurtleState::default(),
turtles: vec![TurtleState::default()], // Start with one default turtle
commands: Vec::new(),
camera: Camera2D {
zoom: vec2(1.0 / screen_width() * 2.0, 1.0 / screen_height() * 2.0),
@ -251,13 +259,44 @@ impl TurtleWorld {
}
}
/// Add a new turtle and return its ID
pub fn add_turtle(&mut self) -> usize {
self.turtles.push(TurtleState::default());
self.turtles.len() - 1
}
/// Get turtle by ID
#[must_use]
pub fn get_turtle(&self, id: usize) -> Option<&TurtleState> {
self.turtles.get(id)
}
/// Get mutable turtle by ID
pub fn get_turtle_mut(&mut self, id: usize) -> Option<&mut TurtleState> {
self.turtles.get_mut(id)
}
/// Reset a specific turtle to default state and remove all its drawings
pub fn reset_turtle(&mut self, turtle_id: usize) {
if let Some(turtle) = self.get_turtle_mut(turtle_id) {
turtle.reset();
}
// Remove all commands created by this turtle
self.commands.retain(|cmd| match cmd {
DrawCommand::Mesh { turtle_id: id, .. } => *id != turtle_id,
});
}
pub fn add_command(&mut self, cmd: DrawCommand) {
self.commands.push(cmd);
}
/// Clear all drawings and reset all turtle states
pub fn clear(&mut self) {
self.commands.clear();
self.turtle = TurtleState::default();
for turtle in &mut self.turtles {
turtle.reset();
}
}
}

View File

@ -302,6 +302,7 @@ pub fn tessellate_arc(
color: Color,
stroke_width: f32,
segments: usize,
direction: crate::circle_geometry::CircleDirection,
) -> Result<MeshData, Box<dyn std::error::Error>> {
// Build arc path manually from segments
let mut builder = Path::builder();
@ -311,16 +312,24 @@ pub fn tessellate_arc(
let step = arc_angle / segments as f32;
// Calculate first point
let first_angle = start_angle;
let first_point = point(
center.x + radius * first_angle.cos(),
center.y + radius * first_angle.sin(),
center.x + radius * start_angle.cos(),
center.y + radius * start_angle.sin(),
);
builder.begin(first_point);
// Add remaining points
// Add remaining points - direction matters!
for i in 1..=segments {
let angle = start_angle + step * i as f32;
let angle = match direction {
crate::circle_geometry::CircleDirection::Left => {
// Counter-clockwise: subtract angle
start_angle - step * i as f32
}
crate::circle_geometry::CircleDirection::Right => {
// Clockwise: add angle
start_angle + step * i as f32
}
};
let pt = point(
center.x + radius * angle.cos(),
center.y + radius * angle.sin(),

View File

@ -45,26 +45,29 @@ impl From<TweenVec2> for Vec2 {
/// Controls tweening of turtle commands
pub struct TweenController {
turtle_id: usize,
queue: CommandQueue,
current_tween: Option<CommandTween>,
speed: AnimationSpeed,
}
pub(crate) struct CommandTween {
pub struct CommandTween {
pub turtle_id: usize,
pub command: TurtleCommand,
pub start_time: f64,
pub duration: f64,
pub start_state: TurtleState,
pub target_state: TurtleState,
pub position_tweener: Tweener<TweenVec2, f64, CubicInOut>,
pub heading_tweener: Tweener<f32, f64, CubicInOut>,
pub pen_width_tweener: Tweener<f32, f64, CubicInOut>,
position_tweener: Tweener<TweenVec2, f64, CubicInOut>,
heading_tweener: Tweener<f32, f64, CubicInOut>,
pen_width_tweener: Tweener<f32, f64, CubicInOut>,
}
impl TweenController {
#[must_use]
pub fn new(queue: CommandQueue, speed: AnimationSpeed) -> Self {
pub fn new(turtle_id: usize, queue: CommandQueue, speed: AnimationSpeed) -> Self {
Self {
turtle_id,
queue,
current_tween: None,
speed,
@ -75,6 +78,11 @@ impl TweenController {
self.speed = speed;
}
/// Append commands to the queue
pub fn append_commands(&mut self, new_queue: CommandQueue) {
self.queue.extend(new_queue);
}
/// Update the tween, returns `Vec` of (`command`, `start_state`, `end_state`) for all completed commands this frame
/// Also takes commands vec to handle side effects like fill operations
/// Each `command` has its own `start_state` and `end_state` pair
@ -285,6 +293,7 @@ impl TweenController {
);
self.current_tween = Some(CommandTween {
turtle_id: self.turtle_id,
command: command_clone,
start_time: get_time(),
duration,