remove the bevy based turtle and rename turtle-lib-macroquad to turtle-lib

This commit is contained in:
2025-10-12 20:31:05 +02:00
parent fe2beb01ed
commit 08a1802bd2
46 changed files with 100 additions and 510 deletions
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "turtle-lib"
version = "0.2.0"
edition = "2021"
license = "MIT OR Apache-2.0"
[dependencies]
macroquad = "0.4"
tween = "2.1.0"
lyon = "1.0"
tracing = { version = "0.1", features = ["log"], default-features = false }
turtle-lib-macros = { path = "../turtle-lib-macros" }
[dev-dependencies]
# For examples and testing
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
+113
View File
@@ -0,0 +1,113 @@
# turtle-lib
The main turtle graphics library built on Macroquad with Lyon tessellation.
**See the [main README](../README.md) for complete documentation.**
## Features
✅ **Complete Lyon Integration** - All drawing operations use GPU-optimized tessellation
- Unified rendering pipeline for lines, arcs, circles, and fills
- ~410 lines of code eliminated through architectural simplification
- Consistent high-quality rendering across all primitives
✅ **Multi-Contour Fill System** - Advanced fill capabilities with automatic hole detection
- EvenOdd fill rule for complex shapes with holes (like cheese or yin-yang symbols)
- `pen_up()` closes current contour, `pen_down()` opens next contour
- Progressive fill preview during animations
- Support for self-intersecting paths
✅ **Smooth Animation** - Tweening system with live rendering
- Configurable speed control
- Frame-rate independent animation
- Live fill preview during circle/arc drawing
## Quick Start
### Using the `turtle_main` Macro (Recommended for Beginners)
The easiest way to create turtle programs is with the `turtle_main` macro:
```rust
use macroquad::prelude::*;
use turtle_lib::*;
#[turtle_main("My First Drawing")]
fn my_drawing(turtle: &mut TurtlePlan) {
turtle.set_pen_color(RED);
turtle.forward(100.0);
turtle.right(90.0);
turtle.forward(100.0);
}
```
The macro automatically handles:
- Window creation and setup
- Turtle initialization
- Rendering loop
- Quit handling (ESC or Q keys)
### Manual Setup (For Advanced Use)
For more control over the application loop:
```rust
use macroquad::prelude::*;
use turtle_lib::*;
#[macroquad::main("Turtle")]
async fn main() {
let mut turtle = create_turtle();
turtle.forward(100.0).right(90.0);
let mut app = TurtleApp::new().with_commands(turtle.build());
loop {
clear_background(WHITE);
app.update();
app.render();
next_frame().await;
}
}
```
## Quick Examples
All examples now use the `turtle_main` macro for simplicity:
```bash
# Run from this directory
cargo run --example hello_turtle # Minimal 10-line example
cargo run --example macro_demo # Simple square with macro
cargo run --example square # Basic square drawing
cargo run --example shapes # Different turtle shapes
cargo run --example yinyang # Multi-contour fills with holes
cargo run --example koch # Recursive fractals
cargo run --example fill_demo # Fill with holes (donut)
cargo run --example cheese_macro # Cheese example using macro
cargo run --example fill_advanced # Complex shapes (manual setup)
```
Most examples use `turtle_main` for simplicity. A few keep manual setup for custom UI or logging.
## Architecture Highlights
### Rendering Pipeline
All drawing operations → Lyon tessellation → GPU mesh rendering
### DrawCommand Enum
Simplified from 5 variants to 1:
- `Mesh(MeshData)` - unified variant for all drawing operations
### Fill System
- `FillState` tracks multiple contours (completed + current)
- Pen state management automatically handles contour creation
- EvenOdd tessellation provides automatic hole detection
See [LYON_COMPLETE.md](LYON_COMPLETE.md) and [MULTI_CONTOUR_FILLS.md](MULTI_CONTOUR_FILLS.md) for implementation details.
## Status
✅ **Stable** - Lyon integration complete, multi-contour fills working, all examples passing.
See [../README.md](../README.md) for full API documentation and project status.
+97
View File
@@ -0,0 +1,97 @@
//! Cheese example - demonstrates multi-contour fills with holes
//!
//! This example creates a cheese-like shape by:
//! 1. Drawing the outer square boundary
//! 2. Lifting the pen (pen_up) to close that contour
//! 3. Drawing circular and triangular holes with pen_up/pen_down
//!
//! Lyon's EvenOdd fill rule automatically creates holes where contours overlap!
use macroquad::prelude::*;
use turtle_lib::*;
#[macroquad::main("Cheese with Holes")]
async fn main() {
let mut turtle = create_turtle();
// Set fill color to yellow (cheese color!)
turtle.set_fill_color(YELLOW);
turtle.set_pen_color(ORANGE);
turtle.set_pen_width(3.0);
println!("=== Starting cheese fill ===");
turtle.begin_fill();
// Draw outer boundary (large square)
println!("Drawing outer square boundary...");
for _ in 0..4 {
turtle.forward(400.0);
turtle.right(90.0);
}
// Close outer contour and start drawing holes
println!("Closing outer contour with pen_up");
turtle.pen_up();
// Draw triangular hole in the middle
println!("Drawing triangular hole...");
turtle.go_to(vec2(200.0, 120.0));
turtle.pen_down(); // Start new contour for hole
for _ in 0..3 {
turtle.forward(160.0);
turtle.right(120.0);
}
println!("Closing triangle contour with pen_up");
turtle.pen_up(); // Close triangle hole contour
// Draw circular hole (top-left) using circle_left
println!("Drawing circular hole (top-left) with circle_left...");
turtle.go_to(vec2(100.0, 100.0));
turtle.pen_down(); // Start new contour for hole
turtle.circle_left(30.0, 360.0, 36); // radius=30, full circle, 36 steps
println!("Closing circle contour with pen_up");
turtle.pen_up(); // Close circle hole contour
// Draw circular hole (bottom-right) using circle_right
println!("Drawing circular hole (bottom-right) with circle_right...");
turtle.go_to(vec2(280.0, 280.0));
turtle.pen_down(); // Start new contour for hole
turtle.circle_right(40.0, 360.0, 36); // radius=40, full circle, 36 steps
println!("Closing circle contour with pen_up");
turtle.pen_up(); // Close circle hole contour
// End fill - Lyon will automatically create holes!
println!("Calling end_fill - Lyon should create holes now!");
turtle.end_fill();
// Set animation speed
turtle.set_speed(300);
println!("Building and executing turtle plan...");
// Execute the plan
let mut app = TurtleApp::new().with_commands(turtle.build());
loop {
clear_background(Color::new(0.95, 0.95, 0.98, 1.0));
app.update();
app.render();
// Instructions
draw_text(
"Cheese with Holes - pen_up/pen_down creates multiple contours!",
10.0,
20.0,
18.0,
BLACK,
);
draw_text("Press ESC or Q to quit", 10.0, 40.0, 16.0, DARKGRAY);
if is_key_pressed(KeyCode::Escape) || is_key_pressed(KeyCode::Q) {
break;
}
next_frame().await;
}
}
+66
View File
@@ -0,0 +1,66 @@
//! Cheese example using the turtle_main macro
//!
//! This is a simplified version of cheese.rs that demonstrates how the
//! turtle_main macro reduces boilerplate code.
use turtle_lib::*;
#[turtle_main("Cheese with Holes - Using Macro")]
fn draw_cheese(turtle: &mut TurtlePlan) {
// Set fill color to yellow (cheese color!)
turtle.set_pen_color(ORANGE);
turtle.set_pen_width(3.0);
turtle.set_fill_color(YELLOW);
println!("=== Starting cheese fill ===");
turtle.begin_fill();
// Draw outer boundary (large square)
println!("Drawing outer square boundary...");
for _ in 0..4 {
turtle.forward(400.0);
turtle.right(90.0);
}
// Close outer contour and start drawing holes
println!("Closing outer contour with pen_up");
turtle.pen_up();
// Draw triangular hole in the middle
println!("Drawing triangular hole...");
turtle.go_to(vec2(200.0, 120.0));
turtle.pen_down(); // Start new contour for hole
for _ in 0..3 {
turtle.forward(160.0);
turtle.right(120.0);
}
println!("Closing triangle contour with pen_up");
turtle.pen_up(); // Close triangle hole contour
// Draw circular hole (top-left) using circle_left
println!("Drawing circular hole (top-left) with circle_left...");
turtle.go_to(vec2(100.0, 100.0));
turtle.pen_down(); // Start new contour for hole
turtle.circle_left(30.0, 360.0, 36); // radius=30, full circle, 36 steps
println!("Closing circle contour with pen_up");
turtle.pen_up(); // Close circle hole contour
// Draw circular hole (bottom-right) using circle_right
println!("Drawing circular hole (bottom-right) with circle_right...");
turtle.go_to(vec2(280.0, 280.0));
turtle.pen_down(); // Start new contour for hole
turtle.circle_right(40.0, 360.0, 36); // radius=40, full circle, 36 steps
println!("Closing circle contour with pen_up");
turtle.pen_up(); // Close circle hole contour
// End fill - Lyon will automatically create holes!
println!("Calling end_fill - Lyon should create holes now!");
turtle.end_fill();
// Set animation speed
turtle.set_speed(300);
println!("Building and executing turtle plan...");
}
+29
View File
@@ -0,0 +1,29 @@
//! Test circle_left and circle_right commands
use turtle_lib::*;
#[turtle_main("Circle Test")]
fn draw(turtle: &mut TurtlePlan) {
turtle.shape(ShapeType::Turtle);
// Draw some circles
turtle.set_pen_color(RED);
turtle.set_pen_width(0.5);
turtle.left(90.0);
turtle.set_speed(999);
turtle.circle_left(100.0, 540.0, 72); // partial circle to the left
turtle.forward(150.0);
turtle.set_speed(100);
turtle.set_pen_color(BLUE);
turtle.circle_right(50.0, 270.0, 72); // partial circle to the right
// Set animation speed
turtle.set_speed(20);
turtle.forward(150.0);
turtle.circle_left(50.0, 180.0, 12);
turtle.circle_right(50.0, 180.0, 12);
turtle.set_speed(700);
turtle.set_pen_color(GREEN);
turtle.circle_left(50.0, 180.0, 36); // Half circle to the left
}
+93
View File
@@ -0,0 +1,93 @@
//! Draw a dragon curve, more specifically a Heighway dragon.
//!
//! (https://en.wikipedia.org/wiki/Dragon_curve)
//!
//! As can be seen in the above Wikipedia article, the Heighway dragon can be
//! constructed by repeatedly folding a strip of paper and looking at the
//! directions of the folds/turns.
//!
//! Starting with a strip going left to right (l2r):
//!
//! start|--->---l2r--->---|end
//!
//! you might fold it like this:
//!
//! end|---<---r2l---<---\
//! start|->---l2r--->---/
//!
//! Getting a l2r strip, followed by a left turn, followed by a r2l strip.
//!
//! Folding a right to left strip:
//!
//! end|---<---r2l---<---|start
//!
//! In the same way:
//!
//! start|-->---l2r--->---\
//! end|----<---r2l---<---/
//!
//! Would give you a l2r, followed by a right turn, followed by a r2l strip.
//!
//! As you can see, the only difference between the two is the direction of
//! the turn in the middle.
//!
//! This folding of paper is simulated by recursively calling the dragon(..)
//! function, passing the direction of the turn for this fold as an angle
//! (+90 for a right turn, -90 for a left turn).
use turtle_lib::*;
#[turtle_main("Dragon Curve")]
fn draw_dragon(turtle: &mut TurtlePlan) {
// Fast drawing
turtle.set_speed(1200);
// Start position
turtle.pen_up();
turtle.backward(160.0);
turtle.right(90.0);
turtle.forward(110.0);
turtle.pen_down();
turtle.set_pen_width(6.);
// Draw the dragon curve with 13 folds
dragon(turtle, -90.0, 13, 0.0, 255.0);
// Hide turtle when done
turtle.hide();
}
/// Draw the dragon curve by simulating folding a strip of paper
///
/// Arguments:
/// `fold_direction`: The direction of the fold, +90 for a right, -90 for a
/// left turn.
/// `num_folds`: The number of times to fold the 'strip of paper'.
/// `color_start`/`color_end`: The color at the start/end of this subsection
/// of the curve as a number 0-255.
fn dragon(
turtle: &mut TurtlePlan,
fold_direction: f32,
num_folds: usize,
color_start: f32,
color_end: f32,
) {
let color_mid = (color_start + color_end) * 0.5;
if num_folds == 0 {
// Mapping a color number 0-255 to an RGB gradient
let red = ((color_mid - 128.0).abs() * 2.0).floor();
let green = color_mid;
let blue = 160.0;
turtle.set_pen_color(Color::new(red / 255.0, green / 255.0, blue / 255.0, 1.0));
turtle.forward(10.0);
return;
}
// Draw a left to right strip (which has a left turn in the middle)
dragon(turtle, -90.0, num_folds - 1, color_start, color_mid);
turtle.right(fold_direction);
// Draw a right to left strip (which has a right turn in the middle)
dragon(turtle, 90.0, num_folds - 1, color_mid, color_end);
}
+159
View File
@@ -0,0 +1,159 @@
//! Advanced fill example with multiple holes and complex shapes
//!
//! This example uses manual setup to demonstrate custom window size and UI elements.
use macroquad::{miniquad::window::set_window_size, prelude::*};
use turtle_lib::*;
#[macroquad::main("Advanced Fill Demo")]
async fn main() {
set_window_size(2000, 1900);
let mut t = create_turtle();
// Example 1: Star shape (concave polygon)
t.pen_up();
t.go_to(vec2(-200.0, 100.0));
t.pen_down();
t.set_heading(0.0);
t.set_fill_color(GOLD);
t.set_pen_color(ORANGE);
t.set_pen_width(2.0);
t.set_speed(500);
t.begin_fill();
// Draw 5-pointed star
for _ in 0..5 {
t.forward(100.0);
t.right(144.0);
}
t.end_fill();
// Example 2: Swiss cheese (polygon with multiple holes)
t.pen_up();
t.go_to(vec2(100.0, 100.0));
t.pen_down();
t.set_heading(0.0);
t.set_fill_color(YELLOW);
t.set_pen_color(ORANGE);
t.begin_fill();
// Outer square
for _ in 0..4 {
t.forward(150.0);
t.right(90.0);
}
// First hole (circle)
t.pen_up();
t.go_to(vec2(140.0, 130.0));
t.pen_down();
t.circle_right(150.0, 360.0, 36);
// Second hole (circle)
t.pen_up();
t.go_to(vec2(200.0, 170.0));
t.pen_down();
t.circle_right(10.0, 360.0, 36);
// Third hole (triangle)
t.pen_up();
t.go_to(vec2(160.0, 200.0));
t.pen_down();
t.circle_right(15.0, 360.0, 3);
// Fourth hole (square)
t.pen_up();
t.go_to(vec2(190.0, 200.0));
t.pen_down();
t.circle_right(15.0, 360.0, 4);
// fifth hole (pentagon)
t.pen_up();
t.go_to(vec2(230.0, 200.0));
t.pen_down();
t.circle_right(15.0, 360.0, 5);
t.end_fill();
// Example 3: Donut (circle with circular hole)
t.pen_up();
t.go_to(vec2(-100.0, -100.0));
t.pen_down();
t.set_heading(0.0);
t.set_fill_color(Color::new(0.8, 0.4, 0.2, 1.0));
t.set_pen_color(Color::new(0.6, 0.3, 0.1, 1.0));
t.begin_fill();
// Outer circle
for _ in 0..72 {
t.forward(3.0);
t.right(5.0);
}
// Move to inner circle
t.pen_up();
t.go_to(vec2(-75.0, -90.0));
t.pen_down();
// Inner circle (hole)
for _ in 0..72 {
t.forward(1.5);
t.right(5.0);
}
t.end_fill();
// Set animation speed
t.set_speed(500);
let mut app = TurtleApp::new().with_commands(t.build());
let target_fps = 1.0; // 1 frame per second for debugging
let frame_time = 1.0 / target_fps;
let mut last_frame_time = macroquad::time::get_time();
loop {
// Frame rate limiting
let current_time = macroquad::time::get_time();
let delta = current_time - last_frame_time;
if delta < frame_time {
// std::thread::sleep(std::time::Duration::from_secs_f64(frame_time - delta));
}
last_frame_time = macroquad::time::get_time();
clear_background(Color::new(0.95, 0.95, 0.98, 1.0));
app.update();
app.render();
// Instructions
draw_text(
"Advanced Fill Demo: Star, Swiss Cheese, Donut",
10.0,
20.0,
20.0,
BLACK,
);
draw_text(
"Features: concave polygons, multiple holes, pen_up during fill",
10.0,
40.0,
16.0,
DARKGRAY,
);
draw_text(
"Mouse: drag to pan, scroll to zoom",
10.0,
60.0,
16.0,
DARKGRAY,
);
next_frame().await
}
}
+14
View File
@@ -0,0 +1,14 @@
//! Minimal turtle example - just 10 lines!
//!
//! This is the simplest possible turtle program using the macro.
use turtle_lib::*;
#[turtle_main("Hello Turtle")]
fn hello() {
turtle.set_pen_color(BLUE);
for _ in 0..4 {
turtle.forward(100.0);
turtle.right(90.0);
}
}
+37
View File
@@ -0,0 +1,37 @@
//! Koch snowflake fractal example
use turtle_lib::*;
fn koch(depth: u32, turtle: &mut TurtlePlan, distance: f32) {
if depth == 0 {
turtle.forward(distance);
} else {
let new_distance = distance / 3.0;
koch(depth - 1, turtle, new_distance);
turtle.left(60.0);
koch(depth - 1, turtle, new_distance);
turtle.right(120.0);
koch(depth - 1, turtle, new_distance);
turtle.left(60.0);
koch(depth - 1, turtle, new_distance);
}
}
#[turtle_main("Koch Snowflake")]
fn draw(turtle: &mut TurtlePlan) {
// Position turtle
turtle.set_speed(1001);
turtle.pen_up();
turtle.backward(150.0);
turtle.pen_down();
// Draw Koch snowflake (triangle of Koch curves)
for _ in 0..3 {
koch(4, turtle, 300.0);
turtle.right(120.0);
turtle.set_speed(1200);
}
turtle.hide(); // Hide turtle when done
}
+100
View File
@@ -0,0 +1,100 @@
//! Example demonstrating how to enable logging/tracing output from the turtle library
//!
//! This example shows how to use `tracing-subscriber` to see debug output from the library.
//! You can control the log level using the `RUST_LOG` environment variable:
//!
//! ```bash
//! # Show all debug output from turtle-lib
//! RUST_LOG=turtle_lib=debug cargo run --example logging_example
//!
//! # Show only warnings and errors
//! RUST_LOG=turtle_lib=warn cargo run --example logging_example
//!
//! # Show trace-level output (very verbose, includes all vertices)
//! RUST_LOG=turtle_lib=trace cargo run --example logging_example
//!
//! # Show debug output from specific modules
//! RUST_LOG=turtle_lib::tessellation=debug cargo run --example logging_example
//! RUST_LOG=turtle_lib::execution=debug cargo run --example logging_example
//! ```
//!
//! Note: This example uses manual setup to demonstrate custom initialization logic.
use macroquad::prelude::*;
use turtle_lib::*;
#[macroquad::main("Turtle Logging Example")]
async fn main() {
// Initialize tracing subscriber to see debug output
// This will respect the RUST_LOG environment variable
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=info")
}),
)
.with_target(true) // Show which module the log came from
.with_thread_ids(false)
.with_line_number(true) // Show line numbers
.with_file(false)
.init();
tracing::info!("Starting turtle graphics example with logging enabled");
tracing::info!(
"Try running with: RUST_LOG=turtle_lib=debug cargo run --example logging_example"
);
// Create a turtle plan with fill operations to see detailed logging
let mut t = create_turtle();
t.set_speed(900);
// Draw a yin-yang symbol with fills (generates lots of debug output)
t.circle_left(90.0, 180.0, 36);
t.begin_fill();
t.circle_left(90.0, 180.0, 36);
t.circle_left(45.0, 180.0, 26);
t.circle_right(45.0, 180.0, 26);
t.pen_up();
t.right(90.0);
t.forward(37.0);
t.left(90.0);
t.pen_down();
t.circle_right(8.0, 360.0, 12);
t.pen_up();
t.right(90.0);
t.forward(90.0);
t.left(90.0);
t.pen_down();
t.circle_right(8.0, 360.0, 12);
t.end_fill();
tracing::info!("Turtle plan created, starting animation");
// Set animation speed
t.set_speed(100); // Slow animation to see the logs in real-time
// Create turtle app
let mut app = TurtleApp::new().with_commands(t.build());
// Main loop
loop {
clear_background(WHITE);
// Update and render - this is where you'll see debug logs
app.update();
app.render();
// Exit when animation is complete
if app.is_complete() {
tracing::info!("Animation complete, press any key to exit");
if is_key_pressed(KeyCode::Space) {
break;
}
}
next_frame().await
}
tracing::info!("Example finished");
}
+17
View File
@@ -0,0 +1,17 @@
//! Simple demo of the turtle_main macro
//!
//! This example shows how the turtle_main macro simplifies turtle programs
//! by automatically handling window setup, turtle creation, and the render loop.
use turtle_lib::*;
#[turtle_main("Macro Demo - Simple Square")]
fn draw_square(turtle: &mut TurtlePlan) {
turtle.set_pen_color(BLUE);
turtle.set_pen_width(3.0);
for _ in 0..4 {
turtle.forward(150.0);
turtle.right(90.0);
}
}
+17
View File
@@ -0,0 +1,17 @@
//! 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);
}
}
+57
View File
@@ -0,0 +1,57 @@
//! Nikolaus example - draws a house-like figure
use turtle_lib::*;
fn nikolausquadrat(turtle: &mut TurtlePlan, groesse: f32) {
turtle.forward(groesse);
turtle.left(90.0);
turtle.forward(groesse);
turtle.left(90.0);
turtle.forward(groesse);
turtle.left(90.0);
turtle.forward(groesse);
turtle.left(90.0);
}
fn nikolausdiag(turtle: &mut TurtlePlan, groesse: f32) {
let quadrat = groesse * groesse;
let diag = (quadrat + quadrat).sqrt();
turtle.left(45.0);
turtle.forward(diag);
turtle.left(45.0);
nikolausdach2(turtle, groesse);
turtle.left(45.0);
turtle.forward(diag);
turtle.left(45.0);
}
fn nikolausdach2(turtle: &mut TurtlePlan, groesse: f32) {
let quadrat = groesse * groesse;
let diag = (quadrat + quadrat).sqrt();
turtle.left(45.0);
turtle.forward(diag / 2.0);
turtle.left(90.0);
turtle.forward(diag / 2.0);
turtle.left(45.0);
}
fn nikolaus(turtle: &mut TurtlePlan, groesse: f32) {
nikolausquadrat(turtle, groesse);
nikolausdiag(turtle, groesse);
}
#[turtle_main("Nikolaus")]
fn draw(turtle: &mut TurtlePlan) {
turtle.shape(ShapeType::Turtle);
// Position the turtle (pen up, move, pen down)
turtle.pen_up();
turtle.backward(80.0);
turtle.left(90.0);
turtle.forward(50.0);
turtle.right(90.0);
turtle.pen_down();
nikolaus(turtle, 100.0);
}
+32
View File
@@ -0,0 +1,32 @@
//! Example demonstrating different turtle shapes
use turtle_lib::*;
#[turtle_main("Turtle Shapes")]
fn draw(turtle: &mut TurtlePlan) {
// Start with triangle (default)
turtle.forward(100.0);
turtle.right(90.0);
// Change to turtle shape
turtle.shape(ShapeType::Turtle);
turtle.forward(100.0);
turtle.right(90.0);
// Change to circle
turtle.shape(ShapeType::Circle);
turtle.forward(100.0);
turtle.right(90.0);
// Change to square
turtle.shape(ShapeType::Square);
turtle.forward(100.0);
turtle.right(90.0);
// Change to arrow
turtle.shape(ShapeType::Arrow);
turtle.forward(100.0);
// Set animation speed
turtle.set_speed(50);
}
@@ -0,0 +1,98 @@
//! Draws a Sierpiński triangle with automatic positioning and sizing.
//!
//! The Sierpiński triangle is a fairly simple self-similar fractal geometric shape: it consists of
//! many nested equilateral triangles. More formally, such a triangle is itself three triangles of
//! one level below and a size divided by two. Level zero means a simple equilateral triangle. The
//! drawing procedure is as follows, for a given level and size:
//!
//! * If level is 0
//! * Draw an equilateral triangle of the given size.
//! * otherwise
//! * Draw the half-sized level - 1 triangle at the bottom left.
//! * Go the start of the bottom-right slot.
//! * Draw a half-sized level - 1 triangle.
//! * Go to the start of the top slot.
//! * Draw a half-sized level - 1 triangle.
//!
//! That is relatively easy to implement, as long as you follow these steps and let recursion do
//! the rest. Another little bonus this example provides is the ability to customize the drawing
//! size: the triangle will stay correctly sized and positioned automatically.
use macroquad::window::{screen_height, screen_width};
use turtle_lib::*;
/// The number of levels to draw following the recursive procedure.
const LEVELS: u8 = 9;
/// Triangle size (adjust to fit nicely in window)
const TRIANGLE_SIZE: f32 = 300.0;
#[turtle_main("Sierpiński Triangle")]
fn draw_sierpinski(turtle: &mut TurtlePlan) {
turtle.set_speed(1500); // Fast drawing
turtle.set_pen_width(0.2);
// Auto-sized procedure
sierpinski_triangle_auto(turtle, LEVELS);
// Hide turtle when done drawing in order to fully reveal the result
turtle.hide();
}
/// Recursive function drawing a Sierpiński triangle.
///
/// It will do it with the given `turtle` and start at its current position and heading. `level`
/// is the depth of the drawing to be done, zero meaning a simple triangle. `size` is the length
/// of the outermost triangle's sides.
fn sierpinski_triangle(turtle: &mut TurtlePlan, level: u8, size: f32) {
// When level 0 is reached, just draw an equilateral triangle.
if level == 0 {
turtle.pen_down();
for _ in 0..3 {
turtle.forward(size);
turtle.left(120.0);
}
turtle.pen_up();
} else {
// Parameters for subsequent calls are the same.
let next_level = level - 1;
let next_size = size / 2.0;
// Bottom-left triangle.
sierpinski_triangle(turtle, next_level, next_size);
turtle.forward(next_size);
// Bottom-right triangle.
sierpinski_triangle(turtle, next_level, next_size);
turtle.left(120.0);
turtle.forward(next_size);
turtle.right(120.0);
// Top triangle.
sierpinski_triangle(turtle, next_level, next_size);
// Go back to the start.
turtle.right(120.0);
turtle.forward(next_size);
turtle.left(120.0);
}
}
/// Draws a Sierpiński triangle with automatic size and start point.
///
/// `level` is still required, it can't be computed automatically. However, given the used
/// canvas size, it will compute the appropriate size and start point so the triangle gets
/// centered and occupies as much drawing space as possible while staying in bounds.
fn sierpinski_triangle_auto(turtle: &mut TurtlePlan, level: u8) {
let size = TRIANGLE_SIZE;
turtle.pen_up();
turtle.go_to((-screen_width() / 2.0 + 20.0, screen_height() / 2.0 - 20.0));
turtle.set_heading(0.0); // 0 = East (pointing right)
// The drawing itself.
sierpinski_triangle(turtle, level, size);
}
+21
View File
@@ -0,0 +1,21 @@
//! Star pattern example demonstrating complex turtle patterns
use turtle_lib::*;
#[turtle_main("Star Pattern")]
fn draw(turtle: &mut TurtlePlan) {
turtle.shape(ShapeType::Turtle);
turtle.set_speed(1500);
turtle.set_pen_width(0.5);
// Draw a 5-pointed star pattern repeatedly
for _i in 0..50000 {
turtle.forward(200.0);
turtle.circle_left(10.0, 72.0, 1000);
turtle.circle_right(5.0, 360.0, 1000);
turtle.circle_left(10.0, 72.0, 1000);
}
// Set animation speed
turtle.set_speed(300);
}
+126
View File
@@ -0,0 +1,126 @@
//! 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);
}
}
+30
View File
@@ -0,0 +1,30 @@
//! Yin-Yang symbol example demonstrating multi-contour fills
use turtle_lib::*;
#[turtle_main("Yin-Yang")]
fn draw(turtle: &mut TurtlePlan) {
turtle.set_speed(900);
turtle.circle_left(90.0, 180.0, 36);
turtle.begin_fill();
turtle.circle_left(90.0, 180.0, 36);
turtle.circle_left(45.0, 180.0, 26);
turtle.circle_right(45.0, 180.0, 26);
turtle.pen_up();
turtle.right(90.0);
turtle.forward(37.0);
turtle.left(90.0);
turtle.pen_down();
turtle.circle_right(8.0, 360.0, 12);
turtle.pen_up();
turtle.right(90.0);
turtle.forward(90.0);
turtle.left(90.0);
turtle.pen_down();
turtle.circle_right(8.0, 360.0, 12);
turtle.end_fill();
// Set animation speed
turtle.set_speed(1000);
}
+673
View File
@@ -0,0 +1,673 @@
//! Builder pattern traits for creating turtle command sequences
use crate::commands::{CommandQueue, TurtleCommand};
use crate::general::{AnimationSpeed, Color, Coordinate, Precision};
use crate::shapes::{ShapeType, TurtleShape};
/// Trait for adding commands to a queue
pub trait WithCommands {
fn get_commands_mut(&mut self) -> &mut CommandQueue;
fn get_commands(self) -> CommandQueue;
}
/// Trait for forward/backward movement
pub trait DirectionalMovement: WithCommands {
/// Moves the turtle forward by the specified distance.
///
/// The turtle moves in the direction of its current heading.
/// If the pen is down, a line is drawn.
///
/// # Examples
///
/// ```no_run
/// # use turtle_lib::*;
/// #
/// #[turtle_main("Forward Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// // Move forward 100 pixels
/// turtle.forward(100.0);
///
/// // Chain movements
/// turtle.forward(50.0).right(90.0).forward(50.0);
/// }
/// ```
fn forward<T>(&mut self, distance: T) -> &mut Self
where
T: Into<Precision>,
{
let dist: Precision = distance.into();
self.get_commands_mut().push(TurtleCommand::Move(dist));
self
}
/// Moves the turtle backward by the specified distance.
///
/// The turtle moves opposite to its current heading without changing
/// the heading direction. If the pen is down, a line is drawn.
///
/// # Examples
///
/// ```no_run
/// # use turtle_lib::*;
/// #
/// #[turtle_main("Backward Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// // Move backward 100 pixels
/// turtle.backward(100.0);
///
/// // Draw a line forward, then retrace backward
/// turtle.forward(100.0).backward(50.0);
/// }
/// ```
fn backward<T>(&mut self, distance: T) -> &mut Self
where
T: Into<Precision>,
{
let dist: Precision = distance.into();
self.get_commands_mut().push(TurtleCommand::Move(-dist));
self
}
}
/// Trait for turning operations
pub trait Turnable: WithCommands {
/// Turns the turtle left (counter-clockwise) by the specified angle in degrees.
///
/// Changes the turtle's heading without moving its position.
/// Does not draw anything.
///
/// # Examples
///
/// ```no_run
/// # use turtle_lib::*;
/// #
/// #[turtle_main("Left Turn Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// // Draw a square using left turns
/// for _ in 0..4 {
/// turtle.forward(100.0).left(90.0);
/// }
/// }
/// ```
fn left<T>(&mut self, angle: T) -> &mut Self
where
T: Into<Precision>,
{
let degrees: Precision = angle.into();
self.get_commands_mut().push(TurtleCommand::Turn(-degrees));
self
}
/// Turns the turtle right (clockwise) by the specified angle in degrees.
///
/// Changes the turtle's heading without moving its position.
/// Does not draw anything.
///
/// # Examples
///
/// ```no_run
/// # use turtle_lib::*;
/// #
/// #[turtle_main("Right Turn Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// // Draw a triangle using right turns
/// for _ in 0..3 {
/// turtle.forward(100.0).right(120.0);
/// }
/// }
/// ```
fn right<T>(&mut self, angle: T) -> &mut Self
where
T: Into<Precision>,
{
let degrees: Precision = angle.into();
self.get_commands_mut().push(TurtleCommand::Turn(degrees));
self
}
}
/// Trait for curved movement (circles)
pub trait CurvedMovement: WithCommands {
/// Draws a circular arc turning to the left (counter-clockwise).
///
/// The turtle draws a circular arc with the specified radius, sweeping through
/// the given angle. The circle center is positioned to the left of the turtle.
///
/// # Parameters
///
/// - `radius`: Distance from turtle to circle center (in pixels)
/// - `angle`: Arc sweep angle in degrees (360° = full circle)
/// - `steps`: Number of line segments to approximate the arc (more = smoother)
///
/// # Examples
///
/// ```no_run
/// # use turtle_lib::*;
/// #
/// #[turtle_main("Circle Left Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// // Draw a full circle
/// turtle.circle_left(50.0, 360.0, 36);
///
/// // Filled circle
/// turtle.pen_up().go_to(vec2(100.0, 0.0)).pen_down();
/// turtle.set_fill_color(RED)
/// .begin_fill()
/// .circle_left(50.0, 360.0, 72)
/// .end_fill();
/// }
/// ```
fn circle_left<R, A>(&mut self, radius: R, angle: A, steps: usize) -> &mut Self
where
R: Into<Precision>,
A: Into<Precision>,
{
let r: Precision = radius.into();
let a: Precision = angle.into();
self.get_commands_mut().push(TurtleCommand::Circle {
radius: r,
angle: a,
steps,
direction: crate::circle_geometry::CircleDirection::Left,
});
self
}
/// Draws a circular arc turning to the right (clockwise).
///
/// The turtle draws a circular arc with the specified radius, sweeping through
/// the given angle. The circle center is positioned to the right of the turtle.
///
/// # Parameters
///
/// - `radius`: Distance from turtle to circle center (in pixels)
/// - `angle`: Arc sweep angle in degrees (360° = full circle)
/// - `steps`: Number of line segments to approximate the arc (more = smoother)
///
/// # Examples
///
/// ```no_run
/// # use turtle_lib::*;
/// #
/// #[turtle_main("Circle Right Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// // Draw an S-curve using both directions
/// turtle.circle_left(50.0, 180.0, 36)
/// .circle_right(50.0, 180.0, 36);
///
/// // Yin-yang pattern uses circle_left and circle_right
/// turtle.set_fill_color(BLACK)
/// .begin_fill()
/// .circle_right(100.0, 180.0, 36)
/// .circle_right(50.0, 180.0, 36)
/// .circle_left(50.0, 180.0, 36)
/// .end_fill();
/// }
/// ```
fn circle_right<R, A>(&mut self, radius: R, angle: A, steps: usize) -> &mut Self
where
R: Into<Precision>,
A: Into<Precision>,
{
let r: Precision = radius.into();
let a: Precision = angle.into();
self.get_commands_mut().push(TurtleCommand::Circle {
radius: r,
angle: a,
steps,
direction: crate::circle_geometry::CircleDirection::Right,
});
self
}
}
/// Builder for creating turtle command sequences
#[derive(Default, Debug)]
pub struct TurtlePlan {
queue: CommandQueue,
}
impl TurtlePlan {
/// Creates a new empty turtle command plan.
///
/// This has to be used when not using the `turtle_main` macro.
///
/// # Examples
///
/// ```no_run
/// use turtle_lib::*;
/// use macroquad::prelude::*;
///
/// #[macroquad::main("Manual Setup")]
/// async fn main() {
/// let mut turtle = TurtlePlan::new();
/// turtle.forward(100.0).right(90.0).forward(100.0);
///
/// let mut app = TurtleApp::new().with_commands(turtle.build());
///
/// loop {
/// clear_background(WHITE);
/// app.update();
/// app.render();
///
/// if is_key_pressed(KeyCode::Escape) || is_key_pressed(KeyCode::Q) {
/// break;
/// }
/// next_frame().await;
/// }
/// }
/// ```
#[must_use]
pub fn new() -> Self {
Self {
queue: CommandQueue::new(),
}
}
/// Sets the animation speed for turtle movements.
///
/// Speed controls how fast the turtle moves during animations:
/// - Values `>= 1000`: Instant mode - commands execute immediately without animation.
/// The bigger the number, the more segments are drawn per frame.
/// - Values `< 1000`: Animated mode - turtle moves at specified pixels per second
///
/// You can dynamically switch between instant and animated modes during execution.
///
/// # Examples
///
/// ```no_run
/// # use turtle_lib::*;
/// #
/// #[turtle_main("Speed Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// // Slow animation at 50 pixels/second
/// turtle.set_speed(50.0)
/// .forward(100.0);
///
/// // Switch to instant mode
/// turtle.set_speed(1000.0)
/// .forward(100.0); // Executes immediately
/// }
/// ```
pub fn set_speed(&mut self, speed: impl Into<AnimationSpeed>) -> &mut Self {
self.queue.push(TurtleCommand::SetSpeed(speed.into()));
self
}
/// Sets the pen color for drawing lines.
///
/// The pen color affects all subsequent drawing operations (forward, backward, circles)
/// until changed again. Does not affect fill color.
///
/// # Examples
///
/// ```no_run
/// # use turtle_lib::*;
/// #
/// #[turtle_main("Pen Color Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// // Draw with predefined colors
/// turtle.set_pen_color(RED)
/// .forward(100.0)
/// .set_pen_color(BLUE)
/// .right(90.0)
/// .forward(100.0);
/// }
/// ```
pub fn set_pen_color(&mut self, color: Color) -> &mut Self {
self.queue.push(TurtleCommand::SetColor(color));
self
}
/// Sets the pen width (thickness) for drawing lines.
///
/// The width is measured in pixels. Default is typically 2.0.
///
/// # Examples
///
/// ```no_run
/// # use turtle_lib::*;
/// #
/// #[turtle_main("Pen Width Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// // Thin line
/// turtle.set_pen_width(1.0)
/// .forward(100.0);
///
/// // Thick line
/// turtle.set_pen_width(10.0)
/// .forward(100.0);
/// }
/// ```
pub fn set_pen_width(&mut self, width: Precision) -> &mut Self {
self.queue.push(TurtleCommand::SetPenWidth(width));
self
}
/// Sets the turtle's absolute heading direction in degrees.
///
/// - `0°` points to the right (east)
/// - `90°` points up (north)
/// - `180°` points left (west)
/// - `270°` points down (south)
///
/// # Examples
///
/// ```no_run
/// # use turtle_lib::*;
/// #
/// #[turtle_main("Heading Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// // Point upward
/// turtle.set_heading(90.0)
/// .forward(100.0);
///
/// // Point left
/// turtle.set_heading(180.0)
/// .forward(100.0);
/// }
/// ```
pub fn set_heading(&mut self, heading: Precision) -> &mut Self {
self.queue.push(TurtleCommand::SetHeading(heading));
self
}
/// Lifts the pen up so the turtle can move without drawing.
///
/// When filling shapes, `pen_up()` also closes the current contour,
/// allowing you to create multi-contour fills (e.g., shapes with holes).
///
/// # Examples
///
/// ```no_run
/// # use turtle_lib::*;
/// #
/// #[turtle_main("Pen Up/Down Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// // Move without drawing
/// turtle.pen_up()
/// .forward(100.0) // No line drawn
/// .pen_down()
/// .forward(100.0); // Line drawn
///
/// // Create a donut shape (outer circle with inner hole)
/// turtle.set_fill_color(BLUE)
/// .begin_fill()
/// .circle_left(100.0, 360.0, 72) // Outer circle
/// .pen_up() // Close first contour
/// .go_to(vec2(0.0, -30.0))
/// .pen_down() // Start second contour
/// .circle_left(30.0, 360.0, 36) // Inner circle (becomes hole)
/// .end_fill();
/// }
/// ```
pub fn pen_up(&mut self) -> &mut Self {
self.queue.push(TurtleCommand::PenUp);
self
}
/// Lowers the pen so the turtle draws when moving.
///
/// This is the default state. When filling shapes, `pen_down()` starts
/// a new contour after `pen_up()` was called.
///
/// # Examples
///
/// ```no_run
/// # use turtle_lib::*;
/// #
/// #[turtle_main("Pen Down Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// turtle.pen_up()
/// .forward(50.0) // Move without drawing
/// .pen_down() // Start drawing
/// .forward(100.0); // Line appears
/// }
/// ```
pub fn pen_down(&mut self) -> &mut Self {
self.queue.push(TurtleCommand::PenDown);
self
}
/// Hides the turtle cursor from view.
///
/// The turtle will still execute commands and draw, but the cursor
/// (typically an arrow or triangle) won't be visible.
///
/// # Examples
///
/// ```no_run
/// # use turtle_lib::*;
/// #
/// #[turtle_main("Hide Turtle Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// turtle.hide() // Turtle cursor invisible
/// .forward(100.0)
/// .right(90.0)
/// .forward(100.0);
/// }
/// ```
pub fn hide(&mut self) -> &mut Self {
self.queue.push(TurtleCommand::HideTurtle);
self
}
/// Shows the turtle cursor.
///
/// Makes the turtle cursor visible if it was previously hidden.
/// This is the default state.
///
/// # Examples
///
/// ```no_run
/// # use turtle_lib::*;
/// #
/// #[turtle_main("Show Turtle Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// turtle.hide()
/// .forward(100.0)
/// .show() // Turtle becomes visible again
/// .forward(100.0);
/// }
/// ```
pub fn show(&mut self) -> &mut Self {
self.queue.push(TurtleCommand::ShowTurtle);
self
}
/// Sets the turtle's shape using a `TurtleShape` object.
///
/// For most use cases, prefer using `shape()` which accepts a `ShapeType` enum.
///
/// # Examples
///
/// ```
/// # use turtle_lib::*;
/// #
/// #[turtle_main("Shape Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// let custom_shape = ShapeType::Arrow.to_shape();
/// turtle.set_shape(custom_shape);
/// }
/// ```
pub fn set_shape(&mut self, shape: TurtleShape) -> &mut Self {
self.queue.push(TurtleCommand::SetShape(shape));
self
}
/// Sets the turtle's visual appearance.
///
/// Available shapes: `Arrow`, `Triangle`, `Square`, `Circle`.
///
/// # Examples
///
/// ```no_run
/// # use turtle_lib::*;
/// #
/// #[turtle_main("Shape Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// // Use different shapes
/// turtle.shape(ShapeType::Arrow)
/// .forward(50.0)
/// .shape(ShapeType::Circle)
/// .forward(50.0);
/// }
/// ```
pub fn shape(&mut self, shape_type: ShapeType) -> &mut Self {
self.set_shape(shape_type.to_shape())
}
/// Starts recording a shape to be filled.
///
/// All turtle movements between `begin_fill()` and `end_fill()` define
/// the shape's outline. The shape is filled using the fill color when
/// `end_fill()` is called.
///
/// Multiple contours can be created using `pen_up()` and `pen_down()`.
/// The `EvenOdd` fill rule automatically creates holes for inner contours.
///
/// # Examples
///
/// ```no_run
/// # use turtle_lib::*;
/// #
/// #[turtle_main("Fill Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// // Fill a square
/// turtle.set_fill_color(BLUE)
/// .begin_fill();
/// for _ in 0..4 {
/// turtle.forward(100.0).right(90.0);
/// }
/// turtle.end_fill();
///
/// // Fill a circle
/// turtle.pen_up().go_to(vec2(150.0, 0.0)).pen_down();
/// turtle.set_fill_color(RED)
/// .begin_fill()
/// .circle_left(50.0, 360.0, 36)
/// .end_fill();
/// }
/// ```
pub fn begin_fill(&mut self) -> &mut Self {
self.queue.push(TurtleCommand::BeginFill);
self
}
/// Completes the fill operation started with `begin_fill()`.
///
/// Closes the current shape and fills it with the fill color.
/// All contours recorded since `begin_fill()` are filled together.
///
/// # Examples
///
/// ```no_run
/// # use turtle_lib::*;
/// #
/// #[turtle_main("End Fill Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// // Triangle with fill
/// turtle.set_fill_color(GREEN)
/// .begin_fill();
/// for _ in 0..3 {
/// turtle.forward(100.0).right(120.0);
/// }
/// turtle.end_fill();
/// }
/// ```
pub fn end_fill(&mut self) -> &mut Self {
self.queue.push(TurtleCommand::EndFill);
self
}
/// Sets the color used to fill shapes.
///
/// This affects all shapes filled with `begin_fill()`/`end_fill()`.
/// Independent from the pen color used for outlines.
///
/// # Examples
///
/// ```no_run
/// # use turtle_lib::*;
/// #
/// #[turtle_main("Fill Color Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// // Yellow fill with blue outline
/// turtle.set_fill_color(YELLOW)
/// .set_pen_color(BLUE)
/// .begin_fill()
/// .circle_left(50.0, 360.0, 36)
/// .end_fill();
/// }
/// ```
pub fn set_fill_color(&mut self, color: impl Into<Color>) -> &mut Self {
self.queue
.push(TurtleCommand::SetFillColor(Some(color.into())));
self
}
/// Moves the turtle to an absolute position.
///
/// The turtle moves in a straight line to the specified coordinates.
/// If the pen is down, a line is drawn. The turtle's heading is not changed.
///
/// Coordinates are in screen space:
/// - `(0, 0)` is at the center
/// - Positive x goes right
/// - Positive y goes down
///
/// # Examples
///
/// ```no_run
/// # use turtle_lib::*;
/// #
/// #[turtle_main("Goto Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// // Draw a triangle by connecting points
/// turtle.go_to(vec2(0.0, 0.0));
/// turtle.go_to(vec2(100.0, 0.0));
/// turtle.go_to(vec2(50.0, 86.6));
/// turtle.go_to(vec2(0.0, 0.0));
/// }
/// ```
pub fn go_to(&mut self, coord: impl Into<Coordinate>) -> &mut Self {
self.queue.push(TurtleCommand::Goto(coord.into()));
self
}
/// Consumes the `TurtlePlan` and returns the command queue.
///
/// Use this to finalize the turtle commands and pass them to `TurtleApp`.
/// This method consumes `self`, so the plan cannot be used afterward.
///
/// # Examples
///
/// ```
/// # use turtle_lib::*;
/// #
/// let mut turtle = TurtlePlan::new();
/// turtle.forward(100.0).right(90.0).forward(100.0);
///
/// // Build and get the command queue
/// let commands = turtle.build();
/// # assert!(!commands.is_empty());
/// ```
#[must_use]
pub fn build(self) -> CommandQueue {
self.queue
}
}
impl WithCommands for TurtlePlan {
fn get_commands_mut(&mut self) -> &mut CommandQueue {
&mut self.queue
}
fn get_commands(self) -> CommandQueue {
self.queue
}
}
impl DirectionalMovement for TurtlePlan {}
impl Turnable for TurtlePlan {}
impl CurvedMovement for TurtlePlan {}
+213
View File
@@ -0,0 +1,213 @@
//! Circle geometry calculations - single source of truth for `circle_left` and `circle_right`
use macroquad::prelude::*;
/// Direction of circular motion (in screen coordinates with Y-down)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CircleDirection {
Left, // Counter-clockwise visually, heading decreases
Right, // Clockwise visually, heading increases
}
/// Encapsulates all geometry for a circular arc
pub struct CircleGeometry {
pub center: Vec2,
pub radius: f32,
pub start_angle_from_center: f32, // radians
pub direction: CircleDirection,
}
impl CircleGeometry {
/// Create geometry for a circle command
#[must_use]
pub fn new(
turtle_pos: Vec2,
turtle_heading: f32,
radius: f32,
direction: CircleDirection,
) -> Self {
use std::f32::consts::FRAC_PI_2;
// Calculate center based on direction
// In screen coordinates (Y-down):
// - Left turn (counter-clockwise visually): center is perpendicular-left from turtle's perspective
// which is heading - π/2 (rotated clockwise from heading vector)
// - Right turn (clockwise visually): center is perpendicular-right from turtle's perspective
// which is heading + π/2 (rotated counter-clockwise from heading vector)
let center_offset_angle = match direction {
CircleDirection::Left => turtle_heading - FRAC_PI_2,
CircleDirection::Right => turtle_heading + FRAC_PI_2,
};
let center = vec2(
turtle_pos.x + radius * center_offset_angle.cos(),
turtle_pos.y + radius * center_offset_angle.sin(),
);
// Angle from center back to turtle position
let start_angle_from_center = match direction {
CircleDirection::Left => turtle_heading + FRAC_PI_2,
CircleDirection::Right => turtle_heading - FRAC_PI_2,
};
Self {
center,
radius,
start_angle_from_center,
direction,
}
}
/// Calculate position after traveling an angle along the arc
#[must_use]
pub fn position_at_angle(&self, angle_traveled: f32) -> Vec2 {
let current_angle = match self.direction {
CircleDirection::Left => self.start_angle_from_center - angle_traveled,
CircleDirection::Right => self.start_angle_from_center + angle_traveled,
};
vec2(
self.center.x + self.radius * current_angle.cos(),
self.center.y + self.radius * current_angle.sin(),
)
}
/// Calculate position at a given progress (0.0 to 1.0) through `total_angle`
#[must_use]
pub fn position_at_progress(&self, total_angle: f32, progress: f32) -> Vec2 {
let angle_traveled = total_angle * progress;
self.position_at_angle(angle_traveled)
}
/// Get the angle traveled from start position to a given position
#[must_use]
pub fn angle_to_position(&self, position: Vec2) -> f32 {
let displacement = position - self.center;
let current_angle = displacement.y.atan2(displacement.x);
let mut angle_diff = match self.direction {
CircleDirection::Left => self.start_angle_from_center - current_angle,
CircleDirection::Right => current_angle - self.start_angle_from_center,
};
// Normalize to [0, 2π)
if angle_diff < 0.0 {
angle_diff += 2.0 * std::f32::consts::PI;
}
angle_diff
}
/// Get `draw_arc` parameters for the full arc
/// Returns (`rotation_degrees`, `arc_degrees`) for macroquad's `draw_arc`
#[must_use]
pub fn draw_arc_params(&self, total_angle_degrees: f32) -> (f32, f32) {
match self.direction {
CircleDirection::Left => {
// For left (counter-clockwise), we need to draw counter-clockwise from end back to start
// so we start at (start - total_angle) and draw total_angle counter-clockwise
let end_angle = self.start_angle_from_center - total_angle_degrees.to_radians();
(end_angle.to_degrees(), total_angle_degrees)
}
CircleDirection::Right => {
// For right (clockwise), draw from start
(
self.start_angle_from_center.to_degrees(),
total_angle_degrees,
)
}
}
}
/// Get `draw_arc` parameters for a partial arc (during tweening)
/// Returns (`rotation_degrees`, `arc_degrees`) for macroquad's `draw_arc`
#[must_use]
pub fn draw_arc_params_partial(&self, angle_traveled: f32) -> (f32, f32) {
let angle_traveled_degrees = angle_traveled.to_degrees();
match self.direction {
CircleDirection::Left => {
// Draw from current position backwards (counter-clockwise) to start
let current_angle = self.start_angle_from_center - angle_traveled;
(current_angle.to_degrees(), angle_traveled_degrees)
}
CircleDirection::Right => {
// Draw from start, counter-clockwise
(
self.start_angle_from_center.to_degrees(),
angle_traveled_degrees,
)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f32::consts::{FRAC_PI_2, PI};
#[test]
fn test_circle_left_geometry() {
let geom = CircleGeometry::new(
vec2(0.0, 0.0),
0.0, // heading east (0 radians)
100.0,
CircleDirection::Left,
);
// For left turn with heading east (0), center should be at heading - π/2
// That's -π/2 radians = south
// Center = start + 100 * (cos(-π/2), sin(-π/2)) = (0, 0) + (0, -100) = (0, -100)
assert!(
(geom.center.x - 0.0).abs() < 0.01,
"center.x = {}",
geom.center.x
);
assert!(
(geom.center.y - (-100.0)).abs() < 0.01,
"center.y = {}",
geom.center.y
);
// After π/2 radians counter-clockwise around a circle centered at (0, -100):
// start_angle = π/2 (pointing north from center, which is where (0,0) is)
// after π/2 counter-clockwise (subtract in screen coords): angle = π/2 - π/2 = 0 (pointing east from center)
// pos = (0, -100) + 100 * (cos(0), sin(0)) = (0, -100) + (100, 0) = (100, -100)
let pos = geom.position_at_angle(FRAC_PI_2);
assert!((pos.x - 100.0).abs() < 0.01, "pos.x = {}", pos.x);
assert!((pos.y - (-100.0)).abs() < 0.01, "pos.y = {}", pos.y);
}
#[test]
fn test_circle_right_geometry() {
let geom = CircleGeometry::new(
vec2(0.0, 0.0),
0.0, // heading east
100.0,
CircleDirection::Right,
);
// For right turn with heading east (0), center should be at heading + π/2
// That's π/2 radians = north
// Center = start + 100 * (cos(π/2), sin(π/2)) = (0, 0) + (0, 100) = (0, 100)
assert!(
(geom.center.x - 0.0).abs() < 0.01,
"center.x = {}",
geom.center.x
);
assert!(
(geom.center.y - 100.0).abs() < 0.01,
"center.y = {}",
geom.center.y
);
// After π/2 radians clockwise around a circle centered at (0, 100):
// start_angle = -π/2 (pointing south from center, which is where (0,0) is)
// after π/2 clockwise (add in screen coords): angle = -π/2 + π/2 = 0 (pointing east from center)
// pos = (0, 100) + 100 * (cos(0), sin(0)) = (0, 100) + (100, 0) = (100, 100)
let pos = geom.position_at_angle(PI / 2.0);
assert!((pos.x - 100.0).abs() < 0.01, "pos.x = {}", pos.x);
assert!((pos.y - 100.0).abs() < 0.01, "pos.y = {}", pos.y);
}
}
+117
View File
@@ -0,0 +1,117 @@
//! Turtle commands and command queue
use crate::general::{AnimationSpeed, Color, Coordinate, Precision};
use crate::shapes::TurtleShape;
/// Individual turtle commands
#[derive(Clone, Debug)]
pub enum TurtleCommand {
// Movement (positive = forward, negative = backward)
Move(Precision),
// Rotation (positive = right/clockwise, negative = left/counter-clockwise in degrees)
Turn(Precision),
// Circle drawing
Circle {
radius: Precision,
angle: Precision, // degrees
steps: usize,
direction: crate::circle_geometry::CircleDirection,
},
// Pen control
PenUp,
PenDown,
// Appearance
SetColor(Color),
SetFillColor(Option<Color>),
SetPenWidth(Precision),
SetSpeed(AnimationSpeed),
SetShape(TurtleShape),
// Position
Goto(Coordinate),
SetHeading(Precision), // radians
// Visibility
ShowTurtle,
HideTurtle,
// Fill operations
BeginFill,
EndFill,
}
/// Queue of turtle commands with execution state
#[derive(Debug)]
pub struct CommandQueue {
commands: Vec<TurtleCommand>,
current_index: usize,
}
impl CommandQueue {
#[must_use]
pub fn new() -> Self {
Self {
commands: Vec::new(),
current_index: 0,
}
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
commands: Vec::with_capacity(capacity),
current_index: 0,
}
}
pub fn push(&mut self, command: TurtleCommand) {
self.commands.push(command);
}
pub fn extend(&mut self, commands: impl IntoIterator<Item = TurtleCommand>) {
self.commands.extend(commands);
}
#[must_use]
pub fn is_complete(&self) -> bool {
self.current_index >= self.commands.len()
}
pub fn reset(&mut self) {
self.current_index = 0;
}
#[must_use]
pub fn len(&self) -> usize {
self.commands.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.commands.is_empty()
}
#[must_use]
pub fn remaining(&self) -> usize {
self.commands.len().saturating_sub(self.current_index)
}
}
impl Default for CommandQueue {
fn default() -> Self {
Self::new()
}
}
impl Iterator for CommandQueue {
type Item = TurtleCommand;
fn next(&mut self) -> Option<Self::Item> {
if self.current_index < self.commands.len() {
let cmd = self.commands[self.current_index].clone();
self.current_index += 1;
Some(cmd)
} else {
None
}
}
}
+340
View File
@@ -0,0 +1,340 @@
//! Rendering logic using Macroquad and Lyon tessellation
use crate::circle_geometry::{CircleDirection, CircleGeometry};
use crate::state::{DrawCommand, TurtleState, TurtleWorld};
use crate::tessellation;
use macroquad::prelude::*;
// Import the easing function from the tween crate
// To change the easing, change both this import and the usage in the draw_tween_arc function below
// Available options: Linear, SineInOut, QuadInOut, CubicInOut, QuartInOut, QuintInOut,
// ExpoInOut, CircInOut, BackInOut, ElasticInOut, BounceInOut, etc.
// See https://easings.net/ for visual demonstrations
use tween::CubicInOut;
/// Render the entire turtle world
pub fn render_world(world: &TurtleWorld) {
// Update camera zoom based on current screen size to prevent stretching
let camera = Camera2D {
zoom: vec2(1.0 / screen_width() * 2.0, 1.0 / screen_height() * 2.0),
target: world.camera.target,
..Default::default()
};
// Set camera
set_camera(&camera);
// Draw all accumulated commands
for cmd in &world.commands {
match cmd {
DrawCommand::Mesh(mesh_data) => {
draw_mesh(&mesh_data.to_mesh());
}
}
}
// Draw turtle if visible
if world.turtle.visible {
draw_turtle(&world.turtle);
}
// Reset to default camera
set_default_camera();
}
/// Render the turtle world with active tween visualization
#[allow(clippy::too_many_lines)]
pub(crate) fn render_world_with_tween(
world: &TurtleWorld,
active_tween: Option<&crate::tweening::CommandTween>,
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 {
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
for cmd in &world.commands {
match cmd {
DrawCommand::Mesh(mesh_data) => {
draw_mesh(&mesh_data.to_mesh());
}
}
}
// Draw in-progress tween line if pen is down
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
draw_line(
tween.start_state.position.x,
tween.start_state.position.y,
world.turtle.position.x,
world.turtle.position.y,
tween.start_state.pen_width,
tween.start_state.color,
);
// Add circle at current position for smooth line joins
draw_circle(
world.turtle.position.x,
world.turtle.position.y,
tween.start_state.pen_width / 2.0,
tween.start_state.color,
);
}
_ => {}
}
}
}
// Draw live fill preview if currently filling (always show, not just during tweens)
if let Some(ref fill_state) = world.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
let mut current_preview: Vec<Vec2> = fill_state
.current_contour
.iter()
.map(|c| Vec2::new(c.x, c.y))
.collect();
// 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 {
if let crate::commands::TurtleCommand::Circle {
radius,
angle,
steps,
direction,
} = &tween.command
{
// 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);
}
} 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(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
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() {
let current_pos = world.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
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_data) => {
draw_mesh(&mesh_data.to_mesh());
}
Err(e) => {
tracing::error!(
error = ?e,
"Lyon multi-contour tessellation error for fill preview"
);
}
}
}
}
// Draw turtle if visible
if world.turtle.visible {
draw_turtle(&world.turtle);
}
// 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 arc segments for circle tween animation
fn draw_tween_arc(
tween: &crate::tweening::CommandTween,
radius: f32,
total_angle: f32,
steps: usize,
direction: CircleDirection,
) {
let geom = CircleGeometry::new(
tween.start_state.position,
tween.start_state.heading,
radius,
direction,
);
// Debug: draw center using Lyon tessellation
if let Ok(mesh_data) = crate::tessellation::tessellate_circle(geom.center, 5.0, GRAY, true, 1.0)
{
draw_mesh(&mesh_data.to_mesh());
}
// Calculate how much of the arc we've traveled based on tween progress
// Use the same eased progress as the turtle position for synchronized animation
let elapsed = get_time() - tween.start_time;
let t = (elapsed / tween.duration).min(1.0);
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
if let Ok(mesh_data) = crate::tessellation::tessellate_arc(
geom.center,
radius,
rotation_degrees,
arc_degrees,
tween.start_state.color,
tween.start_state.pen_width,
steps,
) {
draw_mesh(&mesh_data.to_mesh());
}
}
/// Draw the turtle shape
pub fn draw_turtle(turtle: &TurtleState) {
let rotated_vertices = turtle.shape.rotated_vertices(turtle.heading);
if turtle.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)
.collect();
// Use Lyon for turtle shape too
if let Ok(mesh_data) =
tessellation::tessellate_polygon(&absolute_vertices, Color::new(0.0, 0.5, 1.0, 1.0))
{
draw_mesh(&mesh_data.to_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.position + rotated_vertices[i];
let p2 = turtle.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));
}
}
}
}
+380
View File
@@ -0,0 +1,380 @@
//! Command execution logic
use crate::circle_geometry::{CircleDirection, CircleGeometry};
use crate::commands::TurtleCommand;
use crate::state::{DrawCommand, TurtleState, TurtleWorld};
use crate::tessellation;
use macroquad::prelude::*;
#[cfg(test)]
use crate::general::AnimationSpeed;
/// Execute side effects for commands that don't involve movement
/// Returns true if the command was handled (caller should skip movement processing)
pub fn execute_command_side_effects(
command: &TurtleCommand,
state: &mut TurtleState,
commands: &mut Vec<DrawCommand>,
) -> bool {
match command {
TurtleCommand::BeginFill => {
if state.filling.is_some() {
tracing::warn!("begin_fill() called while already filling");
}
let fill_color = state.fill_color.unwrap_or_else(|| {
tracing::warn!("No fill_color set, using black");
BLACK
});
state.begin_fill(fill_color);
true
}
TurtleCommand::EndFill => {
if let Some(mut fill_state) = state.filling.take() {
if !fill_state.current_contour.is_empty() {
fill_state.contours.push(fill_state.current_contour);
}
let span = tracing::debug_span!("end_fill", contours = fill_state.contours.len());
let _enter = span.enter();
for (i, contour) in fill_state.contours.iter().enumerate() {
tracing::debug!(contour_idx = i, vertices = contour.len(), "Contour info");
}
if !fill_state.contours.is_empty() {
if let Ok(mesh_data) = tessellation::tessellate_multi_contour(
&fill_state.contours,
fill_state.fill_color,
) {
tracing::debug!(
contours = fill_state.contours.len(),
"Successfully tessellated contours"
);
commands.push(DrawCommand::Mesh(mesh_data));
} else {
tracing::error!("Failed to tessellate contours");
}
}
} else {
tracing::warn!("end_fill() called without begin_fill()");
}
true
}
TurtleCommand::PenUp => {
state.pen_down = false;
if state.filling.is_some() {
tracing::debug!("PenUp: Closing current contour");
}
state.close_fill_contour();
true
}
TurtleCommand::PenDown => {
state.pen_down = true;
if state.filling.is_some() {
tracing::debug!(
x = state.position.x,
y = state.position.y,
"PenDown: Starting new contour"
);
}
state.start_fill_contour();
true
}
_ => false, // Not a side-effect-only command
}
}
/// Record fill vertices after movement commands have updated state
pub fn record_fill_vertices_after_movement(
command: &TurtleCommand,
start_state: &TurtleState,
state: &mut TurtleState,
) {
if state.filling.is_none() {
return;
}
match command {
TurtleCommand::Circle {
radius,
angle,
steps,
direction,
} => {
let geom = CircleGeometry::new(
start_state.position,
start_state.heading,
*radius,
*direction,
);
state.record_fill_vertices_for_arc(
geom.center,
*radius,
geom.start_angle_from_center,
angle.to_radians(),
*direction,
*steps as u32,
);
}
TurtleCommand::Move(_) | TurtleCommand::Goto(_) => {
state.record_fill_vertex();
}
_ => {}
}
}
/// Execute a single turtle command, updating state and adding draw commands
pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world: &mut TurtleWorld) {
// Try to execute as side-effect-only command first
if execute_command_side_effects(command, state, &mut world.commands) {
return; // Command fully handled
}
// Store start state for fill vertex recording
let start_state = state.clone();
// Execute movement and appearance commands
match command {
TurtleCommand::Move(distance) => {
let start = state.position;
let dx = distance * state.heading.cos();
let dy = distance * state.heading.sin();
state.position = vec2(state.position.x + dx, state.position.y + dy);
if state.pen_down {
// Draw line segment with round caps (caps handled by tessellate_stroke)
if let Ok(mesh_data) = tessellation::tessellate_stroke(
&[start, state.position],
state.color,
state.pen_width,
false, // not closed
) {
world.add_command(DrawCommand::Mesh(mesh_data));
}
}
}
TurtleCommand::Turn(degrees) => {
state.heading += degrees.to_radians();
}
TurtleCommand::Circle {
radius,
angle,
steps,
direction,
} => {
let start_heading = state.heading;
let geom = CircleGeometry::new(state.position, start_heading, *radius, *direction);
if state.pen_down {
let (rotation_degrees, arc_degrees) = geom.draw_arc_params(*angle);
// Use Lyon to tessellate the arc
if let Ok(mesh_data) = tessellation::tessellate_arc(
geom.center,
*radius,
rotation_degrees,
arc_degrees,
state.color,
state.pen_width,
*steps,
) {
world.add_command(DrawCommand::Mesh(mesh_data));
}
}
// Update turtle position and heading
state.position = geom.position_at_angle(angle.to_radians());
state.heading = match direction {
CircleDirection::Left => start_heading - angle.to_radians(),
CircleDirection::Right => start_heading + angle.to_radians(),
};
}
TurtleCommand::Goto(coord) => {
let start = state.position;
state.position = *coord;
if state.pen_down {
// Draw line segment with round caps
if let Ok(mesh_data) = tessellation::tessellate_stroke(
&[start, state.position],
state.color,
state.pen_width,
false, // not closed
) {
world.add_command(DrawCommand::Mesh(mesh_data));
}
}
}
// Appearance commands
TurtleCommand::SetColor(color) => state.color = *color,
TurtleCommand::SetFillColor(color) => state.fill_color = *color,
TurtleCommand::SetPenWidth(width) => state.pen_width = *width,
TurtleCommand::SetSpeed(speed) => state.set_speed(*speed),
TurtleCommand::SetShape(shape) => state.shape = shape.clone(),
TurtleCommand::SetHeading(heading) => state.heading = *heading,
TurtleCommand::ShowTurtle => state.visible = true,
TurtleCommand::HideTurtle => state.visible = false,
_ => {} // Already handled by execute_command_side_effects
}
// Record fill vertices AFTER movement
record_fill_vertices_after_movement(command, &start_state, state);
}
/// 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, // not closed
) {
world.add_command(DrawCommand::Mesh(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,
);
let (rotation_degrees, arc_degrees) = geom.draw_arc_params(*angle);
// Use Lyon to tessellate the arc
if let Ok(mesh_data) = tessellation::tessellate_arc(
geom.center,
*radius,
rotation_degrees,
arc_degrees,
start_state.color,
start_state.pen_width,
*steps,
) {
world.add_command(DrawCommand::Mesh(mesh_data));
}
}
}
_ => {
// Other commands don't create drawing
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::TurtleCommand;
use crate::shapes::TurtleShape;
#[test]
fn test_forward_left_forward() {
// Test that after forward(100), left(90), forward(50)
// the turtle ends up at (100, -50) from initial position (0, 0)
let mut state = TurtleState {
position: vec2(0.0, 0.0),
heading: 0.0,
pen_down: false, // Disable drawing to avoid needing TurtleWorld
pen_width: 1.0,
color: Color::new(0.0, 0.0, 0.0, 1.0),
fill_color: None,
speed: AnimationSpeed::Animated(100.0),
visible: true,
shape: TurtleShape::turtle(),
filling: None,
};
// We'll use a dummy world but won't actually call drawing commands
let mut world = TurtleWorld {
turtle: state.clone(),
commands: Vec::new(),
camera: macroquad::camera::Camera2D {
zoom: vec2(1.0, 1.0),
target: vec2(0.0, 0.0),
offset: vec2(0.0, 0.0),
rotation: 0.0,
render_target: None,
viewport: None,
},
background_color: Color::new(1.0, 1.0, 1.0, 1.0),
};
// Initial state: position (0, 0), heading 0 (east)
assert_eq!(state.position.x, 0.0);
assert_eq!(state.position.y, 0.0);
assert_eq!(state.heading, 0.0);
// Forward 100 - should move to (100, 0)
execute_command(&TurtleCommand::Move(100.0), &mut state, &mut world);
assert!(
(state.position.x - 100.0).abs() < 0.01,
"After forward(100): x = {}",
state.position.x
);
assert!(
(state.position.y - 0.0).abs() < 0.01,
"After forward(100): y = {}",
state.position.y
);
assert!((state.heading - 0.0).abs() < 0.01);
// Left 90 degrees - should face north (heading decreases by 90°)
// In screen coords: north = -90° = -π/2
execute_command(&TurtleCommand::Turn(-90.0), &mut state, &mut world);
assert!(
(state.position.x - 100.0).abs() < 0.01,
"After left(90): x = {}",
state.position.x
);
assert!(
(state.position.y - 0.0).abs() < 0.01,
"After left(90): y = {}",
state.position.y
);
let expected_heading = -90.0f32.to_radians();
assert!(
(state.heading - expected_heading).abs() < 0.01,
"After left(90): heading = {} (expected {})",
state.heading,
expected_heading
);
// Forward 50 - should move north (negative Y) to (100, -50)
execute_command(&TurtleCommand::Move(50.0), &mut state, &mut world);
assert!(
(state.position.x - 100.0).abs() < 0.01,
"Final position: x = {} (expected 100.0)",
state.position.x
);
assert!(
(state.position.y - (-50.0)).abs() < 0.01,
"Final position: y = {} (expected -50.0)",
state.position.y
);
}
}
+84
View File
@@ -0,0 +1,84 @@
//! General types and type aliases used throughout the turtle library
use macroquad::prelude::*;
pub mod angle;
pub mod length;
pub use angle::Angle;
pub use length::Length;
/// Precision type for calculations
pub type Precision = f32;
/// 2D coordinate in screen space
pub type Coordinate = Vec2;
/// Visibility flag for turtle
pub type Visibility = bool;
/// Execution speed setting
/// - `Instant(draw_calls)`: Fast execution with limited draw calls per frame (speed - 1000, minimum 1)
/// - `Animated(speed)`: Smooth animation at specified pixels/second
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum AnimationSpeed {
Instant(u32), // Number of draw calls per frame (minimum 1)
Animated(f32), // pixels per second
}
impl AnimationSpeed {
/// Check if this is instant mode
#[must_use]
pub fn is_animating(&self) -> bool {
matches!(self, AnimationSpeed::Animated(_))
}
/// Get the speed value (returns encoded value for Instant)
#[must_use]
pub fn value(&self) -> f32 {
match self {
AnimationSpeed::Instant(calls) => 1000.0 + *calls as f32,
AnimationSpeed::Animated(speed) => *speed,
}
}
/// Create from a raw speed value
/// - speed >= 1000 becomes Instant with max(1, speed - 1000) draw calls per frame
/// - speed < 1000 becomes Animated
#[must_use]
pub fn from_value(speed: f32) -> Self {
if speed >= 1000.0 {
let draw_calls = (speed - 1000.0).max(1.0) as u32; // Ensure at least 1
AnimationSpeed::Instant(draw_calls)
} else {
AnimationSpeed::Animated(speed.max(1.0))
}
}
/// Create from a u32 value for backward compatibility
#[must_use]
pub fn from_u32(speed: u32) -> Self {
Self::from_value(speed as f32)
}
}
impl Default for AnimationSpeed {
fn default() -> Self {
AnimationSpeed::Animated(100.0)
}
}
impl From<f32> for AnimationSpeed {
fn from(speed: f32) -> Self {
AnimationSpeed::from_value(speed)
}
}
impl From<u32> for AnimationSpeed {
fn from(speed: u32) -> Self {
AnimationSpeed::from_u32(speed)
}
}
/// Color type re-export from macroquad
pub use macroquad::color::Color;
+205
View File
@@ -0,0 +1,205 @@
//! Angle type with degrees and radians support
use super::Precision;
use std::ops::{Add, Div, Mul, Neg, Rem, Sub};
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum AngleUnit {
Degrees(Precision),
Radians(Precision),
}
impl Default for AngleUnit {
fn default() -> Self {
Self::Degrees(0.0)
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Angle {
value: AngleUnit,
}
impl Default for Angle {
fn default() -> Self {
Self {
value: AngleUnit::Degrees(0.0),
}
}
}
impl From<i16> for Angle {
fn from(i: i16) -> Self {
Self {
value: AngleUnit::Degrees(Precision::from(i)),
}
}
}
impl From<f32> for Angle {
fn from(f: f32) -> Self {
Self {
value: AngleUnit::Degrees(f),
}
}
}
impl Rem<Precision> for Angle {
type Output = Self;
fn rem(self, rhs: Precision) -> Self::Output {
match self.value {
AngleUnit::Degrees(v) => Self::degrees(v % rhs),
AngleUnit::Radians(v) => Self::radians(v % rhs),
}
}
}
impl Mul<Precision> for Angle {
type Output = Self;
fn mul(self, rhs: Precision) -> Self::Output {
match self.value {
AngleUnit::Degrees(v) => Self::degrees(v * rhs),
AngleUnit::Radians(v) => Self::radians(v * rhs),
}
}
}
impl Div<Precision> for Angle {
type Output = Self;
fn div(self, rhs: Precision) -> Self::Output {
match self.value {
AngleUnit::Degrees(v) => Self::degrees(v / rhs),
AngleUnit::Radians(v) => Self::radians(v / rhs),
}
}
}
impl Neg for Angle {
type Output = Self;
fn neg(self) -> Self::Output {
match self.value {
AngleUnit::Degrees(v) => Self::degrees(-v),
AngleUnit::Radians(v) => Self::radians(-v),
}
}
}
impl Neg for &Angle {
type Output = Angle;
fn neg(self) -> Self::Output {
match self.value {
AngleUnit::Degrees(v) => Angle::degrees(-v),
AngleUnit::Radians(v) => Angle::radians(-v),
}
}
}
impl Add for Angle {
type Output = Angle;
fn add(self, rhs: Self) -> Self::Output {
match (self.value, rhs.value) {
(AngleUnit::Degrees(v), AngleUnit::Degrees(o)) => Self::degrees(v + o),
(AngleUnit::Degrees(v), AngleUnit::Radians(o)) => Self::radians(v.to_radians() + o),
(AngleUnit::Radians(v), AngleUnit::Degrees(o)) => Self::radians(v + o.to_radians()),
(AngleUnit::Radians(v), AngleUnit::Radians(o)) => Self::radians(v + o),
}
}
}
impl Sub for Angle {
type Output = Angle;
fn sub(self, rhs: Self) -> Self::Output {
match (self.value, rhs.value) {
(AngleUnit::Degrees(v), AngleUnit::Degrees(o)) => Self::degrees(v - o),
(AngleUnit::Degrees(v), AngleUnit::Radians(o)) => Self::radians(v.to_radians() - o),
(AngleUnit::Radians(v), AngleUnit::Degrees(o)) => Self::radians(v - o.to_radians()),
(AngleUnit::Radians(v), AngleUnit::Radians(o)) => Self::radians(v - o),
}
}
}
impl Angle {
#[must_use]
pub fn degrees(value: Precision) -> Self {
Self {
value: AngleUnit::Degrees(value),
}
}
#[must_use]
pub fn radians(value: Precision) -> Self {
Self {
value: AngleUnit::Radians(value),
}
}
#[must_use]
pub fn value(&self) -> Precision {
match self.value {
AngleUnit::Degrees(v) | AngleUnit::Radians(v) => v,
}
}
#[must_use]
pub fn to_radians(self) -> Self {
match self.value {
AngleUnit::Degrees(v) => Self::radians(v.to_radians()),
AngleUnit::Radians(_) => self,
}
}
#[must_use]
pub fn to_degrees(self) -> Self {
match self.value {
AngleUnit::Degrees(_) => self,
AngleUnit::Radians(v) => Self::degrees(v.to_degrees()),
}
}
#[must_use]
pub fn limit_smaller_than_full_circle(self) -> Self {
use std::f32::consts::PI;
match self.value {
AngleUnit::Degrees(v) => Self::degrees(v % 360.0),
AngleUnit::Radians(v) => Self::radians(v % (2.0 * PI)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn convert_to_radians() {
let radi = Angle::radians(30f32.to_radians());
let degr = Angle::degrees(30f32);
let converted = degr.to_radians();
assert!((radi.value() - converted.value()).abs() < 0.0001);
}
#[test]
fn sum_degrees() {
let fst = Angle::degrees(30f32);
let snd = Angle::degrees(30f32);
let sum = fst + snd;
assert!((sum.value() - 60f32).abs() < 0.0001);
assert!((sum.to_radians().value() - 60f32.to_radians()).abs() < 0.0001);
}
#[test]
fn sum_mixed() {
let fst = Angle::degrees(30f32);
let snd = Angle::radians(30f32.to_radians());
let sum = fst + snd;
assert!((sum.to_degrees().value() - 60f32).abs() < 0.0001);
assert!((sum.to_radians().value() - 60f32.to_radians()).abs() < 0.0001);
}
}
+24
View File
@@ -0,0 +1,24 @@
//! Length type for distance measurements
use super::Precision;
#[derive(Default, Copy, Clone, Debug, PartialEq)]
pub struct Length(pub Precision);
impl From<i16> for Length {
fn from(i: i16) -> Self {
Self(Precision::from(i))
}
}
impl From<f32> for Length {
fn from(f: f32) -> Self {
Self(f)
}
}
impl From<i32> for Length {
fn from(i: i32) -> Self {
Self(i as Precision)
}
}
+243
View File
@@ -0,0 +1,243 @@
//! Turtle graphics library for Macroquad
//!
//! This library provides a turtle graphics API for creating drawings and animations
//! using the Macroquad game framework.
//!
//! # Quick Start with `turtle_main` Macro
//!
//! The easiest way to create a turtle program is using the `turtle_main` macro:
//!
//! ```no_run
//! use macroquad::prelude::*;
//! use turtle_lib::*;
//!
//! #[turtle_main("My Drawing")]
//! fn draw(turtle: &mut TurtlePlan) {
//! turtle.set_pen_color(RED);
//! turtle.forward(100.0);
//! turtle.right(90.0);
//! turtle.forward(100.0);
//! }
//! ```
//!
//! The macro automatically handles window setup, rendering loop, and quit handling.
//!
//! # Manual Setup Example
//!
//! For more control, you can set up the application manually:
//!
//! ```no_run
//! use macroquad::prelude::*;
//! use turtle_lib::*;
//!
//! #[macroquad::main("Turtle")]
//! async fn main() {
//! let mut plan = create_turtle();
//! plan.forward(100.0).right(90.0).forward(100.0);
//!
//! let mut app = TurtleApp::new().with_commands(plan.build());
//!
//! loop {
//! clear_background(WHITE);
//! app.update();
//! app.render();
//! next_frame().await
//! }
//! }
//! ```
pub mod builders;
pub mod circle_geometry;
pub mod commands;
pub mod drawing;
pub mod execution;
pub mod general;
pub mod shapes;
pub mod state;
pub mod tessellation;
pub mod tweening;
// Re-export commonly used types
pub use builders::{CurvedMovement, DirectionalMovement, Turnable, TurtlePlan, WithCommands};
pub use commands::{CommandQueue, TurtleCommand};
pub use general::{Angle, AnimationSpeed, Color, Coordinate, Length, Precision};
pub use shapes::{ShapeType, TurtleShape};
pub use state::{DrawCommand, TurtleState, TurtleWorld};
pub use tweening::TweenController;
// Re-export the turtle_main macro
pub use turtle_lib_macros::turtle_main;
// Re-export common macroquad types and colors for convenience
pub use macroquad::prelude::{
vec2, BLACK, BLUE, DARKGRAY, GOLD, GREEN, ORANGE, PURPLE, RED, WHITE, YELLOW,
};
use macroquad::prelude::*;
/// Main turtle application struct
pub struct TurtleApp {
world: TurtleWorld,
tween_controller: Option<TweenController>,
speed: AnimationSpeed,
// Mouse panning state
is_dragging: bool,
last_mouse_pos: Option<Vec2>,
// Zoom state
zoom_level: f32,
}
impl TurtleApp {
/// Create a new `TurtleApp` with default settings
#[must_use]
pub fn new() -> Self {
Self {
world: TurtleWorld::new(),
tween_controller: None,
speed: AnimationSpeed::default(),
is_dragging: false,
last_mouse_pos: None,
zoom_level: 1.0,
}
}
/// Add commands to the turtle
///
/// 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(mut self, queue: CommandQueue) -> Self {
// The `TweenController` will switch between instant and animated mode
// based on `SetSpeed` commands encountered
self.tween_controller = Some(TweenController::new(queue, self.speed));
self
}
/// Update animation state (call every frame)
pub fn update(&mut self) {
// Handle mouse panning and zoom
self.handle_mouse_panning();
self.handle_mouse_zoom();
if let Some(ref mut controller) = self.tween_controller {
let completed_commands =
controller.update(&mut self.world.turtle, &mut self.world.commands);
// Process all completed commands (multiple in instant mode, 0-1 in animated mode)
for (completed_cmd, start_state, end_state) in completed_commands {
// Add draw commands for the completed tween
execution::add_draw_for_completed_tween(
&completed_cmd,
&start_state,
&end_state,
&mut self.world,
);
}
}
}
/// Handle mouse click and drag for panning
fn handle_mouse_panning(&mut self) {
let mouse_pos = mouse_position();
let mouse_pos = vec2(mouse_pos.0, mouse_pos.1);
if is_mouse_button_pressed(MouseButton::Left) {
self.is_dragging = true;
self.last_mouse_pos = Some(mouse_pos);
}
if is_mouse_button_released(MouseButton::Left) {
self.is_dragging = false;
self.last_mouse_pos = None;
}
if self.is_dragging {
if let Some(last_pos) = self.last_mouse_pos {
// Calculate delta in screen space
let delta = mouse_pos - last_pos;
// Convert screen delta to world space delta
// The camera zoom is 2.0 / screen_width, so world_units = screen_pixels / (screen_size * zoom / 2)
let world_delta = vec2(
-delta.x, -delta.y, // Flip Y because screen Y is down
);
self.world.camera.target += world_delta * self.zoom_level;
}
self.last_mouse_pos = Some(mouse_pos);
}
}
/// Handle mouse wheel for zooming
fn handle_mouse_zoom(&mut self) {
let (_wheel_x, wheel_y) = mouse_wheel();
if wheel_y != 0.0 {
// Zoom factor: positive wheel_y = zoom in, negative = zoom out
let zoom_factor = 1.0 + wheel_y * 0.1;
self.zoom_level *= zoom_factor;
// Clamp zoom level to reasonable values
self.zoom_level = self.zoom_level.clamp(0.1, 10.0);
}
}
/// Render the turtle world (call every frame)
pub fn render(&self) {
// Get active tween if in animated mode
let active_tween = self
.tween_controller
.as_ref()
.and_then(|c| c.current_tween());
drawing::render_world_with_tween(&self.world, active_tween, self.zoom_level);
}
/// Check if all commands have been executed
#[must_use]
pub fn is_complete(&self) -> bool {
self.tween_controller
.as_ref()
.is_none_or(TweenController::is_complete)
}
/// Get reference to the world state
#[must_use]
pub fn world(&self) -> &TurtleWorld {
&self.world
}
/// Get mutable reference to the world state
pub fn world_mut(&mut self) -> &mut TurtleWorld {
&mut self.world
}
}
impl Default for TurtleApp {
fn default() -> Self {
Self::new()
}
}
/// Helper function to create a new turtle plan
///
/// # Example
/// ```
/// use turtle_lib::*;
///
/// let mut turtle = create_turtle();
/// turtle.forward(100.0).right(90.0).forward(50.0);
/// let commands = turtle.build();
/// ```
#[must_use]
pub fn create_turtle() -> TurtlePlan {
TurtlePlan::new()
}
/// Convenience function to get a turtle plan (alias for `create_turtle`)
#[must_use]
pub fn get_a_turtle() -> TurtlePlan {
create_turtle()
}
+165
View File
@@ -0,0 +1,165 @@
//! Turtle shape definitions
use macroquad::prelude::*;
use std::f32::consts::PI;
/// A shape that can be drawn for the turtle
#[derive(Clone, Debug)]
pub struct TurtleShape {
/// Vertices of the shape (relative to turtle position)
pub vertices: Vec<Vec2>,
/// Whether to draw as filled polygon (true) or outline (false)
pub filled: bool,
}
impl TurtleShape {
/// Create a new custom shape from vertices
#[must_use]
pub fn new(vertices: Vec<Vec2>, filled: bool) -> Self {
Self { vertices, filled }
}
/// Get vertices rotated by the given angle
#[must_use]
pub fn rotated_vertices(&self, angle: f32) -> Vec<Vec2> {
self.vertices
.iter()
.map(|v| {
let cos_a = angle.cos();
let sin_a = angle.sin();
vec2(v.x * cos_a - v.y * sin_a, v.x * sin_a + v.y * cos_a)
})
.collect()
}
/// Triangle shape (simple arrow pointing right)
#[must_use]
pub fn triangle() -> Self {
Self {
vertices: vec![
vec2(15.0, 0.0), // Point
vec2(-10.0, -8.0), // Bottom left
vec2(-10.0, 8.0), // Top left
],
filled: true,
}
}
/// Classic turtle shape
#[must_use]
pub fn turtle() -> Self {
// Based on the original turtle shape from turtle-lib
let polygon: &[[f32; 2]; 23] = &[
[-2.5, 14.0],
[-1.25, 10.0],
[-4.0, 7.0],
[-7.0, 9.0],
[-9.0, 8.0],
[-6.0, 5.0],
[-7.0, 1.0],
[-5.0, -3.0],
[-8.0, -6.0],
[-6.0, -8.0],
[-4.0, -5.0],
[0.0, -7.0],
[4.0, -5.0],
[6.0, -8.0],
[8.0, -6.0],
[5.0, -3.0],
[7.0, 1.0],
[6.0, 5.0],
[9.0, 8.0],
[7.0, 9.0],
[4.0, 7.0],
[1.25, 10.0],
[2.5, 14.0],
];
// Rotate by -90 degrees to point right (original points up)
let vertices: Vec<Vec2> = polygon
.iter()
.map(|[x, y]| {
let v = vec2(*x, *y);
let cos_a = (-PI / 2.0).cos();
let sin_a = (-PI / 2.0).sin();
vec2(v.x * cos_a - v.y * sin_a, v.x * sin_a + v.y * cos_a)
})
.collect();
Self {
vertices,
filled: true, // Now uses ear clipping for proper concave polygon rendering
}
}
/// Circle shape
#[must_use]
pub fn circle() -> Self {
let segments = 16;
let radius = 10.0;
let vertices: Vec<Vec2> = (0..segments)
.map(|i| {
let angle = (i as f32 / segments as f32) * 2.0 * PI;
vec2(radius * angle.cos(), radius * angle.sin())
})
.collect();
Self {
vertices,
filled: true,
}
}
/// Square shape
#[must_use]
pub fn square() -> Self {
Self {
vertices: vec![
vec2(8.0, 8.0),
vec2(-8.0, 8.0),
vec2(-8.0, -8.0),
vec2(8.0, -8.0),
],
filled: true,
}
}
/// Arrow shape (simple arrow pointing right)
#[must_use]
pub fn arrow() -> Self {
Self {
vertices: vec![
vec2(12.0, 0.0), // Point
vec2(-8.0, 6.0), // Top back
vec2(-4.0, 0.0), // Middle back
vec2(-8.0, -6.0), // Bottom back
],
filled: true,
}
}
}
/// Pre-defined shape types
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ShapeType {
Triangle,
#[default]
Turtle,
Circle,
Square,
Arrow,
}
impl ShapeType {
/// Get the corresponding `TurtleShape`
#[must_use]
pub fn to_shape(&self) -> TurtleShape {
match self {
ShapeType::Triangle => TurtleShape::triangle(),
ShapeType::Turtle => TurtleShape::turtle(),
ShapeType::Circle => TurtleShape::circle(),
ShapeType::Square => TurtleShape::square(),
ShapeType::Arrow => TurtleShape::arrow(),
}
}
}
+268
View File
@@ -0,0 +1,268 @@
//! Turtle state and world state management
use crate::general::{Angle, AnimationSpeed, Color, Coordinate, Precision};
use crate::shapes::TurtleShape;
use macroquad::prelude::*;
/// State during active fill operation
#[derive(Clone, Debug)]
pub struct FillState {
/// Starting position of the fill
pub start_position: Coordinate,
/// All contours collected so far. Each contour is a separate closed path.
/// The first contour is the outer boundary, subsequent contours are holes.
pub contours: Vec<Vec<Coordinate>>,
/// Current contour being built (vertices for the active `pen_down` segment)
pub current_contour: Vec<Coordinate>,
/// Fill color (cached from when `begin_fill` was called)
pub fill_color: Color,
}
/// State of a single turtle
#[derive(Clone, Debug)]
pub struct TurtleState {
pub position: Coordinate,
pub heading: Precision, // radians
pub pen_down: bool,
pub color: Color,
pub fill_color: Option<Color>,
pub pen_width: Precision,
pub speed: AnimationSpeed,
pub visible: bool,
pub shape: TurtleShape,
// Fill tracking
pub filling: Option<FillState>,
}
impl Default for TurtleState {
fn default() -> Self {
Self {
position: vec2(0.0, 0.0),
heading: 0.0, // pointing right (0 radians)
pen_down: true,
color: BLACK,
fill_color: None,
pen_width: 2.0,
speed: AnimationSpeed::default(),
visible: true,
shape: TurtleShape::turtle(),
filling: None,
}
}
}
impl TurtleState {
pub fn set_speed(&mut self, speed: AnimationSpeed) {
self.speed = speed;
}
#[must_use]
pub fn heading_angle(&self) -> Angle {
Angle::radians(self.heading)
}
/// Start recording fill vertices
pub fn begin_fill(&mut self, fill_color: Color) {
self.filling = Some(FillState {
start_position: self.position,
contours: Vec::new(),
current_contour: vec![self.position],
fill_color,
});
}
/// Record current position if filling and pen is down
pub fn record_fill_vertex(&mut self) {
if let Some(ref mut fill_state) = self.filling {
if self.pen_down {
tracing::trace!(
x = self.position.x,
y = self.position.y,
vertices = fill_state.current_contour.len() + 1,
"Adding vertex to current contour"
);
fill_state.current_contour.push(self.position);
} else {
tracing::trace!("Skipping vertex (pen is up)");
}
}
}
/// Close the current contour and prepare for a new one (called on `pen_up`)
pub fn close_fill_contour(&mut self) {
if let Some(ref mut fill_state) = self.filling {
tracing::debug!(
vertices = fill_state.current_contour.len(),
"close_fill_contour called"
);
// Only close if we have vertices in current contour
if fill_state.current_contour.len() >= 2 {
tracing::debug!(
vertices = fill_state.current_contour.len(),
first_x = fill_state.current_contour[0].x,
first_y = fill_state.current_contour[0].y,
last_x = fill_state.current_contour[fill_state.current_contour.len() - 1].x,
last_y = fill_state.current_contour[fill_state.current_contour.len() - 1].y,
"Closing contour"
);
// Move current contour to completed contours
let contour = std::mem::take(&mut fill_state.current_contour);
fill_state.contours.push(contour);
tracing::debug!(
completed_contours = fill_state.contours.len(),
"Contour moved to completed list"
);
} else if !fill_state.current_contour.is_empty() {
tracing::warn!(
vertices = fill_state.current_contour.len(),
"Current contour has insufficient vertices, not closing"
);
} else {
tracing::warn!("Current contour is empty, nothing to close");
}
} else {
tracing::warn!("close_fill_contour called but no active fill state");
}
}
/// Start a new contour (called on `pen_down`)
pub fn start_fill_contour(&mut self) {
if let Some(ref mut fill_state) = self.filling {
// Start new contour at current position
tracing::debug!(
x = self.position.x,
y = self.position.y,
completed_contours = fill_state.contours.len(),
"Starting new contour"
);
fill_state.current_contour = vec![self.position];
}
}
/// Record multiple vertices along a circle arc for filling
/// This ensures circles are properly filled by sampling points along the arc
pub fn record_fill_vertices_for_arc(
&mut self,
center: Coordinate,
radius: f32,
start_angle: f32,
angle_traveled: f32,
direction: crate::circle_geometry::CircleDirection,
steps: u32,
) {
if let Some(ref mut fill_state) = self.filling {
if self.pen_down {
// Sample points along the arc based on steps
let num_samples = steps as usize;
tracing::trace!(
center_x = center.x,
center_y = center.y,
radius = radius,
steps = steps,
num_samples = num_samples,
"Recording arc vertices"
);
for i in 1..=num_samples {
let progress = i as f32 / num_samples as f32;
let current_angle = match direction {
crate::circle_geometry::CircleDirection::Left => {
start_angle - angle_traveled * progress
}
crate::circle_geometry::CircleDirection::Right => {
start_angle + angle_traveled * progress
}
};
let vertex = Coordinate::new(
center.x + radius * current_angle.cos(),
center.y + radius * current_angle.sin(),
);
tracing::trace!(
vertex_idx = i,
x = vertex.x,
y = vertex.y,
angle_degrees = current_angle.to_degrees(),
"Arc vertex"
);
fill_state.current_contour.push(vertex);
}
}
}
}
/// Clear fill state (called after `end_fill`)
pub fn reset_fill(&mut self) {
self.filling = None;
}
}
/// Cached mesh data that can be cloned and converted to Mesh when needed
#[derive(Clone, Debug)]
pub struct MeshData {
pub vertices: Vec<macroquad::prelude::Vertex>,
pub indices: Vec<u16>,
}
impl MeshData {
#[must_use]
pub fn to_mesh(&self) -> macroquad::prelude::Mesh {
macroquad::prelude::Mesh {
vertices: self.vertices.clone(),
indices: self.indices.clone(),
texture: None,
}
}
}
/// Drawable elements in the world
/// All drawing is done via Lyon-tessellated meshes for consistency and quality
#[derive(Clone, Debug)]
pub enum DrawCommand {
/// Pre-tessellated mesh data (lines, arcs, circles, polygons - all use this)
Mesh(MeshData),
}
/// The complete turtle world containing all drawing state
pub struct TurtleWorld {
pub turtle: TurtleState,
pub commands: Vec<DrawCommand>,
pub camera: Camera2D,
pub background_color: Color,
}
impl TurtleWorld {
#[must_use]
pub fn new() -> Self {
Self {
turtle: TurtleState::default(),
commands: Vec::new(),
camera: Camera2D {
zoom: vec2(1.0 / screen_width() * 2.0, 1.0 / screen_height() * 2.0),
target: vec2(0.0, 0.0),
..Default::default()
},
background_color: WHITE,
}
}
pub fn add_command(&mut self, cmd: DrawCommand) {
self.commands.push(cmd);
}
pub fn clear(&mut self) {
self.commands.clear();
self.turtle = TurtleState::default();
}
}
impl Default for TurtleWorld {
fn default() -> Self {
Self::new()
}
}
+354
View File
@@ -0,0 +1,354 @@
//! Lyon tessellation utilities for turtle graphics
//!
//! This module provides helper functions to tessellate paths using Lyon,
//! which replaces the manual triangulation with GPU-optimized tessellation.
use crate::state::MeshData;
use lyon::math::{point, Point};
use lyon::path::{LineCap, LineJoin, Path};
use lyon::tessellation::{
BuffersBuilder, FillOptions, FillRule, FillTessellator, FillVertex, StrokeOptions,
StrokeTessellator, StrokeVertex, VertexBuffers,
};
use macroquad::prelude::*;
/// Convert macroquad Vec2 to Lyon Point
#[must_use]
pub fn to_lyon_point(v: Vec2) -> Point {
point(v.x, v.y)
}
/// Convert Lyon Point to macroquad Vec2
#[allow(dead_code)]
#[must_use]
pub fn to_macroquad_vec2(p: Point) -> Vec2 {
vec2(p.x, p.y)
}
/// Simple vertex type for Lyon tessellation
#[derive(Copy, Clone, Debug)]
pub struct SimpleVertex {
pub position: [f32; 2],
}
/// Build mesh data from Lyon tessellation
#[must_use]
pub fn build_mesh_data(vertices: &[SimpleVertex], indices: &[u16], color: Color) -> MeshData {
let verts: Vec<Vertex> = vertices
.iter()
.map(|v| Vertex {
position: Vec3::new(v.position[0], v.position[1], 0.0),
uv: Vec2::ZERO,
color: [
(color.r * 255.0) as u8,
(color.g * 255.0) as u8,
(color.b * 255.0) as u8,
(color.a * 255.0) as u8,
],
normal: Vec4::ZERO,
})
.collect();
MeshData {
vertices: verts,
indices: indices.to_vec(),
}
}
/// Tessellate a polygon and return mesh
///
/// This automatically handles holes when the path crosses itself.
///
/// # Errors
///
/// Returns an error if no vertices are provided or if tessellation fails.
pub fn tessellate_polygon(
vertices: &[Vec2],
color: Color,
) -> Result<MeshData, Box<dyn std::error::Error>> {
if vertices.is_empty() {
return Err("No vertices provided".into());
}
// Build path
let mut builder = Path::builder();
builder.begin(to_lyon_point(vertices[0]));
for v in &vertices[1..] {
builder.line_to(to_lyon_point(*v));
}
builder.end(true); // Close the path
let path = builder.build();
// Tessellate with EvenOdd fill rule (automatic hole detection)
let mut geometry: VertexBuffers<SimpleVertex, u16> = VertexBuffers::new();
let mut tessellator = FillTessellator::new();
tessellator.tessellate_path(
&path,
&FillOptions::default().with_fill_rule(FillRule::EvenOdd),
&mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| SimpleVertex {
position: vertex.position().to_array(),
}),
)?;
Ok(build_mesh_data(
&geometry.vertices,
&geometry.indices,
color,
))
}
/// Tessellate multiple contours (outer boundary + holes) and return mesh
///
/// The first contour is the outer boundary, subsequent contours are holes.
/// Lyon's `EvenOdd` fill rule automatically creates holes where contours overlap.
///
/// # Errors
///
/// Returns an error if no contours are provided or if tessellation fails.
pub fn tessellate_multi_contour(
contours: &[Vec<Vec2>],
color: Color,
) -> Result<MeshData, Box<dyn std::error::Error>> {
if contours.is_empty() {
return Err("No contours provided".into());
}
let span = tracing::debug_span!("tessellate_multi_contour", contours = contours.len());
let _enter = span.enter();
tracing::debug!("Starting multi-contour tessellation");
// Build path with multiple sub-paths (contours)
let mut builder = Path::builder();
for (idx, contour) in contours.iter().enumerate() {
if contour.is_empty() {
tracing::warn!(contour_idx = idx, "Contour is empty, skipping");
continue;
}
tracing::trace!(
contour_idx = idx,
vertices = contour.len(),
first_x = contour[0].x,
first_y = contour[0].y,
"Processing contour"
);
if contour.len() > 1 {
tracing::trace!(
last_x = contour[contour.len() - 1].x,
last_y = contour[contour.len() - 1].y,
"Contour end vertex"
);
}
// Each contour is a separate closed sub-path
builder.begin(to_lyon_point(contour[0]));
for (i, v) in contour[1..].iter().enumerate() {
builder.line_to(to_lyon_point(*v));
if i < 3 || i >= contour.len() - 4 {
tracing::trace!(vertex_idx = i + 1, x = v.x, y = v.y, "Contour vertex");
} else if i == 3 {
tracing::trace!(
omitted = contour.len() - 7,
"Additional vertices omitted from trace"
);
}
}
builder.end(true); // Close this contour
tracing::trace!(contour_idx = idx, "Contour closed");
}
tracing::debug!("Building Lyon path");
let path = builder.build();
tracing::debug!("Path built successfully");
// Tessellate with EvenOdd fill rule - overlapping areas become holes
let mut geometry: VertexBuffers<SimpleVertex, u16> = VertexBuffers::new();
let mut tessellator = FillTessellator::new();
tracing::debug!("Starting tessellation with EvenOdd fill rule");
match tessellator.tessellate_path(
&path,
&FillOptions::default().with_fill_rule(FillRule::EvenOdd),
&mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| SimpleVertex {
position: vertex.position().to_array(),
}),
) {
Ok(()) => {
tracing::debug!(
vertices = geometry.vertices.len(),
indices = geometry.indices.len(),
triangles = geometry.indices.len() / 3,
"Tessellation successful"
);
}
Err(e) => {
tracing::error!(error = %e, "Tessellation failed");
return Err(Box::new(e));
}
}
Ok(build_mesh_data(
&geometry.vertices,
&geometry.indices,
color,
))
}
/// Tessellate a stroked path and return mesh
///
/// # Errors
///
/// Returns an error if no vertices are provided or if tessellation fails.
pub fn tessellate_stroke(
vertices: &[Vec2],
color: Color,
width: f32,
closed: bool,
) -> Result<MeshData, Box<dyn std::error::Error>> {
if vertices.is_empty() {
return Err("No vertices provided".into());
}
// Build path
let mut builder = Path::builder();
builder.begin(to_lyon_point(vertices[0]));
for v in &vertices[1..] {
builder.line_to(to_lyon_point(*v));
}
builder.end(closed);
let path = builder.build();
// Tessellate with round caps and joins for smooth lines
let mut geometry: VertexBuffers<SimpleVertex, u16> = VertexBuffers::new();
let mut tessellator = StrokeTessellator::new();
tessellator.tessellate_path(
&path,
&StrokeOptions::default()
.with_line_width(width)
.with_line_cap(LineCap::Round)
.with_line_join(LineJoin::Round),
&mut BuffersBuilder::new(&mut geometry, |vertex: StrokeVertex| SimpleVertex {
position: vertex.position().to_array(),
}),
)?;
Ok(build_mesh_data(
&geometry.vertices,
&geometry.indices,
color,
))
}
/// Tessellate a circle and return mesh
///
/// # Errors
///
/// Returns an error if tessellation fails.
pub fn tessellate_circle(
center: Vec2,
radius: f32,
color: Color,
filled: bool,
stroke_width: f32,
) -> Result<MeshData, Box<dyn std::error::Error>> {
let mut builder = Path::builder();
builder.add_circle(to_lyon_point(center), radius, lyon::path::Winding::Positive);
let path = builder.build();
let mut geometry: VertexBuffers<SimpleVertex, u16> = VertexBuffers::new();
if filled {
let mut tessellator = FillTessellator::new();
tessellator.tessellate_path(
&path,
&FillOptions::default(),
&mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| SimpleVertex {
position: vertex.position().to_array(),
}),
)?;
} else {
let mut tessellator = StrokeTessellator::new();
tessellator.tessellate_path(
&path,
&StrokeOptions::default().with_line_width(stroke_width),
&mut BuffersBuilder::new(&mut geometry, |vertex: StrokeVertex| SimpleVertex {
position: vertex.position().to_array(),
}),
)?;
}
Ok(build_mesh_data(
&geometry.vertices,
&geometry.indices,
color,
))
}
/// Tessellate an arc (partial circle) and return mesh
///
/// # Errors
///
/// Returns an error if tessellation fails.
pub fn tessellate_arc(
center: Vec2,
radius: f32,
start_angle_degrees: f32,
arc_angle_degrees: f32,
color: Color,
stroke_width: f32,
segments: usize,
) -> Result<MeshData, Box<dyn std::error::Error>> {
// Build arc path manually from segments
let mut builder = Path::builder();
let start_angle = start_angle_degrees.to_radians();
let arc_angle = arc_angle_degrees.to_radians();
let step = arc_angle / segments as f32;
// Calculate first point
let first_angle = start_angle;
let first_point = point(
center.x + radius * first_angle.cos(),
center.y + radius * first_angle.sin(),
);
builder.begin(first_point);
// Add remaining points
for i in 1..=segments {
let angle = start_angle + step * i as f32;
let pt = point(
center.x + radius * angle.cos(),
center.y + radius * angle.sin(),
);
builder.line_to(pt);
}
builder.end(false); // Don't close the arc
let path = builder.build();
// Tessellate stroke
let mut geometry: VertexBuffers<SimpleVertex, u16> = VertexBuffers::new();
let mut tessellator = StrokeTessellator::new();
tessellator.tessellate_path(
&path,
&StrokeOptions::default()
.with_line_width(stroke_width)
.with_line_cap(lyon::tessellation::LineCap::Round)
.with_line_join(lyon::tessellation::LineJoin::Round),
&mut BuffersBuilder::new(&mut geometry, |vertex: StrokeVertex| SimpleVertex {
position: vertex.position().to_array(),
}),
)?;
Ok(build_mesh_data(
&geometry.vertices,
&geometry.indices,
color,
))
}
+447
View File
@@ -0,0 +1,447 @@
//! Tweening system for smooth animations
use crate::circle_geometry::{CircleDirection, CircleGeometry};
use crate::commands::{CommandQueue, TurtleCommand};
use crate::general::AnimationSpeed;
use crate::state::TurtleState;
use macroquad::prelude::*;
use tween::{CubicInOut, TweenValue, Tweener};
// Newtype wrapper for Vec2 to implement TweenValue
#[derive(Debug, Clone, Copy)]
pub(crate) struct TweenVec2(Vec2);
impl TweenValue for TweenVec2 {
fn scale(self, scalar: f32) -> Self {
TweenVec2(self.0 * scalar)
}
}
impl std::ops::Add for TweenVec2 {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
TweenVec2(self.0 + rhs.0)
}
}
impl std::ops::Sub for TweenVec2 {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
TweenVec2(self.0 - rhs.0)
}
}
impl From<Vec2> for TweenVec2 {
fn from(v: Vec2) -> Self {
TweenVec2(v)
}
}
impl From<TweenVec2> for Vec2 {
fn from(v: TweenVec2) -> Self {
v.0
}
}
/// Controls tweening of turtle commands
pub struct TweenController {
queue: CommandQueue,
current_tween: Option<CommandTween>,
speed: AnimationSpeed,
}
pub(crate) struct CommandTween {
pub command: TurtleCommand,
pub start_time: f64,
pub duration: f64,
pub start_state: TurtleState,
pub target_state: TurtleState,
pub position_tweener: Tweener<TweenVec2, f64, CubicInOut>,
pub heading_tweener: Tweener<f32, f64, CubicInOut>,
pub pen_width_tweener: Tweener<f32, f64, CubicInOut>,
}
impl TweenController {
#[must_use]
pub fn new(queue: CommandQueue, speed: AnimationSpeed) -> Self {
Self {
queue,
current_tween: None,
speed,
}
}
pub fn set_speed(&mut self, speed: AnimationSpeed) {
self.speed = speed;
}
/// Update the tween, returns `Vec` of (`command`, `start_state`, `end_state`) for all completed commands this frame
/// Also takes commands vec to handle side effects like fill operations
/// Each `command` has its own `start_state` and `end_state` pair
#[allow(clippy::too_many_lines)]
pub fn update(
&mut self,
state: &mut TurtleState,
commands: &mut Vec<crate::state::DrawCommand>,
) -> Vec<(TurtleCommand, TurtleState, TurtleState)> {
// In instant mode, execute commands up to the draw calls per frame limit
if let AnimationSpeed::Instant(max_draw_calls) = self.speed {
let mut completed_commands = Vec::new();
let mut draw_call_count = 0;
for command in self.queue.by_ref() {
let start_state = state.clone();
// Handle SetSpeed command to potentially switch modes
if let TurtleCommand::SetSpeed(new_speed) = &command {
state.set_speed(*new_speed);
self.speed = *new_speed;
if matches!(self.speed, AnimationSpeed::Animated(_)) {
break;
}
continue;
}
// Execute side-effect-only commands using centralized helper
if crate::execution::execute_command_side_effects(&command, state, commands) {
continue; // Command fully handled
}
// Execute movement commands
let target_state = Self::calculate_target_state(state, &command);
*state = target_state.clone();
// Record fill vertices AFTER movement using centralized helper
crate::execution::record_fill_vertices_after_movement(
&command,
&start_state,
state,
);
let end_state = state.clone();
// Collect drawable commands
if Self::command_creates_drawing(&command) && start_state.pen_down {
completed_commands.push((command, start_state, end_state));
draw_call_count += 1;
if draw_call_count >= max_draw_calls {
break;
}
}
}
return completed_commands;
}
// Process current tween
if let Some(ref mut tween) = self.current_tween {
let elapsed = get_time() - tween.start_time;
// Use tweeners to calculate current values
// For circles, calculate position along the arc instead of straight line
let progress = tween.heading_tweener.move_to(elapsed);
state.position = match &tween.command {
TurtleCommand::Circle {
radius,
angle,
direction,
..
} => {
let angle_traveled = angle.to_radians() * progress;
calculate_circle_position(
tween.start_state.position,
tween.start_state.heading,
*radius,
angle_traveled,
*direction,
)
}
_ => {
// For non-circle commands, use normal position tweening
tween.position_tweener.move_to(elapsed).into()
}
};
// Heading changes proportionally with progress for all commands
state.heading = normalize_angle(match &tween.command {
TurtleCommand::Circle {
angle, direction, ..
} => match direction {
CircleDirection::Left => {
tween.start_state.heading - angle.to_radians() * progress
}
CircleDirection::Right => {
tween.start_state.heading + angle.to_radians() * progress
}
},
TurtleCommand::Turn(angle) => {
tween.start_state.heading + angle.to_radians() * progress
}
_ => {
// For other commands that change heading, lerp directly
let heading_diff = tween.target_state.heading - tween.start_state.heading;
tween.start_state.heading + heading_diff * progress
}
});
state.pen_width = tween.pen_width_tweener.move_to(elapsed);
// Discrete properties (switch at 50% progress)
let progress = (elapsed / tween.duration).min(1.0);
if progress >= 0.5 {
state.pen_down = tween.target_state.pen_down;
state.color = tween.target_state.color;
state.fill_color = tween.target_state.fill_color;
state.visible = tween.target_state.visible;
state.shape = tween.target_state.shape.clone();
}
// Check if tween is finished (use heading_tweener as it's used by all commands)
if tween.heading_tweener.is_finished() {
let start_state = tween.start_state.clone();
*state = tween.target_state.clone();
let end_state = state.clone();
let completed_command = tween.command.clone();
self.current_tween = None;
// Execute side-effect-only commands using centralized helper
if crate::execution::execute_command_side_effects(
&completed_command,
state,
commands,
) {
return self.update(state, commands); // Continue to next command
}
// Record fill vertices for movement commands using centralized helper
crate::execution::record_fill_vertices_after_movement(
&completed_command,
&start_state,
state,
);
// Return drawable commands
if Self::command_creates_drawing(&completed_command) && start_state.pen_down {
return vec![(completed_command, start_state, end_state)];
}
return self.update(state, commands); // Continue to next command
}
return Vec::new();
}
// Start next tween
if let Some(command) = self.queue.next() {
let command_clone = command.clone();
// Handle commands that should execute immediately (no animation)
match &command_clone {
TurtleCommand::SetSpeed(new_speed) => {
state.set_speed(*new_speed);
self.speed = *new_speed;
if matches!(self.speed, AnimationSpeed::Instant(_)) {
return self.update(state, commands);
}
return self.update(state, commands);
}
_ => {
// Use centralized helper for side effects
if crate::execution::execute_command_side_effects(
&command_clone,
state,
commands,
) {
return self.update(state, commands);
}
}
}
let speed = state.speed; // Extract speed before borrowing self
let duration = Self::calculate_duration_with_state(&command_clone, state, speed);
// Calculate target state
let target_state = Self::calculate_target_state(state, &command_clone);
// Create tweeners for smooth animation
let position_tweener = Tweener::new(
TweenVec2::from(state.position),
TweenVec2::from(target_state.position),
duration,
CubicInOut,
);
let heading_tweener = Tweener::new(
0.0, // We'll handle angle wrapping separately
1.0, duration, CubicInOut,
);
let pen_width_tweener = Tweener::new(
state.pen_width,
target_state.pen_width,
duration,
CubicInOut,
);
self.current_tween = Some(CommandTween {
command: command_clone,
start_time: get_time(),
duration,
start_state: state.clone(),
target_state,
position_tweener,
heading_tweener,
pen_width_tweener,
});
}
Vec::new()
}
#[must_use]
pub fn is_complete(&self) -> bool {
self.current_tween.is_none() && self.queue.is_complete()
}
/// Get the current active tween if one is in progress
pub(crate) fn current_tween(&self) -> Option<&CommandTween> {
self.current_tween.as_ref()
}
fn command_creates_drawing(command: &TurtleCommand) -> bool {
matches!(
command,
TurtleCommand::Move(_) | TurtleCommand::Circle { .. } | TurtleCommand::Goto(_)
)
}
fn calculate_duration_with_state(
command: &TurtleCommand,
current: &TurtleState,
speed: AnimationSpeed,
) -> f64 {
let speed = speed.value();
let base_time = match command {
TurtleCommand::Move(dist) => dist.abs() / speed,
TurtleCommand::Turn(angle) => {
// Rotation speed: assume 180 degrees per second at speed 100
angle.abs() / (speed * 1.8)
}
TurtleCommand::Circle { radius, angle, .. } => {
let arc_length = radius * angle.to_radians().abs();
arc_length / speed
}
TurtleCommand::Goto(target) => {
// Calculate actual distance from current position to target
let dx = target.x - current.position.x;
let dy = target.y - current.position.y;
let distance = (dx * dx + dy * dy).sqrt();
distance / speed
}
_ => 0.0, // Instant commands
};
f64::from(base_time.max(0.01)) // Minimum duration
}
fn calculate_target_state(current: &TurtleState, command: &TurtleCommand) -> TurtleState {
let mut target = current.clone();
match command {
TurtleCommand::Move(dist) => {
let dx = dist * current.heading.cos();
let dy = dist * current.heading.sin();
target.position = vec2(current.position.x + dx, current.position.y + dy);
}
TurtleCommand::Turn(angle) => {
target.heading = normalize_angle(current.heading + angle.to_radians());
}
TurtleCommand::Circle {
radius,
angle,
direction,
..
} => {
// Use helper function to calculate final position
target.position = calculate_circle_position(
current.position,
current.heading,
*radius,
angle.to_radians(),
*direction,
);
target.heading = normalize_angle(match direction {
CircleDirection::Left => current.heading - angle.to_radians(),
CircleDirection::Right => current.heading + angle.to_radians(),
});
}
TurtleCommand::Goto(coord) => {
target.position = *coord;
}
TurtleCommand::SetHeading(heading) => {
target.heading = normalize_angle(*heading);
}
TurtleCommand::SetColor(color) => {
target.color = *color;
}
TurtleCommand::SetPenWidth(width) => {
target.pen_width = *width;
}
TurtleCommand::SetSpeed(speed) => {
target.speed = *speed;
}
TurtleCommand::SetShape(shape) => {
target.shape = shape.clone();
}
TurtleCommand::PenUp => {
target.pen_down = false;
}
TurtleCommand::PenDown => {
target.pen_down = true;
}
TurtleCommand::ShowTurtle => {
target.visible = true;
}
TurtleCommand::HideTurtle => {
target.visible = false;
}
TurtleCommand::SetFillColor(color) => {
target.fill_color = *color;
}
TurtleCommand::BeginFill | TurtleCommand::EndFill => {
// Fill commands don't change turtle state for tweening purposes
// They're handled directly in execution
}
}
target
}
}
/// Calculate position on a circular arc
fn calculate_circle_position(
start_pos: Vec2,
start_heading: f32,
radius: f32,
angle_traveled: f32, // How much of the total angle we've traveled (in radians)
direction: CircleDirection,
) -> Vec2 {
let geom = CircleGeometry::new(start_pos, start_heading, radius, direction);
geom.position_at_angle(angle_traveled)
}
/// Normalize angle to range [-PI, PI] to prevent floating-point drift
fn normalize_angle(angle: f32) -> f32 {
let two_pi = std::f32::consts::PI * 2.0;
let mut normalized = angle % two_pi;
// Ensure result is in [-PI, PI]
if normalized > std::f32::consts::PI {
normalized -= two_pi;
} else if normalized < -std::f32::consts::PI {
normalized += two_pi;
}
normalized
}