We refactored the builder pattern in [`turtle-lib`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib) to eliminate inherent method asymmetry and organize all turtle capabilities into six cohesive traits. [`builders.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/builders.rs) The legacy traits (`DirectionalMovement`, `Turnable`, `CurvedMovement`) and orphaned inherent methods have been reorganized into six domain-focused traits: - **[`Movement`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/builders.rs#L14)**: - `forward()` - `backward()` - `go_to()` - `circle_left()` - `circle_right()` - **[`Rotation`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/builders.rs#L191)**: - `left()` - `right()` - `set_heading()` - **[`Pen`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/builders.rs#L279)**: - `pen_up()` - `pen_down()` - `set_pen_color()` - `set_pen_width()` - **[`Fill`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/builders.rs#L393)**: - `begin_fill()` - `end_fill()` - `set_fill_color()` - **[`Cursor`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/builders.rs#L481)**: - `hide()` - `show()` - `shape()` - `set_shape()` - `set_speed()` - `reset()` - **[`Text`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/builders.rs#L646)**: - `write_text()` [`TurtlePlan`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/builders.rs#L688) `TurtlePlan`'s inherent methods are now strictly builder lifecycle controls: - `new() -> Self` - `build(self) -> CommandQueue` `TurtlePlan` implements `WithCommands`, `Movement`, `Rotation`, `Pen`, `Fill`, `Cursor`, and `Text`. - **[`lib.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/lib.rs#L62-L65)**: Re-exports `Cursor`, `Fill`, `Movement`, `Pen`, `Rotation`, `Text`, `TurtlePlan`, `WithCommands`. - **Examples**: Updated [`clock.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/examples/clock.rs#L8), [`clock_threaded.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/examples/clock_threaded.rs#L9), [`dashed_circle.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/examples/dashed_circle.rs#L4), and [`bezier.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/examples/bezier.rs#L4) to use `use turtle_lib::*;`. - **[`README.md`](file:///home/dietrich/Projekte/Source/turtlers/README.md#L344)**: Updated trait references in the architecture outline. --- - **Unit & Doc Tests**: ```bash cargo test --package turtle-lib ``` Result: 17 unit tests passed; 34 doc-tests passed (0 failed). - **All Examples**: ```bash cargo check --package turtle-lib --examples ``` Result: Successfully compiled all 30 examples. - **Clippy**: ```bash cargo clippy --package turtle-lib -- -Wclippy::pedantic \ -Aclippy::cast_precision_loss -Aclippy::cast_sign_loss -Aclippy::cast_possible_truncation ``` Result: 0 warnings in `builders.rs`.
357 lines
14 KiB
Rust
357 lines
14 KiB
Rust
//! Rendering logic using Macroquad and Lyon tessellation
|
|
|
|
use crate::circle_geometry::{CircleDirection, CircleGeometry};
|
|
use crate::state::{DrawCommand, TurtleParams, TurtleWorld};
|
|
use crate::tessellation;
|
|
use macroquad::prelude::*;
|
|
|
|
/// Render the turtle world with active tween visualization.
|
|
#[allow(clippy::too_many_lines)]
|
|
pub(crate) 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
|
|
let camera = Camera2D {
|
|
zoom: vec2(
|
|
1.0 / screen_width() * 2.0 / zoom_level,
|
|
1.0 / screen_height() * 2.0 / zoom_level,
|
|
),
|
|
target: world.camera.target,
|
|
..Default::default()
|
|
};
|
|
|
|
// Set camera
|
|
set_camera(&camera);
|
|
|
|
// Draw all accumulated commands from all turtles
|
|
for turtle in &world.turtles {
|
|
for cmd in &turtle.commands {
|
|
match cmd {
|
|
DrawCommand::Mesh(mesh) => {
|
|
draw_mesh(mesh);
|
|
}
|
|
DrawCommand::Text {
|
|
text,
|
|
position,
|
|
heading,
|
|
font_size,
|
|
color,
|
|
} => {
|
|
draw_text_command(text, *position, *heading, *font_size, *color);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw in-progress tween lines for all active tweens
|
|
for turtle in &world.turtles {
|
|
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.value(), *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_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(
|
|
tween.current_position.x,
|
|
tween.current_position.y,
|
|
tween.start_params.pen_width / 2.0,
|
|
tween.start_params.color,
|
|
);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw live fill preview for all turtles that are currently filling
|
|
for turtle in &world.turtles {
|
|
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
|
|
for completed_contour in &fill_state.contours {
|
|
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
|
|
// 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
|
|
.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,
|
|
steps,
|
|
direction,
|
|
} = &tween.command
|
|
{
|
|
// Calculate partial arc vertices based on current progress
|
|
use crate::circle_geometry::{arc_points, CircleGeometry};
|
|
use crate::general::Radians;
|
|
let geom = CircleGeometry::new(
|
|
tween.start_params.position,
|
|
Radians::new(tween.start_params.heading),
|
|
radius.value(),
|
|
*direction,
|
|
);
|
|
// Delegate to the shared arc_points function — same sampling
|
|
// strategy as tessellate_arc, using tween.progress directly.
|
|
let samples_to_draw =
|
|
(((*steps).max(1) as f32 * tween.progress) as usize).max(1);
|
|
let sweep_so_far = angle.as_radians().value() * tween.progress;
|
|
for pt in arc_points(
|
|
geom.center,
|
|
radius.value(),
|
|
geom.start_angle_from_center,
|
|
sweep_so_far,
|
|
samples_to_draw,
|
|
*direction,
|
|
) {
|
|
current_preview.push(pt);
|
|
}
|
|
} else if matches!(
|
|
&tween.command,
|
|
crate::commands::TurtleCommand::Move(_)
|
|
| crate::commands::TurtleCommand::Goto(_)
|
|
) {
|
|
// 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,
|
|
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(
|
|
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 = 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(
|
|
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.params.position;
|
|
if (last.x - current_pos.x).abs() > 0.1
|
|
|| (last.y - current_pos.y).abs() > 0.1
|
|
{
|
|
current_preview.push(Vec2::new(current_pos.x, current_pos.y));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add current contour to all contours if it has enough vertices
|
|
if current_preview.len() >= 3 {
|
|
all_contours.push(current_preview);
|
|
}
|
|
|
|
// Tessellate and draw all contours together using multi-contour tessellation
|
|
if !all_contours.is_empty() {
|
|
match crate::tessellation::tessellate_multi_contour(
|
|
&all_contours,
|
|
fill_state.fill_color,
|
|
) {
|
|
Ok(mesh) => {
|
|
draw_mesh(&mesh);
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to tessellate fill preview: {:?}", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw all visible turtles
|
|
for turtle in &world.turtles {
|
|
if turtle.params.visible {
|
|
draw_turtle(&turtle.params);
|
|
}
|
|
}
|
|
|
|
// Reset to default camera
|
|
set_default_camera();
|
|
}
|
|
|
|
fn should_draw_tween_line(command: &crate::commands::TurtleCommand) -> bool {
|
|
use crate::commands::TurtleCommand;
|
|
matches!(command, TurtleCommand::Move(..) | TurtleCommand::Goto(..))
|
|
}
|
|
|
|
/// Draw a text command with rotation based on turtle heading
|
|
fn draw_text_command(
|
|
text: &str,
|
|
position: Vec2,
|
|
heading_radians: f32,
|
|
font_size: crate::general::FontSize,
|
|
color: Color,
|
|
) {
|
|
// Heading in turtle coordinates: 0 rad = right, positive = counter-clockwise
|
|
// Macroquad rotation: same convention (0 = right, positive = counter-clockwise)
|
|
// So we use the heading directly
|
|
let rotation_rad = heading_radians;
|
|
|
|
// Calculate perpendicular offset (90° clockwise from heading)
|
|
// This places text slightly to the right of the movement direction
|
|
let font_size_val = font_size.value();
|
|
let offset_distance = f32::from(font_size_val) / 3.0;
|
|
|
|
// Perpendicular direction: heading - π/2 (rotated 90° clockwise)
|
|
let perpendicular_angle = heading_radians - std::f32::consts::PI / 2.0;
|
|
let offset_x = offset_distance * perpendicular_angle.cos();
|
|
let offset_y = offset_distance * perpendicular_angle.sin();
|
|
|
|
draw_text_ex(
|
|
text,
|
|
position.x + offset_x,
|
|
position.y + offset_y,
|
|
TextParams {
|
|
font_size: font_size_val,
|
|
rotation: rotation_rad,
|
|
color,
|
|
..Default::default()
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Draw arc segments for circle tween animation
|
|
fn draw_tween_arc(
|
|
tween: &crate::tweening::CommandTween,
|
|
radius: f32,
|
|
total_angle: crate::general::Degrees,
|
|
steps: usize,
|
|
direction: CircleDirection,
|
|
) {
|
|
use crate::general::Radians;
|
|
let geom = CircleGeometry::new(
|
|
tween.start_params.position,
|
|
Radians::new(tween.start_params.heading),
|
|
radius,
|
|
direction,
|
|
);
|
|
|
|
// Draw center using Lyon tessellation this helps visualizing what is done.
|
|
if let Ok(mesh) = crate::tessellation::tessellate_circle(geom.center, 5.0, GRAY, true, 1.0) {
|
|
draw_mesh(&mesh);
|
|
}
|
|
|
|
// Draw the partial arc traveled based on tween progress
|
|
if let Ok(mesh) = crate::tessellation::tessellate_arc(
|
|
geom.center,
|
|
radius,
|
|
geom.start_angle_from_center.to_degrees(),
|
|
total_angle.value() * tween.progress,
|
|
tween.start_params.color,
|
|
tween.start_params.pen_width,
|
|
((steps as f32 * tween.progress).ceil() as usize).max(1),
|
|
direction,
|
|
) {
|
|
draw_mesh(&mesh);
|
|
}
|
|
}
|
|
|
|
/// Draw the turtle shape
|
|
pub(crate) fn draw_turtle(turtle_params: &TurtleParams) {
|
|
let rotated_vertices = turtle_params.shape.rotated_vertices(turtle_params.heading);
|
|
|
|
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_params.position + *v)
|
|
.collect();
|
|
|
|
// Use Lyon for turtle shape too
|
|
if let Ok(mesh) =
|
|
tessellation::tessellate_polygon(&absolute_vertices, Color::new(0.0, 0.5, 1.0, 1.0))
|
|
{
|
|
draw_mesh(&mesh);
|
|
} else {
|
|
// Fallback to simple triangle fan if Lyon fails
|
|
let first = absolute_vertices[0];
|
|
for i in 1..absolute_vertices.len() - 1 {
|
|
draw_triangle(
|
|
first,
|
|
absolute_vertices[i],
|
|
absolute_vertices[i + 1],
|
|
Color::new(0.0, 0.5, 1.0, 1.0),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Draw outline
|
|
if !rotated_vertices.is_empty() {
|
|
for i in 0..rotated_vertices.len() {
|
|
let next_i = (i + 1) % rotated_vertices.len();
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
}
|