initial multi-turtle support
This commit is contained in:
parent
1366f5e77f
commit
bbb9348497
@ -60,9 +60,9 @@ use syn::{parse_macro_input, ItemFn};
|
|||||||
/// turtle.forward(100.0);
|
/// turtle.forward(100.0);
|
||||||
/// turtle.right(90.0);
|
/// turtle.right(90.0);
|
||||||
/// turtle.forward(100.0);
|
/// turtle.forward(100.0);
|
||||||
///
|
///
|
||||||
/// let mut app = TurtleApp::new().with_commands(turtle.build());
|
/// let mut app = TurtleApp::new().with_commands(0, turtle.build());
|
||||||
///
|
///
|
||||||
/// loop {
|
/// loop {
|
||||||
/// clear_background(WHITE);
|
/// clear_background(WHITE);
|
||||||
/// app.update();
|
/// app.update();
|
||||||
@ -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(turtle.build());
|
.with_commands(0, 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(turtle.build());
|
.with_commands(0, turtle.build());
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
macroquad::prelude::clear_background(macroquad::prelude::WHITE);
|
macroquad::prelude::clear_background(macroquad::prelude::WHITE);
|
||||||
|
|||||||
@ -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(turtle.build());
|
let mut app = TurtleApp::new().with_commands(0, 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));
|
||||||
|
|||||||
@ -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(t.build());
|
let mut app = TurtleApp::new().with_commands(0, 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;
|
||||||
|
|||||||
@ -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(t.build());
|
let mut app = TurtleApp::new().with_commands(0, t.build());
|
||||||
|
|
||||||
// Main loop
|
// Main loop
|
||||||
loop {
|
loop {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use turtle_lib::*;
|
|||||||
|
|
||||||
#[turtle_main("Yin-Yang")]
|
#[turtle_main("Yin-Yang")]
|
||||||
fn draw(turtle: &mut TurtlePlan) {
|
fn draw(turtle: &mut TurtlePlan) {
|
||||||
turtle.set_speed(200);
|
turtle.set_speed(100);
|
||||||
|
|
||||||
turtle.circle_left(90.0, 180.0, 36);
|
turtle.circle_left(90.0, 180.0, 36);
|
||||||
turtle.begin_fill();
|
turtle.begin_fill();
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
use crate::circle_geometry::{CircleDirection, CircleGeometry};
|
use crate::circle_geometry::{CircleDirection, CircleGeometry};
|
||||||
use crate::state::{DrawCommand, TurtleState, TurtleWorld};
|
use crate::state::{DrawCommand, TurtleState, 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
|
||||||
@ -27,15 +28,17 @@ pub fn render_world(world: &TurtleWorld) {
|
|||||||
// Draw all accumulated commands
|
// Draw all accumulated commands
|
||||||
for cmd in &world.commands {
|
for cmd in &world.commands {
|
||||||
match cmd {
|
match cmd {
|
||||||
DrawCommand::Mesh(mesh_data) => {
|
DrawCommand::Mesh { data, .. } => {
|
||||||
draw_mesh(&mesh_data.to_mesh());
|
draw_mesh(&data.to_mesh());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw turtle if visible
|
// Draw all visible turtles
|
||||||
if world.turtle.visible {
|
for turtle in &world.turtles {
|
||||||
draw_turtle(&world.turtle);
|
if turtle.visible {
|
||||||
|
draw_turtle(turtle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset to default camera
|
// Reset to default camera
|
||||||
@ -44,9 +47,9 @@ 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(crate) fn render_world_with_tween(
|
pub fn render_world_with_tween(
|
||||||
world: &TurtleWorld,
|
world: &TurtleWorld,
|
||||||
active_tween: Option<&crate::tweening::CommandTween>,
|
active_tween: Option<&CommandTween>,
|
||||||
zoom_level: f32,
|
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
|
||||||
@ -66,13 +69,16 @@ pub(crate) fn render_world_with_tween(
|
|||||||
// Draw all accumulated commands
|
// Draw all accumulated commands
|
||||||
for cmd in &world.commands {
|
for cmd in &world.commands {
|
||||||
match cmd {
|
match cmd {
|
||||||
DrawCommand::Mesh(mesh_data) => {
|
DrawCommand::Mesh { data, .. } => {
|
||||||
draw_mesh(&mesh_data.to_mesh());
|
draw_mesh(&data.to_mesh());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw in-progress tween line if pen is down
|
// Draw in-progress tween line if pen is down
|
||||||
|
// Extract turtle_id from active tween (default to 0 if no active tween)
|
||||||
|
let active_turtle_id = active_tween.map_or(0, |tween| tween.turtle_id);
|
||||||
|
|
||||||
if let Some(tween) = active_tween {
|
if let Some(tween) = active_tween {
|
||||||
if tween.start_state.pen_down {
|
if tween.start_state.pen_down {
|
||||||
match &tween.command {
|
match &tween.command {
|
||||||
@ -86,22 +92,24 @@ pub(crate) 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
|
// Draw straight line for other movement commands (use active turtle)
|
||||||
draw_line(
|
if let Some(turtle) = world.turtles.get(active_turtle_id) {
|
||||||
tween.start_state.position.x,
|
draw_line(
|
||||||
tween.start_state.position.y,
|
tween.start_state.position.x,
|
||||||
world.turtle.position.x,
|
tween.start_state.position.y,
|
||||||
world.turtle.position.y,
|
turtle.position.x,
|
||||||
tween.start_state.pen_width,
|
turtle.position.y,
|
||||||
tween.start_state.color,
|
tween.start_state.pen_width,
|
||||||
);
|
tween.start_state.color,
|
||||||
// Add circle at current position for smooth line joins
|
);
|
||||||
draw_circle(
|
// Add circle at current position for smooth line joins
|
||||||
world.turtle.position.x,
|
draw_circle(
|
||||||
world.turtle.position.y,
|
turtle.position.x,
|
||||||
tween.start_state.pen_width / 2.0,
|
turtle.position.y,
|
||||||
tween.start_state.color,
|
tween.start_state.pen_width / 2.0,
|
||||||
);
|
tween.start_state.color,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@ -109,139 +117,146 @@ pub(crate) fn render_world_with_tween(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw live fill preview if currently filling (always show, not just during tweens)
|
// Draw live fill preview if currently filling (always show, not just during tweens)
|
||||||
if let Some(ref fill_state) = world.turtle.filling {
|
// Use the active turtle if available, otherwise default to turtle 0
|
||||||
// Build all contours: completed contours + current contour with animation
|
if let Some(turtle) = world.turtles.get(active_turtle_id) {
|
||||||
let mut all_contours: Vec<Vec<Vec2>> = Vec::new();
|
if let Some(ref fill_state) = turtle.filling {
|
||||||
|
// Build all contours: completed contours + current contour with animation
|
||||||
|
let mut all_contours: Vec<Vec<Vec2>> = Vec::new();
|
||||||
|
|
||||||
// Add all completed contours
|
// Add all completed contours
|
||||||
for completed_contour in &fill_state.contours {
|
for completed_contour in &fill_state.contours {
|
||||||
let contour_vec2: Vec<Vec2> = completed_contour
|
let contour_vec2: Vec<Vec2> = completed_contour
|
||||||
|
.iter()
|
||||||
|
.map(|c| Vec2::new(c.x, c.y))
|
||||||
|
.collect();
|
||||||
|
all_contours.push(contour_vec2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build current contour with animation
|
||||||
|
let mut current_preview: Vec<Vec2> = fill_state
|
||||||
|
.current_contour
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| Vec2::new(c.x, c.y))
|
.map(|c| Vec2::new(c.x, c.y))
|
||||||
.collect();
|
.collect();
|
||||||
all_contours.push(contour_vec2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build current contour with animation
|
// If we have an active tween, add progressive vertices
|
||||||
let mut current_preview: Vec<Vec2> = fill_state
|
if let Some(tween) = active_tween {
|
||||||
.current_contour
|
// If we're animating a circle command with pen down, add arc vertices
|
||||||
.iter()
|
if tween.start_state.pen_down {
|
||||||
.map(|c| Vec2::new(c.x, c.y))
|
if let crate::commands::TurtleCommand::Circle {
|
||||||
.collect();
|
radius,
|
||||||
|
angle,
|
||||||
// If we have an active tween, add progressive vertices
|
steps,
|
||||||
if let Some(tween) = active_tween {
|
direction,
|
||||||
// If we're animating a circle command with pen down, add arc vertices
|
} = &tween.command
|
||||||
if tween.start_state.pen_down {
|
{
|
||||||
if let crate::commands::TurtleCommand::Circle {
|
// Calculate partial arc vertices based on current progress
|
||||||
radius,
|
use crate::circle_geometry::CircleGeometry;
|
||||||
angle,
|
let geom = CircleGeometry::new(
|
||||||
steps,
|
tween.start_state.position,
|
||||||
direction,
|
tween.start_state.heading,
|
||||||
} = &tween.command
|
*radius,
|
||||||
{
|
*direction,
|
||||||
// Calculate partial arc vertices based on current progress
|
|
||||||
use crate::circle_geometry::CircleGeometry;
|
|
||||||
let geom = CircleGeometry::new(
|
|
||||||
tween.start_state.position,
|
|
||||||
tween.start_state.heading,
|
|
||||||
*radius,
|
|
||||||
*direction,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Calculate progress
|
|
||||||
let elapsed = get_time() - tween.start_time;
|
|
||||||
let progress = (elapsed / tween.duration).min(1.0);
|
|
||||||
let eased_progress = CubicInOut.tween(1.0, progress as f32);
|
|
||||||
|
|
||||||
// Generate arc vertices for the partial arc
|
|
||||||
let num_samples = *steps.max(&1);
|
|
||||||
let samples_to_draw = ((num_samples as f32 * eased_progress) as usize).max(1);
|
|
||||||
|
|
||||||
for i in 1..=samples_to_draw {
|
|
||||||
let sample_progress = i as f32 / num_samples as f32;
|
|
||||||
let current_angle = match direction {
|
|
||||||
crate::circle_geometry::CircleDirection::Left => {
|
|
||||||
geom.start_angle_from_center - angle.to_radians() * sample_progress
|
|
||||||
}
|
|
||||||
crate::circle_geometry::CircleDirection::Right => {
|
|
||||||
geom.start_angle_from_center + angle.to_radians() * sample_progress
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let vertex = Vec2::new(
|
|
||||||
geom.center.x + radius * current_angle.cos(),
|
|
||||||
geom.center.y + radius * current_angle.sin(),
|
|
||||||
);
|
);
|
||||||
current_preview.push(vertex);
|
|
||||||
|
// Calculate progress
|
||||||
|
let elapsed = get_time() - tween.start_time;
|
||||||
|
let progress = (elapsed / tween.duration).min(1.0);
|
||||||
|
let eased_progress = CubicInOut.tween(1.0, progress as f32);
|
||||||
|
|
||||||
|
// Generate arc vertices for the partial arc
|
||||||
|
let num_samples = *steps.max(&1);
|
||||||
|
let samples_to_draw =
|
||||||
|
((num_samples as f32 * eased_progress) as usize).max(1);
|
||||||
|
|
||||||
|
for i in 1..=samples_to_draw {
|
||||||
|
let sample_progress = i as f32 / num_samples as f32;
|
||||||
|
let current_angle = match direction {
|
||||||
|
crate::circle_geometry::CircleDirection::Left => {
|
||||||
|
geom.start_angle_from_center
|
||||||
|
- angle.to_radians() * sample_progress
|
||||||
|
}
|
||||||
|
crate::circle_geometry::CircleDirection::Right => {
|
||||||
|
geom.start_angle_from_center
|
||||||
|
+ angle.to_radians() * sample_progress
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let vertex = Vec2::new(
|
||||||
|
geom.center.x + radius * current_angle.cos(),
|
||||||
|
geom.center.y + radius * current_angle.sin(),
|
||||||
|
);
|
||||||
|
current_preview.push(vertex);
|
||||||
|
}
|
||||||
|
} else if matches!(
|
||||||
|
&tween.command,
|
||||||
|
crate::commands::TurtleCommand::Move(_)
|
||||||
|
| crate::commands::TurtleCommand::Goto(_)
|
||||||
|
) {
|
||||||
|
// For Move/Goto commands, just add the current position
|
||||||
|
current_preview.push(Vec2::new(turtle.position.x, turtle.position.y));
|
||||||
}
|
}
|
||||||
} else if matches!(
|
} else if matches!(
|
||||||
&tween.command,
|
&tween.command,
|
||||||
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 with pen up during filling, still add current position for preview
|
||||||
current_preview
|
current_preview.push(Vec2::new(turtle.position.x, turtle.position.y));
|
||||||
.push(Vec2::new(world.turtle.position.x, world.turtle.position.y));
|
|
||||||
}
|
}
|
||||||
} else if matches!(
|
|
||||||
&tween.command,
|
|
||||||
crate::commands::TurtleCommand::Move(_) | crate::commands::TurtleCommand::Goto(_)
|
|
||||||
) {
|
|
||||||
// For Move/Goto with pen up during filling, still add current position for preview
|
|
||||||
current_preview.push(Vec2::new(world.turtle.position.x, world.turtle.position.y));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add current turtle position if not already included
|
// Add current turtle position if not already included
|
||||||
if let Some(last) = current_preview.last() {
|
|
||||||
let current_pos = world.turtle.position;
|
|
||||||
// Use a larger threshold to reduce flickering from tiny movements
|
|
||||||
if (last.x - current_pos.x).abs() > 0.1 || (last.y - current_pos.y).abs() > 0.1 {
|
|
||||||
current_preview.push(Vec2::new(current_pos.x, current_pos.y));
|
|
||||||
}
|
|
||||||
} else if !current_preview.is_empty() {
|
|
||||||
current_preview.push(Vec2::new(world.turtle.position.x, world.turtle.position.y));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No active tween - just show current state
|
|
||||||
if !current_preview.is_empty() {
|
|
||||||
if let Some(last) = current_preview.last() {
|
if let Some(last) = current_preview.last() {
|
||||||
let current_pos = world.turtle.position;
|
let current_pos = turtle.position;
|
||||||
|
// Use a larger threshold to reduce flickering from tiny movements
|
||||||
if (last.x - current_pos.x).abs() > 0.1 || (last.y - current_pos.y).abs() > 0.1
|
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() {
|
||||||
|
current_preview.push(Vec2::new(turtle.position.x, turtle.position.y));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No active tween - just show current state
|
||||||
|
if !current_preview.is_empty() {
|
||||||
|
if let Some(last) = current_preview.last() {
|
||||||
|
let current_pos = turtle.position;
|
||||||
|
if (last.x - current_pos.x).abs() > 0.1
|
||||||
|
|| (last.y - current_pos.y).abs() > 0.1
|
||||||
|
{
|
||||||
|
current_preview.push(Vec2::new(current_pos.x, current_pos.y));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Add current contour to all contours if it has enough vertices
|
// Add current contour to all contours if it has enough vertices
|
||||||
if current_preview.len() >= 3 {
|
if current_preview.len() >= 3 {
|
||||||
all_contours.push(current_preview);
|
all_contours.push(current_preview);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tessellate and draw all contours together using multi-contour tessellation
|
// Tessellate and draw all contours together using multi-contour tessellation
|
||||||
if !all_contours.is_empty() {
|
if !all_contours.is_empty() {
|
||||||
match crate::tessellation::tessellate_multi_contour(
|
match crate::tessellation::tessellate_multi_contour(
|
||||||
&all_contours,
|
&all_contours,
|
||||||
fill_state.fill_color,
|
fill_state.fill_color,
|
||||||
) {
|
) {
|
||||||
Ok(mesh_data) => {
|
Ok(mesh_data) => {
|
||||||
draw_mesh(&mesh_data.to_mesh());
|
draw_mesh(&mesh_data.to_mesh());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!(
|
tracing::error!("Failed to tessellate fill preview: {:?}", e);
|
||||||
error = ?e,
|
}
|
||||||
"Lyon multi-contour tessellation error for fill preview"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw turtle if visible
|
// Draw all visible turtles
|
||||||
if world.turtle.visible {
|
for turtle in &world.turtles {
|
||||||
draw_turtle(&world.turtle);
|
if turtle.visible {
|
||||||
|
draw_turtle(turtle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset to default camera
|
// Reset to default camera
|
||||||
@ -279,18 +294,17 @@ fn draw_tween_arc(
|
|||||||
let elapsed = get_time() - tween.start_time;
|
let elapsed = get_time() - tween.start_time;
|
||||||
let t = (elapsed / tween.duration).min(1.0);
|
let t = (elapsed / tween.duration).min(1.0);
|
||||||
let progress = CubicInOut.tween(1.0, t as f32); // tween from 0 to 1
|
let progress = CubicInOut.tween(1.0, t as f32); // tween from 0 to 1
|
||||||
let angle_traveled = total_angle.to_radians() * progress;
|
|
||||||
let (rotation_degrees, arc_degrees) = geom.draw_arc_params_partial(angle_traveled);
|
|
||||||
|
|
||||||
// Use Lyon to tessellate and draw the partial arc
|
// Use Lyon to tessellate and draw the partial arc
|
||||||
if let Ok(mesh_data) = crate::tessellation::tessellate_arc(
|
if let Ok(mesh_data) = crate::tessellation::tessellate_arc(
|
||||||
geom.center,
|
geom.center,
|
||||||
radius,
|
radius,
|
||||||
rotation_degrees,
|
geom.start_angle_from_center.to_degrees(),
|
||||||
arc_degrees,
|
total_angle * progress,
|
||||||
tween.start_state.color,
|
tween.start_state.color,
|
||||||
tween.start_state.pen_width,
|
tween.start_state.pen_width,
|
||||||
steps,
|
((steps as f32 * progress).ceil() as usize).max(1),
|
||||||
|
direction,
|
||||||
) {
|
) {
|
||||||
draw_mesh(&mesh_data.to_mesh());
|
draw_mesh(&mesh_data.to_mesh());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,7 +51,10 @@ pub fn execute_command_side_effects(
|
|||||||
contours = fill_state.contours.len(),
|
contours = fill_state.contours.len(),
|
||||||
"Successfully tessellated contours"
|
"Successfully tessellated contours"
|
||||||
);
|
);
|
||||||
commands.push(DrawCommand::Mesh(mesh_data));
|
commands.push(DrawCommand::Mesh {
|
||||||
|
turtle_id: 0,
|
||||||
|
data: mesh_data,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
tracing::error!("Failed to tessellate contours");
|
tracing::error!("Failed to tessellate contours");
|
||||||
}
|
}
|
||||||
@ -153,7 +156,10 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
|
|||||||
state.pen_width,
|
state.pen_width,
|
||||||
false, // not closed
|
false, // not closed
|
||||||
) {
|
) {
|
||||||
world.add_command(DrawCommand::Mesh(mesh_data));
|
world.add_command(DrawCommand::Mesh {
|
||||||
|
turtle_id: 0,
|
||||||
|
data: mesh_data,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -172,19 +178,21 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
|
|||||||
let geom = CircleGeometry::new(state.position, start_heading, *radius, *direction);
|
let geom = CircleGeometry::new(state.position, start_heading, *radius, *direction);
|
||||||
|
|
||||||
if state.pen_down {
|
if state.pen_down {
|
||||||
let (rotation_degrees, arc_degrees) = geom.draw_arc_params(*angle);
|
|
||||||
|
|
||||||
// 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,
|
||||||
rotation_degrees,
|
geom.start_angle_from_center.to_degrees(),
|
||||||
arc_degrees,
|
*angle,
|
||||||
state.color,
|
state.color,
|
||||||
state.pen_width,
|
state.pen_width,
|
||||||
*steps,
|
*steps,
|
||||||
|
*direction,
|
||||||
) {
|
) {
|
||||||
world.add_command(DrawCommand::Mesh(mesh_data));
|
world.add_command(DrawCommand::Mesh {
|
||||||
|
turtle_id: 0,
|
||||||
|
data: mesh_data,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,7 +216,10 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
|
|||||||
state.pen_width,
|
state.pen_width,
|
||||||
false, // not closed
|
false, // not closed
|
||||||
) {
|
) {
|
||||||
world.add_command(DrawCommand::Mesh(mesh_data));
|
world.add_command(DrawCommand::Mesh {
|
||||||
|
turtle_id: 0,
|
||||||
|
data: mesh_data,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -230,24 +241,40 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
|
|||||||
record_fill_vertices_after_movement(command, &start_state, state);
|
record_fill_vertices_after_movement(command, &start_state, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add drawing command for a completed tween (state transition already occurred)
|
/// Execute command on a specific turtle by ID
|
||||||
pub fn add_draw_for_completed_tween(
|
pub fn execute_command_with_id(command: &TurtleCommand, turtle_id: usize, world: &mut TurtleWorld) {
|
||||||
|
// Clone turtle state to avoid borrow checker issues
|
||||||
|
if let Some(turtle) = world.get_turtle(turtle_id) {
|
||||||
|
let mut state = turtle.clone();
|
||||||
|
execute_command(command, &mut state, world);
|
||||||
|
// Update the turtle state back
|
||||||
|
if let Some(turtle_mut) = world.get_turtle_mut(turtle_id) {
|
||||||
|
*turtle_mut = state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add drawing command for a completed tween with turtle_id tracking
|
||||||
|
pub fn add_draw_for_completed_tween_with_id(
|
||||||
command: &TurtleCommand,
|
command: &TurtleCommand,
|
||||||
start_state: &TurtleState,
|
start_state: &TurtleState,
|
||||||
end_state: &TurtleState,
|
end_state: &TurtleState,
|
||||||
world: &mut TurtleWorld,
|
world: &mut TurtleWorld,
|
||||||
|
turtle_id: usize,
|
||||||
) {
|
) {
|
||||||
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, // not closed
|
false,
|
||||||
) {
|
) {
|
||||||
world.add_command(DrawCommand::Mesh(mesh_data));
|
world.add_command(DrawCommand::Mesh {
|
||||||
|
turtle_id,
|
||||||
|
data: mesh_data,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -264,19 +291,79 @@ pub fn add_draw_for_completed_tween(
|
|||||||
*radius,
|
*radius,
|
||||||
*direction,
|
*direction,
|
||||||
);
|
);
|
||||||
let (rotation_degrees, arc_degrees) = geom.draw_arc_params(*angle);
|
|
||||||
|
|
||||||
// Use Lyon to tessellate the arc
|
|
||||||
if let Ok(mesh_data) = tessellation::tessellate_arc(
|
if let Ok(mesh_data) = tessellation::tessellate_arc(
|
||||||
geom.center,
|
geom.center,
|
||||||
*radius,
|
*radius,
|
||||||
rotation_degrees,
|
geom.start_angle_from_center.to_degrees(),
|
||||||
arc_degrees,
|
*angle,
|
||||||
start_state.color,
|
start_state.color,
|
||||||
start_state.pen_width,
|
start_state.pen_width,
|
||||||
*steps,
|
*steps,
|
||||||
|
*direction,
|
||||||
) {
|
) {
|
||||||
world.add_command(DrawCommand::Mesh(mesh_data));
|
world.add_command(DrawCommand::Mesh {
|
||||||
|
turtle_id,
|
||||||
|
data: mesh_data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add drawing command for a completed tween (state transition already occurred)
|
||||||
|
pub fn add_draw_for_completed_tween(
|
||||||
|
command: &TurtleCommand,
|
||||||
|
start_state: &TurtleState,
|
||||||
|
end_state: &TurtleState,
|
||||||
|
world: &mut TurtleWorld,
|
||||||
|
) {
|
||||||
|
match command {
|
||||||
|
TurtleCommand::Move(_) | TurtleCommand::Goto(_) => {
|
||||||
|
if start_state.pen_down {
|
||||||
|
// Draw line segment with round caps
|
||||||
|
if let Ok(mesh_data) = tessellation::tessellate_stroke(
|
||||||
|
&[start_state.position, end_state.position],
|
||||||
|
start_state.color,
|
||||||
|
start_state.pen_width,
|
||||||
|
false,
|
||||||
|
) {
|
||||||
|
world.add_command(DrawCommand::Mesh {
|
||||||
|
turtle_id: 0,
|
||||||
|
data: mesh_data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TurtleCommand::Circle {
|
||||||
|
radius,
|
||||||
|
angle,
|
||||||
|
steps,
|
||||||
|
direction,
|
||||||
|
} => {
|
||||||
|
if start_state.pen_down {
|
||||||
|
let geom = CircleGeometry::new(
|
||||||
|
start_state.position,
|
||||||
|
start_state.heading,
|
||||||
|
*radius,
|
||||||
|
*direction,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Ok(mesh_data) = tessellation::tessellate_arc(
|
||||||
|
geom.center,
|
||||||
|
*radius,
|
||||||
|
geom.start_angle_from_center.to_degrees(),
|
||||||
|
*angle,
|
||||||
|
start_state.color,
|
||||||
|
start_state.pen_width,
|
||||||
|
*steps,
|
||||||
|
*direction,
|
||||||
|
) {
|
||||||
|
world.add_command(DrawCommand::Mesh {
|
||||||
|
turtle_id: 0,
|
||||||
|
data: mesh_data,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -311,7 +398,7 @@ mod tests {
|
|||||||
|
|
||||||
// We'll use a dummy world but won't actually call drawing commands
|
// We'll use a dummy world but won't actually call drawing commands
|
||||||
let mut world = TurtleWorld {
|
let mut world = TurtleWorld {
|
||||||
turtle: state.clone(),
|
turtles: vec![state.clone()],
|
||||||
commands: Vec::new(),
|
commands: Vec::new(),
|
||||||
camera: macroquad::camera::Camera2D {
|
camera: macroquad::camera::Camera2D {
|
||||||
zoom: vec2(1.0, 1.0),
|
zoom: vec2(1.0, 1.0),
|
||||||
@ -323,6 +410,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
background_color: Color::new(1.0, 1.0, 1.0, 1.0),
|
background_color: Color::new(1.0, 1.0, 1.0, 1.0),
|
||||||
};
|
};
|
||||||
|
let mut state = world.turtles[0].clone();
|
||||||
|
|
||||||
// Initial state: position (0, 0), heading 0 (east)
|
// Initial state: position (0, 0), heading 0 (east)
|
||||||
assert_eq!(state.position.x, 0.0);
|
assert_eq!(state.position.x, 0.0);
|
||||||
|
|||||||
@ -78,7 +78,8 @@ use macroquad::prelude::*;
|
|||||||
/// Main turtle application struct
|
/// Main turtle application struct
|
||||||
pub struct TurtleApp {
|
pub struct TurtleApp {
|
||||||
world: TurtleWorld,
|
world: TurtleWorld,
|
||||||
tween_controller: Option<TweenController>,
|
/// One tween controller per turtle (indexed by turtle ID)
|
||||||
|
tween_controllers: Vec<TweenController>,
|
||||||
speed: AnimationSpeed,
|
speed: AnimationSpeed,
|
||||||
// Mouse panning state
|
// Mouse panning state
|
||||||
is_dragging: bool,
|
is_dragging: bool,
|
||||||
@ -93,7 +94,7 @@ impl TurtleApp {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
world: TurtleWorld::new(),
|
world: TurtleWorld::new(),
|
||||||
tween_controller: None,
|
tween_controllers: Vec::new(),
|
||||||
speed: AnimationSpeed::default(),
|
speed: AnimationSpeed::default(),
|
||||||
is_dragging: false,
|
is_dragging: false,
|
||||||
last_mouse_pos: None,
|
last_mouse_pos: None,
|
||||||
@ -101,40 +102,89 @@ impl TurtleApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add commands to the turtle
|
/// Add a new turtle and return its ID
|
||||||
|
pub fn add_turtle(&mut self) -> usize {
|
||||||
|
let id = self.world.add_turtle();
|
||||||
|
let speed = self.speed;
|
||||||
|
self.tween_controllers
|
||||||
|
.push(TweenController::new(id, CommandQueue::new(), speed));
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add commands to a specific turtle
|
||||||
///
|
///
|
||||||
/// Speed is controlled by `SetSpeed` commands in the queue.
|
/// 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.
|
||||||
/// Speed >= 999 = instant mode, speed < 999 = animated mode.
|
/// Speed >= 999 = instant mode, speed < 999 = animated mode.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
/// * `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, queue: CommandQueue) -> Self {
|
pub fn with_commands(mut self, turtle_id: usize, queue: CommandQueue) -> Self {
|
||||||
// The `TweenController` will switch between instant and animated mode
|
// Ensure we have a controller for this turtle
|
||||||
// based on `SetSpeed` commands encountered
|
while self.tween_controllers.len() <= turtle_id {
|
||||||
self.tween_controller = Some(TweenController::new(queue, self.speed));
|
let id = self.tween_controllers.len();
|
||||||
|
let speed = self.speed;
|
||||||
|
self.tween_controllers
|
||||||
|
.push(TweenController::new(id, CommandQueue::new(), speed));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append commands to the controller
|
||||||
|
self.tween_controllers[turtle_id].append_commands(queue);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Execute a plan immediately on a specific turtle (no animation)
|
||||||
|
pub fn execute_immediate(&mut self, turtle_id: usize, plan: TurtlePlan) {
|
||||||
|
for ref cmd in plan.build() {
|
||||||
|
execution::execute_command_with_id(cmd, turtle_id, &mut self.world);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append commands to a turtle's animation queue
|
||||||
|
pub fn append_to_queue(&mut self, turtle_id: usize, plan: TurtlePlan) {
|
||||||
|
// Ensure we have a controller for this turtle
|
||||||
|
while self.tween_controllers.len() <= turtle_id {
|
||||||
|
let id = self.tween_controllers.len();
|
||||||
|
let speed = AnimationSpeed::default();
|
||||||
|
self.tween_controllers
|
||||||
|
.push(TweenController::new(id, CommandQueue::new(), speed));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tween_controllers[turtle_id].append_commands(plan.build());
|
||||||
|
}
|
||||||
|
|
||||||
/// Update animation state (call every frame)
|
/// Update animation state (call every frame)
|
||||||
pub fn update(&mut self) {
|
pub fn update(&mut self) {
|
||||||
// Handle mouse panning and zoom
|
// Handle mouse panning and zoom
|
||||||
self.handle_mouse_panning();
|
self.handle_mouse_panning();
|
||||||
self.handle_mouse_zoom();
|
self.handle_mouse_zoom();
|
||||||
|
|
||||||
if let Some(ref mut controller) = self.tween_controller {
|
// Update all active tween controllers
|
||||||
let completed_commands =
|
// Process each turtle separately to avoid borrow conflicts
|
||||||
controller.update(&mut self.world.turtle, &mut self.world.commands);
|
let turtle_count = self.tween_controllers.len();
|
||||||
|
for turtle_id in 0..turtle_count {
|
||||||
|
// Extract commands temporarily to avoid double mutable borrow
|
||||||
|
let mut commands = std::mem::take(&mut self.world.commands);
|
||||||
|
|
||||||
// Process all completed commands (multiple in instant mode, 0-1 in animated mode)
|
let completed_commands = if let Some(turtle) = self.world.get_turtle_mut(turtle_id) {
|
||||||
|
self.tween_controllers[turtle_id].update(turtle, &mut commands)
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Put commands back
|
||||||
|
self.world.commands = commands;
|
||||||
|
|
||||||
|
// Process all completed commands
|
||||||
for (completed_cmd, start_state, end_state) in completed_commands {
|
for (completed_cmd, start_state, end_state) in completed_commands {
|
||||||
// Add draw commands for the completed tween
|
execution::add_draw_for_completed_tween_with_id(
|
||||||
execution::add_draw_for_completed_tween(
|
|
||||||
&completed_cmd,
|
&completed_cmd,
|
||||||
&start_state,
|
&start_state,
|
||||||
&end_state,
|
&end_state,
|
||||||
&mut self.world,
|
&mut self.world,
|
||||||
|
turtle_id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -187,20 +237,21 @@ impl TurtleApp {
|
|||||||
|
|
||||||
/// Render the turtle world (call every frame)
|
/// Render the turtle world (call every frame)
|
||||||
pub fn render(&self) {
|
pub fn render(&self) {
|
||||||
// Get active tween if in animated mode
|
// Find the first active tween (turtle_id is now stored in the tween itself)
|
||||||
let active_tween = self
|
let active_tween = self
|
||||||
.tween_controller
|
.tween_controllers
|
||||||
.as_ref()
|
.iter()
|
||||||
.and_then(|c| c.current_tween());
|
.find_map(|controller| controller.current_tween());
|
||||||
|
|
||||||
drawing::render_world_with_tween(&self.world, active_tween, self.zoom_level);
|
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_controller
|
self.tween_controllers
|
||||||
.as_ref()
|
.iter()
|
||||||
.is_none_or(TweenController::is_complete)
|
.all(TweenController::is_complete)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get reference to the world state
|
/// Get reference to the world state
|
||||||
|
|||||||
@ -65,6 +65,11 @@ impl TurtleState {
|
|||||||
Angle::radians(self.heading)
|
Angle::radians(self.heading)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reset turtle to default state
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
*self = Self::default();
|
||||||
|
}
|
||||||
|
|
||||||
/// 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 {
|
||||||
@ -225,12 +230,15 @@ 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)
|
||||||
Mesh(MeshData),
|
/// Includes the turtle ID that created this command
|
||||||
|
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 {
|
||||||
pub turtle: TurtleState,
|
/// All turtles in the world (indexed by turtle ID)
|
||||||
|
pub turtles: Vec<TurtleState>,
|
||||||
|
/// All drawing commands from all turtles
|
||||||
pub commands: Vec<DrawCommand>,
|
pub commands: Vec<DrawCommand>,
|
||||||
pub camera: Camera2D,
|
pub camera: Camera2D,
|
||||||
pub background_color: Color,
|
pub background_color: Color,
|
||||||
@ -240,7 +248,7 @@ impl TurtleWorld {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
turtle: TurtleState::default(),
|
turtles: vec![TurtleState::default()], // Start with one default turtle
|
||||||
commands: Vec::new(),
|
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),
|
||||||
@ -251,13 +259,44 @@ impl TurtleWorld {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a new turtle and return its ID
|
||||||
|
pub fn add_turtle(&mut self) -> usize {
|
||||||
|
self.turtles.push(TurtleState::default());
|
||||||
|
self.turtles.len() - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get turtle by ID
|
||||||
|
#[must_use]
|
||||||
|
pub fn get_turtle(&self, id: usize) -> Option<&TurtleState> {
|
||||||
|
self.turtles.get(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get mutable turtle by ID
|
||||||
|
pub fn get_turtle_mut(&mut self, id: usize) -> Option<&mut TurtleState> {
|
||||||
|
self.turtles.get_mut(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset a specific turtle to default state and remove all its drawings
|
||||||
|
pub fn reset_turtle(&mut self, turtle_id: usize) {
|
||||||
|
if let Some(turtle) = self.get_turtle_mut(turtle_id) {
|
||||||
|
turtle.reset();
|
||||||
|
}
|
||||||
|
// Remove all commands created by this turtle
|
||||||
|
self.commands.retain(|cmd| match cmd {
|
||||||
|
DrawCommand::Mesh { turtle_id: id, .. } => *id != turtle_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_command(&mut self, cmd: DrawCommand) {
|
pub fn add_command(&mut self, cmd: DrawCommand) {
|
||||||
self.commands.push(cmd);
|
self.commands.push(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clear all drawings and reset all turtle states
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
self.commands.clear();
|
self.commands.clear();
|
||||||
self.turtle = TurtleState::default();
|
for turtle in &mut self.turtles {
|
||||||
|
turtle.reset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -302,6 +302,7 @@ pub fn tessellate_arc(
|
|||||||
color: Color,
|
color: Color,
|
||||||
stroke_width: f32,
|
stroke_width: f32,
|
||||||
segments: usize,
|
segments: usize,
|
||||||
|
direction: crate::circle_geometry::CircleDirection,
|
||||||
) -> Result<MeshData, Box<dyn std::error::Error>> {
|
) -> Result<MeshData, Box<dyn std::error::Error>> {
|
||||||
// Build arc path manually from segments
|
// Build arc path manually from segments
|
||||||
let mut builder = Path::builder();
|
let mut builder = Path::builder();
|
||||||
@ -311,16 +312,24 @@ pub fn tessellate_arc(
|
|||||||
let step = arc_angle / segments as f32;
|
let step = arc_angle / segments as f32;
|
||||||
|
|
||||||
// Calculate first point
|
// Calculate first point
|
||||||
let first_angle = start_angle;
|
|
||||||
let first_point = point(
|
let first_point = point(
|
||||||
center.x + radius * first_angle.cos(),
|
center.x + radius * start_angle.cos(),
|
||||||
center.y + radius * first_angle.sin(),
|
center.y + radius * start_angle.sin(),
|
||||||
);
|
);
|
||||||
builder.begin(first_point);
|
builder.begin(first_point);
|
||||||
|
|
||||||
// Add remaining points
|
// Add remaining points - direction matters!
|
||||||
for i in 1..=segments {
|
for i in 1..=segments {
|
||||||
let angle = start_angle + step * i as f32;
|
let angle = match direction {
|
||||||
|
crate::circle_geometry::CircleDirection::Left => {
|
||||||
|
// Counter-clockwise: subtract angle
|
||||||
|
start_angle - step * i as f32
|
||||||
|
}
|
||||||
|
crate::circle_geometry::CircleDirection::Right => {
|
||||||
|
// Clockwise: add angle
|
||||||
|
start_angle + step * i as f32
|
||||||
|
}
|
||||||
|
};
|
||||||
let pt = point(
|
let pt = point(
|
||||||
center.x + radius * angle.cos(),
|
center.x + radius * angle.cos(),
|
||||||
center.y + radius * angle.sin(),
|
center.y + radius * angle.sin(),
|
||||||
|
|||||||
@ -45,26 +45,29 @@ impl From<TweenVec2> for Vec2 {
|
|||||||
|
|
||||||
/// Controls tweening of turtle commands
|
/// Controls tweening of turtle commands
|
||||||
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct CommandTween {
|
pub struct CommandTween {
|
||||||
|
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_state: TurtleState,
|
||||||
pub target_state: TurtleState,
|
pub target_state: TurtleState,
|
||||||
pub position_tweener: Tweener<TweenVec2, f64, CubicInOut>,
|
position_tweener: Tweener<TweenVec2, f64, CubicInOut>,
|
||||||
pub heading_tweener: Tweener<f32, f64, CubicInOut>,
|
heading_tweener: Tweener<f32, f64, CubicInOut>,
|
||||||
pub pen_width_tweener: Tweener<f32, f64, CubicInOut>,
|
pen_width_tweener: Tweener<f32, f64, CubicInOut>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TweenController {
|
impl TweenController {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(queue: CommandQueue, speed: AnimationSpeed) -> Self {
|
pub fn new(turtle_id: usize, queue: CommandQueue, speed: AnimationSpeed) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
turtle_id,
|
||||||
queue,
|
queue,
|
||||||
current_tween: None,
|
current_tween: None,
|
||||||
speed,
|
speed,
|
||||||
@ -75,6 +78,11 @@ impl TweenController {
|
|||||||
self.speed = speed;
|
self.speed = speed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Append commands to the queue
|
||||||
|
pub fn append_commands(&mut self, new_queue: CommandQueue) {
|
||||||
|
self.queue.extend(new_queue);
|
||||||
|
}
|
||||||
|
|
||||||
/// Update the tween, returns `Vec` of (`command`, `start_state`, `end_state`) for all completed commands this frame
|
/// 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
|
||||||
@ -285,6 +293,7 @@ impl TweenController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
self.current_tween = Some(CommandTween {
|
self.current_tween = Some(CommandTween {
|
||||||
|
turtle_id: self.turtle_id,
|
||||||
command: command_clone,
|
command: command_clone,
|
||||||
start_time: get_time(),
|
start_time: get_time(),
|
||||||
duration,
|
duration,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user