diff --git a/turtle-lib-macros/src/lib.rs b/turtle-lib-macros/src/lib.rs index 0aaf05f..283e192 100644 --- a/turtle-lib-macros/src/lib.rs +++ b/turtle-lib-macros/src/lib.rs @@ -60,9 +60,9 @@ use syn::{parse_macro_input, ItemFn}; /// turtle.forward(100.0); /// 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); /// app.update(); @@ -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); diff --git a/turtle-lib/examples/cheese.rs b/turtle-lib/examples/cheese.rs index 8fba80b..1678d33 100644 --- a/turtle-lib/examples/cheese.rs +++ b/turtle-lib/examples/cheese.rs @@ -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)); diff --git a/turtle-lib/examples/fill_advanced.rs b/turtle-lib/examples/fill_advanced.rs index 3efef97..d144ad3 100644 --- a/turtle-lib/examples/fill_advanced.rs +++ b/turtle-lib/examples/fill_advanced.rs @@ -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; diff --git a/turtle-lib/examples/logging_example.rs b/turtle-lib/examples/logging_example.rs index ca754d2..7b4294d 100644 --- a/turtle-lib/examples/logging_example.rs +++ b/turtle-lib/examples/logging_example.rs @@ -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 { diff --git a/turtle-lib/examples/yinyang.rs b/turtle-lib/examples/yinyang.rs index 114b6d0..a73d83d 100644 --- a/turtle-lib/examples/yinyang.rs +++ b/turtle-lib/examples/yinyang.rs @@ -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(); diff --git a/turtle-lib/src/drawing.rs b/turtle-lib/src/drawing.rs index 5567bb9..693d68f 100644 --- a/turtle-lib/src/drawing.rs +++ b/turtle-lib/src/drawing.rs @@ -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::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::new(); - // Add all completed contours - for completed_contour in &fill_state.contours { - let contour_vec2: Vec = completed_contour + // Add all completed contours + for completed_contour in &fill_state.contours { + let contour_vec2: Vec = 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 = 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 = 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()); } diff --git a/turtle-lib/src/execution.rs b/turtle-lib/src/execution.rs index 8b51d94..88e7cda 100644 --- a/turtle-lib/src/execution.rs +++ b/turtle-lib/src/execution.rs @@ -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); diff --git a/turtle-lib/src/lib.rs b/turtle-lib/src/lib.rs index b12c359..24e4895 100644 --- a/turtle-lib/src/lib.rs +++ b/turtle-lib/src/lib.rs @@ -78,7 +78,8 @@ use macroquad::prelude::*; /// Main turtle application struct pub struct TurtleApp { world: TurtleWorld, - tween_controller: Option, + /// One tween controller per turtle (indexed by turtle ID) + tween_controllers: Vec, 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 diff --git a/turtle-lib/src/state.rs b/turtle-lib/src/state.rs index 2f9f8a8..3f9b1b8 100644 --- a/turtle-lib/src/state.rs +++ b/turtle-lib/src/state.rs @@ -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, + /// All drawing commands from all turtles pub commands: Vec, 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(); + } } } diff --git a/turtle-lib/src/tessellation.rs b/turtle-lib/src/tessellation.rs index 6ff1f86..433a2ee 100644 --- a/turtle-lib/src/tessellation.rs +++ b/turtle-lib/src/tessellation.rs @@ -302,6 +302,7 @@ pub fn tessellate_arc( color: Color, stroke_width: f32, segments: usize, + direction: crate::circle_geometry::CircleDirection, ) -> Result> { // 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(), diff --git a/turtle-lib/src/tweening.rs b/turtle-lib/src/tweening.rs index b177775..1824a4f 100644 --- a/turtle-lib/src/tweening.rs +++ b/turtle-lib/src/tweening.rs @@ -45,26 +45,29 @@ impl From for Vec2 { /// Controls tweening of turtle commands pub struct TweenController { + turtle_id: usize, queue: CommandQueue, current_tween: Option, 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, - pub heading_tweener: Tweener, - pub pen_width_tweener: Tweener, + position_tweener: Tweener, + heading_tweener: Tweener, + pen_width_tweener: Tweener, } 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,