Implement tessellation for turtle graphics with fill support
- Added tessellation module to handle path tessellation using Lyon. - Updated execution logic to record fill vertices and manage fill contours. - Integrated tessellation into command execution for lines, arcs, and filled shapes. - Enhanced TurtleState to track fill state and contours. - Modified TweenController to handle fill commands and update drawing commands accordingly. - Improved debug output for fill operations and tessellation processes.
This commit is contained in:
parent
c3f5136359
commit
00b9007f00
357
README.md
Normal file
357
README.md
Normal file
@ -0,0 +1,357 @@
|
|||||||
|
# Turtle Graphics Library
|
||||||
|
|
||||||
|
A modern turtle graphics library for Rust built on [Macroquad](https://macroquad.rs/) with [Lyon](https://github.com/nical/lyon) for high-quality GPU-accelerated rendering.
|
||||||
|
|
||||||
|
## Project Status
|
||||||
|
|
||||||
|
✅ **Stable** - Complete Lyon integration with multi-contour fill system and live animation preview.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🎨 **Simple Builder API**: Chain commands like `forward(100).right(90)`
|
||||||
|
- ⚡ **Smooth Animations**: Tweening support with easing functions and live fill preview
|
||||||
|
- 🚀 **Instant Mode**: Execute commands immediately without animation (speed ≥ 999)
|
||||||
|
- 🎯 **High-Quality Rendering**: Complete Lyon tessellation pipeline with GPU acceleration
|
||||||
|
- 🕳️ **Multi-Contour Fills**: Automatic hole detection with EvenOdd fill rule - draw cheese with holes!
|
||||||
|
- 📐 **Self-Intersecting Paths**: Stars, complex shapes - all handled correctly
|
||||||
|
- 🐢 **Multiple Turtle Shapes**: Triangle, classic turtle, circle, square, arrow, and custom shapes
|
||||||
|
- 💨 **Lightweight**: Fast compilation and runtime
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use macroquad::prelude::*;
|
||||||
|
use turtle_lib_macroquad::*;
|
||||||
|
|
||||||
|
#[macroquad::main("Turtle")]
|
||||||
|
async fn main() {
|
||||||
|
// Create a turtle plan
|
||||||
|
let mut plan = create_turtle();
|
||||||
|
|
||||||
|
// Set speed (part of the plan)
|
||||||
|
plan.set_speed(100);
|
||||||
|
|
||||||
|
// Draw a square
|
||||||
|
for _ in 0..4 {
|
||||||
|
plan.forward(100).right(90);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create app (speed is managed by commands)
|
||||||
|
let mut app = TurtleApp::new().with_commands(plan.build());
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Filling (with automatic hole detection)
|
||||||
|
plan.set_fill_color(BLUE);
|
||||||
|
plan.begin_fill();
|
||||||
|
// ... draw shape ...
|
||||||
|
plan.end_fill(); // Auto-closes and applies fill
|
||||||
|
|
||||||
|
// Appearance
|
||||||
|
plan.set_color(RED);
|
||||||
|
plan.set_pen_width(5.0);
|
||||||
|
plan.hide();
|
||||||
|
plan.show();
|
||||||
|
|
||||||
|
// Speed control (dynamic)
|
||||||
|
plan.set_speed(100); // Animated mode (< 999)
|
||||||
|
plan.set_speed(1000); // Instant mode (>= 999)
|
||||||
|
|
||||||
|
// Turtle shapes
|
||||||
|
plan.shape(ShapeType::Triangle);
|
||||||
|
plan.shape(ShapeType::Turtle); // Classic turtle shape
|
||||||
|
plan.shape(ShapeType::Circle);
|
||||||
|
plan.shape(ShapeType::Square);
|
||||||
|
plan.shape(ShapeType::Arrow);
|
||||||
|
|
||||||
|
// Custom shapes
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Method chaining
|
||||||
|
plan.forward(100).right(90).forward(50);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Execution Modes
|
||||||
|
|
||||||
|
Speed is now controlled via commands, allowing dynamic switching during execution:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let mut plan = create_turtle();
|
||||||
|
|
||||||
|
// Fast initial positioning (instant mode)
|
||||||
|
plan.set_speed(1000);
|
||||||
|
plan.pen_up();
|
||||||
|
plan.goto(vec2(-100.0, -100.0));
|
||||||
|
|
||||||
|
// Slow animated drawing
|
||||||
|
plan.set_speed(50);
|
||||||
|
plan.pen_down();
|
||||||
|
plan.forward(200);
|
||||||
|
plan.right(90);
|
||||||
|
|
||||||
|
// Create app (no speed parameter needed)
|
||||||
|
let app = TurtleApp::new().with_commands(plan.build());
|
||||||
|
```
|
||||||
|
|
||||||
|
**Speed Modes:**
|
||||||
|
- **Speed < 999**: Animated mode with smooth tweening
|
||||||
|
- **Speed >= 999**: Instant mode (no animation)
|
||||||
|
- Default speed is 100.0 if not specified
|
||||||
|
|
||||||
|
### 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
|
||||||
|
# From turtle-lib-macroquad directory
|
||||||
|
cargo run --example square
|
||||||
|
cargo run --example koch
|
||||||
|
cargo run --example shapes
|
||||||
|
cargo run --example yinyang
|
||||||
|
cargo run --example stern
|
||||||
|
cargo run --example nikolaus
|
||||||
|
|
||||||
|
# Lyon proof-of-concept examples
|
||||||
|
cargo run --package turtle-lyon-poc --example yinyang --release
|
||||||
|
cargo run --package turtle-lyon-poc --example basic_shapes --release
|
||||||
|
cargo run --package turtle-lyon-poc --example fill_comparison --release
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Examples
|
||||||
|
|
||||||
|
#### Basic Drawing
|
||||||
|
- **square.rs**: Basic square drawing
|
||||||
|
- **koch.rs**: Koch snowflake fractal
|
||||||
|
- **shapes.rs**: Demonstrates different turtle shapes
|
||||||
|
- **stern.rs**: Star pattern drawing
|
||||||
|
- **nikolaus.rs**: Nikolaus (Santa) drawing
|
||||||
|
|
||||||
|
#### Fill Examples
|
||||||
|
- **yinyang.rs**: Yin-yang symbol with automatic hole detection
|
||||||
|
- **fill_demo.rs**: Donut shape with hole
|
||||||
|
- **fill_requirements.rs**: Circle with red fill
|
||||||
|
- **fill_advanced.rs**: Complex shapes (star, swiss cheese, multiple holes)
|
||||||
|
- **fill_circle_test.rs**: Circle fills with different angles
|
||||||
|
- **fill_instant_test.rs**: Quick fill test in instant mode
|
||||||
|
|
||||||
|
## Why Lyon?
|
||||||
|
|
||||||
|
- Automatic hole detection via EvenOdd fill rule
|
||||||
|
- GPU-accelerated rendering
|
||||||
|
- Standards-compliant (matches SVG, HTML Canvas)
|
||||||
|
- Handles any self-intersecting path
|
||||||
|
|
||||||
|
### Basic Fill
|
||||||
|
```rust
|
||||||
|
let mut plan = create_turtle();
|
||||||
|
plan.set_fill_color(RED);
|
||||||
|
plan.begin_fill();
|
||||||
|
|
||||||
|
// Draw shape
|
||||||
|
for _ in 0..4 {
|
||||||
|
plan.forward(100);
|
||||||
|
plan.right(90);
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.end_fill(); // Auto-closes and fills
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fill with Holes (Multi-Contour)
|
||||||
|
```rust
|
||||||
|
plan.set_fill_color(BLUE);
|
||||||
|
plan.begin_fill();
|
||||||
|
|
||||||
|
// Outer circle (first contour)
|
||||||
|
plan.circle_left(90.0, 360.0, 72);
|
||||||
|
|
||||||
|
// pen_up() closes current contour
|
||||||
|
plan.pen_up();
|
||||||
|
plan.goto(vec2(0.0, -30.0));
|
||||||
|
|
||||||
|
// pen_down() starts new contour
|
||||||
|
plan.pen_down();
|
||||||
|
|
||||||
|
// Inner circle (becomes a hole automatically with EvenOdd rule!)
|
||||||
|
plan.circle_left(30.0, 360.0, 36);
|
||||||
|
|
||||||
|
plan.end_fill(); // Auto-detects holes and fills correctly
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fill Features
|
||||||
|
|
||||||
|
- ✅ **Live Preview** - See fills progressively during animation
|
||||||
|
- ✅ **Auto-Close** - Automatically connects end point to start on `end_fill()`
|
||||||
|
- ✅ **Multi-Contour** - `pen_up()` closes contour, `pen_down()` opens next one
|
||||||
|
- ✅ **Automatic Hole Detection** - EvenOdd fill rule handles any complexity
|
||||||
|
- ✅ **Self-Intersecting Paths** - Stars and complex shapes work perfectly
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Module Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
turtle-lib-macroquad/src/
|
||||||
|
├── lib.rs - Public API and TurtleApp
|
||||||
|
├── state.rs - TurtleState and TurtleWorld
|
||||||
|
├── commands.rs - TurtleCommand enum (consolidated commands)
|
||||||
|
├── builders.rs - Builder traits (DirectionalMovement, Turnable, etc.)
|
||||||
|
├── execution.rs - Command execution with fill support
|
||||||
|
├── tweening.rs - Animation/tweening controller with dynamic speed
|
||||||
|
├── drawing.rs - Rendering with Lyon tessellation
|
||||||
|
├── shapes.rs - Turtle shape definitions
|
||||||
|
├── tessellation.rs - Lyon tessellation utilities
|
||||||
|
├── circle_geometry.rs - Circle arc calculations
|
||||||
|
└── general/ - Type definitions (Angle, Length, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Design Principles
|
||||||
|
|
||||||
|
- **State Management**: Clean separation between turtle state and world state
|
||||||
|
- **Command Queue**: Commands queued and executed with optional tweening
|
||||||
|
- **Consolidated Commands**: Unified commands reduce duplication (Move, Turn, Circle)
|
||||||
|
- **Dynamic Speed Control**: Speed managed via SetSpeed commands for flexibility
|
||||||
|
- **Tweening System**: Smooth interpolation with easing functions
|
||||||
|
- **Unified Lyon Rendering**: All drawing operations use GPU-accelerated Lyon tessellation
|
||||||
|
- Lines, arcs, circles, fills - single high-quality rendering pipeline
|
||||||
|
- ~410 lines of code eliminated through architectural simplification
|
||||||
|
- Consistent quality across all primitives
|
||||||
|
|
||||||
|
### Command Consolidation
|
||||||
|
|
||||||
|
The library uses consolidated commands to reduce code duplication:
|
||||||
|
|
||||||
|
- **Move(distance)**: Replaces separate Forward/Backward (negative = backward)
|
||||||
|
- **Turn(angle)**: Replaces separate Left/Right (negative = left, positive = right)
|
||||||
|
- **Circle{direction, ...}**: Unified circle command with CircleDirection enum
|
||||||
|
|
||||||
|
This design eliminates ~250 lines of duplicate code while maintaining the same user-facing API.
|
||||||
|
|
||||||
|
## Workspace Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
turtle/
|
||||||
|
├── turtle-lib-macroquad/ - Main library (Macroquad + Lyon)
|
||||||
|
├── turtle-lib/ - Legacy Bevy-based implementation
|
||||||
|
└── turtle-example/ - Legacy examples
|
||||||
|
```
|
||||||
|
|
||||||
|
The `turtle-lib-macroquad` package is the current and future focus of development.
|
||||||
|
|
||||||
|
## Building and Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check all packages
|
||||||
|
cargo check
|
||||||
|
|
||||||
|
# Run specific example
|
||||||
|
cargo run --package turtle-lib-macroquad --example yinyang
|
||||||
|
|
||||||
|
# Build release version
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
# Run Lyon POC examples to see future rendering
|
||||||
|
cargo run --package turtle-lyon-poc --example yinyang --release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Status
|
||||||
|
|
||||||
|
### ✅ Completed
|
||||||
|
- Complete Lyon integration for all drawing primitives
|
||||||
|
- Multi-contour fill system with automatic hole detection
|
||||||
|
- Turtle movement and rotation (consolidated Move/Turn commands)
|
||||||
|
- Circle arcs (left/right with unified Circle command)
|
||||||
|
- Pen control (up/down) with contour management
|
||||||
|
- Color and pen width
|
||||||
|
- Multiple turtle shapes with custom shape support
|
||||||
|
- Tweening system with easing functions
|
||||||
|
- Dynamic speed control via SetSpeed commands
|
||||||
|
- Instant mode (speed ≥ 999) and animated mode (speed < 999)
|
||||||
|
- **EvenOdd fill rule** for complex self-intersecting paths
|
||||||
|
- **Live fill preview** during animation with progressive rendering
|
||||||
|
- **Multi-contour support** - pen_up/pen_down manage contours
|
||||||
|
- **Command consolidation** (~250 lines eliminated)
|
||||||
|
- **Full Lyon migration** (~410 total lines eliminated)
|
||||||
|
|
||||||
|
### 🎯 Future Possibilities
|
||||||
|
- Advanced stroke styling (caps, joins, dashing)
|
||||||
|
- Bezier curves and custom path primitives
|
||||||
|
- Additional examples and tutorials
|
||||||
|
|
||||||
|
## What's New
|
||||||
|
|
||||||
|
### Complete Lyon Migration ✨
|
||||||
|
All drawing operations now use GPU-accelerated Lyon tessellation:
|
||||||
|
- **Unified pipeline**: Lines, arcs, circles, and fills - all use the same high-quality rendering
|
||||||
|
- **Simplified codebase**: ~410 lines of code eliminated
|
||||||
|
- **Better performance**: GPU tessellation is faster than CPU-based primitives
|
||||||
|
- **Consistent quality**: No more mixed rendering approaches
|
||||||
|
|
||||||
|
### Multi-Contour Fill System 🕳️
|
||||||
|
Advanced fill capabilities with automatic hole detection:
|
||||||
|
- **EvenOdd fill rule**: Draw shapes with holes - works like SVG and HTML Canvas
|
||||||
|
- **Pen state management**: `pen_up()` closes contour, `pen_down()` opens next
|
||||||
|
- **Live preview**: See fills progressively during animations
|
||||||
|
- **Self-intersecting paths**: Stars, complex shapes - all handled correctly
|
||||||
|
|
||||||
|
### Architectural Improvements 🏗️
|
||||||
|
- **Command consolidation**: Unified Move/Turn/Circle commands (~250 lines eliminated)
|
||||||
|
- **Dynamic speed control**: Change speed during execution via commands
|
||||||
|
- **Live animation preview**: Progressive fill rendering during circle/arc drawing
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT OR Apache-2.0
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! The library now has a stable foundation with complete Lyon integration and multi-contour fill support.
|
||||||
@ -1,13 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "turtle-lib-macroquad"
|
name = "turtle-lib-macroquad"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
macroquad = "0.4"
|
macroquad = "0.4"
|
||||||
earcutr = "0.5"
|
|
||||||
tween = "2.1.0"
|
tween = "2.1.0"
|
||||||
|
lyon = "1.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
# For examples and testing
|
# For examples and testing
|
||||||
|
|||||||
@ -1,229 +1,56 @@
|
|||||||
# Turtle Graphics Library for Macroquad
|
# turtle-lib-macroquad
|
||||||
|
|
||||||
A turtle graphics library built on [Macroquad](https://macroquad.rs/), providing an intuitive API for creating drawings and animations.
|
The main turtle graphics library built on Macroquad with Lyon tessellation.
|
||||||
|
|
||||||
|
**See the [main README](../README.md) for complete documentation.**
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Simple Builder API**: Chain commands like `forward(100).right(90)`
|
✅ **Complete Lyon Integration** - All drawing operations use GPU-optimized tessellation
|
||||||
- **Smooth Animations**: Tweening support with easing functions
|
- Unified rendering pipeline for lines, arcs, circles, and fills
|
||||||
- **Instant Mode**: Execute commands immediately without animation (speed > 999.0)
|
- ~410 lines of code eliminated through architectural simplification
|
||||||
- **Lightweight**: Fast compilation (~30-60 seconds from clean build)
|
- Consistent high-quality rendering across all primitives
|
||||||
- **Macroquad Integration**: Built on the simple and fast Macroquad framework
|
|
||||||
|
|
||||||
## Quick Start
|
✅ **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
|
||||||
|
|
||||||
Add to your `Cargo.toml`:
|
✅ **Smooth Animation** - Tweening system with live rendering
|
||||||
```toml
|
- Configurable speed control
|
||||||
[dependencies]
|
- Frame-rate independent animation
|
||||||
turtle-lib-macroquad = { path = "../turtle-lib-macroquad" }
|
- Live fill preview during circle/arc drawing
|
||||||
macroquad = "0.4"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Basic Example
|
## Quick Examples
|
||||||
|
|
||||||
```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
|
```bash
|
||||||
cargo run --example square
|
# Run from this directory
|
||||||
cargo run --example koch
|
cargo run --example square # Basic square drawing
|
||||||
cargo run --example shapes
|
cargo run --example yinyang # Multi-contour fills with holes
|
||||||
cargo run --example yinyang
|
cargo run --example fill_advanced # Self-intersecting fills
|
||||||
cargo run --example stern
|
cargo run --example koch # Recursive fractals
|
||||||
cargo run --example nikolaus
|
cargo run --example fill_demo # Multiple independent fills
|
||||||
```
|
```
|
||||||
|
|
||||||
### Available Examples
|
## Architecture Highlights
|
||||||
|
|
||||||
- **square.rs**: Basic square drawing
|
### Rendering Pipeline
|
||||||
- **koch.rs**: Koch snowflake fractal
|
All drawing operations → Lyon tessellation → GPU mesh rendering
|
||||||
- **shapes.rs**: Demonstrates different turtle shapes
|
|
||||||
- **yinyang.rs**: Yin-yang symbol drawing
|
|
||||||
- **stern.rs**: Star pattern drawing
|
|
||||||
- **nikolaus.rs**: Nikolaus (Santa) drawing
|
|
||||||
|
|
||||||
## Turtle Shapes
|
### DrawCommand Enum
|
||||||
|
Simplified from 5 variants to 1:
|
||||||
|
- `Mesh(MeshData)` - unified variant for all drawing operations
|
||||||
|
|
||||||
The library supports multiple turtle shapes that can be changed during drawing:
|
### Fill System
|
||||||
|
- `FillState` tracks multiple contours (completed + current)
|
||||||
|
- Pen state management automatically handles contour creation
|
||||||
|
- EvenOdd tessellation provides automatic hole detection
|
||||||
|
|
||||||
### Built-in Shapes
|
See [LYON_COMPLETE.md](LYON_COMPLETE.md) and [MULTI_CONTOUR_FILLS.md](MULTI_CONTOUR_FILLS.md) for implementation details.
|
||||||
|
|
||||||
- **Triangle** (default): Simple arrow shape
|
## Status
|
||||||
- **Turtle**: Classic turtle shape with detailed outline
|
|
||||||
- **Circle**: Circular shape
|
|
||||||
- **Square**: Square shape
|
|
||||||
- **Arrow**: Arrow-like shape
|
|
||||||
|
|
||||||
### Using Shapes
|
✅ **Stable** - Lyon integration complete, multi-contour fills working, all examples passing.
|
||||||
|
|
||||||
```rust
|
See [../README.md](../README.md) for full API documentation and project status.
|
||||||
// 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
|
|
||||||
|
|||||||
97
turtle-lib-macroquad/examples/cheese.rs
Normal file
97
turtle-lib-macroquad/examples/cheese.rs
Normal 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::*;
|
||||||
|
|
||||||
|
#[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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,7 +10,7 @@ async fn main() {
|
|||||||
plan.shape(ShapeType::Turtle);
|
plan.shape(ShapeType::Turtle);
|
||||||
|
|
||||||
// Draw some circles
|
// Draw some circles
|
||||||
plan.set_color(RED);
|
plan.set_pen_color(RED);
|
||||||
plan.set_pen_width(0.5);
|
plan.set_pen_width(0.5);
|
||||||
plan.left(90.0);
|
plan.left(90.0);
|
||||||
plan.set_speed(999);
|
plan.set_speed(999);
|
||||||
@ -18,14 +18,16 @@ async fn main() {
|
|||||||
|
|
||||||
plan.forward(150.0);
|
plan.forward(150.0);
|
||||||
plan.set_speed(100);
|
plan.set_speed(100);
|
||||||
plan.set_color(BLUE);
|
plan.set_pen_color(BLUE);
|
||||||
plan.circle_right(50.0, 270.0, 72); // partial circle to the right
|
plan.circle_right(50.0, 270.0, 72); // partial circle to the right
|
||||||
// Set animation speed
|
// Set animation speed
|
||||||
plan.set_speed(20);
|
plan.set_speed(20);
|
||||||
plan.forward(150.0);
|
plan.forward(150.0);
|
||||||
|
plan.circle_left(50.0, 180.0, 12);
|
||||||
|
plan.circle_right(50.0, 180.0, 12);
|
||||||
|
|
||||||
plan.set_speed(700);
|
plan.set_speed(700);
|
||||||
plan.set_color(GREEN);
|
plan.set_pen_color(GREEN);
|
||||||
plan.circle_left(50.0, 180.0, 36); // Half circle to the left
|
plan.circle_left(50.0, 180.0, 36); // Half circle to the left
|
||||||
|
|
||||||
// Create turtle app with animation (speed = 100 pixels/sec)
|
// Create turtle app with animation (speed = 100 pixels/sec)
|
||||||
|
|||||||
157
turtle-lib-macroquad/examples/fill_advanced.rs
Normal file
157
turtle-lib-macroquad/examples/fill_advanced.rs
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
//! Advanced fill example with multiple holes and complex shapes
|
||||||
|
|
||||||
|
use macroquad::{miniquad::window::set_window_size, prelude::*};
|
||||||
|
use turtle_lib_macroquad::*;
|
||||||
|
|
||||||
|
#[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
|
||||||
|
}
|
||||||
|
}
|
||||||
55
turtle-lib-macroquad/examples/fill_demo.rs
Normal file
55
turtle-lib-macroquad/examples/fill_demo.rs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
//! Fill demonstration with holes
|
||||||
|
|
||||||
|
use macroquad::prelude::*;
|
||||||
|
use turtle_lib_macroquad::*;
|
||||||
|
|
||||||
|
#[macroquad::main("Fill Demo")]
|
||||||
|
async fn main() {
|
||||||
|
let mut t = create_turtle();
|
||||||
|
|
||||||
|
// Example from requirements: circle with hole (like a donut)
|
||||||
|
t.set_pen_color(BLUE);
|
||||||
|
t.set_pen_width(3.0);
|
||||||
|
t.right(90.0);
|
||||||
|
|
||||||
|
// Set fill color and begin fill
|
||||||
|
t.set_fill_color(RED);
|
||||||
|
t.begin_fill();
|
||||||
|
|
||||||
|
// Outer circle
|
||||||
|
t.circle_right(150.0, 360.0, 72);
|
||||||
|
|
||||||
|
// Move to start of inner circle (hole)
|
||||||
|
// pen_up doesn't matter for fill - vertices still recorded!
|
||||||
|
t.pen_up();
|
||||||
|
t.forward(50.0);
|
||||||
|
t.pen_down();
|
||||||
|
|
||||||
|
// Inner circle (creates a hole)
|
||||||
|
t.circle_right(150.0, 360.0, 72);
|
||||||
|
|
||||||
|
t.end_fill();
|
||||||
|
|
||||||
|
// Draw a square with no fill
|
||||||
|
t.pen_up();
|
||||||
|
t.forward(100.0);
|
||||||
|
t.pen_down();
|
||||||
|
t.set_pen_color(GREEN);
|
||||||
|
|
||||||
|
for _ in 0..4 {
|
||||||
|
t.forward(100.0);
|
||||||
|
t.right(90.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set animation speed
|
||||||
|
t.set_speed(100);
|
||||||
|
|
||||||
|
let mut app = TurtleApp::new().with_commands(t.build());
|
||||||
|
|
||||||
|
loop {
|
||||||
|
clear_background(WHITE);
|
||||||
|
app.update();
|
||||||
|
app.render();
|
||||||
|
next_frame().await
|
||||||
|
}
|
||||||
|
}
|
||||||
66
turtle-lib-macroquad/examples/fill_requirements.rs
Normal file
66
turtle-lib-macroquad/examples/fill_requirements.rs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
//! Example matching the original requirements exactly
|
||||||
|
|
||||||
|
use macroquad::prelude::*;
|
||||||
|
use turtle_lib_macroquad::*;
|
||||||
|
|
||||||
|
#[macroquad::main("Fill Example - Original Requirements")]
|
||||||
|
async fn main() {
|
||||||
|
let mut turtle = create_turtle();
|
||||||
|
|
||||||
|
turtle.right(90.0);
|
||||||
|
turtle.set_pen_width(3.0);
|
||||||
|
turtle.set_speed(900);
|
||||||
|
|
||||||
|
turtle.set_pen_color(BLUE);
|
||||||
|
turtle.set_fill_color(RED);
|
||||||
|
turtle.begin_fill();
|
||||||
|
|
||||||
|
turtle.circle_left(100.0, 360.0, 16);
|
||||||
|
|
||||||
|
// Draw a circle (36 small steps)
|
||||||
|
for _ in 0..36 {
|
||||||
|
turtle.forward(5.0);
|
||||||
|
turtle.right(10.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
turtle.end_fill();
|
||||||
|
|
||||||
|
// Draw a square with no fill
|
||||||
|
turtle.set_pen_color(GREEN);
|
||||||
|
turtle.forward(120.0);
|
||||||
|
for _ in 0..3 {
|
||||||
|
turtle.right(90.0);
|
||||||
|
turtle.forward(240.0);
|
||||||
|
}
|
||||||
|
turtle.right(90.0);
|
||||||
|
turtle.forward(120.0);
|
||||||
|
|
||||||
|
// Set speed for animation
|
||||||
|
turtle.set_speed(200);
|
||||||
|
|
||||||
|
let mut app = TurtleApp::new().with_commands(turtle.build());
|
||||||
|
|
||||||
|
loop {
|
||||||
|
clear_background(WHITE);
|
||||||
|
app.update();
|
||||||
|
app.render();
|
||||||
|
|
||||||
|
// Instructions
|
||||||
|
draw_text(
|
||||||
|
"Fill Example - Circle filled with red, square not filled",
|
||||||
|
10.0,
|
||||||
|
20.0,
|
||||||
|
20.0,
|
||||||
|
BLACK,
|
||||||
|
);
|
||||||
|
draw_text(
|
||||||
|
"Mouse: drag to pan, scroll to zoom",
|
||||||
|
10.0,
|
||||||
|
40.0,
|
||||||
|
16.0,
|
||||||
|
DARKGRAY,
|
||||||
|
);
|
||||||
|
|
||||||
|
next_frame().await
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,8 +7,10 @@ use turtle_lib_macroquad::*;
|
|||||||
async fn main() {
|
async fn main() {
|
||||||
// Create a turtle plan
|
// Create a turtle plan
|
||||||
let mut t = create_turtle();
|
let mut t = create_turtle();
|
||||||
|
t.set_speed(900);
|
||||||
|
|
||||||
t.circle_left(90.0, 180.0, 36);
|
t.circle_left(90.0, 180.0, 36);
|
||||||
|
t.begin_fill();
|
||||||
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_left(45.0, 180.0, 26);
|
||||||
t.circle_right(45.0, 180.0, 26);
|
t.circle_right(45.0, 180.0, 26);
|
||||||
@ -24,6 +26,7 @@ async fn main() {
|
|||||||
t.left(90.0);
|
t.left(90.0);
|
||||||
t.pen_down();
|
t.pen_down();
|
||||||
t.circle_right(8.0, 360.0, 12);
|
t.circle_right(8.0, 360.0, 12);
|
||||||
|
t.end_fill();
|
||||||
|
|
||||||
// Set animation speed
|
// Set animation speed
|
||||||
t.set_speed(1000);
|
t.set_speed(1000);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
//! Builder pattern traits for creating turtle command sequences
|
//! Builder pattern traits for creating turtle command sequences
|
||||||
|
|
||||||
use crate::commands::{CommandQueue, TurtleCommand};
|
use crate::commands::{CommandQueue, TurtleCommand};
|
||||||
use crate::general::{AnimationSpeed, Color, Precision};
|
use crate::general::{AnimationSpeed, Color, Coordinate, Precision};
|
||||||
use crate::shapes::{ShapeType, TurtleShape};
|
use crate::shapes::{ShapeType, TurtleShape};
|
||||||
|
|
||||||
/// Trait for adding commands to a queue
|
/// Trait for adding commands to a queue
|
||||||
@ -114,7 +114,7 @@ impl TurtlePlan {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_color(&mut self, color: Color) -> &mut Self {
|
pub fn set_pen_color(&mut self, color: Color) -> &mut Self {
|
||||||
self.queue.push(TurtleCommand::SetColor(color));
|
self.queue.push(TurtleCommand::SetColor(color));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@ -158,6 +158,27 @@ impl TurtlePlan {
|
|||||||
self.set_shape(shape_type.to_shape())
|
self.set_shape(shape_type.to_shape())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn begin_fill(&mut self) -> &mut Self {
|
||||||
|
self.queue.push(TurtleCommand::BeginFill);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end_fill(&mut self) -> &mut Self {
|
||||||
|
self.queue.push(TurtleCommand::EndFill);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_fill_color(&mut self, color: impl Into<Color>) -> &mut Self {
|
||||||
|
self.queue
|
||||||
|
.push(TurtleCommand::SetFillColor(Some(color.into())));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn go_to(&mut self, coord: impl Into<Coordinate>) -> &mut Self {
|
||||||
|
self.queue.push(TurtleCommand::Goto(coord.into()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build(self) -> CommandQueue {
|
pub fn build(self) -> CommandQueue {
|
||||||
self.queue
|
self.queue
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,10 @@ pub enum TurtleCommand {
|
|||||||
// Visibility
|
// Visibility
|
||||||
ShowTurtle,
|
ShowTurtle,
|
||||||
HideTurtle,
|
HideTurtle,
|
||||||
|
|
||||||
|
// Fill operations
|
||||||
|
BeginFill,
|
||||||
|
EndFill,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Queue of turtle commands with execution state
|
/// Queue of turtle commands with execution state
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
//! Rendering logic using Macroquad
|
//! Rendering logic using Macroquad and Lyon tessellation
|
||||||
|
|
||||||
use crate::circle_geometry::{CircleDirection, CircleGeometry};
|
use crate::circle_geometry::{CircleDirection, CircleGeometry};
|
||||||
use crate::state::{DrawCommand, TurtleState, TurtleWorld};
|
use crate::state::{DrawCommand, TurtleState, TurtleWorld};
|
||||||
|
use crate::tessellation;
|
||||||
use macroquad::prelude::*;
|
use macroquad::prelude::*;
|
||||||
|
|
||||||
// Import the easing function from the tween crate
|
// 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
|
// 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,
|
// Available options: Linear, SineInOut, QuadInOut, CubicInOut, QuartInOut, QuintInOut,
|
||||||
// ExpoInOut, CircInOut, BackInOut, ElasticInOut, BounceInOut, etc.
|
// ExpoInOut, CircInOut, BackInOut, ElasticInOut, BounceInOut, etc.
|
||||||
// See https://easings.net/ for visual demonstrations
|
// See https://easings.net/ for visual demonstrations
|
||||||
@ -26,43 +27,8 @@ pub fn render_world(world: &TurtleWorld) {
|
|||||||
// Draw all accumulated commands
|
// Draw all accumulated commands
|
||||||
for cmd in &world.commands {
|
for cmd in &world.commands {
|
||||||
match cmd {
|
match cmd {
|
||||||
DrawCommand::Line {
|
DrawCommand::Mesh(mesh_data) => {
|
||||||
start,
|
draw_mesh(&mesh_data.to_mesh());
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,43 +65,8 @@ pub(crate) fn render_world_with_tween(
|
|||||||
// Draw all accumulated commands
|
// Draw all accumulated commands
|
||||||
for cmd in &world.commands {
|
for cmd in &world.commands {
|
||||||
match cmd {
|
match cmd {
|
||||||
DrawCommand::Line {
|
DrawCommand::Mesh(mesh_data) => {
|
||||||
start,
|
draw_mesh(&mesh_data.to_mesh());
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,6 +107,137 @@ pub(crate) fn render_world_with_tween(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) as f32;
|
||||||
|
let progress = (elapsed / tween.duration as f32).min(1.0);
|
||||||
|
let eased_progress = CubicInOut.tween(1.0, progress);
|
||||||
|
|
||||||
|
// Generate arc vertices for the partial arc
|
||||||
|
let num_samples = (*steps as usize).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) => {
|
||||||
|
eprintln!(
|
||||||
|
"#### Lyon multi-contour tessellation error for fill preview: {:?}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw turtle if visible
|
// Draw turtle if visible
|
||||||
if world.turtle.visible {
|
if world.turtle.visible {
|
||||||
draw_turtle(&world.turtle);
|
draw_turtle(&world.turtle);
|
||||||
@ -237,14 +299,32 @@ pub fn draw_turtle(turtle: &TurtleState) {
|
|||||||
let rotated_vertices = turtle.shape.rotated_vertices(turtle.heading);
|
let rotated_vertices = turtle.shape.rotated_vertices(turtle.heading);
|
||||||
|
|
||||||
if turtle.shape.filled {
|
if turtle.shape.filled {
|
||||||
// Draw filled polygon (now supports concave shapes via ear clipping)
|
// Draw filled polygon using Lyon tessellation
|
||||||
if rotated_vertices.len() >= 3 {
|
if rotated_vertices.len() >= 3 {
|
||||||
let absolute_vertices: Vec<Vec2> = rotated_vertices
|
let absolute_vertices: Vec<Vec2> = rotated_vertices
|
||||||
.iter()
|
.iter()
|
||||||
.map(|v| turtle.position + *v)
|
.map(|v| turtle.position + *v)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
draw_filled_polygon(&absolute_vertices, Color::new(0.0, 0.5, 1.0, 1.0));
|
// Use Lyon for turtle shape too
|
||||||
|
match tessellation::tessellate_polygon(
|
||||||
|
&absolute_vertices,
|
||||||
|
Color::new(0.0, 0.5, 1.0, 1.0),
|
||||||
|
) {
|
||||||
|
Ok(mesh_data) => draw_mesh(&mesh_data.to_mesh()),
|
||||||
|
Err(_) => {
|
||||||
|
// 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 {
|
} else {
|
||||||
// Draw outline
|
// Draw outline
|
||||||
@ -258,38 +338,3 @@ pub fn draw_turtle(turtle: &TurtleState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
use crate::circle_geometry::{CircleDirection, CircleGeometry};
|
use crate::circle_geometry::{CircleDirection, CircleGeometry};
|
||||||
use crate::commands::TurtleCommand;
|
use crate::commands::TurtleCommand;
|
||||||
use crate::state::{DrawCommand, TurtleState, TurtleWorld};
|
use crate::state::{DrawCommand, TurtleState, TurtleWorld};
|
||||||
|
use crate::tessellation;
|
||||||
use macroquad::prelude::*;
|
use macroquad::prelude::*;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -17,20 +18,19 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
|
|||||||
let dy = distance * state.heading.sin();
|
let dy = distance * state.heading.sin();
|
||||||
state.position = vec2(state.position.x + dx, state.position.y + dy);
|
state.position = vec2(state.position.x + dx, state.position.y + dy);
|
||||||
|
|
||||||
|
// Record vertex for fill if filling
|
||||||
|
state.record_fill_vertex();
|
||||||
|
|
||||||
if state.pen_down {
|
if state.pen_down {
|
||||||
world.add_command(DrawCommand::Line {
|
// Draw line segment with round caps (caps handled by tessellate_stroke)
|
||||||
start,
|
if let Ok(mesh_data) = tessellation::tessellate_stroke(
|
||||||
end: state.position,
|
&[start, state.position],
|
||||||
color: state.color,
|
state.color,
|
||||||
width: state.pen_width,
|
state.pen_width,
|
||||||
});
|
false, // not closed
|
||||||
// Add circle at end point for smooth line joins
|
) {
|
||||||
world.add_command(DrawCommand::Circle {
|
world.add_command(DrawCommand::Mesh(mesh_data));
|
||||||
center: state.position,
|
}
|
||||||
radius: state.pen_width / 2.0,
|
|
||||||
color: state.color,
|
|
||||||
filled: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,15 +50,18 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
|
|||||||
if state.pen_down {
|
if state.pen_down {
|
||||||
let (rotation_degrees, arc_degrees) = geom.draw_arc_params(*angle);
|
let (rotation_degrees, arc_degrees) = geom.draw_arc_params(*angle);
|
||||||
|
|
||||||
world.add_command(DrawCommand::Arc {
|
// Use Lyon to tessellate the arc
|
||||||
center: geom.center,
|
if let Ok(mesh_data) = tessellation::tessellate_arc(
|
||||||
radius: *radius - state.pen_width, // Adjust radius for pen width to keep arc inside
|
geom.center,
|
||||||
rotation: rotation_degrees,
|
*radius,
|
||||||
arc: arc_degrees,
|
rotation_degrees,
|
||||||
color: state.color,
|
arc_degrees,
|
||||||
width: state.pen_width,
|
state.color,
|
||||||
sides: *steps as u8,
|
state.pen_width,
|
||||||
});
|
*steps as u8,
|
||||||
|
) {
|
||||||
|
world.add_command(DrawCommand::Mesh(mesh_data));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update turtle position and heading
|
// Update turtle position and heading
|
||||||
@ -67,14 +70,37 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
|
|||||||
CircleDirection::Left => start_heading - angle.to_radians(),
|
CircleDirection::Left => start_heading - angle.to_radians(),
|
||||||
CircleDirection::Right => start_heading + angle.to_radians(),
|
CircleDirection::Right => start_heading + angle.to_radians(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Record vertices along arc for fill if filling
|
||||||
|
state.record_fill_vertices_for_arc(
|
||||||
|
geom.center,
|
||||||
|
*radius,
|
||||||
|
geom.start_angle_from_center,
|
||||||
|
angle.to_radians(),
|
||||||
|
*direction,
|
||||||
|
*steps as u32,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
TurtleCommand::PenUp => {
|
TurtleCommand::PenUp => {
|
||||||
state.pen_down = false;
|
state.pen_down = false;
|
||||||
|
// Close current contour if filling
|
||||||
|
if state.filling.is_some() {
|
||||||
|
eprintln!("PenUp: Closing current contour");
|
||||||
|
}
|
||||||
|
state.close_fill_contour();
|
||||||
}
|
}
|
||||||
|
|
||||||
TurtleCommand::PenDown => {
|
TurtleCommand::PenDown => {
|
||||||
state.pen_down = true;
|
state.pen_down = true;
|
||||||
|
// Start new contour if filling
|
||||||
|
if state.filling.is_some() {
|
||||||
|
eprintln!(
|
||||||
|
"PenDown: Starting new contour at position ({}, {})",
|
||||||
|
state.position.x, state.position.y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
state.start_fill_contour();
|
||||||
}
|
}
|
||||||
|
|
||||||
TurtleCommand::SetColor(color) => {
|
TurtleCommand::SetColor(color) => {
|
||||||
@ -101,20 +127,19 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
|
|||||||
let start = state.position;
|
let start = state.position;
|
||||||
state.position = *coord;
|
state.position = *coord;
|
||||||
|
|
||||||
|
// Record vertex for fill if filling
|
||||||
|
state.record_fill_vertex();
|
||||||
|
|
||||||
if state.pen_down {
|
if state.pen_down {
|
||||||
world.add_command(DrawCommand::Line {
|
// Draw line segment with round caps
|
||||||
start,
|
if let Ok(mesh_data) = tessellation::tessellate_stroke(
|
||||||
end: state.position,
|
&[start, state.position],
|
||||||
color: state.color,
|
state.color,
|
||||||
width: state.pen_width,
|
state.pen_width,
|
||||||
});
|
false, // not closed
|
||||||
// Add circle at end point for smooth line joins
|
) {
|
||||||
world.add_command(DrawCommand::Circle {
|
world.add_command(DrawCommand::Mesh(mesh_data));
|
||||||
center: state.position,
|
}
|
||||||
radius: state.pen_width / 2.0,
|
|
||||||
color: state.color,
|
|
||||||
filled: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,6 +154,53 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
|
|||||||
TurtleCommand::HideTurtle => {
|
TurtleCommand::HideTurtle => {
|
||||||
state.visible = false;
|
state.visible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TurtleCommand::BeginFill => {
|
||||||
|
if state.filling.is_some() {
|
||||||
|
eprintln!("Warning: begin_fill() called while already filling");
|
||||||
|
}
|
||||||
|
|
||||||
|
let fill_color = state.fill_color.unwrap_or_else(|| {
|
||||||
|
eprintln!("Warning: No fill_color set, using black");
|
||||||
|
BLACK
|
||||||
|
});
|
||||||
|
|
||||||
|
state.begin_fill(fill_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
TurtleCommand::EndFill => {
|
||||||
|
if let Some(mut fill_state) = state.filling.take() {
|
||||||
|
// Close final contour if it has vertices
|
||||||
|
if !fill_state.current_contour.is_empty() {
|
||||||
|
fill_state.contours.push(fill_state.current_contour);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug output
|
||||||
|
eprintln!("=== EndFill Debug ===");
|
||||||
|
eprintln!("Total contours: {}", fill_state.contours.len());
|
||||||
|
for (i, contour) in fill_state.contours.iter().enumerate() {
|
||||||
|
eprintln!(" Contour {}: {} vertices", i, contour.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create fill command - Lyon will handle EvenOdd automatically with multiple contours
|
||||||
|
if !fill_state.contours.is_empty() {
|
||||||
|
if let Ok(mesh_data) = tessellation::tessellate_multi_contour(
|
||||||
|
&fill_state.contours,
|
||||||
|
fill_state.fill_color,
|
||||||
|
) {
|
||||||
|
eprintln!(
|
||||||
|
"Successfully tessellated {} contours",
|
||||||
|
fill_state.contours.len()
|
||||||
|
);
|
||||||
|
world.add_command(DrawCommand::Mesh(mesh_data));
|
||||||
|
} else {
|
||||||
|
eprintln!("ERROR: Failed to tessellate contours!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("Warning: end_fill() called without begin_fill()");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,19 +214,15 @@ pub fn add_draw_for_completed_tween(
|
|||||||
match command {
|
match command {
|
||||||
TurtleCommand::Move(_) | TurtleCommand::Goto(_) => {
|
TurtleCommand::Move(_) | TurtleCommand::Goto(_) => {
|
||||||
if start_state.pen_down {
|
if start_state.pen_down {
|
||||||
world.add_command(DrawCommand::Line {
|
// Draw line segment with round caps
|
||||||
start: start_state.position,
|
if let Ok(mesh_data) = tessellation::tessellate_stroke(
|
||||||
end: end_state.position,
|
&[start_state.position, end_state.position],
|
||||||
color: start_state.color,
|
start_state.color,
|
||||||
width: start_state.pen_width,
|
start_state.pen_width,
|
||||||
});
|
false, // not closed
|
||||||
// Add circle at end point for smooth line joins
|
) {
|
||||||
world.add_command(DrawCommand::Circle {
|
world.add_command(DrawCommand::Mesh(mesh_data));
|
||||||
center: end_state.position,
|
}
|
||||||
radius: start_state.pen_width / 2.0,
|
|
||||||
color: start_state.color,
|
|
||||||
filled: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TurtleCommand::Circle {
|
TurtleCommand::Circle {
|
||||||
@ -172,31 +240,23 @@ pub fn add_draw_for_completed_tween(
|
|||||||
);
|
);
|
||||||
let (rotation_degrees, arc_degrees) = geom.draw_arc_params(*angle);
|
let (rotation_degrees, arc_degrees) = geom.draw_arc_params(*angle);
|
||||||
|
|
||||||
world.add_command(DrawCommand::Arc {
|
// Use Lyon to tessellate the arc
|
||||||
center: geom.center,
|
if let Ok(mesh_data) = tessellation::tessellate_arc(
|
||||||
radius: *radius - start_state.pen_width / 2.0,
|
geom.center,
|
||||||
rotation: rotation_degrees,
|
*radius,
|
||||||
arc: arc_degrees,
|
rotation_degrees,
|
||||||
color: start_state.color,
|
arc_degrees,
|
||||||
width: start_state.pen_width,
|
start_state.color,
|
||||||
sides: *steps as u8,
|
start_state.pen_width,
|
||||||
});
|
*steps as u8,
|
||||||
|
) {
|
||||||
// Add endpoint circles for smooth joins
|
world.add_command(DrawCommand::Mesh(mesh_data));
|
||||||
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::BeginFill | TurtleCommand::EndFill => {
|
||||||
|
// No immediate drawing for fill commands, handled in execute_command
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Other commands don't create drawing
|
// Other commands don't create drawing
|
||||||
}
|
}
|
||||||
@ -223,6 +283,7 @@ mod tests {
|
|||||||
speed: AnimationSpeed::Animated(100.0),
|
speed: AnimationSpeed::Animated(100.0),
|
||||||
visible: true,
|
visible: true,
|
||||||
shape: TurtleShape::turtle(),
|
shape: TurtleShape::turtle(),
|
||||||
|
filling: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// We'll use a dummy world but won't actually call drawing commands
|
// We'll use a dummy world but won't actually call drawing commands
|
||||||
|
|||||||
@ -32,6 +32,7 @@ pub mod execution;
|
|||||||
pub mod general;
|
pub mod general;
|
||||||
pub mod shapes;
|
pub mod shapes;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
pub mod tessellation;
|
||||||
pub mod tweening;
|
pub mod tweening;
|
||||||
|
|
||||||
// Re-export commonly used types
|
// Re-export commonly used types
|
||||||
@ -91,7 +92,8 @@ impl TurtleApp {
|
|||||||
self.handle_mouse_zoom();
|
self.handle_mouse_zoom();
|
||||||
|
|
||||||
if let Some(ref mut controller) = self.tween_controller {
|
if let Some(ref mut controller) = self.tween_controller {
|
||||||
let completed_commands = controller.update(&mut self.world.turtle);
|
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)
|
// Process all completed commands (multiple in instant mode, 0-1 in animated mode)
|
||||||
for (completed_cmd, start_state, end_state) in completed_commands {
|
for (completed_cmd, start_state, end_state) in completed_commands {
|
||||||
|
|||||||
@ -4,6 +4,23 @@ use crate::general::{Angle, AnimationSpeed, Color, Coordinate, Precision};
|
|||||||
use crate::shapes::TurtleShape;
|
use crate::shapes::TurtleShape;
|
||||||
use macroquad::prelude::*;
|
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
|
/// State of a single turtle
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct TurtleState {
|
pub struct TurtleState {
|
||||||
@ -16,6 +33,9 @@ pub struct TurtleState {
|
|||||||
pub speed: AnimationSpeed,
|
pub speed: AnimationSpeed,
|
||||||
pub visible: bool,
|
pub visible: bool,
|
||||||
pub shape: TurtleShape,
|
pub shape: TurtleShape,
|
||||||
|
|
||||||
|
// Fill tracking
|
||||||
|
pub filling: Option<FillState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TurtleState {
|
impl Default for TurtleState {
|
||||||
@ -30,6 +50,7 @@ impl Default for TurtleState {
|
|||||||
speed: AnimationSpeed::default(),
|
speed: AnimationSpeed::default(),
|
||||||
visible: true,
|
visible: true,
|
||||||
shape: TurtleShape::turtle(),
|
shape: TurtleShape::turtle(),
|
||||||
|
filling: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,36 +63,168 @@ impl TurtleState {
|
|||||||
pub fn heading_angle(&self) -> Angle {
|
pub fn heading_angle(&self) -> Angle {
|
||||||
Angle::radians(self.heading)
|
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 {
|
||||||
|
eprintln!(
|
||||||
|
" [FILL] Adding vertex ({:.2}, {:.2}) to current contour (now {} vertices)",
|
||||||
|
self.position.x,
|
||||||
|
self.position.y,
|
||||||
|
fill_state.current_contour.len() + 1
|
||||||
|
);
|
||||||
|
fill_state.current_contour.push(self.position);
|
||||||
|
} else {
|
||||||
|
eprintln!(" [FILL] 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 {
|
||||||
|
eprintln!(
|
||||||
|
" close_fill_contour called: current_contour has {} vertices",
|
||||||
|
fill_state.current_contour.len()
|
||||||
|
);
|
||||||
|
// Only close if we have vertices in current contour
|
||||||
|
if fill_state.current_contour.len() >= 2 {
|
||||||
|
eprintln!(
|
||||||
|
" Closing contour with {} vertices",
|
||||||
|
fill_state.current_contour.len()
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
" First: ({:.2}, {:.2})",
|
||||||
|
fill_state.current_contour[0].x, fill_state.current_contour[0].y
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
" Last: ({:.2}, {:.2})",
|
||||||
|
fill_state.current_contour[fill_state.current_contour.len() - 1].x,
|
||||||
|
fill_state.current_contour[fill_state.current_contour.len() - 1].y
|
||||||
|
);
|
||||||
|
// Move current contour to completed contours
|
||||||
|
let contour = std::mem::take(&mut fill_state.current_contour);
|
||||||
|
fill_state.contours.push(contour);
|
||||||
|
eprintln!(
|
||||||
|
" Contour moved to completed list. Total completed contours: {}",
|
||||||
|
fill_state.contours.len()
|
||||||
|
);
|
||||||
|
} else if !fill_state.current_contour.is_empty() {
|
||||||
|
eprintln!(
|
||||||
|
" WARNING: Current contour only has {} vertex/vertices, not closing",
|
||||||
|
fill_state.current_contour.len()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
eprintln!(" WARNING: Current contour is EMPTY, nothing to close");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!(" 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
|
||||||
|
eprintln!(
|
||||||
|
" Starting NEW contour at ({:.2}, {:.2})",
|
||||||
|
self.position.x, self.position.y
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
" Previous contour had {} completed contours",
|
||||||
|
fill_state.contours.len()
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
|
||||||
|
eprintln!(" [FILL ARC] Recording arc vertices: center=({:.2}, {:.2}), radius={:.2}, steps={}, num_samples={}",
|
||||||
|
center.x, center.y, radius, steps, num_samples);
|
||||||
|
|
||||||
|
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(),
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
" [FILL ARC] Vertex {}: ({:.2}, {:.2}) at angle {:.2}°",
|
||||||
|
i,
|
||||||
|
vertex.x,
|
||||||
|
vertex.y,
|
||||||
|
current_angle.to_degrees()
|
||||||
|
);
|
||||||
|
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 {
|
||||||
|
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
|
/// Drawable elements in the world
|
||||||
|
/// All drawing is done via Lyon-tessellated meshes for consistency and quality
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum DrawCommand {
|
pub enum DrawCommand {
|
||||||
Line {
|
/// Pre-tessellated mesh data (lines, arcs, circles, polygons - all use this)
|
||||||
start: Coordinate,
|
Mesh(MeshData),
|
||||||
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
|
/// The complete turtle world containing all drawing state
|
||||||
|
|||||||
319
turtle-lib-macroquad/src/tessellation.rs
Normal file
319
turtle-lib-macroquad/src/tessellation.rs
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
//! 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::Path;
|
||||||
|
use lyon::tessellation::*;
|
||||||
|
use macroquad::prelude::*;
|
||||||
|
|
||||||
|
/// Convert macroquad Vec2 to Lyon Point
|
||||||
|
pub fn to_lyon_point(v: Vec2) -> Point {
|
||||||
|
point(v.x, v.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert Lyon Point to macroquad Vec2
|
||||||
|
#[allow(dead_code)]
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!("\n=== tessellate_multi_contour Debug ===");
|
||||||
|
eprintln!("Total contours to tessellate: {}", contours.len());
|
||||||
|
|
||||||
|
// Build path with multiple sub-paths (contours)
|
||||||
|
let mut builder = Path::builder();
|
||||||
|
|
||||||
|
for (idx, contour) in contours.iter().enumerate() {
|
||||||
|
if contour.is_empty() {
|
||||||
|
eprintln!("WARNING: Contour {} is empty, skipping", idx);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!("\nContour {}: {} vertices", idx, contour.len());
|
||||||
|
eprintln!(" First vertex: ({:.2}, {:.2})", contour[0].x, contour[0].y);
|
||||||
|
if contour.len() > 1 {
|
||||||
|
eprintln!(
|
||||||
|
" Last vertex: ({:.2}, {:.2})",
|
||||||
|
contour[contour.len() - 1].x,
|
||||||
|
contour[contour.len() - 1].y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
eprintln!(" Vertex {}: ({:.2}, {:.2})", i + 1, v.x, v.y);
|
||||||
|
} else if i == 3 {
|
||||||
|
eprintln!(" ... ({} more vertices)", contour.len() - 7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.end(true); // Close this contour
|
||||||
|
eprintln!(" Contour closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!("\nBuilding Lyon path...");
|
||||||
|
let path = builder.build();
|
||||||
|
eprintln!("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();
|
||||||
|
|
||||||
|
eprintln!("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(_) => {
|
||||||
|
eprintln!("Tessellation successful!");
|
||||||
|
eprintln!(
|
||||||
|
" Generated {} vertices, {} indices",
|
||||||
|
geometry.vertices.len(),
|
||||||
|
geometry.indices.len()
|
||||||
|
);
|
||||||
|
eprintln!(" Triangles: {}", geometry.indices.len() / 3);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("ERROR: Tessellation failed: {}", e);
|
||||||
|
return Err(Box::new(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(build_mesh_data(
|
||||||
|
&geometry.vertices,
|
||||||
|
&geometry.indices,
|
||||||
|
color,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tessellate a stroked path and return mesh
|
||||||
|
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
|
||||||
|
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
|
||||||
|
pub fn tessellate_arc(
|
||||||
|
center: Vec2,
|
||||||
|
radius: f32,
|
||||||
|
start_angle_degrees: f32,
|
||||||
|
arc_angle_degrees: f32,
|
||||||
|
color: Color,
|
||||||
|
stroke_width: f32,
|
||||||
|
segments: u8,
|
||||||
|
) -> 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,
|
||||||
|
))
|
||||||
|
}
|
||||||
@ -3,7 +3,8 @@
|
|||||||
use crate::circle_geometry::{CircleDirection, CircleGeometry};
|
use crate::circle_geometry::{CircleDirection, CircleGeometry};
|
||||||
use crate::commands::{CommandQueue, TurtleCommand};
|
use crate::commands::{CommandQueue, TurtleCommand};
|
||||||
use crate::general::AnimationSpeed;
|
use crate::general::AnimationSpeed;
|
||||||
use crate::state::TurtleState;
|
use crate::state::{DrawCommand, TurtleState};
|
||||||
|
use crate::tessellation;
|
||||||
use macroquad::prelude::*;
|
use macroquad::prelude::*;
|
||||||
use tween::{CubicInOut, TweenValue, Tweener};
|
use tween::{CubicInOut, TweenValue, Tweener};
|
||||||
|
|
||||||
@ -75,10 +76,12 @@ impl TweenController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Update the tween, returns Vec of (command, start_state, end_state) for all completed commands this frame
|
/// Update the tween, returns Vec of (command, start_state, end_state) for all completed commands this frame
|
||||||
|
/// Also takes commands vec to handle side effects like fill operations
|
||||||
/// Each command has its own start_state and end_state pair
|
/// Each command has its own start_state and end_state pair
|
||||||
pub fn update(
|
pub fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
state: &mut TurtleState,
|
state: &mut TurtleState,
|
||||||
|
commands: &mut Vec<crate::state::DrawCommand>,
|
||||||
) -> Vec<(TurtleCommand, TurtleState, TurtleState)> {
|
) -> Vec<(TurtleCommand, TurtleState, TurtleState)> {
|
||||||
// In instant mode, execute commands up to the draw calls per frame limit
|
// In instant mode, execute commands up to the draw calls per frame limit
|
||||||
if let AnimationSpeed::Instant(max_draw_calls) = self.speed {
|
if let AnimationSpeed::Instant(max_draw_calls) = self.speed {
|
||||||
@ -105,15 +108,89 @@ impl TweenController {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For commands with side effects (fill operations), handle specially
|
||||||
|
match &command {
|
||||||
|
TurtleCommand::BeginFill => {
|
||||||
|
let fill_color = state.fill_color.unwrap_or(macroquad::prelude::BLACK);
|
||||||
|
state.begin_fill(fill_color);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
TurtleCommand::PenUp => {
|
||||||
|
state.pen_down = false;
|
||||||
|
// Close current contour if filling
|
||||||
|
state.close_fill_contour();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
TurtleCommand::PenDown => {
|
||||||
|
state.pen_down = true;
|
||||||
|
// Start new contour if filling
|
||||||
|
state.start_fill_contour();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
TurtleCommand::EndFill => {
|
||||||
|
if let Some(mut fill_state) = state.filling.take() {
|
||||||
|
// Close final contour if it has vertices
|
||||||
|
if !fill_state.current_contour.is_empty() {
|
||||||
|
fill_state.contours.push(fill_state.current_contour);
|
||||||
|
}
|
||||||
|
// Create fill command - Lyon will handle EvenOdd automatically
|
||||||
|
if !fill_state.contours.is_empty() {
|
||||||
|
if let Ok(mesh_data) = tessellation::tessellate_multi_contour(
|
||||||
|
&fill_state.contours,
|
||||||
|
fill_state.fill_color,
|
||||||
|
) {
|
||||||
|
commands.push(DrawCommand::Mesh(mesh_data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
// Execute command immediately
|
// Execute command immediately
|
||||||
let target_state = self.calculate_target_state(state, &command);
|
let target_state = self.calculate_target_state(state, &command);
|
||||||
*state = target_state.clone();
|
*state = target_state.clone();
|
||||||
|
|
||||||
|
// Record vertices after position update if filling
|
||||||
|
match &command {
|
||||||
|
TurtleCommand::Circle {
|
||||||
|
radius,
|
||||||
|
angle,
|
||||||
|
steps,
|
||||||
|
direction,
|
||||||
|
} => {
|
||||||
|
// For circles, record multiple vertices along the arc
|
||||||
|
if state.filling.is_some() {
|
||||||
|
use crate::circle_geometry::CircleGeometry;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
// Capture end state AFTER executing this command
|
// Capture end state AFTER executing this command
|
||||||
let end_state = state.clone();
|
let end_state = state.clone();
|
||||||
|
|
||||||
// Collect drawable commands with their individual start and end states
|
// Collect drawable commands with their individual start and end states
|
||||||
if Self::command_creates_drawing(&command) {
|
// Only create line drawing if pen is down
|
||||||
|
if Self::command_creates_drawing(&command) && start_state.pen_down {
|
||||||
completed_commands.push((command, start_state, end_state));
|
completed_commands.push((command, start_state, end_state));
|
||||||
draw_call_count += 1;
|
draw_call_count += 1;
|
||||||
|
|
||||||
@ -197,13 +274,92 @@ impl TweenController {
|
|||||||
*state = tween.target_state.clone();
|
*state = tween.target_state.clone();
|
||||||
let end_state = state.clone();
|
let end_state = state.clone();
|
||||||
|
|
||||||
// Return the completed command and start/end states to add draw commands
|
// Return the completed command and start/end states
|
||||||
let completed_command = tween.command.clone();
|
let completed_command = tween.command.clone();
|
||||||
self.current_tween = None;
|
self.current_tween = None;
|
||||||
|
|
||||||
// Only return command if it creates drawable elements
|
// Handle fill commands that have side effects
|
||||||
if Self::command_creates_drawing(&completed_command) {
|
match &completed_command {
|
||||||
return vec![(completed_command, start_state, end_state)];
|
TurtleCommand::BeginFill => {
|
||||||
|
let fill_color = state.fill_color.unwrap_or(macroquad::prelude::BLACK);
|
||||||
|
state.begin_fill(fill_color);
|
||||||
|
// Don't return, continue to next command
|
||||||
|
return self.update(state, commands);
|
||||||
|
}
|
||||||
|
TurtleCommand::EndFill => {
|
||||||
|
if let Some(mut fill_state) = state.filling.take() {
|
||||||
|
// Close final contour if it has vertices
|
||||||
|
if !fill_state.current_contour.is_empty() {
|
||||||
|
fill_state.contours.push(fill_state.current_contour);
|
||||||
|
}
|
||||||
|
// Create fill command - Lyon will handle EvenOdd automatically
|
||||||
|
if !fill_state.contours.is_empty() {
|
||||||
|
if let Ok(mesh_data) = tessellation::tessellate_multi_contour(
|
||||||
|
&fill_state.contours,
|
||||||
|
fill_state.fill_color,
|
||||||
|
) {
|
||||||
|
commands.push(DrawCommand::Mesh(mesh_data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Don't return, continue to next command
|
||||||
|
return self.update(state, commands);
|
||||||
|
}
|
||||||
|
TurtleCommand::Circle {
|
||||||
|
radius,
|
||||||
|
angle,
|
||||||
|
steps,
|
||||||
|
direction,
|
||||||
|
} => {
|
||||||
|
// For circles, record multiple vertices along the arc
|
||||||
|
if state.filling.is_some() {
|
||||||
|
use crate::circle_geometry::CircleGeometry;
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if Self::command_creates_drawing(&completed_command) && start_state.pen_down
|
||||||
|
{
|
||||||
|
return vec![(completed_command, start_state, end_state)];
|
||||||
|
} else {
|
||||||
|
// Movement but no drawing (pen up) - continue
|
||||||
|
return self.update(state, commands);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TurtleCommand::Move(_) | TurtleCommand::Goto(_) => {
|
||||||
|
// Movement commands: record vertex if filling
|
||||||
|
state.record_fill_vertex();
|
||||||
|
|
||||||
|
if Self::command_creates_drawing(&completed_command) && start_state.pen_down
|
||||||
|
{
|
||||||
|
return vec![(completed_command, start_state, end_state)];
|
||||||
|
} else {
|
||||||
|
// Movement but no drawing (pen up) - continue
|
||||||
|
return self.update(state, commands);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ if Self::command_creates_drawing(&completed_command)
|
||||||
|
&& start_state.pen_down =>
|
||||||
|
{
|
||||||
|
// Return drawable commands
|
||||||
|
return vec![(completed_command, start_state, end_state)];
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Non-drawable, non-fill commands - continue to next
|
||||||
|
return self.update(state, commands);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,16 +370,52 @@ impl TweenController {
|
|||||||
if let Some(command) = self.queue.next() {
|
if let Some(command) = self.queue.next() {
|
||||||
let command_clone = command.clone();
|
let command_clone = command.clone();
|
||||||
|
|
||||||
// Handle SetSpeed command specially
|
// Handle commands that should execute immediately (no animation)
|
||||||
if let TurtleCommand::SetSpeed(new_speed) = &command_clone {
|
match &command_clone {
|
||||||
state.set_speed(*new_speed);
|
TurtleCommand::SetSpeed(new_speed) => {
|
||||||
self.speed = *new_speed;
|
state.set_speed(*new_speed);
|
||||||
// If switched to instant mode, process commands immediately
|
self.speed = *new_speed;
|
||||||
if matches!(self.speed, AnimationSpeed::Instant(_)) {
|
// If switched to instant mode, process commands immediately
|
||||||
return self.update(state); // Recursively process in instant mode
|
if matches!(self.speed, AnimationSpeed::Instant(_)) {
|
||||||
|
return self.update(state, commands); // Recursively process in instant mode
|
||||||
|
}
|
||||||
|
// For animated mode speed changes, continue to next command
|
||||||
|
return self.update(state, commands);
|
||||||
}
|
}
|
||||||
// For animated mode speed changes, continue to next command
|
TurtleCommand::PenUp => {
|
||||||
return self.update(state);
|
state.pen_down = false;
|
||||||
|
state.close_fill_contour();
|
||||||
|
return self.update(state, commands);
|
||||||
|
}
|
||||||
|
TurtleCommand::PenDown => {
|
||||||
|
state.pen_down = true;
|
||||||
|
state.start_fill_contour();
|
||||||
|
return self.update(state, commands);
|
||||||
|
}
|
||||||
|
TurtleCommand::BeginFill => {
|
||||||
|
let fill_color = state.fill_color.unwrap_or(macroquad::prelude::BLACK);
|
||||||
|
state.begin_fill(fill_color);
|
||||||
|
return self.update(state, commands);
|
||||||
|
}
|
||||||
|
TurtleCommand::EndFill => {
|
||||||
|
if let Some(mut fill_state) = state.filling.take() {
|
||||||
|
// Close final contour if it has vertices
|
||||||
|
if !fill_state.current_contour.is_empty() {
|
||||||
|
fill_state.contours.push(fill_state.current_contour);
|
||||||
|
}
|
||||||
|
// Create fill command - Lyon will handle EvenOdd automatically
|
||||||
|
if !fill_state.contours.is_empty() {
|
||||||
|
if let Ok(mesh_data) = tessellation::tessellate_multi_contour(
|
||||||
|
&fill_state.contours,
|
||||||
|
fill_state.fill_color,
|
||||||
|
) {
|
||||||
|
commands.push(DrawCommand::Mesh(mesh_data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self.update(state, commands);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let speed = state.speed; // Extract speed before borrowing self
|
let speed = state.speed; // Extract speed before borrowing self
|
||||||
@ -375,6 +567,10 @@ impl TweenController {
|
|||||||
TurtleCommand::SetFillColor(color) => {
|
TurtleCommand::SetFillColor(color) => {
|
||||||
target.fill_color = *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
|
target
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user