add multi turtle support

This commit is contained in:
2025-10-17 08:59:29 +02:00
parent bbb9348497
commit 3509060390
19 changed files with 624 additions and 628 deletions
+100 -84
View File
@@ -1,9 +1,8 @@
//! Rendering logic using Macroquad and Lyon tessellation
use crate::circle_geometry::{CircleDirection, CircleGeometry};
use crate::state::{DrawCommand, TurtleState, TurtleWorld};
use crate::state::{DrawCommand, TurtleParams, TurtleWorld};
use crate::tessellation;
use crate::tweening::CommandTween;
use macroquad::prelude::*;
// Import the easing function from the tween crate
@@ -25,19 +24,21 @@ pub fn render_world(world: &TurtleWorld) {
// Set camera
set_camera(&camera);
// Draw all accumulated commands
for cmd in &world.commands {
match cmd {
DrawCommand::Mesh { data, .. } => {
draw_mesh(&data.to_mesh());
// Draw all accumulated commands from all turtles
for turtle in &world.turtles {
for cmd in &turtle.commands {
match cmd {
DrawCommand::Mesh { data } => {
draw_mesh(&data.to_mesh());
}
}
}
}
// Draw all visible turtles
for turtle in &world.turtles {
if turtle.visible {
draw_turtle(turtle);
if turtle.params.visible {
draw_turtle(&turtle.params);
}
}
@@ -47,11 +48,7 @@ pub fn render_world(world: &TurtleWorld) {
/// Render the turtle world with active tween visualization
#[allow(clippy::too_many_lines)]
pub fn render_world_with_tween(
world: &TurtleWorld,
active_tween: Option<&CommandTween>,
zoom_level: f32,
) {
pub fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) {
// Update camera zoom based on current screen size to prevent stretching
// Apply user zoom level by dividing by it (smaller zoom value = more zoomed in)
let camera = Camera2D {
@@ -66,59 +63,58 @@ pub fn render_world_with_tween(
// Set camera
set_camera(&camera);
// Draw all accumulated commands
for cmd in &world.commands {
match cmd {
DrawCommand::Mesh { data, .. } => {
draw_mesh(&data.to_mesh());
// Draw all accumulated commands from all turtles
for turtle in &world.turtles {
for cmd in &turtle.commands {
match cmd {
DrawCommand::Mesh { data } => {
draw_mesh(&data.to_mesh());
}
}
}
}
// Draw in-progress tween line if pen is down
// Extract turtle_id from active tween (default to 0 if no active tween)
let active_turtle_id = active_tween.map_or(0, |tween| tween.turtle_id);
if let Some(tween) = active_tween {
if tween.start_state.pen_down {
match &tween.command {
crate::commands::TurtleCommand::Circle {
radius,
angle,
steps,
direction,
} => {
// Draw arc segments from start to current position
draw_tween_arc(tween, *radius, *angle, *steps, *direction);
}
_ if should_draw_tween_line(&tween.command) => {
// Draw straight line for other movement commands (use active turtle)
if let Some(turtle) = world.turtles.get(active_turtle_id) {
// Draw in-progress tween lines for all active tweens
for turtle in world.turtles.iter() {
if let Some(tween) = turtle.tween_controller.current_tween() {
// Only draw if pen is down
if tween.start_params.pen_down {
match &tween.command {
crate::commands::TurtleCommand::Circle {
radius,
angle,
steps,
direction,
} => {
// Draw arc segments from start to current position
draw_tween_arc(tween, *radius, *angle, *steps, *direction);
}
_ if should_draw_tween_line(&tween.command) => {
// Draw straight line for other movement commands (use tween's current position)
draw_line(
tween.start_state.position.x,
tween.start_state.position.y,
turtle.position.x,
turtle.position.y,
tween.start_state.pen_width,
tween.start_state.color,
tween.start_params.position.x,
tween.start_params.position.y,
tween.current_position.x,
tween.current_position.y,
tween.start_params.pen_width,
tween.start_params.color,
);
// Add circle at current position for smooth line joins
draw_circle(
turtle.position.x,
turtle.position.y,
tween.start_state.pen_width / 2.0,
tween.start_state.color,
tween.current_position.x,
tween.current_position.y,
tween.start_params.pen_width / 2.0,
tween.start_params.color,
);
}
_ => {}
}
_ => {}
}
}
}
// Draw live fill preview if currently filling (always show, not just during tweens)
// Use the active turtle if available, otherwise default to turtle 0
if let Some(turtle) = world.turtles.get(active_turtle_id) {
// Draw live fill preview for all turtles that are currently filling
for turtle in world.turtles.iter() {
if let Some(ref fill_state) = turtle.filling {
// Build all contours: completed contours + current contour with animation
let mut all_contours: Vec<Vec<Vec2>> = Vec::new();
@@ -133,16 +129,22 @@ pub fn render_world_with_tween(
}
// Build current contour with animation
let mut current_preview: Vec<Vec2> = fill_state
.current_contour
.iter()
.map(|c| Vec2::new(c.x, c.y))
.collect();
// Find the tween for this specific turtle
let turtle_tween = turtle.tween_controller.current_tween();
// If we have an active tween, add progressive vertices
if let Some(tween) = active_tween {
// If we're animating a circle command with pen down, add arc vertices
if tween.start_state.pen_down {
let mut current_preview: Vec<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
.iter()
.map(|c| Vec2::new(c.x, c.y))
.collect();
// If we're animating a circle command with pen down, add progressive arc vertices
if tween.start_params.pen_down {
if let crate::commands::TurtleCommand::Circle {
radius,
angle,
@@ -153,13 +155,11 @@ pub fn render_world_with_tween(
// Calculate partial arc vertices based on current progress
use crate::circle_geometry::CircleGeometry;
let geom = CircleGeometry::new(
tween.start_state.position,
tween.start_state.heading,
tween.start_params.position,
tween.start_params.heading,
*radius,
*direction,
);
// Calculate progress
); // Calculate progress
let elapsed = get_time() - tween.start_time;
let progress = (elapsed / tween.duration).min(1.0);
let eased_progress = CubicInOut.tween(1.0, progress as f32);
@@ -193,8 +193,11 @@ pub fn render_world_with_tween(
crate::commands::TurtleCommand::Move(_)
| crate::commands::TurtleCommand::Goto(_)
) {
// For Move/Goto commands, just add the current position
current_preview.push(Vec2::new(turtle.position.x, turtle.position.y));
// For Move/Goto commands, just add the current position from tween
current_preview.push(Vec2::new(
tween.current_position.x,
tween.current_position.y,
));
}
} else if matches!(
&tween.command,
@@ -202,25 +205,38 @@ pub fn render_world_with_tween(
| crate::commands::TurtleCommand::Goto(_)
) {
// For Move/Goto with pen up during filling, still add current position for preview
current_preview.push(Vec2::new(turtle.position.x, turtle.position.y));
current_preview.push(Vec2::new(
tween.current_position.x,
tween.current_position.y,
));
}
// Add current turtle position if not already included
if let Some(last) = current_preview.last() {
let current_pos = turtle.position;
let current_pos = tween.current_position;
// Use a larger threshold to reduce flickering from tiny movements
if (last.x - current_pos.x).abs() > 0.1 || (last.y - current_pos.y).abs() > 0.1
{
current_preview.push(Vec2::new(current_pos.x, current_pos.y));
}
} else if !current_preview.is_empty() {
current_preview.push(Vec2::new(turtle.position.x, turtle.position.y));
current_preview.push(Vec2::new(
tween.current_position.x,
tween.current_position.y,
));
}
} else {
// No active tween - use the actual current contour from fill state
current_preview = fill_state
.current_contour
.iter()
.map(|c| Vec2::new(c.x, c.y))
.collect();
// No active tween - just show current state
if !current_preview.is_empty() {
if let Some(last) = current_preview.last() {
let current_pos = turtle.position;
let current_pos = turtle.params.position;
if (last.x - current_pos.x).abs() > 0.1
|| (last.y - current_pos.y).abs() > 0.1
{
@@ -254,8 +270,8 @@ pub fn render_world_with_tween(
// Draw all visible turtles
for turtle in &world.turtles {
if turtle.visible {
draw_turtle(turtle);
if turtle.params.visible {
draw_turtle(&turtle.params);
}
}
@@ -277,8 +293,8 @@ fn draw_tween_arc(
direction: CircleDirection,
) {
let geom = CircleGeometry::new(
tween.start_state.position,
tween.start_state.heading,
tween.start_params.position,
tween.start_params.heading,
radius,
direction,
);
@@ -301,8 +317,8 @@ fn draw_tween_arc(
radius,
geom.start_angle_from_center.to_degrees(),
total_angle * progress,
tween.start_state.color,
tween.start_state.pen_width,
tween.start_params.color,
tween.start_params.pen_width,
((steps as f32 * progress).ceil() as usize).max(1),
direction,
) {
@@ -311,15 +327,15 @@ fn draw_tween_arc(
}
/// Draw the turtle shape
pub fn draw_turtle(turtle: &TurtleState) {
let rotated_vertices = turtle.shape.rotated_vertices(turtle.heading);
pub fn draw_turtle(turtle_params: &TurtleParams) {
let rotated_vertices = turtle_params.shape.rotated_vertices(turtle_params.heading);
if turtle.shape.filled {
if turtle_params.shape.filled {
// Draw filled polygon using Lyon tessellation
if rotated_vertices.len() >= 3 {
let absolute_vertices: Vec<Vec2> = rotated_vertices
.iter()
.map(|v| turtle.position + *v)
.map(|v| turtle_params.position + *v)
.collect();
// Use Lyon for turtle shape too
@@ -345,8 +361,8 @@ pub fn draw_turtle(turtle: &TurtleState) {
if !rotated_vertices.is_empty() {
for i in 0..rotated_vertices.len() {
let next_i = (i + 1) % rotated_vertices.len();
let p1 = turtle.position + rotated_vertices[i];
let p2 = turtle.position + rotated_vertices[next_i];
let p1 = turtle_params.position + rotated_vertices[i];
let p2 = turtle_params.position + rotated_vertices[next_i];
draw_line(p1.x, p1.y, p2.x, p2.y, 2.0, Color::new(0.0, 0.5, 1.0, 1.0));
}
}