add multi turtle support
This commit is contained in:
parent
bbb9348497
commit
3509060390
@ -19,7 +19,7 @@ use syn::{parse_macro_input, ItemFn};
|
|||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```ignore
|
||||||
/// use turtle_lib::*;
|
/// use turtle_lib::*;
|
||||||
///
|
///
|
||||||
/// #[turtle_main("My Turtle Drawing")]
|
/// #[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:
|
/// If you need macroquad types not re-exported by turtle_lib:
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```ignore
|
||||||
/// use macroquad::prelude::SKYBLUE; // Import specific items
|
/// use macroquad::prelude::SKYBLUE; // Import specific items
|
||||||
/// use turtle_lib::*;
|
/// use turtle_lib::*;
|
||||||
///
|
///
|
||||||
@ -47,7 +47,7 @@ use syn::{parse_macro_input, ItemFn};
|
|||||||
///
|
///
|
||||||
/// This expands to approximately:
|
/// This expands to approximately:
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```ignore
|
||||||
/// use macroquad::prelude::*;
|
/// use macroquad::prelude::*;
|
||||||
/// use turtle_lib::*;
|
/// use turtle_lib::*;
|
||||||
///
|
///
|
||||||
@ -61,7 +61,7 @@ use syn::{parse_macro_input, ItemFn};
|
|||||||
/// turtle.right(90.0);
|
/// turtle.right(90.0);
|
||||||
/// turtle.forward(100.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 {
|
/// loop {
|
||||||
/// clear_background(WHITE);
|
/// clear_background(WHITE);
|
||||||
@ -108,7 +108,7 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream {
|
|||||||
#fn_name(&mut turtle);
|
#fn_name(&mut turtle);
|
||||||
|
|
||||||
let mut app = turtle_lib::TurtleApp::new()
|
let mut app = turtle_lib::TurtleApp::new()
|
||||||
.with_commands(0, turtle.build());
|
.with_commands(turtle.build());
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
macroquad::prelude::clear_background(macroquad::prelude::WHITE);
|
macroquad::prelude::clear_background(macroquad::prelude::WHITE);
|
||||||
@ -145,7 +145,7 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream {
|
|||||||
#fn_block
|
#fn_block
|
||||||
|
|
||||||
let mut app = turtle_lib::TurtleApp::new()
|
let mut app = turtle_lib::TurtleApp::new()
|
||||||
.with_commands(0, turtle.build());
|
.with_commands(turtle.build());
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
macroquad::prelude::clear_background(macroquad::prelude::WHITE);
|
macroquad::prelude::clear_background(macroquad::prelude::WHITE);
|
||||||
|
|||||||
@ -8,7 +8,10 @@ license = "MIT OR Apache-2.0"
|
|||||||
macroquad = "0.4"
|
macroquad = "0.4"
|
||||||
tween = "2.1.0"
|
tween = "2.1.0"
|
||||||
lyon = "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" }
|
turtle-lib-macros = { path = "../turtle-lib-macros" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@ -71,7 +71,7 @@ async fn main() {
|
|||||||
|
|
||||||
println!("Building and executing turtle plan...");
|
println!("Building and executing turtle plan...");
|
||||||
// Execute the plan
|
// Execute the plan
|
||||||
let mut app = TurtleApp::new().with_commands(0, turtle.build());
|
let mut app = TurtleApp::new().with_commands(turtle.build());
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
clear_background(Color::new(0.95, 0.95, 0.98, 1.0));
|
clear_background(Color::new(0.95, 0.95, 0.98, 1.0));
|
||||||
|
|||||||
@ -13,11 +13,13 @@ fn draw(turtle: &mut TurtlePlan) {
|
|||||||
turtle.set_speed(999);
|
turtle.set_speed(999);
|
||||||
turtle.circle_left(100.0, 540.0, 72); // partial circle to the left
|
turtle.circle_left(100.0, 540.0, 72); // partial circle to the left
|
||||||
|
|
||||||
|
turtle.begin_fill();
|
||||||
turtle.forward(150.0);
|
turtle.forward(150.0);
|
||||||
turtle.set_speed(100);
|
turtle.set_speed(100);
|
||||||
turtle.set_pen_color(BLUE);
|
turtle.set_pen_color(BLUE);
|
||||||
turtle.circle_right(50.0, 270.0, 72); // partial circle to the right
|
turtle.circle_right(50.0, 270.0, 72); // partial circle to the right
|
||||||
// Set animation speed
|
// Set animation speed
|
||||||
|
turtle.end_fill();
|
||||||
turtle.set_speed(20);
|
turtle.set_speed(20);
|
||||||
turtle.forward(150.0);
|
turtle.forward(150.0);
|
||||||
turtle.circle_left(50.0, 180.0, 12);
|
turtle.circle_left(50.0, 180.0, 12);
|
||||||
|
|||||||
@ -40,7 +40,7 @@ use turtle_lib::*;
|
|||||||
#[turtle_main("Dragon Curve")]
|
#[turtle_main("Dragon Curve")]
|
||||||
fn draw_dragon(turtle: &mut TurtlePlan) {
|
fn draw_dragon(turtle: &mut TurtlePlan) {
|
||||||
// Fast drawing
|
// Fast drawing
|
||||||
turtle.set_speed(1200);
|
turtle.set_speed(1020);
|
||||||
|
|
||||||
// Start position
|
// Start position
|
||||||
turtle.pen_up();
|
turtle.pen_up();
|
||||||
|
|||||||
@ -111,7 +111,7 @@ async fn main() {
|
|||||||
// Set animation speed
|
// Set animation speed
|
||||||
t.set_speed(500);
|
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 target_fps = 1.0; // 1 frame per second for debugging
|
||||||
let frame_time = 1.0 / target_fps;
|
let frame_time = 1.0 / target_fps;
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
use turtle_lib::*;
|
use turtle_lib::*;
|
||||||
|
|
||||||
#[turtle_main("Hello Turtle")]
|
#[turtle_main]
|
||||||
fn hello() {
|
fn hello() {
|
||||||
turtle.set_pen_color(BLUE);
|
turtle.set_pen_color(BLUE);
|
||||||
for _ in 0..4 {
|
for _ in 0..4 {
|
||||||
|
|||||||
@ -31,7 +31,7 @@ async fn main() {
|
|||||||
.with_env_filter(
|
.with_env_filter(
|
||||||
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||||
// Default to showing info-level logs if RUST_LOG is not set
|
// 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
|
.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
|
t.set_speed(100); // Slow animation to see the logs in real-time
|
||||||
|
|
||||||
// Create turtle app
|
// Create turtle app
|
||||||
let mut app = TurtleApp::new().with_commands(0, t.build());
|
let mut app = TurtleApp::new().with_commands(t.build());
|
||||||
|
|
||||||
// Main loop
|
// Main loop
|
||||||
loop {
|
loop {
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
125
turtle-lib/examples/multi_turtle.rs
Normal file
125
turtle-lib/examples/multi_turtle.rs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -49,7 +49,7 @@ fn draw(turtle: &mut TurtlePlan) {
|
|||||||
turtle.pen_up();
|
turtle.pen_up();
|
||||||
turtle.backward(80.0);
|
turtle.backward(80.0);
|
||||||
turtle.left(90.0);
|
turtle.left(90.0);
|
||||||
turtle.forward(50.0);
|
turtle.backward(50.0);
|
||||||
turtle.right(90.0);
|
turtle.right(90.0);
|
||||||
turtle.pen_down();
|
turtle.pen_down();
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -24,7 +24,4 @@ fn draw(turtle: &mut TurtlePlan) {
|
|||||||
turtle.pen_down();
|
turtle.pen_down();
|
||||||
turtle.circle_right(8.0, 360.0, 12);
|
turtle.circle_right(8.0, 360.0, 12);
|
||||||
turtle.end_fill();
|
turtle.end_fill();
|
||||||
|
|
||||||
// Set animation speed
|
|
||||||
turtle.set_speed(1000);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,7 +45,7 @@ pub enum TurtleCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Queue of turtle commands with execution state
|
/// Queue of turtle commands with execution state
|
||||||
#[derive(Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct CommandQueue {
|
pub struct CommandQueue {
|
||||||
commands: Vec<TurtleCommand>,
|
commands: Vec<TurtleCommand>,
|
||||||
current_index: usize,
|
current_index: usize,
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
//! Rendering logic using Macroquad and Lyon tessellation
|
//! Rendering logic using Macroquad and Lyon tessellation
|
||||||
|
|
||||||
use crate::circle_geometry::{CircleDirection, CircleGeometry};
|
use crate::circle_geometry::{CircleDirection, CircleGeometry};
|
||||||
use crate::state::{DrawCommand, TurtleState, TurtleWorld};
|
use crate::state::{DrawCommand, TurtleParams, TurtleWorld};
|
||||||
use crate::tessellation;
|
use crate::tessellation;
|
||||||
use crate::tweening::CommandTween;
|
|
||||||
use macroquad::prelude::*;
|
use macroquad::prelude::*;
|
||||||
|
|
||||||
// Import the easing function from the tween crate
|
// Import the easing function from the tween crate
|
||||||
@ -25,19 +24,21 @@ pub fn render_world(world: &TurtleWorld) {
|
|||||||
// Set camera
|
// Set camera
|
||||||
set_camera(&camera);
|
set_camera(&camera);
|
||||||
|
|
||||||
// Draw all accumulated commands
|
// Draw all accumulated commands from all turtles
|
||||||
for cmd in &world.commands {
|
for turtle in &world.turtles {
|
||||||
|
for cmd in &turtle.commands {
|
||||||
match cmd {
|
match cmd {
|
||||||
DrawCommand::Mesh { data, .. } => {
|
DrawCommand::Mesh { data } => {
|
||||||
draw_mesh(&data.to_mesh());
|
draw_mesh(&data.to_mesh());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw all visible turtles
|
// Draw all visible turtles
|
||||||
for turtle in &world.turtles {
|
for turtle in &world.turtles {
|
||||||
if turtle.visible {
|
if turtle.params.visible {
|
||||||
draw_turtle(turtle);
|
draw_turtle(&turtle.params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,11 +48,7 @@ pub fn render_world(world: &TurtleWorld) {
|
|||||||
|
|
||||||
/// Render the turtle world with active tween visualization
|
/// Render the turtle world with active tween visualization
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
pub fn render_world_with_tween(
|
pub fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) {
|
||||||
world: &TurtleWorld,
|
|
||||||
active_tween: Option<&CommandTween>,
|
|
||||||
zoom_level: f32,
|
|
||||||
) {
|
|
||||||
// Update camera zoom based on current screen size to prevent stretching
|
// 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)
|
// Apply user zoom level by dividing by it (smaller zoom value = more zoomed in)
|
||||||
let camera = Camera2D {
|
let camera = Camera2D {
|
||||||
@ -66,21 +63,22 @@ pub fn render_world_with_tween(
|
|||||||
// Set camera
|
// Set camera
|
||||||
set_camera(&camera);
|
set_camera(&camera);
|
||||||
|
|
||||||
// Draw all accumulated commands
|
// Draw all accumulated commands from all turtles
|
||||||
for cmd in &world.commands {
|
for turtle in &world.turtles {
|
||||||
|
for cmd in &turtle.commands {
|
||||||
match cmd {
|
match cmd {
|
||||||
DrawCommand::Mesh { data, .. } => {
|
DrawCommand::Mesh { data } => {
|
||||||
draw_mesh(&data.to_mesh());
|
draw_mesh(&data.to_mesh());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw in-progress tween line if pen is down
|
// Draw in-progress tween lines for all active tweens
|
||||||
// Extract turtle_id from active tween (default to 0 if no active tween)
|
for turtle in world.turtles.iter() {
|
||||||
let active_turtle_id = active_tween.map_or(0, |tween| tween.turtle_id);
|
if let Some(tween) = turtle.tween_controller.current_tween() {
|
||||||
|
// Only draw if pen is down
|
||||||
if let Some(tween) = active_tween {
|
if tween.start_params.pen_down {
|
||||||
if tween.start_state.pen_down {
|
|
||||||
match &tween.command {
|
match &tween.command {
|
||||||
crate::commands::TurtleCommand::Circle {
|
crate::commands::TurtleCommand::Circle {
|
||||||
radius,
|
radius,
|
||||||
@ -92,33 +90,31 @@ pub fn render_world_with_tween(
|
|||||||
draw_tween_arc(tween, *radius, *angle, *steps, *direction);
|
draw_tween_arc(tween, *radius, *angle, *steps, *direction);
|
||||||
}
|
}
|
||||||
_ if should_draw_tween_line(&tween.command) => {
|
_ if should_draw_tween_line(&tween.command) => {
|
||||||
// Draw straight line for other movement commands (use active turtle)
|
// Draw straight line for other movement commands (use tween's current position)
|
||||||
if let Some(turtle) = world.turtles.get(active_turtle_id) {
|
|
||||||
draw_line(
|
draw_line(
|
||||||
tween.start_state.position.x,
|
tween.start_params.position.x,
|
||||||
tween.start_state.position.y,
|
tween.start_params.position.y,
|
||||||
turtle.position.x,
|
tween.current_position.x,
|
||||||
turtle.position.y,
|
tween.current_position.y,
|
||||||
tween.start_state.pen_width,
|
tween.start_params.pen_width,
|
||||||
tween.start_state.color,
|
tween.start_params.color,
|
||||||
);
|
);
|
||||||
// Add circle at current position for smooth line joins
|
// Add circle at current position for smooth line joins
|
||||||
draw_circle(
|
draw_circle(
|
||||||
turtle.position.x,
|
tween.current_position.x,
|
||||||
turtle.position.y,
|
tween.current_position.y,
|
||||||
tween.start_state.pen_width / 2.0,
|
tween.start_params.pen_width / 2.0,
|
||||||
tween.start_state.color,
|
tween.start_params.color,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw live fill preview if currently filling (always show, not just during tweens)
|
// Draw live fill preview for all turtles that are currently filling
|
||||||
// Use the active turtle if available, otherwise default to turtle 0
|
for turtle in world.turtles.iter() {
|
||||||
if let Some(turtle) = world.turtles.get(active_turtle_id) {
|
|
||||||
if let Some(ref fill_state) = turtle.filling {
|
if let Some(ref fill_state) = turtle.filling {
|
||||||
// Build all contours: completed contours + current contour with animation
|
// Build all contours: completed contours + current contour with animation
|
||||||
let mut all_contours: Vec<Vec<Vec2>> = Vec::new();
|
let mut all_contours: Vec<Vec<Vec2>> = Vec::new();
|
||||||
@ -133,16 +129,22 @@ pub fn render_world_with_tween(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build current contour with animation
|
// Build current contour with animation
|
||||||
let mut current_preview: Vec<Vec2> = fill_state
|
// Find the tween for this specific turtle
|
||||||
|
let turtle_tween = turtle.tween_controller.current_tween();
|
||||||
|
|
||||||
|
let mut current_preview: Vec<Vec2>;
|
||||||
|
|
||||||
|
// 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
|
.current_contour
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| Vec2::new(c.x, c.y))
|
.map(|c| Vec2::new(c.x, c.y))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// If we have an active tween, add progressive vertices
|
// If we're animating a circle command with pen down, add progressive arc vertices
|
||||||
if let Some(tween) = active_tween {
|
if tween.start_params.pen_down {
|
||||||
// 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 {
|
if let crate::commands::TurtleCommand::Circle {
|
||||||
radius,
|
radius,
|
||||||
angle,
|
angle,
|
||||||
@ -153,13 +155,11 @@ pub fn render_world_with_tween(
|
|||||||
// Calculate partial arc vertices based on current progress
|
// Calculate partial arc vertices based on current progress
|
||||||
use crate::circle_geometry::CircleGeometry;
|
use crate::circle_geometry::CircleGeometry;
|
||||||
let geom = CircleGeometry::new(
|
let geom = CircleGeometry::new(
|
||||||
tween.start_state.position,
|
tween.start_params.position,
|
||||||
tween.start_state.heading,
|
tween.start_params.heading,
|
||||||
*radius,
|
*radius,
|
||||||
*direction,
|
*direction,
|
||||||
);
|
); // Calculate progress
|
||||||
|
|
||||||
// Calculate progress
|
|
||||||
let elapsed = get_time() - tween.start_time;
|
let elapsed = get_time() - tween.start_time;
|
||||||
let progress = (elapsed / tween.duration).min(1.0);
|
let progress = (elapsed / tween.duration).min(1.0);
|
||||||
let eased_progress = CubicInOut.tween(1.0, progress as f32);
|
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::Move(_)
|
||||||
| crate::commands::TurtleCommand::Goto(_)
|
| crate::commands::TurtleCommand::Goto(_)
|
||||||
) {
|
) {
|
||||||
// For Move/Goto commands, just add the current position
|
// For Move/Goto commands, just add the current position from tween
|
||||||
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 if matches!(
|
} else if matches!(
|
||||||
&tween.command,
|
&tween.command,
|
||||||
@ -202,25 +205,38 @@ pub fn render_world_with_tween(
|
|||||||
| crate::commands::TurtleCommand::Goto(_)
|
| crate::commands::TurtleCommand::Goto(_)
|
||||||
) {
|
) {
|
||||||
// For Move/Goto with pen up during filling, still add current position for preview
|
// 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
|
// Add current turtle position if not already included
|
||||||
if let Some(last) = current_preview.last() {
|
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
|
// 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
|
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));
|
current_preview.push(Vec2::new(current_pos.x, current_pos.y));
|
||||||
}
|
}
|
||||||
} else if !current_preview.is_empty() {
|
} 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 {
|
} 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
|
// No active tween - just show current state
|
||||||
if !current_preview.is_empty() {
|
if !current_preview.is_empty() {
|
||||||
if let Some(last) = current_preview.last() {
|
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
|
if (last.x - current_pos.x).abs() > 0.1
|
||||||
|| (last.y - current_pos.y).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
|
// Draw all visible turtles
|
||||||
for turtle in &world.turtles {
|
for turtle in &world.turtles {
|
||||||
if turtle.visible {
|
if turtle.params.visible {
|
||||||
draw_turtle(turtle);
|
draw_turtle(&turtle.params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,8 +293,8 @@ fn draw_tween_arc(
|
|||||||
direction: CircleDirection,
|
direction: CircleDirection,
|
||||||
) {
|
) {
|
||||||
let geom = CircleGeometry::new(
|
let geom = CircleGeometry::new(
|
||||||
tween.start_state.position,
|
tween.start_params.position,
|
||||||
tween.start_state.heading,
|
tween.start_params.heading,
|
||||||
radius,
|
radius,
|
||||||
direction,
|
direction,
|
||||||
);
|
);
|
||||||
@ -301,8 +317,8 @@ fn draw_tween_arc(
|
|||||||
radius,
|
radius,
|
||||||
geom.start_angle_from_center.to_degrees(),
|
geom.start_angle_from_center.to_degrees(),
|
||||||
total_angle * progress,
|
total_angle * progress,
|
||||||
tween.start_state.color,
|
tween.start_params.color,
|
||||||
tween.start_state.pen_width,
|
tween.start_params.pen_width,
|
||||||
((steps as f32 * progress).ceil() as usize).max(1),
|
((steps as f32 * progress).ceil() as usize).max(1),
|
||||||
direction,
|
direction,
|
||||||
) {
|
) {
|
||||||
@ -311,15 +327,15 @@ fn draw_tween_arc(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the turtle shape
|
/// Draw the turtle shape
|
||||||
pub fn draw_turtle(turtle: &TurtleState) {
|
pub fn draw_turtle(turtle_params: &TurtleParams) {
|
||||||
let rotated_vertices = turtle.shape.rotated_vertices(turtle.heading);
|
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
|
// Draw filled polygon using Lyon tessellation
|
||||||
if rotated_vertices.len() >= 3 {
|
if rotated_vertices.len() >= 3 {
|
||||||
let absolute_vertices: Vec<Vec2> = rotated_vertices
|
let absolute_vertices: Vec<Vec2> = rotated_vertices
|
||||||
.iter()
|
.iter()
|
||||||
.map(|v| turtle.position + *v)
|
.map(|v| turtle_params.position + *v)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Use Lyon for turtle shape too
|
// Use Lyon for turtle shape too
|
||||||
@ -345,8 +361,8 @@ pub fn draw_turtle(turtle: &TurtleState) {
|
|||||||
if !rotated_vertices.is_empty() {
|
if !rotated_vertices.is_empty() {
|
||||||
for i in 0..rotated_vertices.len() {
|
for i in 0..rotated_vertices.len() {
|
||||||
let next_i = (i + 1) % rotated_vertices.len();
|
let next_i = (i + 1) % rotated_vertices.len();
|
||||||
let p1 = turtle.position + rotated_vertices[i];
|
let p1 = turtle_params.position + rotated_vertices[i];
|
||||||
let p2 = turtle.position + rotated_vertices[next_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));
|
draw_line(p1.x, p1.y, p2.x, p2.y, 2.0, Color::new(0.0, 0.5, 1.0, 1.0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use crate::circle_geometry::{CircleDirection, CircleGeometry};
|
use crate::circle_geometry::{CircleDirection, CircleGeometry};
|
||||||
use crate::commands::TurtleCommand;
|
use crate::commands::TurtleCommand;
|
||||||
use crate::state::{DrawCommand, TurtleState, TurtleWorld};
|
use crate::state::{DrawCommand, Turtle, TurtleParams, TurtleWorld};
|
||||||
use crate::tessellation;
|
use crate::tessellation;
|
||||||
use macroquad::prelude::*;
|
use macroquad::prelude::*;
|
||||||
|
|
||||||
@ -11,18 +11,20 @@ use crate::general::AnimationSpeed;
|
|||||||
|
|
||||||
/// Execute side effects for commands that don't involve movement
|
/// Execute side effects for commands that don't involve movement
|
||||||
/// Returns true if the command was handled (caller should skip movement processing)
|
/// Returns true if the command was handled (caller should skip movement processing)
|
||||||
pub fn execute_command_side_effects(
|
pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle) -> bool {
|
||||||
command: &TurtleCommand,
|
|
||||||
state: &mut TurtleState,
|
|
||||||
commands: &mut Vec<DrawCommand>,
|
|
||||||
) -> bool {
|
|
||||||
match command {
|
match command {
|
||||||
TurtleCommand::BeginFill => {
|
TurtleCommand::BeginFill => {
|
||||||
if state.filling.is_some() {
|
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(|| {
|
let fill_color = state.params.fill_color.unwrap_or_else(|| {
|
||||||
tracing::warn!("No fill_color set, using black");
|
tracing::warn!(
|
||||||
|
turtle_id = state.turtle_id,
|
||||||
|
"No fill_color set, using black"
|
||||||
|
);
|
||||||
BLACK
|
BLACK
|
||||||
});
|
});
|
||||||
state.begin_fill(fill_color);
|
state.begin_fill(fill_color);
|
||||||
@ -35,11 +37,20 @@ pub fn execute_command_side_effects(
|
|||||||
fill_state.contours.push(fill_state.current_contour);
|
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();
|
let _enter = span.enter();
|
||||||
|
|
||||||
for (i, contour) in fill_state.contours.iter().enumerate() {
|
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() {
|
if !fill_state.contours.is_empty() {
|
||||||
@ -48,38 +59,46 @@ pub fn execute_command_side_effects(
|
|||||||
fill_state.fill_color,
|
fill_state.fill_color,
|
||||||
) {
|
) {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
|
turtle_id = state.turtle_id,
|
||||||
contours = fill_state.contours.len(),
|
contours = fill_state.contours.len(),
|
||||||
"Successfully tessellated contours"
|
"Successfully created fill mesh - persisting to commands"
|
||||||
);
|
);
|
||||||
commands.push(DrawCommand::Mesh {
|
state.commands.push(DrawCommand::Mesh { data: mesh_data });
|
||||||
turtle_id: 0,
|
|
||||||
data: mesh_data,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
tracing::error!("Failed to tessellate contours");
|
tracing::error!(
|
||||||
|
turtle_id = state.turtle_id,
|
||||||
|
"Failed to tessellate contours"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!("end_fill() called without begin_fill()");
|
tracing::warn!(
|
||||||
|
turtle_id = state.turtle_id,
|
||||||
|
"end_fill() called without begin_fill()"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
TurtleCommand::PenUp => {
|
TurtleCommand::PenUp => {
|
||||||
state.pen_down = false;
|
state.params.pen_down = false;
|
||||||
if state.filling.is_some() {
|
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();
|
state.close_fill_contour();
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
TurtleCommand::PenDown => {
|
TurtleCommand::PenDown => {
|
||||||
state.pen_down = true;
|
state.params.pen_down = true;
|
||||||
if state.filling.is_some() {
|
if state.filling.is_some() {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
x = state.position.x,
|
turtle_id = state.turtle_id,
|
||||||
y = state.position.y,
|
x = state.params.position.x,
|
||||||
|
y = state.params.position.y,
|
||||||
"PenDown: Starting new contour"
|
"PenDown: Starting new contour"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -92,10 +111,11 @@ pub fn execute_command_side_effects(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Record fill vertices after movement commands have updated state
|
/// Record fill vertices after movement commands have updated state
|
||||||
|
#[tracing::instrument]
|
||||||
pub fn record_fill_vertices_after_movement(
|
pub fn record_fill_vertices_after_movement(
|
||||||
command: &TurtleCommand,
|
command: &TurtleCommand,
|
||||||
start_state: &TurtleState,
|
start_state: &TurtleParams,
|
||||||
state: &mut TurtleState,
|
state: &mut Turtle,
|
||||||
) {
|
) {
|
||||||
if state.filling.is_none() {
|
if state.filling.is_none() {
|
||||||
return;
|
return;
|
||||||
@ -131,9 +151,10 @@ pub fn record_fill_vertices_after_movement(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Execute a single turtle command, updating state and adding draw commands
|
/// 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
|
// 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
|
return; // Command fully handled
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,29 +164,27 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
|
|||||||
// Execute movement and appearance commands
|
// Execute movement and appearance commands
|
||||||
match command {
|
match command {
|
||||||
TurtleCommand::Move(distance) => {
|
TurtleCommand::Move(distance) => {
|
||||||
let start = state.position;
|
let start = state.params.position;
|
||||||
let dx = distance * state.heading.cos();
|
let dx = distance * state.params.heading.cos();
|
||||||
let dy = distance * state.heading.sin();
|
let dy = distance * state.params.heading.sin();
|
||||||
state.position = vec2(state.position.x + dx, state.position.y + dy);
|
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)
|
// Draw line segment with round caps (caps handled by tessellate_stroke)
|
||||||
if let Ok(mesh_data) = tessellation::tessellate_stroke(
|
if let Ok(mesh_data) = tessellation::tessellate_stroke(
|
||||||
&[start, state.position],
|
&[start, state.params.position],
|
||||||
state.color,
|
state.params.color,
|
||||||
state.pen_width,
|
state.params.pen_width,
|
||||||
false, // not closed
|
false, // not closed
|
||||||
) {
|
) {
|
||||||
world.add_command(DrawCommand::Mesh {
|
state.commands.push(DrawCommand::Mesh { data: mesh_data });
|
||||||
turtle_id: 0,
|
|
||||||
data: mesh_data,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TurtleCommand::Turn(degrees) => {
|
TurtleCommand::Turn(degrees) => {
|
||||||
state.heading += degrees.to_radians();
|
state.params.heading += degrees.to_radians();
|
||||||
}
|
}
|
||||||
|
|
||||||
TurtleCommand::Circle {
|
TurtleCommand::Circle {
|
||||||
@ -174,71 +193,66 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
|
|||||||
steps,
|
steps,
|
||||||
direction,
|
direction,
|
||||||
} => {
|
} => {
|
||||||
let start_heading = state.heading;
|
let start_heading = state.params.heading;
|
||||||
let geom = CircleGeometry::new(state.position, start_heading, *radius, *direction);
|
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
|
// Use Lyon to tessellate the arc
|
||||||
if let Ok(mesh_data) = tessellation::tessellate_arc(
|
if let Ok(mesh_data) = tessellation::tessellate_arc(
|
||||||
geom.center,
|
geom.center,
|
||||||
*radius,
|
*radius,
|
||||||
geom.start_angle_from_center.to_degrees(),
|
geom.start_angle_from_center.to_degrees(),
|
||||||
*angle,
|
*angle,
|
||||||
state.color,
|
state.params.color,
|
||||||
state.pen_width,
|
state.params.pen_width,
|
||||||
*steps,
|
*steps,
|
||||||
*direction,
|
*direction,
|
||||||
) {
|
) {
|
||||||
world.add_command(DrawCommand::Mesh {
|
state.commands.push(DrawCommand::Mesh { data: mesh_data });
|
||||||
turtle_id: 0,
|
|
||||||
data: mesh_data,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update turtle position and heading
|
// Update turtle position and heading
|
||||||
state.position = geom.position_at_angle(angle.to_radians());
|
state.params.position = geom.position_at_angle(angle.to_radians());
|
||||||
state.heading = match direction {
|
state.params.heading = match direction {
|
||||||
CircleDirection::Left => start_heading - angle.to_radians(),
|
CircleDirection::Left => start_heading - angle.to_radians(),
|
||||||
CircleDirection::Right => start_heading + angle.to_radians(),
|
CircleDirection::Right => start_heading + angle.to_radians(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
TurtleCommand::Goto(coord) => {
|
TurtleCommand::Goto(coord) => {
|
||||||
let start = state.position;
|
let start = state.params.position;
|
||||||
state.position = *coord;
|
state.params.position = *coord;
|
||||||
|
|
||||||
if state.pen_down {
|
if state.params.pen_down {
|
||||||
// Draw line segment with round caps
|
// Draw line segment with round caps
|
||||||
if let Ok(mesh_data) = tessellation::tessellate_stroke(
|
if let Ok(mesh_data) = tessellation::tessellate_stroke(
|
||||||
&[start, state.position],
|
&[start, state.params.position],
|
||||||
state.color,
|
state.params.color,
|
||||||
state.pen_width,
|
state.params.pen_width,
|
||||||
false, // not closed
|
false, // not closed
|
||||||
) {
|
) {
|
||||||
world.add_command(DrawCommand::Mesh {
|
state.commands.push(DrawCommand::Mesh { data: mesh_data });
|
||||||
turtle_id: 0,
|
|
||||||
data: mesh_data,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Appearance commands
|
// Appearance commands
|
||||||
TurtleCommand::SetColor(color) => state.color = *color,
|
TurtleCommand::SetColor(color) => state.params.color = *color,
|
||||||
TurtleCommand::SetFillColor(color) => state.fill_color = *color,
|
TurtleCommand::SetFillColor(color) => state.params.fill_color = *color,
|
||||||
TurtleCommand::SetPenWidth(width) => state.pen_width = *width,
|
TurtleCommand::SetPenWidth(width) => state.params.pen_width = *width,
|
||||||
TurtleCommand::SetSpeed(speed) => state.set_speed(*speed),
|
TurtleCommand::SetSpeed(speed) => state.set_speed(*speed),
|
||||||
TurtleCommand::SetShape(shape) => state.shape = shape.clone(),
|
TurtleCommand::SetShape(shape) => state.params.shape = shape.clone(),
|
||||||
TurtleCommand::SetHeading(heading) => state.heading = *heading,
|
TurtleCommand::SetHeading(heading) => state.params.heading = *heading,
|
||||||
TurtleCommand::ShowTurtle => state.visible = true,
|
TurtleCommand::ShowTurtle => state.params.visible = true,
|
||||||
TurtleCommand::HideTurtle => state.visible = false,
|
TurtleCommand::HideTurtle => state.params.visible = false,
|
||||||
|
|
||||||
_ => {} // Already handled by execute_command_side_effects
|
_ => {} // Already handled by execute_command_side_effects
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record fill vertices AFTER movement
|
// 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
|
/// 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
|
// Clone turtle state to avoid borrow checker issues
|
||||||
if let Some(turtle) = world.get_turtle(turtle_id) {
|
if let Some(turtle) = world.get_turtle(turtle_id) {
|
||||||
let mut state = turtle.clone();
|
let mut state = turtle.clone();
|
||||||
execute_command(command, &mut state, world);
|
execute_command(command, &mut state);
|
||||||
// Update the turtle state back
|
// Update the turtle state back
|
||||||
if let Some(turtle_mut) = world.get_turtle_mut(turtle_id) {
|
if let Some(turtle_mut) = world.get_turtle_mut(turtle_id) {
|
||||||
*turtle_mut = state;
|
*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
|
/// Add drawing command for a completed tween
|
||||||
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)
|
|
||||||
pub fn add_draw_for_completed_tween(
|
pub fn add_draw_for_completed_tween(
|
||||||
command: &TurtleCommand,
|
command: &TurtleCommand,
|
||||||
start_state: &TurtleState,
|
start_state: &TurtleParams,
|
||||||
end_state: &TurtleState,
|
end_state: &mut TurtleParams,
|
||||||
world: &mut TurtleWorld,
|
) -> Option<DrawCommand> {
|
||||||
) {
|
|
||||||
match command {
|
match command {
|
||||||
TurtleCommand::Move(_) | TurtleCommand::Goto(_) => {
|
TurtleCommand::Move(_) | TurtleCommand::Goto(_) => {
|
||||||
if start_state.pen_down {
|
if start_state.pen_down {
|
||||||
// Draw line segment with round caps
|
|
||||||
if let Ok(mesh_data) = tessellation::tessellate_stroke(
|
if let Ok(mesh_data) = tessellation::tessellate_stroke(
|
||||||
&[start_state.position, end_state.position],
|
&[start_state.position, end_state.position],
|
||||||
start_state.color,
|
start_state.color,
|
||||||
start_state.pen_width,
|
start_state.pen_width,
|
||||||
false,
|
false,
|
||||||
) {
|
) {
|
||||||
world.add_command(DrawCommand::Mesh {
|
return Some(DrawCommand::Mesh { data: mesh_data });
|
||||||
turtle_id: 0,
|
|
||||||
data: mesh_data,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -349,7 +300,6 @@ pub fn add_draw_for_completed_tween(
|
|||||||
*radius,
|
*radius,
|
||||||
*direction,
|
*direction,
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Ok(mesh_data) = tessellation::tessellate_arc(
|
if let Ok(mesh_data) = tessellation::tessellate_arc(
|
||||||
geom.center,
|
geom.center,
|
||||||
*radius,
|
*radius,
|
||||||
@ -360,17 +310,13 @@ pub fn add_draw_for_completed_tween(
|
|||||||
*steps,
|
*steps,
|
||||||
*direction,
|
*direction,
|
||||||
) {
|
) {
|
||||||
world.add_command(DrawCommand::Mesh {
|
return Some(DrawCommand::Mesh { data: mesh_data });
|
||||||
turtle_id: 0,
|
|
||||||
data: mesh_data,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => (),
|
||||||
// Other commands don't create drawing
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -378,28 +324,35 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::commands::TurtleCommand;
|
use crate::commands::TurtleCommand;
|
||||||
use crate::shapes::TurtleShape;
|
use crate::shapes::TurtleShape;
|
||||||
|
use crate::TweenController;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_forward_left_forward() {
|
fn test_forward_left_forward() {
|
||||||
// Test that after forward(100), left(90), forward(50)
|
// Test that after forward(100), left(90), forward(50)
|
||||||
// the turtle ends up at (100, -50) from initial position (0, 0)
|
// the turtle ends up at (100, -50) from initial position (0, 0)
|
||||||
let mut state = TurtleState {
|
use crate::state::TurtleParams;
|
||||||
|
|
||||||
|
let state = Turtle {
|
||||||
|
turtle_id: 0,
|
||||||
|
params: TurtleParams {
|
||||||
position: vec2(0.0, 0.0),
|
position: vec2(0.0, 0.0),
|
||||||
heading: 0.0,
|
heading: 0.0,
|
||||||
pen_down: false, // Disable drawing to avoid needing TurtleWorld
|
pen_down: false, // Disable drawing to avoid needing TurtleWorld
|
||||||
pen_width: 1.0,
|
pen_width: 1.0,
|
||||||
color: Color::new(0.0, 0.0, 0.0, 1.0),
|
color: Color::new(0.0, 0.0, 0.0, 1.0),
|
||||||
fill_color: None,
|
fill_color: None,
|
||||||
speed: AnimationSpeed::Animated(100.0),
|
|
||||||
visible: true,
|
visible: true,
|
||||||
shape: TurtleShape::turtle(),
|
shape: TurtleShape::turtle(),
|
||||||
|
speed: AnimationSpeed::Instant(100),
|
||||||
|
},
|
||||||
filling: None,
|
filling: None,
|
||||||
|
commands: Vec::new(),
|
||||||
|
tween_controller: TweenController::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// We'll use a dummy world but won't actually call drawing commands
|
// We'll use a dummy world but won't actually call drawing commands
|
||||||
let mut world = TurtleWorld {
|
let world = TurtleWorld {
|
||||||
turtles: vec![state.clone()],
|
turtles: vec![state.clone()],
|
||||||
commands: Vec::new(),
|
|
||||||
camera: macroquad::camera::Camera2D {
|
camera: macroquad::camera::Camera2D {
|
||||||
zoom: vec2(1.0, 1.0),
|
zoom: vec2(1.0, 1.0),
|
||||||
target: vec2(0.0, 0.0),
|
target: vec2(0.0, 0.0),
|
||||||
@ -413,56 +366,56 @@ mod tests {
|
|||||||
let mut state = world.turtles[0].clone();
|
let mut state = world.turtles[0].clone();
|
||||||
|
|
||||||
// Initial state: position (0, 0), heading 0 (east)
|
// Initial state: position (0, 0), heading 0 (east)
|
||||||
assert_eq!(state.position.x, 0.0);
|
assert_eq!(state.params.position.x, 0.0);
|
||||||
assert_eq!(state.position.y, 0.0);
|
assert_eq!(state.params.position.y, 0.0);
|
||||||
assert_eq!(state.heading, 0.0);
|
assert_eq!(state.params.heading, 0.0);
|
||||||
|
|
||||||
// Forward 100 - should move to (100, 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!(
|
assert!(
|
||||||
(state.position.x - 100.0).abs() < 0.01,
|
(state.params.position.x - 100.0).abs() < 0.01,
|
||||||
"After forward(100): x = {}",
|
"After forward(100): x = {}",
|
||||||
state.position.x
|
state.params.position.x
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
(state.position.y - 0.0).abs() < 0.01,
|
(state.params.position.y - 0.0).abs() < 0.01,
|
||||||
"After forward(100): y = {}",
|
"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°)
|
// Left 90 degrees - should face north (heading decreases by 90°)
|
||||||
// In screen coords: north = -90° = -π/2
|
// 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!(
|
assert!(
|
||||||
(state.position.x - 100.0).abs() < 0.01,
|
(state.params.position.x - 100.0).abs() < 0.01,
|
||||||
"After left(90): x = {}",
|
"After left(90): x = {}",
|
||||||
state.position.x
|
state.params.position.x
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
(state.position.y - 0.0).abs() < 0.01,
|
(state.params.position.y - 0.0).abs() < 0.01,
|
||||||
"After left(90): y = {}",
|
"After left(90): y = {}",
|
||||||
state.position.y
|
state.params.position.y
|
||||||
);
|
);
|
||||||
let expected_heading = -90.0f32.to_radians();
|
let expected_heading = -90.0f32.to_radians();
|
||||||
assert!(
|
assert!(
|
||||||
(state.heading - expected_heading).abs() < 0.01,
|
(state.params.heading - expected_heading).abs() < 0.01,
|
||||||
"After left(90): heading = {} (expected {})",
|
"After left(90): heading = {} (expected {})",
|
||||||
state.heading,
|
state.params.heading,
|
||||||
expected_heading
|
expected_heading
|
||||||
);
|
);
|
||||||
|
|
||||||
// Forward 50 - should move north (negative Y) to (100, -50)
|
// 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!(
|
assert!(
|
||||||
(state.position.x - 100.0).abs() < 0.01,
|
(state.params.position.x - 100.0).abs() < 0.01,
|
||||||
"Final position: x = {} (expected 100.0)",
|
"Final position: x = {} (expected 100.0)",
|
||||||
state.position.x
|
state.params.position.x
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
(state.position.y - (-50.0)).abs() < 0.01,
|
(state.params.position.y - (-50.0)).abs() < 0.01,
|
||||||
"Final position: y = {} (expected -50.0)",
|
"Final position: y = {} (expected -50.0)",
|
||||||
state.position.y
|
state.params.position.y
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,7 +62,7 @@ pub use builders::{CurvedMovement, DirectionalMovement, Turnable, TurtlePlan, Wi
|
|||||||
pub use commands::{CommandQueue, TurtleCommand};
|
pub use commands::{CommandQueue, TurtleCommand};
|
||||||
pub use general::{Angle, AnimationSpeed, Color, Coordinate, Length, Precision};
|
pub use general::{Angle, AnimationSpeed, Color, Coordinate, Length, Precision};
|
||||||
pub use shapes::{ShapeType, TurtleShape};
|
pub use shapes::{ShapeType, TurtleShape};
|
||||||
pub use state::{DrawCommand, TurtleState, TurtleWorld};
|
pub use state::{DrawCommand, Turtle, TurtleWorld};
|
||||||
pub use tweening::TweenController;
|
pub use tweening::TweenController;
|
||||||
|
|
||||||
// Re-export the turtle_main macro
|
// Re-export the turtle_main macro
|
||||||
@ -78,9 +78,6 @@ use macroquad::prelude::*;
|
|||||||
/// Main turtle application struct
|
/// Main turtle application struct
|
||||||
pub struct TurtleApp {
|
pub struct TurtleApp {
|
||||||
world: TurtleWorld,
|
world: TurtleWorld,
|
||||||
/// One tween controller per turtle (indexed by turtle ID)
|
|
||||||
tween_controllers: Vec<TweenController>,
|
|
||||||
speed: AnimationSpeed,
|
|
||||||
// Mouse panning state
|
// Mouse panning state
|
||||||
is_dragging: bool,
|
is_dragging: bool,
|
||||||
last_mouse_pos: Option<Vec2>,
|
last_mouse_pos: Option<Vec2>,
|
||||||
@ -94,8 +91,6 @@ impl TurtleApp {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
world: TurtleWorld::new(),
|
world: TurtleWorld::new(),
|
||||||
tween_controllers: Vec::new(),
|
|
||||||
speed: AnimationSpeed::default(),
|
|
||||||
is_dragging: false,
|
is_dragging: false,
|
||||||
last_mouse_pos: None,
|
last_mouse_pos: None,
|
||||||
zoom_level: 1.0,
|
zoom_level: 1.0,
|
||||||
@ -104,14 +99,23 @@ impl TurtleApp {
|
|||||||
|
|
||||||
/// Add a new turtle and return its ID
|
/// Add a new turtle and return its ID
|
||||||
pub fn add_turtle(&mut self) -> usize {
|
pub fn add_turtle(&mut self) -> usize {
|
||||||
let id = self.world.add_turtle();
|
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
|
/// 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.
|
/// Speed is controlled by `SetSpeed` commands in the queue.
|
||||||
/// Use `set_speed()` on the turtle plan to set animation speed.
|
/// 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
|
/// * `turtle_id` - The ID of the turtle to control
|
||||||
/// * `queue` - The command queue to execute
|
/// * `queue` - The command queue to execute
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_commands(mut self, turtle_id: usize, queue: CommandQueue) -> Self {
|
pub fn with_commands_for_turtle(mut self, turtle_id: usize, queue: CommandQueue) -> Self {
|
||||||
// Ensure we have a controller for this turtle
|
// Ensure turtle exists
|
||||||
while self.tween_controllers.len() <= turtle_id {
|
while self.world.turtles.len() <= turtle_id {
|
||||||
let id = self.tween_controllers.len();
|
self.world.add_turtle();
|
||||||
let speed = self.speed;
|
|
||||||
self.tween_controllers
|
|
||||||
.push(TweenController::new(id, CommandQueue::new(), speed));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append commands to the controller
|
// Append commands to the turtle's controller
|
||||||
self.tween_controllers[turtle_id].append_commands(queue);
|
if let Some(turtle) = self.world.get_turtle_mut(turtle_id) {
|
||||||
|
turtle.tween_controller.append_commands(queue);
|
||||||
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,15 +147,14 @@ impl TurtleApp {
|
|||||||
|
|
||||||
/// Append commands to a turtle's animation queue
|
/// Append commands to a turtle's animation queue
|
||||||
pub fn append_to_queue(&mut self, turtle_id: usize, plan: TurtlePlan) {
|
pub fn append_to_queue(&mut self, turtle_id: usize, plan: TurtlePlan) {
|
||||||
// Ensure we have a controller for this turtle
|
// Ensure turtle exists
|
||||||
while self.tween_controllers.len() <= turtle_id {
|
while self.world.turtles.len() <= turtle_id {
|
||||||
let id = self.tween_controllers.len();
|
self.world.add_turtle();
|
||||||
let speed = AnimationSpeed::default();
|
|
||||||
self.tween_controllers
|
|
||||||
.push(TweenController::new(id, CommandQueue::new(), speed));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
/// Update animation state (call every frame)
|
||||||
@ -161,31 +163,22 @@ impl TurtleApp {
|
|||||||
self.handle_mouse_panning();
|
self.handle_mouse_panning();
|
||||||
self.handle_mouse_zoom();
|
self.handle_mouse_zoom();
|
||||||
|
|
||||||
// Update all active tween controllers
|
// Update all turtles' tween controllers
|
||||||
// Process each turtle separately to avoid borrow conflicts
|
for turtle in self.world.turtles.iter_mut() {
|
||||||
let turtle_count = self.tween_controllers.len();
|
// Extract draw_commands and controller temporarily to avoid borrow conflicts
|
||||||
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);
|
|
||||||
|
|
||||||
let completed_commands = if let Some(turtle) = self.world.get_turtle_mut(turtle_id) {
|
// Update the controller
|
||||||
self.tween_controllers[turtle_id].update(turtle, &mut commands)
|
let completed_commands = TweenController::update(turtle);
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Put commands back
|
// Process all completed commands and add to the turtle's commands
|
||||||
self.world.commands = commands;
|
for (completed_cmd, tween_start, mut end_state) in completed_commands {
|
||||||
|
let draw_command = execution::add_draw_for_completed_tween(
|
||||||
// Process all completed commands
|
|
||||||
for (completed_cmd, start_state, end_state) in completed_commands {
|
|
||||||
execution::add_draw_for_completed_tween_with_id(
|
|
||||||
&completed_cmd,
|
&completed_cmd,
|
||||||
&start_state,
|
&tween_start,
|
||||||
&end_state,
|
&mut end_state,
|
||||||
&mut self.world,
|
|
||||||
turtle_id,
|
|
||||||
);
|
);
|
||||||
|
// 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)
|
/// Render the turtle world (call every frame)
|
||||||
pub fn render(&self) {
|
pub fn render(&self) {
|
||||||
// Find the first active tween (turtle_id is now stored in the tween itself)
|
drawing::render_world_with_tweens(&self.world, self.zoom_level);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if all commands have been executed
|
/// Check if all commands have been executed
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn is_complete(&self) -> bool {
|
pub fn is_complete(&self) -> bool {
|
||||||
self.tween_controllers
|
self.world
|
||||||
|
.turtles
|
||||||
.iter()
|
.iter()
|
||||||
.all(TweenController::is_complete)
|
.all(|turtle| turtle.tween_controller.is_complete())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get reference to the world state
|
/// Get reference to the world state
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
//! Turtle state and world state management
|
//! 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::shapes::TurtleShape;
|
||||||
|
use crate::tweening::TweenController;
|
||||||
use macroquad::prelude::*;
|
use macroquad::prelude::*;
|
||||||
|
|
||||||
/// State during active fill operation
|
/// State during active fill operation
|
||||||
@ -21,61 +23,88 @@ pub struct FillState {
|
|||||||
pub fill_color: Color,
|
pub fill_color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// State of a single turtle
|
/// Parameters that define a turtle's visual state
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct TurtleState {
|
pub struct TurtleParams {
|
||||||
pub position: Coordinate,
|
pub position: Vec2,
|
||||||
pub heading: Precision, // radians
|
pub heading: f32,
|
||||||
pub pen_down: bool,
|
pub pen_down: bool,
|
||||||
|
pub pen_width: f32,
|
||||||
pub color: Color,
|
pub color: Color,
|
||||||
pub fill_color: Option<Color>,
|
pub fill_color: Option<Color>,
|
||||||
pub pen_width: Precision,
|
|
||||||
pub speed: AnimationSpeed,
|
|
||||||
pub visible: bool,
|
pub visible: bool,
|
||||||
pub shape: TurtleShape,
|
pub shape: crate::shapes::TurtleShape,
|
||||||
|
pub speed: AnimationSpeed,
|
||||||
// Fill tracking
|
|
||||||
pub filling: Option<FillState>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TurtleState {
|
impl Default for TurtleParams {
|
||||||
|
/// Create TurtleParams from default values
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
position: vec2(0.0, 0.0),
|
position: vec2(0.0, 0.0),
|
||||||
heading: 0.0, // pointing right (0 radians)
|
heading: 0.0,
|
||||||
pen_down: true,
|
pen_down: true,
|
||||||
|
pen_width: 2.0,
|
||||||
color: BLACK,
|
color: BLACK,
|
||||||
fill_color: None,
|
fill_color: None,
|
||||||
pen_width: 2.0,
|
|
||||||
speed: AnimationSpeed::default(),
|
|
||||||
visible: true,
|
visible: true,
|
||||||
shape: TurtleShape::turtle(),
|
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<FillState>,
|
||||||
|
|
||||||
|
// Drawing commands created by this turtle
|
||||||
|
pub commands: Vec<DrawCommand>,
|
||||||
|
|
||||||
|
// 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) {
|
pub fn set_speed(&mut self, speed: AnimationSpeed) {
|
||||||
self.speed = speed;
|
self.params.speed = speed;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn heading_angle(&self) -> Angle {
|
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) {
|
pub fn reset(&mut self) {
|
||||||
|
let id = self.turtle_id;
|
||||||
*self = Self::default();
|
*self = Self::default();
|
||||||
|
self.turtle_id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start recording fill vertices
|
/// Start recording fill vertices
|
||||||
pub fn begin_fill(&mut self, fill_color: Color) {
|
pub fn begin_fill(&mut self, fill_color: Color) {
|
||||||
self.filling = Some(FillState {
|
self.filling = Some(FillState {
|
||||||
start_position: self.position,
|
start_position: self.params.position,
|
||||||
contours: Vec::new(),
|
contours: Vec::new(),
|
||||||
current_contour: vec![self.position],
|
current_contour: vec![self.params.position],
|
||||||
fill_color,
|
fill_color,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -83,16 +112,17 @@ impl TurtleState {
|
|||||||
/// Record current position if filling and pen is down
|
/// Record current position if filling and pen is down
|
||||||
pub fn record_fill_vertex(&mut self) {
|
pub fn record_fill_vertex(&mut self) {
|
||||||
if let Some(ref mut fill_state) = self.filling {
|
if let Some(ref mut fill_state) = self.filling {
|
||||||
if self.pen_down {
|
if self.params.pen_down {
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
x = self.position.x,
|
turtle_id = self.turtle_id,
|
||||||
y = self.position.y,
|
x = self.params.position.x,
|
||||||
|
y = self.params.position.y,
|
||||||
vertices = fill_state.current_contour.len() + 1,
|
vertices = fill_state.current_contour.len() + 1,
|
||||||
"Adding vertex to current contour"
|
"Adding vertex to current contour"
|
||||||
);
|
);
|
||||||
fill_state.current_contour.push(self.position);
|
fill_state.current_contour.push(self.params.position);
|
||||||
} else {
|
} 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) {
|
pub fn close_fill_contour(&mut self) {
|
||||||
if let Some(ref mut fill_state) = self.filling {
|
if let Some(ref mut fill_state) = self.filling {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
|
turtle_id = self.turtle_id,
|
||||||
vertices = fill_state.current_contour.len(),
|
vertices = fill_state.current_contour.len(),
|
||||||
"close_fill_contour called"
|
"close_fill_contour called"
|
||||||
);
|
);
|
||||||
// Only close if we have vertices in current contour
|
// Only close if we have vertices in current contour
|
||||||
if fill_state.current_contour.len() >= 2 {
|
if fill_state.current_contour.len() >= 2 {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
|
turtle_id = self.turtle_id,
|
||||||
vertices = fill_state.current_contour.len(),
|
vertices = fill_state.current_contour.len(),
|
||||||
first_x = fill_state.current_contour[0].x,
|
first_x = fill_state.current_contour[0].x,
|
||||||
first_y = fill_state.current_contour[0].y,
|
first_y = fill_state.current_contour[0].y,
|
||||||
@ -118,19 +150,27 @@ impl TurtleState {
|
|||||||
let contour = std::mem::take(&mut fill_state.current_contour);
|
let contour = std::mem::take(&mut fill_state.current_contour);
|
||||||
fill_state.contours.push(contour);
|
fill_state.contours.push(contour);
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
|
turtle_id = self.turtle_id,
|
||||||
completed_contours = fill_state.contours.len(),
|
completed_contours = fill_state.contours.len(),
|
||||||
"Contour moved to completed list"
|
"Contour moved to completed list"
|
||||||
);
|
);
|
||||||
} else if !fill_state.current_contour.is_empty() {
|
} else if !fill_state.current_contour.is_empty() {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
|
turtle_id = self.turtle_id,
|
||||||
vertices = fill_state.current_contour.len(),
|
vertices = fill_state.current_contour.len(),
|
||||||
"Current contour has insufficient vertices, not closing"
|
"Current contour has insufficient vertices, not closing"
|
||||||
);
|
);
|
||||||
} else {
|
} 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 {
|
} 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 {
|
if let Some(ref mut fill_state) = self.filling {
|
||||||
// Start new contour at current position
|
// Start new contour at current position
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
x = self.position.x,
|
x = self.params.position.x,
|
||||||
y = self.position.y,
|
y = self.params.position.y,
|
||||||
completed_contours = fill_state.contours.len(),
|
completed_contours = fill_state.contours.len(),
|
||||||
|
self.turtle_id = self.turtle_id,
|
||||||
"Starting new contour"
|
"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,
|
steps: u32,
|
||||||
) {
|
) {
|
||||||
if let Some(ref mut fill_state) = self.filling {
|
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
|
// Sample points along the arc based on steps
|
||||||
let num_samples = steps as usize;
|
let num_samples = steps.max(1);
|
||||||
|
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
|
turtle_id = self.turtle_id,
|
||||||
center_x = center.x,
|
center_x = center.x,
|
||||||
center_y = center.y,
|
center_y = center.y,
|
||||||
radius = radius,
|
radius = radius,
|
||||||
@ -189,6 +231,7 @@ impl TurtleState {
|
|||||||
center.y + radius * current_angle.sin(),
|
center.y + radius * current_angle.sin(),
|
||||||
);
|
);
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
|
turtle_id = self.turtle_id,
|
||||||
vertex_idx = i,
|
vertex_idx = i,
|
||||||
x = vertex.x,
|
x = vertex.x,
|
||||||
y = vertex.y,
|
y = vertex.y,
|
||||||
@ -230,16 +273,13 @@ impl MeshData {
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum DrawCommand {
|
pub enum DrawCommand {
|
||||||
/// Pre-tessellated mesh data (lines, arcs, circles, polygons - all use this)
|
/// Pre-tessellated mesh data (lines, arcs, circles, polygons - all use this)
|
||||||
/// Includes the turtle ID that created this command
|
Mesh { data: MeshData },
|
||||||
Mesh { turtle_id: usize, data: MeshData },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The complete turtle world containing all drawing state
|
/// The complete turtle world containing all drawing state
|
||||||
pub struct TurtleWorld {
|
pub struct TurtleWorld {
|
||||||
/// All turtles in the world (indexed by turtle ID)
|
/// All turtles in the world (indexed by turtle ID)
|
||||||
pub turtles: Vec<TurtleState>,
|
pub turtles: Vec<Turtle>,
|
||||||
/// All drawing commands from all turtles
|
|
||||||
pub commands: Vec<DrawCommand>,
|
|
||||||
pub camera: Camera2D,
|
pub camera: Camera2D,
|
||||||
pub background_color: Color,
|
pub background_color: Color,
|
||||||
}
|
}
|
||||||
@ -247,9 +287,12 @@ pub struct TurtleWorld {
|
|||||||
impl TurtleWorld {
|
impl TurtleWorld {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
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 {
|
Self {
|
||||||
turtles: vec![TurtleState::default()], // Start with one default turtle
|
turtles: vec![default_turtle], // Start with one default turtle
|
||||||
commands: Vec::new(),
|
|
||||||
camera: Camera2D {
|
camera: Camera2D {
|
||||||
zoom: vec2(1.0 / screen_width() * 2.0, 1.0 / screen_height() * 2.0),
|
zoom: vec2(1.0 / screen_width() * 2.0, 1.0 / screen_height() * 2.0),
|
||||||
target: vec2(0.0, 0.0),
|
target: vec2(0.0, 0.0),
|
||||||
@ -261,18 +304,23 @@ impl TurtleWorld {
|
|||||||
|
|
||||||
/// Add a new turtle and return its ID
|
/// Add a new turtle and return its ID
|
||||||
pub fn add_turtle(&mut self) -> usize {
|
pub fn add_turtle(&mut self) -> usize {
|
||||||
self.turtles.push(TurtleState::default());
|
let turtle_id = self.turtles.len();
|
||||||
self.turtles.len() - 1
|
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
|
/// Get turtle by ID
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get_turtle(&self, id: usize) -> Option<&TurtleState> {
|
pub fn get_turtle(&self, id: usize) -> Option<&Turtle> {
|
||||||
self.turtles.get(id)
|
self.turtles.get(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get mutable turtle by 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)
|
self.turtles.get_mut(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,22 +328,15 @@ impl TurtleWorld {
|
|||||||
pub fn reset_turtle(&mut self, turtle_id: usize) {
|
pub fn reset_turtle(&mut self, turtle_id: usize) {
|
||||||
if let Some(turtle) = self.get_turtle_mut(turtle_id) {
|
if let Some(turtle) = self.get_turtle_mut(turtle_id) {
|
||||||
turtle.reset();
|
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
|
/// Clear all drawings and reset all turtle states
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
self.commands.clear();
|
for (id, turtle) in self.turtles.iter_mut().enumerate() {
|
||||||
for turtle in &mut self.turtles {
|
|
||||||
turtle.reset();
|
turtle.reset();
|
||||||
|
turtle.turtle_id = id; // Preserve turtle_id after reset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
use crate::circle_geometry::{CircleDirection, CircleGeometry};
|
use crate::circle_geometry::{CircleDirection, CircleGeometry};
|
||||||
use crate::commands::{CommandQueue, TurtleCommand};
|
use crate::commands::{CommandQueue, TurtleCommand};
|
||||||
use crate::general::AnimationSpeed;
|
use crate::general::AnimationSpeed;
|
||||||
use crate::state::TurtleState;
|
use crate::state::{Turtle, TurtleParams};
|
||||||
use macroquad::prelude::*;
|
use macroquad::prelude::*;
|
||||||
use tween::{CubicInOut, TweenValue, Tweener};
|
use tween::{CubicInOut, TweenValue, Tweener};
|
||||||
|
|
||||||
@ -44,20 +44,33 @@ impl From<TweenVec2> for Vec2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Controls tweening of turtle commands
|
/// Controls tweening of turtle commands
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct TweenController {
|
pub struct TweenController {
|
||||||
turtle_id: usize,
|
|
||||||
queue: CommandQueue,
|
queue: CommandQueue,
|
||||||
current_tween: Option<CommandTween>,
|
current_tween: Option<CommandTween>,
|
||||||
speed: AnimationSpeed,
|
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 struct CommandTween {
|
||||||
pub turtle_id: usize,
|
pub turtle_id: usize,
|
||||||
pub command: TurtleCommand,
|
pub command: TurtleCommand,
|
||||||
pub start_time: f64,
|
pub start_time: f64,
|
||||||
pub duration: f64,
|
pub duration: f64,
|
||||||
pub start_state: TurtleState,
|
pub start_params: TurtleParams,
|
||||||
pub target_state: TurtleState,
|
pub target_params: TurtleParams,
|
||||||
|
pub current_position: Vec2,
|
||||||
|
pub current_heading: f32,
|
||||||
position_tweener: Tweener<TweenVec2, f64, CubicInOut>,
|
position_tweener: Tweener<TweenVec2, f64, CubicInOut>,
|
||||||
heading_tweener: Tweener<f32, f64, CubicInOut>,
|
heading_tweener: Tweener<f32, f64, CubicInOut>,
|
||||||
pen_width_tweener: Tweener<f32, f64, CubicInOut>,
|
pen_width_tweener: Tweener<f32, f64, CubicInOut>,
|
||||||
@ -65,9 +78,8 @@ pub struct CommandTween {
|
|||||||
|
|
||||||
impl TweenController {
|
impl TweenController {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(turtle_id: usize, queue: CommandQueue, speed: AnimationSpeed) -> Self {
|
pub fn new(queue: CommandQueue, speed: AnimationSpeed) -> Self {
|
||||||
Self {
|
Self {
|
||||||
turtle_id,
|
|
||||||
queue,
|
queue,
|
||||||
current_tween: None,
|
current_tween: None,
|
||||||
speed,
|
speed,
|
||||||
@ -86,53 +98,53 @@ impl TweenController {
|
|||||||
/// Update the tween, returns `Vec` of (`command`, `start_state`, `end_state`) for all completed commands this frame
|
/// 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
|
/// Also takes commands vec to handle side effects like fill operations
|
||||||
/// Each `command` has its own `start_state` and `end_state` pair
|
/// Each `command` has its own `start_state` and `end_state` pair
|
||||||
#[allow(clippy::too_many_lines)]
|
pub fn update(state: &mut Turtle) -> Vec<(TurtleCommand, TurtleParams, TurtleParams)> {
|
||||||
pub fn update(
|
|
||||||
&mut self,
|
|
||||||
state: &mut TurtleState,
|
|
||||||
commands: &mut Vec<crate::state::DrawCommand>,
|
|
||||||
) -> Vec<(TurtleCommand, TurtleState, TurtleState)> {
|
|
||||||
// In instant mode, execute commands up to the draw calls per frame limit
|
// In instant mode, execute commands up to the draw calls per frame limit
|
||||||
if let AnimationSpeed::Instant(max_draw_calls) = self.speed {
|
if let AnimationSpeed::Instant(max_draw_calls) = state.tween_controller.speed {
|
||||||
let mut completed_commands = Vec::new();
|
let mut completed_commands: Vec<(TurtleCommand, TurtleParams, TurtleParams)> =
|
||||||
|
Vec::new();
|
||||||
let mut draw_call_count = 0;
|
let mut draw_call_count = 0;
|
||||||
|
|
||||||
for command in self.queue.by_ref() {
|
// Consume commands from the real queue so the current_index advances
|
||||||
let start_state = state.clone();
|
loop {
|
||||||
|
let command = match state.tween_controller.queue.next() {
|
||||||
|
Some(cmd) => cmd,
|
||||||
|
None => break,
|
||||||
|
};
|
||||||
|
|
||||||
// Handle SetSpeed command to potentially switch modes
|
// Handle SetSpeed command to potentially switch modes
|
||||||
if let TurtleCommand::SetSpeed(new_speed) = &command {
|
if let TurtleCommand::SetSpeed(new_speed) = &command {
|
||||||
state.set_speed(*new_speed);
|
state.params.speed = *new_speed;
|
||||||
self.speed = *new_speed;
|
state.tween_controller.speed = *new_speed;
|
||||||
if matches!(self.speed, AnimationSpeed::Animated(_)) {
|
if matches!(state.tween_controller.speed, AnimationSpeed::Animated(_)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute side-effect-only commands using centralized helper
|
// Execute side-effect-only commands using centralized helper
|
||||||
if crate::execution::execute_command_side_effects(&command, state, commands) {
|
if crate::execution::execute_command_side_effects(&command, state) {
|
||||||
continue; // Command fully handled
|
continue; // Command fully handled
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute movement commands
|
// Save start state and compute target state
|
||||||
let target_state = Self::calculate_target_state(state, &command);
|
let start_params = state.params.clone();
|
||||||
*state = target_state.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(
|
crate::execution::record_fill_vertices_after_movement(
|
||||||
&command,
|
&command,
|
||||||
&start_state,
|
&start_params,
|
||||||
state,
|
state,
|
||||||
);
|
);
|
||||||
|
|
||||||
let end_state = state.clone();
|
// Collect drawable commands (return start and target so caller can create draw meshes)
|
||||||
|
if Self::command_creates_drawing(&command) && start_params.pen_down {
|
||||||
// Collect drawable commands
|
completed_commands.push((command, start_params.clone(), target_params.clone()));
|
||||||
if Self::command_creates_drawing(&command) && start_state.pen_down {
|
|
||||||
completed_commands.push((command, start_state, end_state));
|
|
||||||
draw_call_count += 1;
|
draw_call_count += 1;
|
||||||
|
|
||||||
if draw_call_count >= max_draw_calls {
|
if draw_call_count >= max_draw_calls {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -143,14 +155,14 @@ impl TweenController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process current tween
|
// 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;
|
let elapsed = get_time() - tween.start_time;
|
||||||
|
|
||||||
// Use tweeners to calculate current values
|
// Use tweeners to calculate current values
|
||||||
// For circles, calculate position along the arc instead of straight line
|
// For circles, calculate position along the arc instead of straight line
|
||||||
let progress = tween.heading_tweener.move_to(elapsed);
|
let progress = tween.heading_tweener.move_to(elapsed);
|
||||||
|
|
||||||
state.position = match &tween.command {
|
let current_position = match &tween.command {
|
||||||
TurtleCommand::Circle {
|
TurtleCommand::Circle {
|
||||||
radius,
|
radius,
|
||||||
angle,
|
angle,
|
||||||
@ -159,8 +171,8 @@ impl TweenController {
|
|||||||
} => {
|
} => {
|
||||||
let angle_traveled = angle.to_radians() * progress;
|
let angle_traveled = angle.to_radians() * progress;
|
||||||
calculate_circle_position(
|
calculate_circle_position(
|
||||||
tween.start_state.position,
|
tween.start_params.position,
|
||||||
tween.start_state.heading,
|
tween.start_params.heading,
|
||||||
*radius,
|
*radius,
|
||||||
angle_traveled,
|
angle_traveled,
|
||||||
*direction,
|
*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
|
// 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 {
|
TurtleCommand::Circle {
|
||||||
angle, direction, ..
|
angle, direction, ..
|
||||||
} => match direction {
|
} => match direction {
|
||||||
CircleDirection::Left => {
|
CircleDirection::Left => {
|
||||||
tween.start_state.heading - angle.to_radians() * progress
|
tween.start_params.heading - angle.to_radians() * progress
|
||||||
}
|
}
|
||||||
CircleDirection::Right => {
|
CircleDirection::Right => {
|
||||||
tween.start_state.heading + angle.to_radians() * progress
|
tween.start_params.heading + angle.to_radians() * progress
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
TurtleCommand::Turn(angle) => {
|
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
|
// For other commands that change heading, lerp directly
|
||||||
let heading_diff = tween.target_state.heading - tween.start_state.heading;
|
let heading_diff = tween.target_params.heading - tween.start_params.heading;
|
||||||
tween.start_state.heading + heading_diff * progress
|
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)
|
// Discrete properties (switch at 50% progress)
|
||||||
let progress = (elapsed / tween.duration).min(1.0);
|
let progress = (elapsed / tween.duration).min(1.0);
|
||||||
if progress >= 0.5 {
|
if progress >= 0.5 {
|
||||||
state.pen_down = tween.target_state.pen_down;
|
state.params.pen_down = tween.target_params.pen_down;
|
||||||
state.color = tween.target_state.color;
|
state.params.color = tween.target_params.color;
|
||||||
state.fill_color = tween.target_state.fill_color;
|
state.params.fill_color = tween.target_params.fill_color;
|
||||||
state.visible = tween.target_state.visible;
|
state.params.visible = tween.target_params.visible;
|
||||||
state.shape = tween.target_state.shape.clone();
|
state.params.shape = tween.target_params.shape.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if tween is finished (use heading_tweener as it's used by all commands)
|
// Check if tween is finished (use heading_tweener as it's used by all commands)
|
||||||
if tween.heading_tweener.is_finished() {
|
if tween.heading_tweener.is_finished() {
|
||||||
let start_state = tween.start_state.clone();
|
let start_params = tween.start_params.clone();
|
||||||
*state = tween.target_state.clone();
|
let target_params = tween.target_params.clone();
|
||||||
let end_state = state.clone();
|
let command = tween.command.clone();
|
||||||
|
|
||||||
let completed_command = tween.command.clone();
|
// Drop the mutable borrow of tween before mutably borrowing state
|
||||||
self.current_tween = None;
|
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(
|
crate::execution::record_fill_vertices_after_movement(
|
||||||
&completed_command,
|
&command,
|
||||||
&start_state,
|
&start_params,
|
||||||
state,
|
state,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Return drawable commands
|
state.tween_controller.current_tween = None;
|
||||||
if Self::command_creates_drawing(&completed_command) && start_state.pen_down {
|
|
||||||
return vec![(completed_command, start_state, end_state)];
|
// 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();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start next tween
|
// Start next tween
|
||||||
if let Some(command) = self.queue.next() {
|
if let Some(command) = state.tween_controller.queue.next() {
|
||||||
let command_clone = command.clone();
|
let command_clone = command.clone();
|
||||||
|
|
||||||
// Handle commands that should execute immediately (no animation)
|
// Handle commands that should execute immediately (no animation)
|
||||||
match &command_clone {
|
match &command_clone {
|
||||||
TurtleCommand::SetSpeed(new_speed) => {
|
TurtleCommand::SetSpeed(new_speed) => {
|
||||||
state.set_speed(*new_speed);
|
state.set_speed(*new_speed);
|
||||||
self.speed = *new_speed;
|
state.tween_controller.speed = *new_speed;
|
||||||
if matches!(self.speed, AnimationSpeed::Instant(_)) {
|
if matches!(state.tween_controller.speed, AnimationSpeed::Instant(_)) {
|
||||||
return self.update(state, commands);
|
return Self::update(state);
|
||||||
}
|
}
|
||||||
return self.update(state, commands);
|
return Self::update(state);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Use centralized helper for side effects
|
// Use centralized helper for side effects
|
||||||
if crate::execution::execute_command_side_effects(
|
if crate::execution::execute_command_side_effects(&command_clone, state) {
|
||||||
&command_clone,
|
return Self::update(state);
|
||||||
state,
|
|
||||||
commands,
|
|
||||||
) {
|
|
||||||
return self.update(state, commands);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
let duration = Self::calculate_duration_with_state(&command_clone, state, speed);
|
||||||
|
|
||||||
// Calculate target state
|
// 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
|
// Create tweeners for smooth animation
|
||||||
let position_tweener = Tweener::new(
|
let position_tweener = Tweener::new(
|
||||||
TweenVec2::from(state.position),
|
TweenVec2::from(state.params.position),
|
||||||
TweenVec2::from(target_state.position),
|
TweenVec2::from(target_state.position),
|
||||||
duration,
|
duration,
|
||||||
CubicInOut,
|
CubicInOut,
|
||||||
@ -286,19 +298,21 @@ impl TweenController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let pen_width_tweener = Tweener::new(
|
let pen_width_tweener = Tweener::new(
|
||||||
state.pen_width,
|
state.params.pen_width,
|
||||||
target_state.pen_width,
|
target_state.pen_width,
|
||||||
duration,
|
duration,
|
||||||
CubicInOut,
|
CubicInOut,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.current_tween = Some(CommandTween {
|
state.tween_controller.current_tween = Some(CommandTween {
|
||||||
turtle_id: self.turtle_id,
|
turtle_id: state.turtle_id,
|
||||||
command: command_clone,
|
command: command_clone,
|
||||||
start_time: get_time(),
|
start_time: get_time(),
|
||||||
duration,
|
duration,
|
||||||
start_state: state.clone(),
|
start_params: state.params.clone(),
|
||||||
target_state,
|
target_params: target_state.clone(),
|
||||||
|
current_position: state.params.position,
|
||||||
|
current_heading: state.params.heading,
|
||||||
position_tweener,
|
position_tweener,
|
||||||
heading_tweener,
|
heading_tweener,
|
||||||
pen_width_tweener,
|
pen_width_tweener,
|
||||||
@ -327,7 +341,7 @@ impl TweenController {
|
|||||||
|
|
||||||
fn calculate_duration_with_state(
|
fn calculate_duration_with_state(
|
||||||
command: &TurtleCommand,
|
command: &TurtleCommand,
|
||||||
current: &TurtleState,
|
current: &Turtle,
|
||||||
speed: AnimationSpeed,
|
speed: AnimationSpeed,
|
||||||
) -> f64 {
|
) -> f64 {
|
||||||
let speed = speed.value();
|
let speed = speed.value();
|
||||||
@ -344,8 +358,8 @@ impl TweenController {
|
|||||||
}
|
}
|
||||||
TurtleCommand::Goto(target) => {
|
TurtleCommand::Goto(target) => {
|
||||||
// Calculate actual distance from current position to target
|
// Calculate actual distance from current position to target
|
||||||
let dx = target.x - current.position.x;
|
let dx = target.x - current.params.position.x;
|
||||||
let dy = target.y - current.position.y;
|
let dy = target.y - current.params.position.y;
|
||||||
let distance = (dx * dx + dy * dy).sqrt();
|
let distance = (dx * dx + dy * dy).sqrt();
|
||||||
distance / speed
|
distance / speed
|
||||||
}
|
}
|
||||||
@ -354,7 +368,7 @@ impl TweenController {
|
|||||||
f64::from(base_time.max(0.01)) // Minimum duration
|
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();
|
let mut target = current.clone();
|
||||||
|
|
||||||
match command {
|
match command {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user