diff --git a/turtle-lib-macros/src/lib.rs b/turtle-lib-macros/src/lib.rs index 283e192..d00d012 100644 --- a/turtle-lib-macros/src/lib.rs +++ b/turtle-lib-macros/src/lib.rs @@ -19,7 +19,7 @@ use syn::{parse_macro_input, ItemFn}; /// /// # Example /// -/// ```no_run +/// ```ignore /// use turtle_lib::*; /// /// #[turtle_main("My Turtle Drawing")] @@ -34,7 +34,7 @@ use syn::{parse_macro_input, ItemFn}; /// /// If you need macroquad types not re-exported by turtle_lib: /// -/// ```no_run +/// ```ignore /// use macroquad::prelude::SKYBLUE; // Import specific items /// use turtle_lib::*; /// @@ -47,7 +47,7 @@ use syn::{parse_macro_input, ItemFn}; /// /// This expands to approximately: /// -/// ```no_run +/// ```ignore /// use macroquad::prelude::*; /// use turtle_lib::*; /// @@ -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(0, turtle.build()); +/// let mut app = TurtleApp::new().with_commands(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(0, turtle.build()); + .with_commands(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(0, turtle.build()); + .with_commands(turtle.build()); loop { macroquad::prelude::clear_background(macroquad::prelude::WHITE); diff --git a/turtle-lib/Cargo.toml b/turtle-lib/Cargo.toml index 74974f1..e453079 100644 --- a/turtle-lib/Cargo.toml +++ b/turtle-lib/Cargo.toml @@ -8,7 +8,10 @@ license = "MIT OR Apache-2.0" macroquad = "0.4" tween = "2.1.0" lyon = "1.0" -tracing = { version = "0.1", features = ["log"], default-features = false } +tracing = { version = "0.1", features = [ + "log", + "attributes", +], default-features = false } turtle-lib-macros = { path = "../turtle-lib-macros" } [dev-dependencies] diff --git a/turtle-lib/examples/cheese.rs b/turtle-lib/examples/cheese.rs index 1678d33..8fba80b 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(0, turtle.build()); + let mut app = TurtleApp::new().with_commands(turtle.build()); loop { clear_background(Color::new(0.95, 0.95, 0.98, 1.0)); diff --git a/turtle-lib/examples/circle_test.rs b/turtle-lib/examples/circle_test.rs index fd9038e..3284075 100644 --- a/turtle-lib/examples/circle_test.rs +++ b/turtle-lib/examples/circle_test.rs @@ -13,11 +13,13 @@ fn draw(turtle: &mut TurtlePlan) { turtle.set_speed(999); turtle.circle_left(100.0, 540.0, 72); // partial circle to the left + turtle.begin_fill(); turtle.forward(150.0); turtle.set_speed(100); turtle.set_pen_color(BLUE); turtle.circle_right(50.0, 270.0, 72); // partial circle to the right // Set animation speed + turtle.end_fill(); turtle.set_speed(20); turtle.forward(150.0); turtle.circle_left(50.0, 180.0, 12); diff --git a/turtle-lib/examples/dragon.rs b/turtle-lib/examples/dragon.rs index 31d47cf..9bf4630 100644 --- a/turtle-lib/examples/dragon.rs +++ b/turtle-lib/examples/dragon.rs @@ -40,7 +40,7 @@ use turtle_lib::*; #[turtle_main("Dragon Curve")] fn draw_dragon(turtle: &mut TurtlePlan) { // Fast drawing - turtle.set_speed(1200); + turtle.set_speed(1020); // Start position turtle.pen_up(); diff --git a/turtle-lib/examples/fill_advanced.rs b/turtle-lib/examples/fill_advanced.rs index d144ad3..3efef97 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(0, t.build()); + let mut app = TurtleApp::new().with_commands(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/hello_turtle.rs b/turtle-lib/examples/hello_turtle.rs index a40e738..835e037 100644 --- a/turtle-lib/examples/hello_turtle.rs +++ b/turtle-lib/examples/hello_turtle.rs @@ -4,7 +4,7 @@ use turtle_lib::*; -#[turtle_main("Hello Turtle")] +#[turtle_main] fn hello() { turtle.set_pen_color(BLUE); for _ in 0..4 { diff --git a/turtle-lib/examples/logging_example.rs b/turtle-lib/examples/logging_example.rs index 7b4294d..6a57aaf 100644 --- a/turtle-lib/examples/logging_example.rs +++ b/turtle-lib/examples/logging_example.rs @@ -31,7 +31,7 @@ async fn main() { .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { // Default to showing info-level logs if RUST_LOG is not set - tracing_subscriber::EnvFilter::new("turtle_lib=info") + tracing_subscriber::EnvFilter::new("turtle_lib=trace") }), ) .with_target(true) // Show which module the log came from @@ -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(0, t.build()); + let mut app = TurtleApp::new().with_commands(t.build()); // Main loop loop { diff --git a/turtle-lib/examples/macro_demo_inline.rs b/turtle-lib/examples/macro_demo_inline.rs deleted file mode 100644 index f63035d..0000000 --- a/turtle-lib/examples/macro_demo_inline.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Demo of the turtle_main macro with inline code -//! -//! This example shows that you can write your turtle code directly -//! in the function body without taking a turtle parameter. - -use turtle_lib::*; - -#[turtle_main("Macro Demo - Inline Spiral")] -fn draw_spiral() { - turtle.set_pen_color(RED); - turtle.set_pen_width(2.0); - - for i in 0..36 { - turtle.forward(i as f32 * 3.0); - turtle.right(25.0); - } -} diff --git a/turtle-lib/examples/multi_turtle.rs b/turtle-lib/examples/multi_turtle.rs new file mode 100644 index 0000000..2934bf5 --- /dev/null +++ b/turtle-lib/examples/multi_turtle.rs @@ -0,0 +1,125 @@ +//! Multi-turtle example demonstrating multiple independent turtles drawing simultaneously +//! +//! This example shows how to: +//! - Create multiple turtle instances using `add_turtle()` +//! - Control each turtle independently with separate command queues +//! - Position turtles at different locations using `go_to()` +//! - Use different colors and pen widths for each turtle +//! - Combine all turtle animations in a single rendering loop + +use macroquad::{miniquad::window::set_window_size, prelude::*}; +use turtle_lib::*; + +#[macroquad::main("Multi-Turtle Example")] +async fn main() { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { + // Default to showing info-level logs if RUST_LOG is not set + tracing_subscriber::EnvFilter::new("turtle_lib=error") + }), + ) + .with_target(true) // Show which module the log came from + .with_thread_ids(false) + .with_line_number(true) // Show line numbers + .with_file(false) + .without_time() + .init(); + + let mut app = TurtleApp::new(); + set_window_size(1900, 1000); + + // Turtle 0 (default turtle) - Draw a spiral (red) + let mut turtle0 = create_turtle(); + turtle0.right(45.0); + turtle0.set_speed(1900.0); + turtle0.set_pen_color(RED); + turtle0.set_fill_color(RED); + turtle0.set_pen_width(2.0); + turtle0.begin_fill(); + for i in 0..36 { + turtle0.forward(5.0 + i as f32 * 2.0).right(10.0); + } + turtle0.pen_up(); + turtle0.go_to(vec2(-200.0, -200.0)); + turtle0.pen_down(); + turtle0.circle_left(30.0, 360.0, 36); + turtle0.end_fill(); + + // Turtle 1 - Draw a square (blue) + let turtle1_id = app.add_turtle(); + let mut turtle1 = create_turtle(); + turtle1.set_speed(1900.0); + turtle1.pen_up(); + turtle1.go_to(vec2(-200.0, 0.0)); + turtle1.pen_down(); + turtle1.set_pen_color(BLUE); + turtle1.set_fill_color(BLUE); + turtle1.set_pen_width(3.0); + turtle1.begin_fill(); + for _ in 0..4 { + turtle1.forward(100.0).right(90.0); + } + turtle1.end_fill(); + + // Turtle 2 - Draw a hexagon (green) + let turtle2_id = app.add_turtle(); + let mut turtle2 = create_turtle(); + turtle2.set_speed(150.0); + turtle2.pen_up(); + turtle2.go_to(vec2(200.0, 0.0)); + turtle2.pen_down(); + turtle2.set_pen_color(GREEN); + turtle2.set_pen_width(3.0); + for _ in 0..6 { + turtle2.forward(80.0).right(60.0); + } + + // Turtle 3 - Draw a star (yellow) + let turtle3_id = app.add_turtle(); + let mut turtle3 = create_turtle(); + turtle3.set_fill_color(ORANGE); + turtle3.begin_fill(); + turtle3.set_speed(150.0); + turtle3.pen_up(); + turtle3.go_to(vec2(0.0, 150.0)); + turtle3.pen_down(); + turtle3.set_pen_color(YELLOW); + turtle3.set_pen_width(3.0); + for _ in 0..5 { + turtle3.forward(120.0).right(144.0); + } + turtle3.end_fill(); + // Turtle 4 - Draw a filled circle (purple) + let turtle4_id = app.add_turtle(); + let mut turtle4 = create_turtle(); + turtle4.set_speed(150.0); + turtle4.pen_up(); + turtle4.go_to(vec2(0.0, -150.0)); + turtle4.pen_down(); + turtle4.set_pen_color(PURPLE); + turtle4.set_fill_color(Color::new(0.5, 0.0, 0.5, 0.5)); + turtle4.begin_fill(); + turtle4.circle_left(60.0, 360.0, 36); + turtle4.end_fill(); + + // Add all commands to the app + app = app.with_commands(turtle0.build()); + app = app.with_commands_for_turtle(turtle1_id, turtle1.build()); + app = app.with_commands_for_turtle(turtle2_id, turtle2.build()); + app = app.with_commands_for_turtle(turtle3_id, turtle3.build()); + app = app.with_commands_for_turtle(turtle4_id, turtle4.build()); + + // Main loop + loop { + clear_background(WHITE); + app.update(); + app.render(); + + if is_key_pressed(KeyCode::Escape) || is_key_pressed(KeyCode::Q) { + break; + } + + next_frame().await; + } +} diff --git a/turtle-lib/examples/nikolaus.rs b/turtle-lib/examples/nikolaus.rs index 198ced6..74db8be 100644 --- a/turtle-lib/examples/nikolaus.rs +++ b/turtle-lib/examples/nikolaus.rs @@ -49,7 +49,7 @@ fn draw(turtle: &mut TurtlePlan) { turtle.pen_up(); turtle.backward(80.0); turtle.left(90.0); - turtle.forward(50.0); + turtle.backward(50.0); turtle.right(90.0); turtle.pen_down(); diff --git a/turtle-lib/examples/v1_0_0.rs b/turtle-lib/examples/v1_0_0.rs deleted file mode 100644 index 1c78b2f..0000000 --- a/turtle-lib/examples/v1_0_0.rs +++ /dev/null @@ -1,126 +0,0 @@ -//! Celebrates the 1.0.0 release of the original sunjay/turtle library. -//! -//! This example draws "1.0.0" with decorative background lines and filled shapes. -//! Ported from the original sunjay/turtle example. - -use turtle_lib::*; - -#[turtle_main("Version 1.0.0")] -fn draw_version(turtle: &mut TurtlePlan) { - turtle.set_pen_width(10.0); - turtle.set_speed(999); // instant - turtle.pen_up(); - turtle.go_to(vec2(350.0, 178.0)); - turtle.pen_down(); - - bg_lines(turtle); - - turtle.pen_up(); - turtle.go_to(vec2(-270.0, -200.0)); - turtle.set_heading(90.0); - turtle.pen_down(); - - turtle.set_speed(100); // normal - turtle.set_pen_color(BLUE); - // Cyan with alpha - using RGB values for Color::from("#00E5FF") - turtle.set_fill_color([0.0, 0.898, 1.0, 0.75]); - - one(turtle); - - turtle.set_speed(200); // faster - - turtle.pen_up(); - turtle.left(90.0); - turtle.backward(50.0); - turtle.pen_down(); - - small_circle(turtle); - - turtle.pen_up(); - turtle.backward(150.0); - turtle.pen_down(); - - zero(turtle); - - turtle.pen_up(); - turtle.backward(150.0); - turtle.pen_down(); - - small_circle(turtle); - - turtle.pen_up(); - turtle.backward(150.0); - turtle.pen_down(); - - zero(turtle); -} - -fn bg_lines(turtle: &mut TurtlePlan) { - // Light green color for background lines (#76FF03) - turtle.set_pen_color([0.463, 1.0, 0.012, 1.0].into()); - turtle.set_heading(165.0); - turtle.forward(280.0); - - turtle.left(147.0); - turtle.forward(347.0); - - turtle.right(158.0); - turtle.forward(547.0); - - turtle.left(138.0); - turtle.forward(539.0); - - turtle.right(168.0); - turtle.forward(477.0); - - turtle.left(154.0); - turtle.forward(377.0); - - turtle.right(158.0); - turtle.forward(329.0); -} - -fn small_circle(turtle: &mut TurtlePlan) { - turtle.begin_fill(); - for _ in 0..90 { - turtle.forward(1.0); - turtle.right(4.0); - } - turtle.end_fill(); -} - -fn one(turtle: &mut TurtlePlan) { - turtle.begin_fill(); - for _ in 0..2 { - turtle.forward(420.0); - turtle.left(90.0); - turtle.forward(50.0); - turtle.left(90.0); - } - turtle.end_fill(); -} - -fn zero(turtle: &mut TurtlePlan) { - turtle.begin_fill(); - for _ in 0..2 { - arc_right(turtle); - arc_forward(turtle); - } - turtle.end_fill(); -} - -fn arc_right(turtle: &mut TurtlePlan) { - // Draw an arc that moves right faster than it moves forward - for i in 0..90 { - turtle.forward(3.0); - turtle.right((90.0 - i as f32) / 45.0); - } -} - -fn arc_forward(turtle: &mut TurtlePlan) { - // Draw an arc that moves forward faster than it moves right - for i in 0..90 { - turtle.forward(3.0); - turtle.right(i as f32 / 45.0); - } -} diff --git a/turtle-lib/examples/yinyang.rs b/turtle-lib/examples/yinyang.rs index a73d83d..01965e3 100644 --- a/turtle-lib/examples/yinyang.rs +++ b/turtle-lib/examples/yinyang.rs @@ -24,7 +24,4 @@ fn draw(turtle: &mut TurtlePlan) { turtle.pen_down(); turtle.circle_right(8.0, 360.0, 12); turtle.end_fill(); - - // Set animation speed - turtle.set_speed(1000); } diff --git a/turtle-lib/src/commands.rs b/turtle-lib/src/commands.rs index 9ccdde3..3ab3cd7 100644 --- a/turtle-lib/src/commands.rs +++ b/turtle-lib/src/commands.rs @@ -45,7 +45,7 @@ pub enum TurtleCommand { } /// Queue of turtle commands with execution state -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct CommandQueue { commands: Vec, current_index: usize, diff --git a/turtle-lib/src/drawing.rs b/turtle-lib/src/drawing.rs index 693d68f..2763a28 100644 --- a/turtle-lib/src/drawing.rs +++ b/turtle-lib/src/drawing.rs @@ -1,9 +1,8 @@ //! Rendering logic using Macroquad and Lyon tessellation use crate::circle_geometry::{CircleDirection, CircleGeometry}; -use crate::state::{DrawCommand, TurtleState, TurtleWorld}; +use crate::state::{DrawCommand, TurtleParams, TurtleWorld}; use crate::tessellation; -use crate::tweening::CommandTween; use macroquad::prelude::*; // Import the easing function from the tween crate @@ -25,19 +24,21 @@ pub fn render_world(world: &TurtleWorld) { // Set camera set_camera(&camera); - // Draw all accumulated commands - for cmd in &world.commands { - match cmd { - DrawCommand::Mesh { data, .. } => { - draw_mesh(&data.to_mesh()); + // Draw all accumulated commands from all turtles + for turtle in &world.turtles { + for cmd in &turtle.commands { + match cmd { + DrawCommand::Mesh { data } => { + draw_mesh(&data.to_mesh()); + } } } } // Draw all visible turtles for turtle in &world.turtles { - if turtle.visible { - draw_turtle(turtle); + if turtle.params.visible { + draw_turtle(&turtle.params); } } @@ -47,11 +48,7 @@ pub fn render_world(world: &TurtleWorld) { /// Render the turtle world with active tween visualization #[allow(clippy::too_many_lines)] -pub fn render_world_with_tween( - world: &TurtleWorld, - active_tween: Option<&CommandTween>, - zoom_level: f32, -) { +pub fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) { // Update camera zoom based on current screen size to prevent stretching // Apply user zoom level by dividing by it (smaller zoom value = more zoomed in) let camera = Camera2D { @@ -66,59 +63,58 @@ pub fn render_world_with_tween( // Set camera set_camera(&camera); - // Draw all accumulated commands - for cmd in &world.commands { - match cmd { - DrawCommand::Mesh { data, .. } => { - draw_mesh(&data.to_mesh()); + // Draw all accumulated commands from all turtles + for turtle in &world.turtles { + for cmd in &turtle.commands { + match cmd { + 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 { - crate::commands::TurtleCommand::Circle { - radius, - angle, - steps, - direction, - } => { - // Draw arc segments from start to current position - draw_tween_arc(tween, *radius, *angle, *steps, *direction); - } - _ if should_draw_tween_line(&tween.command) => { - // Draw straight line for other movement commands (use active turtle) - if let Some(turtle) = world.turtles.get(active_turtle_id) { + // Draw in-progress tween lines for all active tweens + for turtle in world.turtles.iter() { + if let Some(tween) = turtle.tween_controller.current_tween() { + // Only draw if pen is down + if tween.start_params.pen_down { + match &tween.command { + crate::commands::TurtleCommand::Circle { + radius, + angle, + steps, + direction, + } => { + // Draw arc segments from start to current position + draw_tween_arc(tween, *radius, *angle, *steps, *direction); + } + _ if should_draw_tween_line(&tween.command) => { + // Draw straight line for other movement commands (use tween's current position) 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, + tween.start_params.position.x, + tween.start_params.position.y, + tween.current_position.x, + tween.current_position.y, + tween.start_params.pen_width, + tween.start_params.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, + tween.current_position.x, + tween.current_position.y, + tween.start_params.pen_width / 2.0, + tween.start_params.color, ); } + _ => {} } - _ => {} } } } - // Draw live fill preview if currently filling (always show, not just during tweens) - // Use the active turtle if available, otherwise default to turtle 0 - if let Some(turtle) = world.turtles.get(active_turtle_id) { + // Draw live fill preview for all turtles that are currently filling + for turtle in world.turtles.iter() { if let Some(ref fill_state) = turtle.filling { // Build all contours: completed contours + current contour with animation let mut all_contours: Vec> = Vec::new(); @@ -133,16 +129,22 @@ pub fn render_world_with_tween( } // Build current contour with animation - let mut current_preview: Vec = fill_state - .current_contour - .iter() - .map(|c| Vec2::new(c.x, c.y)) - .collect(); + // Find the tween for this specific turtle + let turtle_tween = turtle.tween_controller.current_tween(); - // 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 { + let mut current_preview: Vec; + + // If we have an active tween for this turtle, build the preview from tween state + if let Some(tween) = turtle_tween { + // Start with the existing contour vertices (vertices before the current tween) + current_preview = fill_state + .current_contour + .iter() + .map(|c| Vec2::new(c.x, c.y)) + .collect(); + + // If we're animating a circle command with pen down, add progressive arc vertices + if tween.start_params.pen_down { if let crate::commands::TurtleCommand::Circle { radius, angle, @@ -153,13 +155,11 @@ pub fn render_world_with_tween( // 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, + tween.start_params.position, + tween.start_params.heading, *radius, *direction, - ); - - // Calculate progress + ); // 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); @@ -193,8 +193,11 @@ pub fn render_world_with_tween( 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)); + // For Move/Goto commands, just add the current position from tween + current_preview.push(Vec2::new( + tween.current_position.x, + tween.current_position.y, + )); } } else if matches!( &tween.command, @@ -202,25 +205,38 @@ pub fn render_world_with_tween( | crate::commands::TurtleCommand::Goto(_) ) { // 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)); + current_preview.push(Vec2::new( + tween.current_position.x, + tween.current_position.y, + )); } // Add current turtle position if not already included if let Some(last) = current_preview.last() { - let current_pos = turtle.position; + let current_pos = tween.current_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)); + current_preview.push(Vec2::new( + tween.current_position.x, + tween.current_position.y, + )); } } else { + // No active tween - use the actual current contour from fill state + current_preview = fill_state + .current_contour + .iter() + .map(|c| Vec2::new(c.x, c.y)) + .collect(); + // No active tween - just show current state if !current_preview.is_empty() { if let Some(last) = current_preview.last() { - let current_pos = turtle.position; + let current_pos = turtle.params.position; if (last.x - current_pos.x).abs() > 0.1 || (last.y - current_pos.y).abs() > 0.1 { @@ -254,8 +270,8 @@ pub fn render_world_with_tween( // Draw all visible turtles for turtle in &world.turtles { - if turtle.visible { - draw_turtle(turtle); + if turtle.params.visible { + draw_turtle(&turtle.params); } } @@ -277,8 +293,8 @@ fn draw_tween_arc( direction: CircleDirection, ) { let geom = CircleGeometry::new( - tween.start_state.position, - tween.start_state.heading, + tween.start_params.position, + tween.start_params.heading, radius, direction, ); @@ -301,8 +317,8 @@ fn draw_tween_arc( radius, geom.start_angle_from_center.to_degrees(), total_angle * progress, - tween.start_state.color, - tween.start_state.pen_width, + tween.start_params.color, + tween.start_params.pen_width, ((steps as f32 * progress).ceil() as usize).max(1), direction, ) { @@ -311,15 +327,15 @@ fn draw_tween_arc( } /// Draw the turtle shape -pub fn draw_turtle(turtle: &TurtleState) { - let rotated_vertices = turtle.shape.rotated_vertices(turtle.heading); +pub fn draw_turtle(turtle_params: &TurtleParams) { + let rotated_vertices = turtle_params.shape.rotated_vertices(turtle_params.heading); - if turtle.shape.filled { + if turtle_params.shape.filled { // Draw filled polygon using Lyon tessellation if rotated_vertices.len() >= 3 { let absolute_vertices: Vec = rotated_vertices .iter() - .map(|v| turtle.position + *v) + .map(|v| turtle_params.position + *v) .collect(); // Use Lyon for turtle shape too @@ -345,8 +361,8 @@ pub fn draw_turtle(turtle: &TurtleState) { if !rotated_vertices.is_empty() { for i in 0..rotated_vertices.len() { let next_i = (i + 1) % rotated_vertices.len(); - let p1 = turtle.position + rotated_vertices[i]; - let p2 = turtle.position + rotated_vertices[next_i]; + let p1 = turtle_params.position + rotated_vertices[i]; + let p2 = turtle_params.position + rotated_vertices[next_i]; draw_line(p1.x, p1.y, p2.x, p2.y, 2.0, Color::new(0.0, 0.5, 1.0, 1.0)); } } diff --git a/turtle-lib/src/execution.rs b/turtle-lib/src/execution.rs index 88e7cda..c9f208f 100644 --- a/turtle-lib/src/execution.rs +++ b/turtle-lib/src/execution.rs @@ -2,7 +2,7 @@ use crate::circle_geometry::{CircleDirection, CircleGeometry}; use crate::commands::TurtleCommand; -use crate::state::{DrawCommand, TurtleState, TurtleWorld}; +use crate::state::{DrawCommand, Turtle, TurtleParams, TurtleWorld}; use crate::tessellation; use macroquad::prelude::*; @@ -11,18 +11,20 @@ use crate::general::AnimationSpeed; /// Execute side effects for commands that don't involve movement /// Returns true if the command was handled (caller should skip movement processing) -pub fn execute_command_side_effects( - command: &TurtleCommand, - state: &mut TurtleState, - commands: &mut Vec, -) -> bool { +pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle) -> bool { match command { TurtleCommand::BeginFill => { if state.filling.is_some() { - tracing::warn!("begin_fill() called while already filling"); + tracing::warn!( + turtle_id = state.turtle_id, + "begin_fill() called while already filling" + ); } - let fill_color = state.fill_color.unwrap_or_else(|| { - tracing::warn!("No fill_color set, using black"); + let fill_color = state.params.fill_color.unwrap_or_else(|| { + tracing::warn!( + turtle_id = state.turtle_id, + "No fill_color set, using black" + ); BLACK }); state.begin_fill(fill_color); @@ -35,11 +37,20 @@ pub fn execute_command_side_effects( fill_state.contours.push(fill_state.current_contour); } - let span = tracing::debug_span!("end_fill", contours = fill_state.contours.len()); + let span = tracing::debug_span!( + "end_fill", + turtle_id = state.turtle_id, + contours = fill_state.contours.len() + ); let _enter = span.enter(); for (i, contour) in fill_state.contours.iter().enumerate() { - tracing::debug!(contour_idx = i, vertices = contour.len(), "Contour info"); + tracing::debug!( + turtle_id = state.turtle_id, + contour_idx = i, + vertices = contour.len(), + "Contour info" + ); } if !fill_state.contours.is_empty() { @@ -48,38 +59,46 @@ pub fn execute_command_side_effects( fill_state.fill_color, ) { tracing::debug!( + turtle_id = state.turtle_id, contours = fill_state.contours.len(), - "Successfully tessellated contours" + "Successfully created fill mesh - persisting to commands" ); - commands.push(DrawCommand::Mesh { - turtle_id: 0, - data: mesh_data, - }); + state.commands.push(DrawCommand::Mesh { data: mesh_data }); } else { - tracing::error!("Failed to tessellate contours"); + tracing::error!( + turtle_id = state.turtle_id, + "Failed to tessellate contours" + ); } } } else { - tracing::warn!("end_fill() called without begin_fill()"); + tracing::warn!( + turtle_id = state.turtle_id, + "end_fill() called without begin_fill()" + ); } true } TurtleCommand::PenUp => { - state.pen_down = false; + state.params.pen_down = false; if state.filling.is_some() { - tracing::debug!("PenUp: Closing current contour"); + tracing::debug!( + turtle_id = state.turtle_id, + "PenUp: Closing current contour" + ); } state.close_fill_contour(); true } TurtleCommand::PenDown => { - state.pen_down = true; + state.params.pen_down = true; if state.filling.is_some() { tracing::debug!( - x = state.position.x, - y = state.position.y, + turtle_id = state.turtle_id, + x = state.params.position.x, + y = state.params.position.y, "PenDown: Starting new contour" ); } @@ -92,10 +111,11 @@ pub fn execute_command_side_effects( } /// Record fill vertices after movement commands have updated state +#[tracing::instrument] pub fn record_fill_vertices_after_movement( command: &TurtleCommand, - start_state: &TurtleState, - state: &mut TurtleState, + start_state: &TurtleParams, + state: &mut Turtle, ) { if state.filling.is_none() { return; @@ -131,9 +151,10 @@ pub fn record_fill_vertices_after_movement( } /// Execute a single turtle command, updating state and adding draw commands -pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world: &mut TurtleWorld) { +#[tracing::instrument] +pub fn execute_command(command: &TurtleCommand, state: &mut Turtle) { // Try to execute as side-effect-only command first - if execute_command_side_effects(command, state, &mut world.commands) { + if execute_command_side_effects(command, state) { return; // Command fully handled } @@ -143,29 +164,27 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world: // Execute movement and appearance commands match command { TurtleCommand::Move(distance) => { - let start = state.position; - let dx = distance * state.heading.cos(); - let dy = distance * state.heading.sin(); - state.position = vec2(state.position.x + dx, state.position.y + dy); + let start = state.params.position; + let dx = distance * state.params.heading.cos(); + let dy = distance * state.params.heading.sin(); + state.params.position = + vec2(state.params.position.x + dx, state.params.position.y + dy); - if state.pen_down { + if state.params.pen_down { // Draw line segment with round caps (caps handled by tessellate_stroke) if let Ok(mesh_data) = tessellation::tessellate_stroke( - &[start, state.position], - state.color, - state.pen_width, + &[start, state.params.position], + state.params.color, + state.params.pen_width, false, // not closed ) { - world.add_command(DrawCommand::Mesh { - turtle_id: 0, - data: mesh_data, - }); + state.commands.push(DrawCommand::Mesh { data: mesh_data }); } } } TurtleCommand::Turn(degrees) => { - state.heading += degrees.to_radians(); + state.params.heading += degrees.to_radians(); } TurtleCommand::Circle { @@ -174,71 +193,66 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world: steps, direction, } => { - let start_heading = state.heading; - let geom = CircleGeometry::new(state.position, start_heading, *radius, *direction); + let start_heading = state.params.heading; + let geom = + CircleGeometry::new(state.params.position, start_heading, *radius, *direction); - if state.pen_down { + if state.params.pen_down { // Use Lyon to tessellate the arc if let Ok(mesh_data) = tessellation::tessellate_arc( geom.center, *radius, geom.start_angle_from_center.to_degrees(), *angle, - state.color, - state.pen_width, + state.params.color, + state.params.pen_width, *steps, *direction, ) { - world.add_command(DrawCommand::Mesh { - turtle_id: 0, - data: mesh_data, - }); + state.commands.push(DrawCommand::Mesh { data: mesh_data }); } } // Update turtle position and heading - state.position = geom.position_at_angle(angle.to_radians()); - state.heading = match direction { + state.params.position = geom.position_at_angle(angle.to_radians()); + state.params.heading = match direction { CircleDirection::Left => start_heading - angle.to_radians(), CircleDirection::Right => start_heading + angle.to_radians(), }; } TurtleCommand::Goto(coord) => { - let start = state.position; - state.position = *coord; + let start = state.params.position; + state.params.position = *coord; - if state.pen_down { + if state.params.pen_down { // Draw line segment with round caps if let Ok(mesh_data) = tessellation::tessellate_stroke( - &[start, state.position], - state.color, - state.pen_width, + &[start, state.params.position], + state.params.color, + state.params.pen_width, false, // not closed ) { - world.add_command(DrawCommand::Mesh { - turtle_id: 0, - data: mesh_data, - }); + state.commands.push(DrawCommand::Mesh { data: mesh_data }); } } } // Appearance commands - TurtleCommand::SetColor(color) => state.color = *color, - TurtleCommand::SetFillColor(color) => state.fill_color = *color, - TurtleCommand::SetPenWidth(width) => state.pen_width = *width, + TurtleCommand::SetColor(color) => state.params.color = *color, + TurtleCommand::SetFillColor(color) => state.params.fill_color = *color, + TurtleCommand::SetPenWidth(width) => state.params.pen_width = *width, TurtleCommand::SetSpeed(speed) => state.set_speed(*speed), - TurtleCommand::SetShape(shape) => state.shape = shape.clone(), - TurtleCommand::SetHeading(heading) => state.heading = *heading, - TurtleCommand::ShowTurtle => state.visible = true, - TurtleCommand::HideTurtle => state.visible = false, + TurtleCommand::SetShape(shape) => state.params.shape = shape.clone(), + TurtleCommand::SetHeading(heading) => state.params.heading = *heading, + TurtleCommand::ShowTurtle => state.params.visible = true, + TurtleCommand::HideTurtle => state.params.visible = false, _ => {} // Already handled by execute_command_side_effects } // Record fill vertices AFTER movement - record_fill_vertices_after_movement(command, &start_state, state); + record_fill_vertices_after_movement(command, &start_state.params, state); } /// Execute command on a specific turtle by ID @@ -246,7 +260,7 @@ pub fn execute_command_with_id(command: &TurtleCommand, turtle_id: usize, world: // 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); + execute_command(command, &mut state); // Update the turtle state back if let Some(turtle_mut) = world.get_turtle_mut(turtle_id) { *turtle_mut = state; @@ -254,85 +268,22 @@ pub fn execute_command_with_id(command: &TurtleCommand, turtle_id: usize, world: } } -/// 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 { - 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, - 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, - data: mesh_data, - }); - } - } - } - _ => {} - } -} - -/// Add drawing command for a completed tween (state transition already occurred) +/// Add drawing command for a completed tween pub fn add_draw_for_completed_tween( command: &TurtleCommand, - start_state: &TurtleState, - end_state: &TurtleState, - world: &mut TurtleWorld, -) { + start_state: &TurtleParams, + end_state: &mut TurtleParams, +) -> Option { 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, - }); + return Some(DrawCommand::Mesh { data: mesh_data }); } } } @@ -349,7 +300,6 @@ pub fn add_draw_for_completed_tween( *radius, *direction, ); - if let Ok(mesh_data) = tessellation::tessellate_arc( geom.center, *radius, @@ -360,17 +310,13 @@ pub fn add_draw_for_completed_tween( *steps, *direction, ) { - world.add_command(DrawCommand::Mesh { - turtle_id: 0, - data: mesh_data, - }); + return Some(DrawCommand::Mesh { data: mesh_data }); } } } - _ => { - // Other commands don't create drawing - } + _ => (), } + None } #[cfg(test)] @@ -378,28 +324,35 @@ mod tests { use super::*; use crate::commands::TurtleCommand; use crate::shapes::TurtleShape; + use crate::TweenController; #[test] fn test_forward_left_forward() { // Test that after forward(100), left(90), forward(50) // the turtle ends up at (100, -50) from initial position (0, 0) - let mut state = TurtleState { - position: vec2(0.0, 0.0), - heading: 0.0, - pen_down: false, // Disable drawing to avoid needing TurtleWorld - pen_width: 1.0, - color: Color::new(0.0, 0.0, 0.0, 1.0), - fill_color: None, - speed: AnimationSpeed::Animated(100.0), - visible: true, - shape: TurtleShape::turtle(), + use crate::state::TurtleParams; + + let state = Turtle { + turtle_id: 0, + params: TurtleParams { + position: vec2(0.0, 0.0), + heading: 0.0, + pen_down: false, // Disable drawing to avoid needing TurtleWorld + pen_width: 1.0, + color: Color::new(0.0, 0.0, 0.0, 1.0), + fill_color: None, + visible: true, + shape: TurtleShape::turtle(), + speed: AnimationSpeed::Instant(100), + }, filling: None, + commands: Vec::new(), + tween_controller: TweenController::default(), }; // We'll use a dummy world but won't actually call drawing commands - let mut world = TurtleWorld { + let world = TurtleWorld { turtles: vec![state.clone()], - commands: Vec::new(), camera: macroquad::camera::Camera2D { zoom: vec2(1.0, 1.0), target: vec2(0.0, 0.0), @@ -413,56 +366,56 @@ mod tests { let mut state = world.turtles[0].clone(); // Initial state: position (0, 0), heading 0 (east) - assert_eq!(state.position.x, 0.0); - assert_eq!(state.position.y, 0.0); - assert_eq!(state.heading, 0.0); + assert_eq!(state.params.position.x, 0.0); + assert_eq!(state.params.position.y, 0.0); + assert_eq!(state.params.heading, 0.0); // Forward 100 - should move to (100, 0) - execute_command(&TurtleCommand::Move(100.0), &mut state, &mut world); + execute_command(&TurtleCommand::Move(100.0), &mut state); assert!( - (state.position.x - 100.0).abs() < 0.01, + (state.params.position.x - 100.0).abs() < 0.01, "After forward(100): x = {}", - state.position.x + state.params.position.x ); assert!( - (state.position.y - 0.0).abs() < 0.01, + (state.params.position.y - 0.0).abs() < 0.01, "After forward(100): y = {}", - state.position.y + state.params.position.y ); - assert!((state.heading - 0.0).abs() < 0.01); + assert!((state.params.heading - 0.0).abs() < 0.01); // Left 90 degrees - should face north (heading decreases by 90°) // In screen coords: north = -90° = -π/2 - execute_command(&TurtleCommand::Turn(-90.0), &mut state, &mut world); + execute_command(&TurtleCommand::Turn(-90.0), &mut state); assert!( - (state.position.x - 100.0).abs() < 0.01, + (state.params.position.x - 100.0).abs() < 0.01, "After left(90): x = {}", - state.position.x + state.params.position.x ); assert!( - (state.position.y - 0.0).abs() < 0.01, + (state.params.position.y - 0.0).abs() < 0.01, "After left(90): y = {}", - state.position.y + state.params.position.y ); let expected_heading = -90.0f32.to_radians(); assert!( - (state.heading - expected_heading).abs() < 0.01, + (state.params.heading - expected_heading).abs() < 0.01, "After left(90): heading = {} (expected {})", - state.heading, + state.params.heading, expected_heading ); // Forward 50 - should move north (negative Y) to (100, -50) - execute_command(&TurtleCommand::Move(50.0), &mut state, &mut world); + execute_command(&TurtleCommand::Move(50.0), &mut state); assert!( - (state.position.x - 100.0).abs() < 0.01, + (state.params.position.x - 100.0).abs() < 0.01, "Final position: x = {} (expected 100.0)", - state.position.x + state.params.position.x ); assert!( - (state.position.y - (-50.0)).abs() < 0.01, + (state.params.position.y - (-50.0)).abs() < 0.01, "Final position: y = {} (expected -50.0)", - state.position.y + state.params.position.y ); } } diff --git a/turtle-lib/src/lib.rs b/turtle-lib/src/lib.rs index 24e4895..55a82c0 100644 --- a/turtle-lib/src/lib.rs +++ b/turtle-lib/src/lib.rs @@ -62,7 +62,7 @@ pub use builders::{CurvedMovement, DirectionalMovement, Turnable, TurtlePlan, Wi pub use commands::{CommandQueue, TurtleCommand}; pub use general::{Angle, AnimationSpeed, Color, Coordinate, Length, Precision}; pub use shapes::{ShapeType, TurtleShape}; -pub use state::{DrawCommand, TurtleState, TurtleWorld}; +pub use state::{DrawCommand, Turtle, TurtleWorld}; pub use tweening::TweenController; // Re-export the turtle_main macro @@ -78,9 +78,6 @@ use macroquad::prelude::*; /// Main turtle application struct pub struct TurtleApp { world: TurtleWorld, - /// One tween controller per turtle (indexed by turtle ID) - tween_controllers: Vec, - speed: AnimationSpeed, // Mouse panning state is_dragging: bool, last_mouse_pos: Option, @@ -94,8 +91,6 @@ impl TurtleApp { pub fn new() -> Self { Self { world: TurtleWorld::new(), - tween_controllers: Vec::new(), - speed: AnimationSpeed::default(), is_dragging: false, last_mouse_pos: None, zoom_level: 1.0, @@ -104,14 +99,23 @@ impl TurtleApp { /// 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 + self.world.add_turtle() } - /// Add commands to a specific turtle + /// Add commands from a turtle plan to the application for the default turtle (ID 0) + /// + /// 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 + /// * `queue` - The command queue to execute + #[must_use] + pub fn with_commands(self, queue: CommandQueue) -> Self { + self.with_commands_for_turtle(0, queue) + } + + /// Add commands from a turtle plan to the application for a specific turtle /// /// Speed is controlled by `SetSpeed` commands in the queue. /// Use `set_speed()` on the turtle plan to set animation speed. @@ -121,17 +125,16 @@ impl TurtleApp { /// * `turtle_id` - The ID of the turtle to control /// * `queue` - The command queue to execute #[must_use] - 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)); + pub fn with_commands_for_turtle(mut self, turtle_id: usize, queue: CommandQueue) -> Self { + // Ensure turtle exists + while self.world.turtles.len() <= turtle_id { + self.world.add_turtle(); } - // Append commands to the controller - self.tween_controllers[turtle_id].append_commands(queue); + // Append commands to the turtle's controller + if let Some(turtle) = self.world.get_turtle_mut(turtle_id) { + turtle.tween_controller.append_commands(queue); + } self } @@ -144,15 +147,14 @@ impl TurtleApp { /// 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)); + // Ensure turtle exists + while self.world.turtles.len() <= turtle_id { + self.world.add_turtle(); } - self.tween_controllers[turtle_id].append_commands(plan.build()); + if let Some(turtle) = self.world.get_turtle_mut(turtle_id) { + turtle.tween_controller.append_commands(plan.build()); + } } /// Update animation state (call every frame) @@ -161,31 +163,22 @@ impl TurtleApp { self.handle_mouse_panning(); self.handle_mouse_zoom(); - // 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); + // Update all turtles' tween controllers + for turtle in self.world.turtles.iter_mut() { + // Extract draw_commands and controller temporarily to avoid borrow conflicts - 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() - }; + // Update the controller + let completed_commands = TweenController::update(turtle); - // Put commands back - self.world.commands = commands; - - // Process all completed commands - for (completed_cmd, start_state, end_state) in completed_commands { - execution::add_draw_for_completed_tween_with_id( + // Process all completed commands and add to the turtle's commands + for (completed_cmd, tween_start, mut end_state) in completed_commands { + let draw_command = execution::add_draw_for_completed_tween( &completed_cmd, - &start_state, - &end_state, - &mut self.world, - turtle_id, + &tween_start, + &mut end_state, ); + // Add the new draw commands to the turtle + turtle.commands.extend(draw_command); } } } @@ -237,21 +230,16 @@ impl TurtleApp { /// Render the turtle world (call every frame) pub fn render(&self) { - // Find the first active tween (turtle_id is now stored in the tween itself) - let active_tween = self - .tween_controllers - .iter() - .find_map(|controller| controller.current_tween()); - - drawing::render_world_with_tween(&self.world, active_tween, self.zoom_level); + drawing::render_world_with_tweens(&self.world, self.zoom_level); } /// Check if all commands have been executed #[must_use] pub fn is_complete(&self) -> bool { - self.tween_controllers + self.world + .turtles .iter() - .all(TweenController::is_complete) + .all(|turtle| turtle.tween_controller.is_complete()) } /// Get reference to the world state diff --git a/turtle-lib/src/state.rs b/turtle-lib/src/state.rs index 3f9b1b8..f5f2aff 100644 --- a/turtle-lib/src/state.rs +++ b/turtle-lib/src/state.rs @@ -1,7 +1,9 @@ //! Turtle state and world state management -use crate::general::{Angle, AnimationSpeed, Color, Coordinate, Precision}; +use crate::commands::CommandQueue; +use crate::general::{Angle, AnimationSpeed, Color, Coordinate}; use crate::shapes::TurtleShape; +use crate::tweening::TweenController; use macroquad::prelude::*; /// State during active fill operation @@ -21,61 +23,88 @@ pub struct FillState { pub fill_color: Color, } -/// State of a single turtle +/// Parameters that define a turtle's visual state #[derive(Clone, Debug)] -pub struct TurtleState { - pub position: Coordinate, - pub heading: Precision, // radians +pub struct TurtleParams { + pub position: Vec2, + pub heading: f32, pub pen_down: bool, + pub pen_width: f32, pub color: Color, pub fill_color: Option, - pub pen_width: Precision, - pub speed: AnimationSpeed, pub visible: bool, - pub shape: TurtleShape, - - // Fill tracking - pub filling: Option, + pub shape: crate::shapes::TurtleShape, + pub speed: AnimationSpeed, } -impl Default for TurtleState { +impl Default for TurtleParams { + /// Create TurtleParams from default values fn default() -> Self { Self { position: vec2(0.0, 0.0), - heading: 0.0, // pointing right (0 radians) + heading: 0.0, pen_down: true, + pen_width: 2.0, color: BLACK, fill_color: None, - pen_width: 2.0, - speed: AnimationSpeed::default(), visible: true, shape: TurtleShape::turtle(), - filling: None, + speed: AnimationSpeed::default(), } } } -impl TurtleState { +/// State of a single turtle +#[derive(Clone, Debug)] +pub struct Turtle { + pub turtle_id: usize, + pub params: TurtleParams, + + // Fill tracking + pub filling: Option, + + // Drawing commands created by this turtle + pub commands: Vec, + + // Animation controller for this turtle + pub tween_controller: TweenController, +} + +impl Default for Turtle { + fn default() -> Self { + Self { + turtle_id: 0, + params: TurtleParams::default(), + filling: None, + commands: Vec::new(), + tween_controller: TweenController::new(CommandQueue::new(), AnimationSpeed::default()), + } + } +} + +impl Turtle { pub fn set_speed(&mut self, speed: AnimationSpeed) { - self.speed = speed; + self.params.speed = speed; } #[must_use] pub fn heading_angle(&self) -> Angle { - Angle::radians(self.heading) + Angle::radians(self.params.heading) } - /// Reset turtle to default state + /// Reset turtle to default state (preserves turtle_id) pub fn reset(&mut self) { + let id = self.turtle_id; *self = Self::default(); + self.turtle_id = id; } /// Start recording fill vertices pub fn begin_fill(&mut self, fill_color: Color) { self.filling = Some(FillState { - start_position: self.position, + start_position: self.params.position, contours: Vec::new(), - current_contour: vec![self.position], + current_contour: vec![self.params.position], fill_color, }); } @@ -83,16 +112,17 @@ impl TurtleState { /// Record current position if filling and pen is down pub fn record_fill_vertex(&mut self) { if let Some(ref mut fill_state) = self.filling { - if self.pen_down { + if self.params.pen_down { tracing::trace!( - x = self.position.x, - y = self.position.y, + turtle_id = self.turtle_id, + x = self.params.position.x, + y = self.params.position.y, vertices = fill_state.current_contour.len() + 1, "Adding vertex to current contour" ); - fill_state.current_contour.push(self.position); + fill_state.current_contour.push(self.params.position); } else { - tracing::trace!("Skipping vertex (pen is up)"); + tracing::trace!(turtle_id = self.turtle_id, "Skipping vertex (pen is up)"); } } } @@ -101,12 +131,14 @@ impl TurtleState { pub fn close_fill_contour(&mut self) { if let Some(ref mut fill_state) = self.filling { tracing::debug!( + turtle_id = self.turtle_id, vertices = fill_state.current_contour.len(), "close_fill_contour called" ); // Only close if we have vertices in current contour if fill_state.current_contour.len() >= 2 { tracing::debug!( + turtle_id = self.turtle_id, vertices = fill_state.current_contour.len(), first_x = fill_state.current_contour[0].x, first_y = fill_state.current_contour[0].y, @@ -118,19 +150,27 @@ impl TurtleState { let contour = std::mem::take(&mut fill_state.current_contour); fill_state.contours.push(contour); tracing::debug!( + turtle_id = self.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 = self.turtle_id, vertices = fill_state.current_contour.len(), "Current contour has insufficient vertices, not closing" ); } else { - tracing::warn!("Current contour is empty, nothing to close"); + tracing::warn!( + turtle_id = self.turtle_id, + "Current contour is empty, nothing to close" + ); } } else { - tracing::warn!("close_fill_contour called but no active fill state"); + tracing::warn!( + turtle_id = self.turtle_id, + "close_fill_contour called but no active fill state" + ); } } @@ -139,12 +179,13 @@ impl TurtleState { if let Some(ref mut fill_state) = self.filling { // Start new contour at current position tracing::debug!( - x = self.position.x, - y = self.position.y, + x = self.params.position.x, + y = self.params.position.y, completed_contours = fill_state.contours.len(), + self.turtle_id = self.turtle_id, "Starting new contour" ); - fill_state.current_contour = vec![self.position]; + fill_state.current_contour = vec![self.params.position]; } } @@ -160,11 +201,12 @@ impl TurtleState { steps: u32, ) { if let Some(ref mut fill_state) = self.filling { - if self.pen_down { + if self.params.pen_down { // Sample points along the arc based on steps - let num_samples = steps as usize; + let num_samples = steps.max(1); tracing::trace!( + turtle_id = self.turtle_id, center_x = center.x, center_y = center.y, radius = radius, @@ -189,6 +231,7 @@ impl TurtleState { center.y + radius * current_angle.sin(), ); tracing::trace!( + turtle_id = self.turtle_id, vertex_idx = i, x = vertex.x, y = vertex.y, @@ -230,16 +273,13 @@ impl MeshData { #[derive(Clone, Debug)] pub enum DrawCommand { /// Pre-tessellated mesh data (lines, arcs, circles, polygons - all use this) - /// Includes the turtle ID that created this command - Mesh { turtle_id: usize, data: MeshData }, + Mesh { data: MeshData }, } /// The complete turtle world containing all drawing state pub struct TurtleWorld { /// All turtles in the world (indexed by turtle ID) - pub turtles: Vec, - /// All drawing commands from all turtles - pub commands: Vec, + pub turtles: Vec, pub camera: Camera2D, pub background_color: Color, } @@ -247,9 +287,12 @@ pub struct TurtleWorld { impl TurtleWorld { #[must_use] pub fn new() -> Self { + let mut default_turtle = Turtle::default(); + default_turtle.turtle_id = 0; + default_turtle.tween_controller = + TweenController::new(CommandQueue::new(), AnimationSpeed::default()); Self { - turtles: vec![TurtleState::default()], // Start with one default turtle - commands: Vec::new(), + turtles: vec![default_turtle], // Start with one default turtle camera: Camera2D { zoom: vec2(1.0 / screen_width() * 2.0, 1.0 / screen_height() * 2.0), target: vec2(0.0, 0.0), @@ -261,18 +304,23 @@ 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 + let turtle_id = self.turtles.len(); + let mut new_turtle = Turtle::default(); + new_turtle.turtle_id = turtle_id; + new_turtle.tween_controller = + TweenController::new(CommandQueue::new(), AnimationSpeed::default()); + self.turtles.push(new_turtle); + turtle_id } /// Get turtle by ID #[must_use] - pub fn get_turtle(&self, id: usize) -> Option<&TurtleState> { + pub fn get_turtle(&self, id: usize) -> Option<&Turtle> { self.turtles.get(id) } /// Get mutable turtle by ID - pub fn get_turtle_mut(&mut self, id: usize) -> Option<&mut TurtleState> { + pub fn get_turtle_mut(&mut self, id: usize) -> Option<&mut Turtle> { self.turtles.get_mut(id) } @@ -280,22 +328,15 @@ impl TurtleWorld { pub fn reset_turtle(&mut self, turtle_id: usize) { if let Some(turtle) = self.get_turtle_mut(turtle_id) { turtle.reset(); + turtle.turtle_id = turtle_id; // Preserve turtle_id after 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(); - for turtle in &mut self.turtles { + for (id, turtle) in self.turtles.iter_mut().enumerate() { turtle.reset(); + turtle.turtle_id = id; // Preserve turtle_id after reset } } } diff --git a/turtle-lib/src/tweening.rs b/turtle-lib/src/tweening.rs index 1824a4f..c6ce7d7 100644 --- a/turtle-lib/src/tweening.rs +++ b/turtle-lib/src/tweening.rs @@ -3,7 +3,7 @@ use crate::circle_geometry::{CircleDirection, CircleGeometry}; use crate::commands::{CommandQueue, TurtleCommand}; use crate::general::AnimationSpeed; -use crate::state::TurtleState; +use crate::state::{Turtle, TurtleParams}; use macroquad::prelude::*; use tween::{CubicInOut, TweenValue, Tweener}; @@ -44,20 +44,33 @@ impl From for Vec2 { } /// Controls tweening of turtle commands +#[derive(Clone, Debug)] pub struct TweenController { - turtle_id: usize, queue: CommandQueue, current_tween: Option, speed: AnimationSpeed, } +impl Default for TweenController { + fn default() -> Self { + Self { + queue: CommandQueue::new(), + current_tween: None, + speed: AnimationSpeed::default(), + } + } +} + +#[derive(Clone, Debug)] 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 start_params: TurtleParams, + pub target_params: TurtleParams, + pub current_position: Vec2, + pub current_heading: f32, position_tweener: Tweener, heading_tweener: Tweener, pen_width_tweener: Tweener, @@ -65,9 +78,8 @@ pub struct CommandTween { impl TweenController { #[must_use] - pub fn new(turtle_id: usize, queue: CommandQueue, speed: AnimationSpeed) -> Self { + pub fn new(queue: CommandQueue, speed: AnimationSpeed) -> Self { Self { - turtle_id, queue, current_tween: None, speed, @@ -86,53 +98,53 @@ impl TweenController { /// 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 - #[allow(clippy::too_many_lines)] - pub fn update( - &mut self, - state: &mut TurtleState, - commands: &mut Vec, - ) -> Vec<(TurtleCommand, TurtleState, TurtleState)> { + pub fn update(state: &mut Turtle) -> Vec<(TurtleCommand, TurtleParams, TurtleParams)> { // In instant mode, execute commands up to the draw calls per frame limit - if let AnimationSpeed::Instant(max_draw_calls) = self.speed { - let mut completed_commands = Vec::new(); + if let AnimationSpeed::Instant(max_draw_calls) = state.tween_controller.speed { + let mut completed_commands: Vec<(TurtleCommand, TurtleParams, TurtleParams)> = + Vec::new(); let mut draw_call_count = 0; - for command in self.queue.by_ref() { - let start_state = state.clone(); + // Consume commands from the real queue so the current_index advances + loop { + let command = match state.tween_controller.queue.next() { + Some(cmd) => cmd, + None => break, + }; // Handle SetSpeed command to potentially switch modes if let TurtleCommand::SetSpeed(new_speed) = &command { - state.set_speed(*new_speed); - self.speed = *new_speed; - if matches!(self.speed, AnimationSpeed::Animated(_)) { + state.params.speed = *new_speed; + state.tween_controller.speed = *new_speed; + if matches!(state.tween_controller.speed, AnimationSpeed::Animated(_)) { break; } continue; } // Execute side-effect-only commands using centralized helper - if crate::execution::execute_command_side_effects(&command, state, commands) { + if crate::execution::execute_command_side_effects(&command, state) { continue; // Command fully handled } - // Execute movement commands - let target_state = Self::calculate_target_state(state, &command); - *state = target_state.clone(); + // Save start state and compute target state + let start_params = state.params.clone(); + let target_params = Self::calculate_target_state(&start_params, &command); - // Record fill vertices AFTER movement using centralized helper + // Update state to the target (instant execution) + state.params = target_params.clone(); + + // Record fill vertices AFTER movement crate::execution::record_fill_vertices_after_movement( &command, - &start_state, + &start_params, state, ); - let end_state = state.clone(); - - // Collect drawable commands - if Self::command_creates_drawing(&command) && start_state.pen_down { - completed_commands.push((command, start_state, end_state)); + // Collect drawable commands (return start and target so caller can create draw meshes) + if Self::command_creates_drawing(&command) && start_params.pen_down { + completed_commands.push((command, start_params.clone(), target_params.clone())); draw_call_count += 1; - if draw_call_count >= max_draw_calls { break; } @@ -143,14 +155,14 @@ impl TweenController { } // Process current tween - if let Some(ref mut tween) = self.current_tween { + if let Some(ref mut tween) = state.tween_controller.current_tween { let elapsed = get_time() - tween.start_time; // Use tweeners to calculate current values // For circles, calculate position along the arc instead of straight line let progress = tween.heading_tweener.move_to(elapsed); - state.position = match &tween.command { + let current_position = match &tween.command { TurtleCommand::Circle { radius, angle, @@ -159,8 +171,8 @@ impl TweenController { } => { let angle_traveled = angle.to_radians() * progress; calculate_circle_position( - tween.start_state.position, - tween.start_state.heading, + tween.start_params.position, + tween.start_params.heading, *radius, angle_traveled, *direction, @@ -172,109 +184,109 @@ impl TweenController { } }; + state.params.position = current_position; + tween.current_position = current_position; + // Heading changes proportionally with progress for all commands - state.heading = normalize_angle(match &tween.command { + let current_heading = normalize_angle(match &tween.command { TurtleCommand::Circle { angle, direction, .. } => match direction { CircleDirection::Left => { - tween.start_state.heading - angle.to_radians() * progress + tween.start_params.heading - angle.to_radians() * progress } CircleDirection::Right => { - tween.start_state.heading + angle.to_radians() * progress + tween.start_params.heading + angle.to_radians() * progress } }, TurtleCommand::Turn(angle) => { - tween.start_state.heading + angle.to_radians() * progress + tween.start_params.heading + angle.to_radians() * progress } _ => { // For other commands that change heading, lerp directly - let heading_diff = tween.target_state.heading - tween.start_state.heading; - tween.start_state.heading + heading_diff * progress + let heading_diff = tween.target_params.heading - tween.start_params.heading; + tween.start_params.heading + heading_diff * progress } }); - state.pen_width = tween.pen_width_tweener.move_to(elapsed); + + state.params.heading = current_heading; + tween.current_heading = current_heading; + state.params.pen_width = tween.pen_width_tweener.move_to(elapsed); // Discrete properties (switch at 50% progress) let progress = (elapsed / tween.duration).min(1.0); if progress >= 0.5 { - state.pen_down = tween.target_state.pen_down; - state.color = tween.target_state.color; - state.fill_color = tween.target_state.fill_color; - state.visible = tween.target_state.visible; - state.shape = tween.target_state.shape.clone(); + state.params.pen_down = tween.target_params.pen_down; + state.params.color = tween.target_params.color; + state.params.fill_color = tween.target_params.fill_color; + state.params.visible = tween.target_params.visible; + state.params.shape = tween.target_params.shape.clone(); } // Check if tween is finished (use heading_tweener as it's used by all commands) if tween.heading_tweener.is_finished() { - let start_state = tween.start_state.clone(); - *state = tween.target_state.clone(); - let end_state = state.clone(); + let start_params = tween.start_params.clone(); + let target_params = tween.target_params.clone(); + let command = tween.command.clone(); - let completed_command = tween.command.clone(); - self.current_tween = None; + // Drop the mutable borrow of tween before mutably borrowing state + state.params = target_params.clone(); - // Execute side-effect-only commands using centralized helper - if crate::execution::execute_command_side_effects( - &completed_command, - state, - commands, - ) { - return self.update(state, commands); // Continue to next command - } - - // Record fill vertices for movement commands using centralized helper crate::execution::record_fill_vertices_after_movement( - &completed_command, - &start_state, + &command, + &start_params, state, ); - // Return drawable commands - if Self::command_creates_drawing(&completed_command) && start_state.pen_down { - return vec![(completed_command, start_state, end_state)]; + state.tween_controller.current_tween = None; + + // Execute side-effect-only commands using centralized helper + if crate::execution::execute_command_side_effects(&command, state) { + return Self::update(state); // Continue to next command } - return self.update(state, commands); // Continue to next command + + // Return drawable commands using the original start and target params + if Self::command_creates_drawing(&command) && start_params.pen_down { + return vec![(command, start_params.clone(), target_params.clone())]; + } + + return Self::update(state); // Continue to next command } return Vec::new(); } // Start next tween - if let Some(command) = self.queue.next() { + if let Some(command) = state.tween_controller.queue.next() { let command_clone = command.clone(); // Handle commands that should execute immediately (no animation) match &command_clone { TurtleCommand::SetSpeed(new_speed) => { state.set_speed(*new_speed); - self.speed = *new_speed; - if matches!(self.speed, AnimationSpeed::Instant(_)) { - return self.update(state, commands); + state.tween_controller.speed = *new_speed; + if matches!(state.tween_controller.speed, AnimationSpeed::Instant(_)) { + return Self::update(state); } - return self.update(state, commands); + return Self::update(state); } _ => { // Use centralized helper for side effects - if crate::execution::execute_command_side_effects( - &command_clone, - state, - commands, - ) { - return self.update(state, commands); + if crate::execution::execute_command_side_effects(&command_clone, state) { + return Self::update(state); } } } - let speed = state.speed; // Extract speed before borrowing self + let speed = state.tween_controller.speed; // Extract speed before borrowing self let duration = Self::calculate_duration_with_state(&command_clone, state, speed); // Calculate target state - let target_state = Self::calculate_target_state(state, &command_clone); + let target_state = Self::calculate_target_state(&state.params, &command_clone); // Create tweeners for smooth animation let position_tweener = Tweener::new( - TweenVec2::from(state.position), + TweenVec2::from(state.params.position), TweenVec2::from(target_state.position), duration, CubicInOut, @@ -286,19 +298,21 @@ impl TweenController { ); let pen_width_tweener = Tweener::new( - state.pen_width, + state.params.pen_width, target_state.pen_width, duration, CubicInOut, ); - self.current_tween = Some(CommandTween { - turtle_id: self.turtle_id, + state.tween_controller.current_tween = Some(CommandTween { + turtle_id: state.turtle_id, command: command_clone, start_time: get_time(), duration, - start_state: state.clone(), - target_state, + start_params: state.params.clone(), + target_params: target_state.clone(), + current_position: state.params.position, + current_heading: state.params.heading, position_tweener, heading_tweener, pen_width_tweener, @@ -327,7 +341,7 @@ impl TweenController { fn calculate_duration_with_state( command: &TurtleCommand, - current: &TurtleState, + current: &Turtle, speed: AnimationSpeed, ) -> f64 { let speed = speed.value(); @@ -344,8 +358,8 @@ impl TweenController { } TurtleCommand::Goto(target) => { // Calculate actual distance from current position to target - let dx = target.x - current.position.x; - let dy = target.y - current.position.y; + let dx = target.x - current.params.position.x; + let dy = target.y - current.params.position.y; let distance = (dx * dx + dy * dy).sqrt(); distance / speed } @@ -354,7 +368,7 @@ impl TweenController { f64::from(base_time.max(0.01)) // Minimum duration } - fn calculate_target_state(current: &TurtleState, command: &TurtleCommand) -> TurtleState { + fn calculate_target_state(current: &TurtleParams, command: &TurtleCommand) -> TurtleParams { let mut target = current.clone(); match command {