Initial macroquad version for compiletime reasons
```rust
// Movement
plan.forward(100);
plan.backward(50);
// Rotation
plan.left(90); // degrees
plan.right(45);
// Circular arcs
plan.circle_left(50.0, 180.0, 36); // radius, angle (degrees), segments
plan.circle_right(50.0, 180.0, 36); // draws arc to the right
// Pen control
plan.pen_up();
plan.pen_down();
// Appearance
plan.set_color(RED);
plan.set_pen_width(5.0);
plan.hide();
plan.show();
// Turtle shape
plan.shape(ShapeType::Triangle);
plan.shape(ShapeType::Turtle); // Default classic turtle shape
plan.shape(ShapeType::Circle);
plan.shape(ShapeType::Square);
plan.shape(ShapeType::Arrow);
// Custom shape
let custom = TurtleShape::new(
vec![vec2(10.0, 0.0), vec2(-5.0, 5.0), vec2(-5.0, -5.0)],
true // filled
);
plan.set_shape(custom);
// Chaining
plan.forward(100).right(90).forward(50);
```
This commit is contained in:
parent
9ab58e39e7
commit
25753b47ce
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
15
.vscode/tasks.json
vendored
Normal file
15
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "cargo check (workspace)",
|
||||
"type": "shell",
|
||||
"command": "cargo check",
|
||||
"isBackground": false,
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
],
|
||||
"group": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
members = ["turtle-lib", "turtle-example"]
|
||||
members = ["turtle-lib", "turtle-example", "turtle-lib-macroquad"]
|
||||
|
||||
[workspace.dependencies]
|
||||
# Pin Bevy across the workspace
|
||||
|
||||
13
turtle-lib-macroquad/Cargo.toml
Normal file
13
turtle-lib-macroquad/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "turtle-lib-macroquad"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
macroquad = "0.4"
|
||||
earcutr = "0.5"
|
||||
tween = "2.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
# For examples and testing
|
||||
229
turtle-lib-macroquad/README.md
Normal file
229
turtle-lib-macroquad/README.md
Normal file
@ -0,0 +1,229 @@
|
||||
# Turtle Graphics Library for Macroquad
|
||||
|
||||
A turtle graphics library built on [Macroquad](https://macroquad.rs/), providing an intuitive API for creating drawings and animations.
|
||||
|
||||
## Features
|
||||
|
||||
- **Simple Builder API**: Chain commands like `forward(100).right(90)`
|
||||
- **Smooth Animations**: Tweening support with easing functions
|
||||
- **Instant Mode**: Execute commands immediately without animation (speed > 999.0)
|
||||
- **Lightweight**: Fast compilation (~30-60 seconds from clean build)
|
||||
- **Macroquad Integration**: Built on the simple and fast Macroquad framework
|
||||
|
||||
## Quick Start
|
||||
|
||||
Add to your `Cargo.toml`:
|
||||
```toml
|
||||
[dependencies]
|
||||
turtle-lib-macroquad = { path = "../turtle-lib-macroquad" }
|
||||
macroquad = "0.4"
|
||||
```
|
||||
|
||||
### Basic Example
|
||||
|
||||
```rust
|
||||
use macroquad::prelude::*;
|
||||
use turtle_lib_macroquad::*;
|
||||
|
||||
#[macroquad::main("Turtle")]
|
||||
async fn main() {
|
||||
// Create a turtle plan
|
||||
let mut plan = create_turtle();
|
||||
|
||||
// Draw a square
|
||||
for _ in 0..4 {
|
||||
plan.forward(100).right(90);
|
||||
}
|
||||
|
||||
// Create app with animation (100 pixels/sec)
|
||||
let mut app = TurtleApp::new().with_commands(plan.build(), 100.0);
|
||||
|
||||
loop {
|
||||
clear_background(WHITE);
|
||||
app.update();
|
||||
app.render();
|
||||
next_frame().await
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Overview
|
||||
|
||||
### Creating Plans
|
||||
|
||||
```rust
|
||||
let mut plan = create_turtle();
|
||||
|
||||
// Movement
|
||||
plan.forward(100);
|
||||
plan.backward(50);
|
||||
|
||||
// Rotation
|
||||
plan.left(90); // degrees
|
||||
plan.right(45);
|
||||
|
||||
// Circular arcs
|
||||
plan.circle_left(50.0, 180.0, 36); // radius, angle (degrees), segments
|
||||
plan.circle_right(50.0, 180.0, 36); // draws arc to the right
|
||||
|
||||
// Pen control
|
||||
plan.pen_up();
|
||||
plan.pen_down();
|
||||
|
||||
// Appearance
|
||||
plan.set_color(RED);
|
||||
plan.set_pen_width(5.0);
|
||||
plan.hide();
|
||||
plan.show();
|
||||
|
||||
// Turtle shape
|
||||
plan.shape(ShapeType::Triangle);
|
||||
plan.shape(ShapeType::Turtle); // Default classic turtle shape
|
||||
plan.shape(ShapeType::Circle);
|
||||
plan.shape(ShapeType::Square);
|
||||
plan.shape(ShapeType::Arrow);
|
||||
|
||||
// Custom shape
|
||||
let custom = TurtleShape::new(
|
||||
vec![vec2(10.0, 0.0), vec2(-5.0, 5.0), vec2(-5.0, -5.0)],
|
||||
true // filled
|
||||
);
|
||||
plan.set_shape(custom);
|
||||
|
||||
// Chaining
|
||||
plan.forward(100).right(90).forward(50);
|
||||
```
|
||||
|
||||
### Execution Modes
|
||||
|
||||
```rust
|
||||
// Animated mode (speed in pixels/sec, 0.5-999.0)
|
||||
let app = TurtleApp::new().with_commands(queue, 100.0);
|
||||
|
||||
// Instant mode (speed >= 999.0)
|
||||
let app = TurtleApp::new().with_commands(queue, 1000.0);
|
||||
```
|
||||
|
||||
### Animation Loop
|
||||
|
||||
```rust
|
||||
loop {
|
||||
clear_background(WHITE);
|
||||
|
||||
app.update(); // Update animation state
|
||||
app.render(); // Draw to screen
|
||||
|
||||
if app.is_complete() {
|
||||
// All commands executed
|
||||
}
|
||||
|
||||
next_frame().await
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Run examples with:
|
||||
```bash
|
||||
cargo run --example square
|
||||
cargo run --example koch
|
||||
cargo run --example shapes
|
||||
cargo run --example yinyang
|
||||
cargo run --example stern
|
||||
cargo run --example nikolaus
|
||||
```
|
||||
|
||||
### Available Examples
|
||||
|
||||
- **square.rs**: Basic square drawing
|
||||
- **koch.rs**: Koch snowflake fractal
|
||||
- **shapes.rs**: Demonstrates different turtle shapes
|
||||
- **yinyang.rs**: Yin-yang symbol drawing
|
||||
- **stern.rs**: Star pattern drawing
|
||||
- **nikolaus.rs**: Nikolaus (Santa) drawing
|
||||
|
||||
## Turtle Shapes
|
||||
|
||||
The library supports multiple turtle shapes that can be changed during drawing:
|
||||
|
||||
### Built-in Shapes
|
||||
|
||||
- **Triangle** (default): Simple arrow shape
|
||||
- **Turtle**: Classic turtle shape with detailed outline
|
||||
- **Circle**: Circular shape
|
||||
- **Square**: Square shape
|
||||
- **Arrow**: Arrow-like shape
|
||||
|
||||
### Using Shapes
|
||||
|
||||
```rust
|
||||
// Using built-in shapes
|
||||
plan.shape(ShapeType::Turtle);
|
||||
|
||||
// Creating custom shapes
|
||||
let my_shape = TurtleShape::new(
|
||||
vec![
|
||||
vec2(15.0, 0.0), // Point at front
|
||||
vec2(-10.0, -8.0), // Bottom back
|
||||
vec2(-10.0, 8.0), // Top back
|
||||
],
|
||||
true // filled
|
||||
);
|
||||
plan.set_shape(my_shape);
|
||||
```
|
||||
|
||||
Shapes are automatically rotated to match the turtle's heading direction.
|
||||
|
||||
## Architecture
|
||||
|
||||
The library is designed for easy extension and potential multi-threading support:
|
||||
|
||||
- **State Management**: Clean separation between turtle state and world state
|
||||
- **Command Queue**: Commands are queued and can be executed immediately or with tweening
|
||||
- **Tweening System**: Smooth interpolation between states with easing functions
|
||||
- **Rendering**: Direct Macroquad drawing calls with earcutr polygon triangulation
|
||||
- **Shape System**: Extensible turtle shapes with support for both convex and concave polygons
|
||||
|
||||
### Module Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── lib.rs - Public API and TurtleApp
|
||||
├── state.rs - TurtleState and TurtleWorld
|
||||
├── commands.rs - TurtleCommand enum and CommandQueue
|
||||
├── builders.rs - Builder traits (DirectionalMovement, Turnable, etc.)
|
||||
├── execution.rs - Command execution logic
|
||||
├── tweening.rs - Animation/tweening controller
|
||||
├── drawing.rs - Macroquad rendering
|
||||
├── shapes.rs - Turtle shape definitions
|
||||
└── general/ - Type definitions (Angle, Length, etc.)
|
||||
```
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Multi-threading Ready
|
||||
|
||||
While multi-threading is not implemented yet, the architecture supports future additions:
|
||||
- State and world are separated
|
||||
- Commands can be generated on separate threads
|
||||
- Rendering happens on main thread (Macroquad requirement)
|
||||
|
||||
### Tweening vs Interpolation
|
||||
|
||||
We use "tweening" terminology throughout the codebase for clarity and game development conventions.
|
||||
|
||||
### No API Compatibility Constraints
|
||||
|
||||
This library is designed from scratch without backwards compatibility requirements, allowing for optimal design choices.
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential additions (not yet implemented):
|
||||
- Multi-threading support for interactive games
|
||||
- Filled shapes and polygons
|
||||
- Text rendering
|
||||
- Image stamps
|
||||
|
||||
## License
|
||||
|
||||
MIT OR Apache-2.0
|
||||
41
turtle-lib-macroquad/examples/circle_test.rs
Normal file
41
turtle-lib-macroquad/examples/circle_test.rs
Normal file
@ -0,0 +1,41 @@
|
||||
//! Test circle_left and circle_right commands
|
||||
|
||||
use macroquad::prelude::*;
|
||||
use turtle_lib_macroquad::*;
|
||||
|
||||
#[macroquad::main("Circle Test")]
|
||||
async fn main() {
|
||||
// Create a turtle plan
|
||||
let mut plan = create_turtle();
|
||||
plan.shape(ShapeType::Turtle);
|
||||
|
||||
// Draw some circles
|
||||
plan.set_color(RED);
|
||||
plan.set_pen_width(0.5);
|
||||
plan.left(90.0);
|
||||
plan.circle_left(100.0, 540.0, 72); // partial circle to the left
|
||||
|
||||
plan.forward(150.0);
|
||||
|
||||
plan.set_color(BLUE);
|
||||
plan.circle_right(50.0, 270.0, 72); // partial circle to the right
|
||||
|
||||
plan.forward(150.0);
|
||||
|
||||
plan.set_color(GREEN);
|
||||
plan.circle_left(50.0, 180.0, 36); // Half circle to the left
|
||||
|
||||
// Create turtle app with animation (speed = 100 pixels/sec)
|
||||
let mut app = TurtleApp::new().with_commands(plan.build(), 100.0);
|
||||
|
||||
// Main loop
|
||||
loop {
|
||||
clear_background(WHITE);
|
||||
|
||||
// Update and render
|
||||
app.update();
|
||||
app.render();
|
||||
|
||||
next_frame().await
|
||||
}
|
||||
}
|
||||
30
turtle-lib-macroquad/examples/direction_test_1.rs
Normal file
30
turtle-lib-macroquad/examples/direction_test_1.rs
Normal file
@ -0,0 +1,30 @@
|
||||
//! Test circle_left and circle_right commands
|
||||
|
||||
use macroquad::prelude::*;
|
||||
use turtle_lib_macroquad::*;
|
||||
|
||||
#[macroquad::main("Circle Test")]
|
||||
async fn main() {
|
||||
// Create a turtle plan
|
||||
let mut plan = create_turtle();
|
||||
|
||||
plan.right(45.0);
|
||||
plan.forward(100.0);
|
||||
plan.right(45.0);
|
||||
plan.forward(100.0);
|
||||
//plan.circle_left(100.0, 90.0, 72); // Full circle to the left
|
||||
|
||||
// Create turtle app with animation (speed = 100 pixels/sec)
|
||||
let mut app = TurtleApp::new().with_commands(plan.build(), 10.0);
|
||||
|
||||
// Main loop
|
||||
loop {
|
||||
clear_background(WHITE);
|
||||
|
||||
// Update and render
|
||||
app.update();
|
||||
app.render();
|
||||
|
||||
next_frame().await
|
||||
}
|
||||
}
|
||||
49
turtle-lib-macroquad/examples/koch.rs
Normal file
49
turtle-lib-macroquad/examples/koch.rs
Normal file
@ -0,0 +1,49 @@
|
||||
//! Koch snowflake fractal example
|
||||
|
||||
use macroquad::prelude::*;
|
||||
use turtle_lib_macroquad::*;
|
||||
|
||||
fn koch(depth: u32, plan: &mut TurtlePlan, distance: f32) {
|
||||
if depth == 0 {
|
||||
plan.forward(distance);
|
||||
} else {
|
||||
let new_distance = distance / 3.0;
|
||||
koch(depth - 1, plan, new_distance);
|
||||
plan.left(60.0);
|
||||
koch(depth - 1, plan, new_distance);
|
||||
plan.right(120.0);
|
||||
koch(depth - 1, plan, new_distance);
|
||||
plan.left(60.0);
|
||||
koch(depth - 1, plan, new_distance);
|
||||
}
|
||||
}
|
||||
|
||||
#[macroquad::main("Koch Snowflake")]
|
||||
async fn main() {
|
||||
let mut plan = create_turtle();
|
||||
|
||||
// Position turtle
|
||||
plan.set_speed(10);
|
||||
plan.pen_up();
|
||||
plan.backward(150.0);
|
||||
|
||||
plan.pen_down();
|
||||
|
||||
// Draw Koch snowflake (triangle of Koch curves)
|
||||
for _ in 0..3 {
|
||||
koch(4, &mut plan, 300.0);
|
||||
plan.right(120.0);
|
||||
}
|
||||
|
||||
plan.hide(); // Hide turtle when done
|
||||
|
||||
// Create app with animation
|
||||
let mut app = TurtleApp::new().with_commands(plan.build(), 1000.0);
|
||||
|
||||
loop {
|
||||
clear_background(WHITE);
|
||||
app.update();
|
||||
app.render();
|
||||
next_frame().await
|
||||
}
|
||||
}
|
||||
74
turtle-lib-macroquad/examples/nikolaus.rs
Normal file
74
turtle-lib-macroquad/examples/nikolaus.rs
Normal file
@ -0,0 +1,74 @@
|
||||
//! Nikolaus example - draws a house-like figure
|
||||
|
||||
use macroquad::prelude::*;
|
||||
use turtle_lib_macroquad::*;
|
||||
|
||||
fn nikolausquadrat(plan: &mut TurtlePlan, groesse: f32) {
|
||||
plan.forward(groesse);
|
||||
plan.left(90.0);
|
||||
plan.forward(groesse);
|
||||
plan.left(90.0);
|
||||
plan.forward(groesse);
|
||||
plan.left(90.0);
|
||||
plan.forward(groesse);
|
||||
plan.left(90.0);
|
||||
}
|
||||
|
||||
fn nikolausdiag(plan: &mut TurtlePlan, groesse: f32) {
|
||||
let quadrat = groesse * groesse;
|
||||
let diag = (quadrat + quadrat).sqrt();
|
||||
|
||||
plan.left(45.0);
|
||||
plan.forward(diag);
|
||||
plan.left(45.0);
|
||||
nikolausdach2(plan, groesse);
|
||||
plan.left(45.0);
|
||||
plan.forward(diag);
|
||||
plan.left(45.0);
|
||||
}
|
||||
|
||||
fn nikolausdach2(plan: &mut TurtlePlan, groesse: f32) {
|
||||
let quadrat = groesse * groesse;
|
||||
let diag = (quadrat + quadrat).sqrt();
|
||||
plan.left(45.0);
|
||||
plan.forward(diag / 2.0);
|
||||
plan.left(90.0);
|
||||
plan.forward(diag / 2.0);
|
||||
plan.left(45.0);
|
||||
}
|
||||
|
||||
fn nikolaus(plan: &mut TurtlePlan, groesse: f32) {
|
||||
nikolausquadrat(plan, groesse);
|
||||
nikolausdiag(plan, groesse);
|
||||
}
|
||||
|
||||
#[macroquad::main("Nikolaus")]
|
||||
async fn main() {
|
||||
// Create a turtle plan
|
||||
let mut plan = create_turtle();
|
||||
plan.shape(ShapeType::Turtle);
|
||||
|
||||
// Position the turtle (pen up, move, pen down)
|
||||
plan.pen_up();
|
||||
plan.backward(80.0);
|
||||
plan.left(90.0);
|
||||
plan.forward(50.0);
|
||||
plan.right(90.0);
|
||||
plan.pen_down();
|
||||
|
||||
nikolaus(&mut plan, 100.0);
|
||||
|
||||
// Create turtle app with animation (speed = 100 pixels/sec)
|
||||
let mut app = TurtleApp::new().with_commands(plan.build(), 100.0);
|
||||
|
||||
// Main loop
|
||||
loop {
|
||||
clear_background(WHITE);
|
||||
|
||||
// Update and render
|
||||
app.update();
|
||||
app.render();
|
||||
|
||||
next_frame().await
|
||||
}
|
||||
}
|
||||
47
turtle-lib-macroquad/examples/shapes.rs
Normal file
47
turtle-lib-macroquad/examples/shapes.rs
Normal file
@ -0,0 +1,47 @@
|
||||
//! Example demonstrating different turtle shapes
|
||||
|
||||
use macroquad::prelude::*;
|
||||
use turtle_lib_macroquad::*;
|
||||
|
||||
#[macroquad::main("Turtle Shapes")]
|
||||
async fn main() {
|
||||
// Create a turtle plan that demonstrates different shapes
|
||||
let mut plan = create_turtle();
|
||||
|
||||
// Start with triangle (default)
|
||||
plan.forward(100.0);
|
||||
plan.right(90.0);
|
||||
|
||||
// Change to turtle shape
|
||||
plan.shape(ShapeType::Turtle);
|
||||
plan.forward(100.0);
|
||||
plan.right(90.0);
|
||||
|
||||
// Change to circle
|
||||
plan.shape(ShapeType::Circle);
|
||||
plan.forward(100.0);
|
||||
plan.right(90.0);
|
||||
|
||||
// Change to square
|
||||
plan.shape(ShapeType::Square);
|
||||
plan.forward(100.0);
|
||||
plan.right(90.0);
|
||||
|
||||
// Change to arrow
|
||||
plan.shape(ShapeType::Arrow);
|
||||
plan.forward(100.0);
|
||||
|
||||
// Create turtle app with animation (speed = 100 pixels/sec for slower animation)
|
||||
let mut app = TurtleApp::new().with_commands(plan.build(), 700.0);
|
||||
|
||||
// Main loop
|
||||
loop {
|
||||
clear_background(WHITE);
|
||||
|
||||
// Update and render
|
||||
app.update();
|
||||
app.render();
|
||||
|
||||
next_frame().await
|
||||
}
|
||||
}
|
||||
30
turtle-lib-macroquad/examples/square.rs
Normal file
30
turtle-lib-macroquad/examples/square.rs
Normal file
@ -0,0 +1,30 @@
|
||||
//! Simple square example demonstrating basic turtle graphics
|
||||
|
||||
use macroquad::prelude::*;
|
||||
use turtle_lib_macroquad::*;
|
||||
|
||||
#[macroquad::main("Turtle Square")]
|
||||
async fn main() {
|
||||
// Create a turtle plan
|
||||
let mut plan = create_turtle();
|
||||
plan.shape(ShapeType::Turtle);
|
||||
|
||||
// Draw a square
|
||||
for _ in 0..4 {
|
||||
plan.forward(100.0).right(90.0);
|
||||
}
|
||||
|
||||
// Create turtle app with animation (speed = 100 pixels/sec)
|
||||
let mut app = TurtleApp::new().with_commands(plan.build(), 100.0);
|
||||
|
||||
// Main loop
|
||||
loop {
|
||||
clear_background(WHITE);
|
||||
|
||||
// Update and render
|
||||
app.update();
|
||||
app.render();
|
||||
|
||||
next_frame().await
|
||||
}
|
||||
}
|
||||
34
turtle-lib-macroquad/examples/stern.rs
Normal file
34
turtle-lib-macroquad/examples/stern.rs
Normal file
@ -0,0 +1,34 @@
|
||||
//! Simple square example demonstrating basic turtle graphics
|
||||
|
||||
use macroquad::prelude::*;
|
||||
use turtle_lib_macroquad::*;
|
||||
|
||||
#[macroquad::main("Turtle Square")]
|
||||
async fn main() {
|
||||
// Create a turtle plan
|
||||
let mut plan = create_turtle();
|
||||
plan.shape(ShapeType::Turtle);
|
||||
plan.set_speed(800);
|
||||
|
||||
// Draw a square
|
||||
for _ in 0..5 {
|
||||
plan.forward(200.0);
|
||||
plan.circle_left(10.0, 72.0, 1000);
|
||||
plan.circle_right(5.0, 360.0, 1000);
|
||||
plan.circle_left(10.0, 72.0, 1000);
|
||||
}
|
||||
|
||||
// Create turtle app with animation (speed = 100 pixels/sec)
|
||||
let mut app = TurtleApp::new().with_commands(plan.build(), 100.0);
|
||||
|
||||
// Main loop
|
||||
loop {
|
||||
clear_background(WHITE);
|
||||
|
||||
// Update and render
|
||||
app.update();
|
||||
app.render();
|
||||
|
||||
next_frame().await
|
||||
}
|
||||
}
|
||||
41
turtle-lib-macroquad/examples/yinyang.rs
Normal file
41
turtle-lib-macroquad/examples/yinyang.rs
Normal file
@ -0,0 +1,41 @@
|
||||
//! Simple square example demonstrating basic turtle graphics
|
||||
|
||||
use macroquad::prelude::*;
|
||||
use turtle_lib_macroquad::*;
|
||||
|
||||
#[macroquad::main("Turtle Square")]
|
||||
async fn main() {
|
||||
// Create a turtle plan
|
||||
let mut t = create_turtle();
|
||||
|
||||
t.circle_left(90.0, 180.0, 36);
|
||||
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);
|
||||
|
||||
// Create turtle app with animation (speed = 100 pixels/sec)
|
||||
let mut app = TurtleApp::new().with_commands(t.build(), 100.0);
|
||||
|
||||
// Main loop
|
||||
loop {
|
||||
clear_background(WHITE);
|
||||
|
||||
// Update and render
|
||||
app.update();
|
||||
app.render();
|
||||
|
||||
next_frame().await
|
||||
}
|
||||
}
|
||||
168
turtle-lib-macroquad/src/builders.rs
Normal file
168
turtle-lib-macroquad/src/builders.rs
Normal file
@ -0,0 +1,168 @@
|
||||
//! Builder pattern traits for creating turtle command sequences
|
||||
|
||||
use crate::commands::{CommandQueue, TurtleCommand};
|
||||
use crate::general::{Color, 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 {
|
||||
fn forward<T>(&mut self, distance: T) -> &mut Self
|
||||
where
|
||||
T: Into<Precision>,
|
||||
{
|
||||
let dist: Precision = distance.into();
|
||||
self.get_commands_mut().push(TurtleCommand::Forward(dist));
|
||||
self
|
||||
}
|
||||
|
||||
fn backward<T>(&mut self, distance: T) -> &mut Self
|
||||
where
|
||||
T: Into<Precision>,
|
||||
{
|
||||
let dist: Precision = distance.into();
|
||||
self.get_commands_mut().push(TurtleCommand::Backward(dist));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for turning operations
|
||||
pub trait Turnable: WithCommands {
|
||||
fn left<T>(&mut self, angle: T) -> &mut Self
|
||||
where
|
||||
T: Into<Precision>,
|
||||
{
|
||||
let degrees: Precision = angle.into();
|
||||
self.get_commands_mut().push(TurtleCommand::Left(degrees));
|
||||
self
|
||||
}
|
||||
|
||||
fn right<T>(&mut self, angle: T) -> &mut Self
|
||||
where
|
||||
T: Into<Precision>,
|
||||
{
|
||||
let degrees: Precision = angle.into();
|
||||
self.get_commands_mut().push(TurtleCommand::Right(degrees));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for curved movement (circles)
|
||||
pub trait CurvedMovement: WithCommands {
|
||||
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::CircleLeft {
|
||||
radius: r,
|
||||
angle: a,
|
||||
steps,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
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::CircleRight {
|
||||
radius: r,
|
||||
angle: a,
|
||||
steps,
|
||||
});
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for creating turtle command sequences
|
||||
#[derive(Default, Debug)]
|
||||
pub struct TurtlePlan {
|
||||
queue: CommandQueue,
|
||||
}
|
||||
|
||||
impl TurtlePlan {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
queue: CommandQueue::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self {
|
||||
queue: CommandQueue::with_capacity(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_speed(&mut self, speed: u32) -> &mut Self {
|
||||
self.queue.push(TurtleCommand::SetSpeed(speed));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_color(&mut self, color: Color) -> &mut Self {
|
||||
self.queue.push(TurtleCommand::SetColor(color));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_pen_width(&mut self, width: Precision) -> &mut Self {
|
||||
self.queue.push(TurtleCommand::SetPenWidth(width));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn pen_up(&mut self) -> &mut Self {
|
||||
self.queue.push(TurtleCommand::PenUp);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn pen_down(&mut self) -> &mut Self {
|
||||
self.queue.push(TurtleCommand::PenDown);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn hide(&mut self) -> &mut Self {
|
||||
self.queue.push(TurtleCommand::HideTurtle);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn show(&mut self) -> &mut Self {
|
||||
self.queue.push(TurtleCommand::ShowTurtle);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_shape(&mut self, shape: TurtleShape) -> &mut Self {
|
||||
self.queue.push(TurtleCommand::SetShape(shape));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn shape(&mut self, shape_type: ShapeType) -> &mut Self {
|
||||
self.set_shape(shape_type.to_shape())
|
||||
}
|
||||
|
||||
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 {}
|
||||
207
turtle-lib-macroquad/src/circle_geometry.rs
Normal file
207
turtle-lib-macroquad/src/circle_geometry.rs
Normal file
@ -0,0 +1,207 @@
|
||||
//! 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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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);
|
||||
}
|
||||
}
|
||||
114
turtle-lib-macroquad/src/commands.rs
Normal file
114
turtle-lib-macroquad/src/commands.rs
Normal file
@ -0,0 +1,114 @@
|
||||
//! Turtle commands and command queue
|
||||
|
||||
use crate::general::{Color, Coordinate, Precision};
|
||||
use crate::shapes::TurtleShape;
|
||||
|
||||
/// Individual turtle commands
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum TurtleCommand {
|
||||
// Movement
|
||||
Forward(Precision),
|
||||
Backward(Precision),
|
||||
|
||||
// Rotation
|
||||
Left(Precision), // degrees
|
||||
Right(Precision), // degrees
|
||||
|
||||
// Circle drawing
|
||||
CircleLeft {
|
||||
radius: Precision,
|
||||
angle: Precision, // degrees
|
||||
steps: usize,
|
||||
},
|
||||
CircleRight {
|
||||
radius: Precision,
|
||||
angle: Precision, // degrees
|
||||
steps: usize,
|
||||
},
|
||||
|
||||
// Pen control
|
||||
PenUp,
|
||||
PenDown,
|
||||
|
||||
// Appearance
|
||||
SetColor(Color),
|
||||
SetFillColor(Option<Color>),
|
||||
SetPenWidth(Precision),
|
||||
SetSpeed(u32),
|
||||
SetShape(TurtleShape),
|
||||
|
||||
// Position
|
||||
Goto(Coordinate),
|
||||
SetHeading(Precision), // radians
|
||||
|
||||
// Visibility
|
||||
ShowTurtle,
|
||||
HideTurtle,
|
||||
}
|
||||
|
||||
/// Queue of turtle commands with execution state
|
||||
#[derive(Debug)]
|
||||
pub struct CommandQueue {
|
||||
commands: Vec<TurtleCommand>,
|
||||
current_index: usize,
|
||||
}
|
||||
|
||||
impl CommandQueue {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
commands: Vec::new(),
|
||||
current_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
pub fn next(&mut self) -> Option<&TurtleCommand> {
|
||||
if self.current_index < self.commands.len() {
|
||||
let cmd = &self.commands[self.current_index];
|
||||
self.current_index += 1;
|
||||
Some(cmd)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.current_index >= self.commands.len()
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.current_index = 0;
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.commands.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.commands.is_empty()
|
||||
}
|
||||
|
||||
pub fn remaining(&self) -> usize {
|
||||
self.commands.len().saturating_sub(self.current_index)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CommandQueue {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
345
turtle-lib-macroquad/src/drawing.rs
Normal file
345
turtle-lib-macroquad/src/drawing.rs
Normal file
@ -0,0 +1,345 @@
|
||||
//! Rendering logic using Macroquad
|
||||
|
||||
use crate::circle_geometry::{CircleDirection, CircleGeometry};
|
||||
use crate::state::{DrawCommand, TurtleState, TurtleWorld};
|
||||
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_* functions 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::Line {
|
||||
start,
|
||||
end,
|
||||
color,
|
||||
width,
|
||||
} => {
|
||||
draw_line(start.x, start.y, end.x, end.y, *width, *color);
|
||||
}
|
||||
DrawCommand::Circle {
|
||||
center,
|
||||
radius,
|
||||
color,
|
||||
filled,
|
||||
} => {
|
||||
if *filled {
|
||||
draw_circle(center.x, center.y, *radius, *color);
|
||||
} else {
|
||||
draw_circle_lines(center.x, center.y, *radius, 2.0, *color);
|
||||
}
|
||||
}
|
||||
DrawCommand::Arc {
|
||||
center,
|
||||
radius,
|
||||
rotation,
|
||||
arc,
|
||||
color,
|
||||
width,
|
||||
sides,
|
||||
} => {
|
||||
draw_arc(
|
||||
center.x, center.y, *sides, *radius, *rotation, *width, *arc, *color,
|
||||
);
|
||||
}
|
||||
DrawCommand::FilledPolygon { vertices, color } => {
|
||||
if vertices.len() >= 3 {
|
||||
draw_filled_polygon(vertices, *color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
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::Line {
|
||||
start,
|
||||
end,
|
||||
color,
|
||||
width,
|
||||
} => {
|
||||
draw_line(start.x, start.y, end.x, end.y, *width, *color);
|
||||
}
|
||||
DrawCommand::Circle {
|
||||
center,
|
||||
radius,
|
||||
color,
|
||||
filled,
|
||||
} => {
|
||||
if *filled {
|
||||
draw_circle(center.x, center.y, *radius, *color);
|
||||
} else {
|
||||
draw_circle_lines(center.x, center.y, *radius, 2.0, *color);
|
||||
}
|
||||
}
|
||||
DrawCommand::Arc {
|
||||
center,
|
||||
radius,
|
||||
rotation,
|
||||
arc,
|
||||
color,
|
||||
width,
|
||||
sides,
|
||||
} => {
|
||||
draw_arc(
|
||||
center.x, center.y, *sides, *radius, *rotation, *width, *arc, *color,
|
||||
);
|
||||
}
|
||||
DrawCommand::FilledPolygon { vertices, color } => {
|
||||
if vertices.len() >= 3 {
|
||||
draw_filled_polygon(vertices, *color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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::CircleLeft {
|
||||
radius,
|
||||
angle,
|
||||
steps,
|
||||
} => {
|
||||
// Draw arc segments from start to current position
|
||||
draw_tween_arc_left(tween, *radius, *angle, *steps);
|
||||
}
|
||||
crate::commands::TurtleCommand::CircleRight {
|
||||
radius,
|
||||
angle,
|
||||
steps,
|
||||
} => {
|
||||
// Draw arc segments from start to current position
|
||||
draw_tween_arc_right(tween, *radius, *angle, *steps);
|
||||
}
|
||||
_ 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 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::Forward(..) | TurtleCommand::Backward(..) | TurtleCommand::Goto(..)
|
||||
)
|
||||
}
|
||||
|
||||
/// Draw arc segments for circle_left tween animation
|
||||
fn draw_tween_arc_left(
|
||||
tween: &crate::tweening::CommandTween,
|
||||
radius: f32,
|
||||
total_angle: f32,
|
||||
steps: usize,
|
||||
) {
|
||||
let geom = CircleGeometry::new(
|
||||
tween.start_state.position,
|
||||
tween.start_state.heading,
|
||||
radius,
|
||||
CircleDirection::Left,
|
||||
);
|
||||
|
||||
// Debug: draw center
|
||||
draw_circle(geom.center.x, geom.center.y, 5.0, GRAY);
|
||||
|
||||
// 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) as f32;
|
||||
let t = (elapsed / tween.duration as f32).min(1.0);
|
||||
let progress = CubicInOut.tween(1.0, t); // 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);
|
||||
|
||||
// Adjust radius inward by half the line width so the line sits on the turtle's path
|
||||
let draw_radius = radius - tween.start_state.pen_width / 2.0;
|
||||
|
||||
// Draw the partial arc
|
||||
draw_arc(
|
||||
geom.center.x,
|
||||
geom.center.y,
|
||||
steps as u8,
|
||||
draw_radius,
|
||||
rotation_degrees,
|
||||
tween.start_state.pen_width,
|
||||
arc_degrees,
|
||||
tween.start_state.color,
|
||||
);
|
||||
}
|
||||
|
||||
/// Draw arc segments for circle_right tween animation
|
||||
fn draw_tween_arc_right(
|
||||
tween: &crate::tweening::CommandTween,
|
||||
radius: f32,
|
||||
total_angle: f32,
|
||||
steps: usize,
|
||||
) {
|
||||
let geom = CircleGeometry::new(
|
||||
tween.start_state.position,
|
||||
tween.start_state.heading,
|
||||
radius,
|
||||
CircleDirection::Right,
|
||||
);
|
||||
|
||||
// Debug: draw center
|
||||
draw_circle(geom.center.x, geom.center.y, 5.0, GRAY);
|
||||
|
||||
// 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) as f32;
|
||||
let t = (elapsed / tween.duration as f32).min(1.0);
|
||||
let progress = CubicInOut.tween(1.0, t); // 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);
|
||||
|
||||
// Adjust radius inward by half the line width so the line sits on the turtle's path
|
||||
let draw_radius = radius - tween.start_state.pen_width / 2.0;
|
||||
|
||||
// Draw the partial arc
|
||||
draw_arc(
|
||||
geom.center.x,
|
||||
geom.center.y,
|
||||
steps as u8,
|
||||
draw_radius,
|
||||
rotation_degrees,
|
||||
tween.start_state.pen_width,
|
||||
arc_degrees,
|
||||
tween.start_state.color,
|
||||
);
|
||||
}
|
||||
|
||||
/// 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 (now supports concave shapes via ear clipping)
|
||||
if rotated_vertices.len() >= 3 {
|
||||
let absolute_vertices: Vec<Vec2> = rotated_vertices
|
||||
.iter()
|
||||
.map(|v| turtle.position + *v)
|
||||
.collect();
|
||||
|
||||
draw_filled_polygon(&absolute_vertices, 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a filled polygon using triangulation
|
||||
fn draw_filled_polygon(vertices: &[Vec2], color: Color) {
|
||||
if vertices.len() < 3 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Flatten vertices into the format expected by earcutr: [x0, y0, x1, y1, ...]
|
||||
let flattened: Vec<f64> = vertices
|
||||
.iter()
|
||||
.flat_map(|v| vec![v.x as f64, v.y as f64])
|
||||
.collect();
|
||||
|
||||
// Triangulate using earcutr (no holes, 2 dimensions)
|
||||
match earcutr::earcut(&flattened, &[], 2) {
|
||||
Ok(indices) => {
|
||||
// Draw each triangle
|
||||
for triangle in indices.chunks(3) {
|
||||
if triangle.len() == 3 {
|
||||
let v0 = vertices[triangle[0]];
|
||||
let v1 = vertices[triangle[1]];
|
||||
let v2 = vertices[triangle[2]];
|
||||
draw_triangle(v0, v1, v2, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Fallback: if triangulation fails, try simple fan triangulation
|
||||
let first = vertices[0];
|
||||
for i in 1..vertices.len() - 1 {
|
||||
draw_triangle(first, vertices[i], vertices[i + 1], color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
402
turtle-lib-macroquad/src/execution.rs
Normal file
402
turtle-lib-macroquad/src/execution.rs
Normal file
@ -0,0 +1,402 @@
|
||||
//! Command execution logic
|
||||
|
||||
use crate::circle_geometry::{CircleDirection, CircleGeometry};
|
||||
use crate::commands::TurtleCommand;
|
||||
use crate::state::{DrawCommand, TurtleState, TurtleWorld};
|
||||
use macroquad::prelude::*;
|
||||
|
||||
/// Execute a single turtle command, updating state and adding draw commands
|
||||
pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world: &mut TurtleWorld) {
|
||||
match command {
|
||||
TurtleCommand::Forward(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 {
|
||||
world.add_command(DrawCommand::Line {
|
||||
start,
|
||||
end: state.position,
|
||||
color: state.color,
|
||||
width: state.pen_width,
|
||||
});
|
||||
// Add circle at end point for smooth line joins
|
||||
world.add_command(DrawCommand::Circle {
|
||||
center: state.position,
|
||||
radius: state.pen_width / 2.0,
|
||||
color: state.color,
|
||||
filled: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TurtleCommand::Backward(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 {
|
||||
world.add_command(DrawCommand::Line {
|
||||
start,
|
||||
end: state.position,
|
||||
color: state.color,
|
||||
width: state.pen_width,
|
||||
});
|
||||
// Add circle at end point for smooth line joins
|
||||
world.add_command(DrawCommand::Circle {
|
||||
center: state.position,
|
||||
radius: state.pen_width / 2.0,
|
||||
color: state.color,
|
||||
filled: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TurtleCommand::Left(degrees) => {
|
||||
state.heading -= degrees.to_radians();
|
||||
}
|
||||
|
||||
TurtleCommand::Right(degrees) => {
|
||||
state.heading += degrees.to_radians();
|
||||
}
|
||||
|
||||
TurtleCommand::CircleLeft {
|
||||
radius,
|
||||
angle,
|
||||
steps,
|
||||
} => {
|
||||
let start_heading = state.heading;
|
||||
let geom = CircleGeometry::new(
|
||||
state.position,
|
||||
start_heading,
|
||||
*radius,
|
||||
CircleDirection::Left,
|
||||
);
|
||||
|
||||
if state.pen_down {
|
||||
let (rotation_degrees, arc_degrees) = geom.draw_arc_params(*angle);
|
||||
|
||||
world.add_command(DrawCommand::Arc {
|
||||
center: geom.center,
|
||||
radius: *radius - state.pen_width, // Adjust radius for pen width to keep arc inside
|
||||
rotation: rotation_degrees,
|
||||
arc: arc_degrees,
|
||||
color: state.color,
|
||||
width: state.pen_width,
|
||||
sides: *steps as u8,
|
||||
});
|
||||
}
|
||||
|
||||
// Update turtle position and heading
|
||||
state.position = geom.position_at_angle(angle.to_radians());
|
||||
state.heading = start_heading - angle.to_radians();
|
||||
}
|
||||
|
||||
TurtleCommand::CircleRight {
|
||||
radius,
|
||||
angle,
|
||||
steps,
|
||||
} => {
|
||||
let start_heading = state.heading;
|
||||
let geom = CircleGeometry::new(
|
||||
state.position,
|
||||
start_heading,
|
||||
*radius,
|
||||
CircleDirection::Right,
|
||||
);
|
||||
|
||||
if state.pen_down {
|
||||
let (rotation_degrees, arc_degrees) = geom.draw_arc_params(*angle);
|
||||
|
||||
world.add_command(DrawCommand::Arc {
|
||||
center: geom.center,
|
||||
radius: *radius - state.pen_width, // Adjust radius for pen width to keep arc inside
|
||||
rotation: rotation_degrees,
|
||||
arc: arc_degrees,
|
||||
color: state.color,
|
||||
width: state.pen_width,
|
||||
sides: *steps as u8,
|
||||
});
|
||||
}
|
||||
|
||||
// Update turtle position and heading
|
||||
state.position = geom.position_at_angle(angle.to_radians());
|
||||
state.heading = start_heading + angle.to_radians();
|
||||
}
|
||||
|
||||
TurtleCommand::PenUp => {
|
||||
state.pen_down = false;
|
||||
}
|
||||
|
||||
TurtleCommand::PenDown => {
|
||||
state.pen_down = true;
|
||||
}
|
||||
|
||||
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::Goto(coord) => {
|
||||
let start = state.position;
|
||||
state.position = *coord;
|
||||
|
||||
if state.pen_down {
|
||||
world.add_command(DrawCommand::Line {
|
||||
start,
|
||||
end: state.position,
|
||||
color: state.color,
|
||||
width: state.pen_width,
|
||||
});
|
||||
// Add circle at end point for smooth line joins
|
||||
world.add_command(DrawCommand::Circle {
|
||||
center: state.position,
|
||||
radius: state.pen_width / 2.0,
|
||||
color: state.color,
|
||||
filled: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TurtleCommand::SetHeading(heading) => {
|
||||
state.heading = *heading;
|
||||
}
|
||||
|
||||
TurtleCommand::ShowTurtle => {
|
||||
state.visible = true;
|
||||
}
|
||||
|
||||
TurtleCommand::HideTurtle => {
|
||||
state.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute all commands immediately (no animation)
|
||||
pub fn execute_all_immediate(
|
||||
queue: &mut crate::commands::CommandQueue,
|
||||
state: &mut TurtleState,
|
||||
world: &mut TurtleWorld,
|
||||
) {
|
||||
while let Some(command) = queue.next() {
|
||||
execute_command(command, state, world);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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::Forward(_) | TurtleCommand::Backward(_) | TurtleCommand::Goto(_) => {
|
||||
if start_state.pen_down {
|
||||
world.add_command(DrawCommand::Line {
|
||||
start: start_state.position,
|
||||
end: end_state.position,
|
||||
color: start_state.color,
|
||||
width: start_state.pen_width,
|
||||
});
|
||||
// Add circle at end point for smooth line joins
|
||||
world.add_command(DrawCommand::Circle {
|
||||
center: end_state.position,
|
||||
radius: start_state.pen_width / 2.0,
|
||||
color: start_state.color,
|
||||
filled: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
TurtleCommand::CircleLeft {
|
||||
radius,
|
||||
angle,
|
||||
steps,
|
||||
} => {
|
||||
if start_state.pen_down {
|
||||
let geom = CircleGeometry::new(
|
||||
start_state.position,
|
||||
start_state.heading,
|
||||
*radius,
|
||||
CircleDirection::Left,
|
||||
);
|
||||
let (rotation_degrees, arc_degrees) = geom.draw_arc_params(*angle);
|
||||
|
||||
world.add_command(DrawCommand::Arc {
|
||||
center: geom.center,
|
||||
radius: *radius - start_state.pen_width / 2.0,
|
||||
rotation: rotation_degrees,
|
||||
arc: arc_degrees,
|
||||
color: start_state.color,
|
||||
width: start_state.pen_width,
|
||||
sides: *steps as u8,
|
||||
});
|
||||
|
||||
// Add endpoint circles for smooth joins
|
||||
world.add_command(DrawCommand::Circle {
|
||||
center: start_state.position,
|
||||
radius: start_state.pen_width / 2.0,
|
||||
color: start_state.color,
|
||||
filled: true,
|
||||
});
|
||||
world.add_command(DrawCommand::Circle {
|
||||
center: end_state.position,
|
||||
radius: start_state.pen_width / 2.0,
|
||||
color: start_state.color,
|
||||
filled: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
TurtleCommand::CircleRight {
|
||||
radius,
|
||||
angle,
|
||||
steps,
|
||||
} => {
|
||||
if start_state.pen_down {
|
||||
let geom = CircleGeometry::new(
|
||||
start_state.position,
|
||||
start_state.heading,
|
||||
*radius,
|
||||
CircleDirection::Right,
|
||||
);
|
||||
let (rotation_degrees, arc_degrees) = geom.draw_arc_params(*angle);
|
||||
|
||||
world.add_command(DrawCommand::Arc {
|
||||
center: geom.center,
|
||||
radius: *radius - start_state.pen_width / 2.0,
|
||||
rotation: rotation_degrees,
|
||||
arc: arc_degrees,
|
||||
color: start_state.color,
|
||||
width: start_state.pen_width,
|
||||
sides: *steps as u8,
|
||||
});
|
||||
|
||||
// Add endpoint circles for smooth joins
|
||||
world.add_command(DrawCommand::Circle {
|
||||
center: start_state.position,
|
||||
radius: start_state.pen_width / 2.0,
|
||||
color: start_state.color,
|
||||
filled: true,
|
||||
});
|
||||
world.add_command(DrawCommand::Circle {
|
||||
center: end_state.position,
|
||||
radius: start_state.pen_width / 2.0,
|
||||
color: start_state.color,
|
||||
filled: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// 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: 100,
|
||||
visible: true,
|
||||
shape: TurtleShape::turtle(),
|
||||
};
|
||||
|
||||
// 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::Forward(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::Left(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::Forward(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
|
||||
);
|
||||
}
|
||||
}
|
||||
24
turtle-lib-macroquad/src/general.rs
Normal file
24
turtle-lib-macroquad/src/general.rs
Normal file
@ -0,0 +1,24 @@
|
||||
//! 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;
|
||||
|
||||
/// Speed of animations (higher = faster, >= 999 = instant)
|
||||
pub type Speed = u32;
|
||||
|
||||
/// Color type re-export from macroquad
|
||||
pub use macroquad::color::Color;
|
||||
200
turtle-lib-macroquad/src/general/angle.rs
Normal file
200
turtle-lib-macroquad/src/general/angle.rs
Normal file
@ -0,0 +1,200 @@
|
||||
//! 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(i as Precision),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
pub fn degrees(value: Precision) -> Self {
|
||||
Self {
|
||||
value: AngleUnit::Degrees(value),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn radians(value: Precision) -> Self {
|
||||
Self {
|
||||
value: AngleUnit::Radians(value),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value(&self) -> Precision {
|
||||
match self.value {
|
||||
AngleUnit::Degrees(v) => v,
|
||||
AngleUnit::Radians(v) => v,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_radians(self) -> Self {
|
||||
match self.value {
|
||||
AngleUnit::Degrees(v) => Self::radians(v.to_radians()),
|
||||
AngleUnit::Radians(_) => self,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_degrees(self) -> Self {
|
||||
match self.value {
|
||||
AngleUnit::Degrees(_) => self,
|
||||
AngleUnit::Radians(v) => Self::degrees(v.to_degrees()),
|
||||
}
|
||||
}
|
||||
|
||||
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
turtle-lib-macroquad/src/general/length.rs
Normal file
24
turtle-lib-macroquad/src/general/length.rs
Normal 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(i as Precision)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
222
turtle-lib-macroquad/src/lib.rs
Normal file
222
turtle-lib-macroquad/src/lib.rs
Normal file
@ -0,0 +1,222 @@
|
||||
//! Turtle graphics library for Macroquad
|
||||
//!
|
||||
//! This library provides a turtle graphics API for creating drawings and animations
|
||||
//! using the Macroquad game framework.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```no_run
|
||||
//! use macroquad::prelude::*;
|
||||
//! use turtle_lib_macroquad::*;
|
||||
//!
|
||||
//! #[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(), 100.0);
|
||||
//!
|
||||
//! 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 tweening;
|
||||
|
||||
// Re-export commonly used types
|
||||
pub use builders::{CurvedMovement, DirectionalMovement, Turnable, TurtlePlan, WithCommands};
|
||||
pub use commands::{CommandQueue, TurtleCommand};
|
||||
pub use general::{Angle, Color, Coordinate, Length, Precision, Speed};
|
||||
pub use shapes::{ShapeType, TurtleShape};
|
||||
pub use state::{DrawCommand, TurtleState, TurtleWorld};
|
||||
pub use tweening::TweenController;
|
||||
|
||||
use macroquad::prelude::*;
|
||||
|
||||
/// Main turtle application struct
|
||||
pub struct TurtleApp {
|
||||
world: TurtleWorld,
|
||||
tween_controller: Option<TweenController>,
|
||||
mode: ExecutionMode,
|
||||
// Mouse panning state
|
||||
is_dragging: bool,
|
||||
last_mouse_pos: Option<Vec2>,
|
||||
// Zoom state
|
||||
zoom_level: f32,
|
||||
}
|
||||
|
||||
enum ExecutionMode {
|
||||
Immediate,
|
||||
Animated,
|
||||
}
|
||||
|
||||
impl TurtleApp {
|
||||
/// Create a new turtle application with default settings
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
world: TurtleWorld::new(),
|
||||
tween_controller: None,
|
||||
mode: ExecutionMode::Immediate,
|
||||
is_dragging: false,
|
||||
last_mouse_pos: None,
|
||||
zoom_level: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add commands to the turtle with specified speed
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `queue` - The command queue to execute
|
||||
/// * `speed` - Animation speed in pixels/sec (>= 999.0 = instant, 0.5-999.0 = animated)
|
||||
pub fn with_commands(mut self, queue: CommandQueue, speed: f32) -> Self {
|
||||
if speed <= 0.5 || speed.is_infinite() || speed.is_nan() {
|
||||
// Compiler error speed should be between 0.5 and 1000.0
|
||||
panic!("Speed must be greater than 0.5 and less than 1000.0");
|
||||
}
|
||||
if speed >= 999.0 {
|
||||
// Immediate mode - execute all commands instantly
|
||||
self.mode = ExecutionMode::Immediate;
|
||||
let mut state = TurtleState::default();
|
||||
let mut queue_mut = queue;
|
||||
execution::execute_all_immediate(&mut queue_mut, &mut state, &mut self.world);
|
||||
self.world.turtle = state;
|
||||
} else {
|
||||
// Animated mode - tween between states
|
||||
self.mode = ExecutionMode::Animated;
|
||||
self.tween_controller = Some(TweenController::new(queue, 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 {
|
||||
if let Some((completed_cmd, start_state)) = controller.update(&mut self.world.turtle) {
|
||||
// Copy end state before we borrow world mutably
|
||||
let end_state = self.world.turtle.clone();
|
||||
|
||||
// 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
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.tween_controller
|
||||
.as_ref()
|
||||
.map(|c| c.is_complete())
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
/// Get reference to the world state
|
||||
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_macroquad::*;
|
||||
///
|
||||
/// let mut turtle = create_turtle();
|
||||
/// turtle.forward(100.0).right(90.0).forward(50.0);
|
||||
/// let commands = turtle.build();
|
||||
/// ```
|
||||
pub fn create_turtle() -> TurtlePlan {
|
||||
TurtlePlan::new()
|
||||
}
|
||||
|
||||
/// Convenience function to get a turtle plan (alias for create_turtle)
|
||||
pub fn get_a_turtle() -> TurtlePlan {
|
||||
create_turtle()
|
||||
}
|
||||
162
turtle-lib-macroquad/src/shapes.rs
Normal file
162
turtle-lib-macroquad/src/shapes.rs
Normal file
@ -0,0 +1,162 @@
|
||||
//! 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
|
||||
pub fn new(vertices: Vec<Vec2>, filled: bool) -> Self {
|
||||
Self { vertices, filled }
|
||||
}
|
||||
|
||||
/// Get vertices rotated by the given angle
|
||||
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)
|
||||
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
|
||||
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
|
||||
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
|
||||
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)
|
||||
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)]
|
||||
pub enum ShapeType {
|
||||
Triangle,
|
||||
Turtle,
|
||||
Circle,
|
||||
Square,
|
||||
Arrow,
|
||||
}
|
||||
|
||||
impl ShapeType {
|
||||
/// Get the corresponding TurtleShape
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ShapeType {
|
||||
fn default() -> Self {
|
||||
ShapeType::Turtle
|
||||
}
|
||||
}
|
||||
113
turtle-lib-macroquad/src/state.rs
Normal file
113
turtle-lib-macroquad/src/state.rs
Normal file
@ -0,0 +1,113 @@
|
||||
//! Turtle state and world state management
|
||||
|
||||
use crate::general::{Angle, Color, Coordinate, Precision, Speed};
|
||||
use crate::shapes::TurtleShape;
|
||||
use macroquad::prelude::*;
|
||||
|
||||
/// 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: Speed,
|
||||
pub visible: bool,
|
||||
pub shape: TurtleShape,
|
||||
}
|
||||
|
||||
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: 100, // pixels per second
|
||||
visible: true,
|
||||
shape: TurtleShape::turtle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TurtleState {
|
||||
pub fn set_speed(&mut self, speed: Speed) {
|
||||
self.speed = speed.max(1);
|
||||
}
|
||||
|
||||
pub fn heading_angle(&self) -> Angle {
|
||||
Angle::radians(self.heading)
|
||||
}
|
||||
}
|
||||
|
||||
/// Drawable elements in the world
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DrawCommand {
|
||||
Line {
|
||||
start: Coordinate,
|
||||
end: Coordinate,
|
||||
color: Color,
|
||||
width: Precision,
|
||||
},
|
||||
Circle {
|
||||
center: Coordinate,
|
||||
radius: Precision,
|
||||
color: Color,
|
||||
filled: bool,
|
||||
},
|
||||
Arc {
|
||||
center: Coordinate,
|
||||
radius: Precision,
|
||||
rotation: Precision, // Start angle in degrees
|
||||
arc: Precision, // Arc extent in degrees
|
||||
color: Color,
|
||||
width: Precision,
|
||||
sides: u8, // Number of segments for quality
|
||||
},
|
||||
FilledPolygon {
|
||||
vertices: Vec<Coordinate>,
|
||||
color: Color,
|
||||
},
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
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
turtle-lib-macroquad/src/tweening.rs
Normal file
354
turtle-lib-macroquad/src/tweening.rs
Normal file
@ -0,0 +1,354 @@
|
||||
//! Tweening system for smooth animations
|
||||
|
||||
use crate::circle_geometry::{CircleDirection, CircleGeometry};
|
||||
use crate::commands::{CommandQueue, TurtleCommand};
|
||||
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: f32, // pixels per second (or degrees per second for rotations)
|
||||
}
|
||||
|
||||
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, f32, CubicInOut>,
|
||||
pub heading_tweener: Tweener<f32, f32, CubicInOut>,
|
||||
pub pen_width_tweener: Tweener<f32, f32, CubicInOut>,
|
||||
}
|
||||
|
||||
impl TweenController {
|
||||
pub fn new(queue: CommandQueue, speed: f32) -> Self {
|
||||
Self {
|
||||
queue,
|
||||
current_tween: None,
|
||||
speed: speed.max(1.0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_speed(&mut self, speed: f32) {
|
||||
self.speed = speed.max(1.0);
|
||||
}
|
||||
|
||||
/// Update the tween, returns (command, start_state) if command completed
|
||||
pub fn update(&mut self, state: &mut TurtleState) -> Option<(TurtleCommand, TurtleState)> {
|
||||
// Process current tween
|
||||
if let Some(ref mut tween) = self.current_tween {
|
||||
let elapsed = (get_time() - tween.start_time) as f32;
|
||||
|
||||
// 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::CircleLeft { radius, angle, .. } => {
|
||||
let angle_traveled = angle.to_radians() * progress;
|
||||
calculate_circle_left_position(
|
||||
tween.start_state.position,
|
||||
tween.start_state.heading,
|
||||
*radius,
|
||||
angle_traveled,
|
||||
)
|
||||
}
|
||||
TurtleCommand::CircleRight { radius, angle, .. } => {
|
||||
let angle_traveled = angle.to_radians() * progress;
|
||||
calculate_circle_right_position(
|
||||
tween.start_state.position,
|
||||
tween.start_state.heading,
|
||||
*radius,
|
||||
angle_traveled,
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
// 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 = match &tween.command {
|
||||
TurtleCommand::CircleLeft { angle, .. } => {
|
||||
tween.start_state.heading - angle.to_radians() * progress
|
||||
}
|
||||
TurtleCommand::CircleRight { angle, .. } => {
|
||||
tween.start_state.heading + angle.to_radians() * progress
|
||||
}
|
||||
TurtleCommand::Left(angle) => {
|
||||
tween.start_state.heading - angle.to_radians() * progress
|
||||
}
|
||||
TurtleCommand::Right(angle) => {
|
||||
tween.start_state.heading + angle.to_radians() * progress
|
||||
}
|
||||
TurtleCommand::SetHeading(_) | _ => {
|
||||
// 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 as f32).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() {
|
||||
// Tween complete, finalize state
|
||||
let start_state = tween.start_state.clone();
|
||||
*state = tween.target_state.clone();
|
||||
|
||||
// Return the completed command and start state to add draw commands
|
||||
let completed_command = tween.command.clone();
|
||||
self.current_tween = None;
|
||||
|
||||
// Only return command if it creates drawable elements
|
||||
if Self::command_creates_drawing(&completed_command) {
|
||||
return Some((completed_command, start_state));
|
||||
}
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
// Start next tween
|
||||
if let Some(command) = self.queue.next() {
|
||||
let command_clone = command.clone();
|
||||
let speed = state.speed; // Extract speed before borrowing self
|
||||
let duration = self.calculate_duration(&command_clone, 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 as f32,
|
||||
CubicInOut,
|
||||
);
|
||||
|
||||
let heading_tweener = Tweener::new(
|
||||
0.0, // We'll handle angle wrapping separately
|
||||
1.0,
|
||||
duration as f32,
|
||||
CubicInOut,
|
||||
);
|
||||
|
||||
let pen_width_tweener = Tweener::new(
|
||||
state.pen_width,
|
||||
target_state.pen_width,
|
||||
duration as f32,
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
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::Forward(_)
|
||||
| TurtleCommand::Backward(_)
|
||||
| TurtleCommand::CircleLeft { .. }
|
||||
| TurtleCommand::CircleRight { .. }
|
||||
| TurtleCommand::Goto(_)
|
||||
)
|
||||
}
|
||||
|
||||
fn calculate_duration(&self, command: &TurtleCommand, speed: u32) -> f64 {
|
||||
let speed = speed.max(1) as f32;
|
||||
|
||||
let base_time = match command {
|
||||
TurtleCommand::Forward(dist) | TurtleCommand::Backward(dist) => dist.abs() / speed,
|
||||
TurtleCommand::Left(angle) | TurtleCommand::Right(angle) => {
|
||||
// Rotation speed: assume 180 degrees per second at speed 100
|
||||
angle.abs() / (speed * 1.8)
|
||||
}
|
||||
TurtleCommand::CircleLeft { radius, angle, .. }
|
||||
| TurtleCommand::CircleRight { radius, angle, .. } => {
|
||||
let arc_length = radius * angle.to_radians().abs();
|
||||
arc_length / speed
|
||||
}
|
||||
TurtleCommand::Goto(_target) => {
|
||||
// Calculate distance (handled in calculate_target_state)
|
||||
0.1 // Placeholder, will be calculated properly
|
||||
}
|
||||
_ => 0.0, // Instant commands
|
||||
};
|
||||
base_time.max(0.01) as f64 // Minimum duration
|
||||
}
|
||||
|
||||
fn calculate_target_state(
|
||||
&self,
|
||||
current: &TurtleState,
|
||||
command: &TurtleCommand,
|
||||
) -> TurtleState {
|
||||
let mut target = current.clone();
|
||||
|
||||
match command {
|
||||
TurtleCommand::Forward(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::Backward(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::Left(angle) => {
|
||||
target.heading -= angle.to_radians();
|
||||
}
|
||||
TurtleCommand::Right(angle) => {
|
||||
target.heading += angle.to_radians();
|
||||
}
|
||||
TurtleCommand::CircleLeft { radius, angle, .. } => {
|
||||
// Use helper function to calculate final position
|
||||
target.position = calculate_circle_left_position(
|
||||
current.position,
|
||||
current.heading,
|
||||
*radius,
|
||||
angle.to_radians(),
|
||||
);
|
||||
target.heading = current.heading - angle.to_radians();
|
||||
}
|
||||
TurtleCommand::CircleRight { radius, angle, .. } => {
|
||||
// Use helper function to calculate final position
|
||||
target.position = calculate_circle_right_position(
|
||||
current.position,
|
||||
current.heading,
|
||||
*radius,
|
||||
angle.to_radians(),
|
||||
);
|
||||
target.heading = current.heading + angle.to_radians();
|
||||
}
|
||||
TurtleCommand::Goto(coord) => {
|
||||
target.position = *coord;
|
||||
}
|
||||
TurtleCommand::SetHeading(heading) => {
|
||||
target.heading = *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;
|
||||
}
|
||||
}
|
||||
|
||||
target
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate position on a circular arc for circle_left
|
||||
fn calculate_circle_left_position(
|
||||
start_pos: Vec2,
|
||||
start_heading: f32,
|
||||
radius: f32,
|
||||
angle_traveled: f32, // How much of the total angle we've traveled (in radians)
|
||||
) -> Vec2 {
|
||||
let geom = CircleGeometry::new(start_pos, start_heading, radius, CircleDirection::Left);
|
||||
geom.position_at_angle(angle_traveled)
|
||||
}
|
||||
|
||||
/// Calculate position on a circular arc for circle_right
|
||||
fn calculate_circle_right_position(
|
||||
start_pos: Vec2,
|
||||
start_heading: f32,
|
||||
radius: f32,
|
||||
angle_traveled: f32, // How much of the total angle we've traveled (in radians)
|
||||
) -> Vec2 {
|
||||
let geom = CircleGeometry::new(start_pos, start_heading, radius, CircleDirection::Right);
|
||||
geom.position_at_angle(angle_traveled)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user