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]
|
||||
name = "turtle-lib-macroquad"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
macroquad = "0.4"
|
||||
earcutr = "0.5"
|
||||
tween = "2.1.0"
|
||||
lyon = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
# 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
|
||||
|
||||
- **Simple Builder API**: Chain commands like `forward(100).right(90)`
|
||||
- **Smooth Animations**: Tweening support with easing functions
|
||||
- **Instant Mode**: Execute commands immediately without animation (speed > 999.0)
|
||||
- **Lightweight**: Fast compilation (~30-60 seconds from clean build)
|
||||
- **Macroquad Integration**: Built on the simple and fast Macroquad framework
|
||||
✅ **Complete Lyon Integration** - All drawing operations use GPU-optimized tessellation
|
||||
- Unified rendering pipeline for lines, arcs, circles, and fills
|
||||
- ~410 lines of code eliminated through architectural simplification
|
||||
- Consistent high-quality rendering across all primitives
|
||||
|
||||
## 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`:
|
||||
```toml
|
||||
[dependencies]
|
||||
turtle-lib-macroquad = { path = "../turtle-lib-macroquad" }
|
||||
macroquad = "0.4"
|
||||
```
|
||||
✅ **Smooth Animation** - Tweening system with live rendering
|
||||
- Configurable speed control
|
||||
- Frame-rate independent animation
|
||||
- Live fill preview during circle/arc drawing
|
||||
|
||||
### 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
|
||||
cargo run --example square
|
||||
cargo run --example koch
|
||||
cargo run --example shapes
|
||||
cargo run --example yinyang
|
||||
cargo run --example stern
|
||||
cargo run --example nikolaus
|
||||
# Run from this directory
|
||||
cargo run --example square # Basic square drawing
|
||||
cargo run --example yinyang # Multi-contour fills with holes
|
||||
cargo run --example fill_advanced # Self-intersecting fills
|
||||
cargo run --example koch # Recursive fractals
|
||||
cargo run --example fill_demo # Multiple independent fills
|
||||
```
|
||||
|
||||
### Available Examples
|
||||
## Architecture Highlights
|
||||
|
||||
- **square.rs**: Basic square drawing
|
||||
- **koch.rs**: Koch snowflake fractal
|
||||
- **shapes.rs**: Demonstrates different turtle shapes
|
||||
- **yinyang.rs**: Yin-yang symbol drawing
|
||||
- **stern.rs**: Star pattern drawing
|
||||
- **nikolaus.rs**: Nikolaus (Santa) drawing
|
||||
### Rendering Pipeline
|
||||
All drawing operations → Lyon tessellation → GPU mesh rendering
|
||||
|
||||
## 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
|
||||
- **Turtle**: Classic turtle shape with detailed outline
|
||||
- **Circle**: Circular shape
|
||||
- **Square**: Square shape
|
||||
- **Arrow**: Arrow-like shape
|
||||
## Status
|
||||
|
||||
### Using Shapes
|
||||
✅ **Stable** - Lyon integration complete, multi-contour fills working, all examples passing.
|
||||
|
||||
```rust
|
||||
// Using built-in shapes
|
||||
plan.shape(ShapeType::Turtle);
|
||||
|
||||
// Creating custom shapes
|
||||
let my_shape = TurtleShape::new(
|
||||
vec![
|
||||
vec2(15.0, 0.0), // Point at front
|
||||
vec2(-10.0, -8.0), // Bottom back
|
||||
vec2(-10.0, 8.0), // Top back
|
||||
],
|
||||
true // filled
|
||||
);
|
||||
plan.set_shape(my_shape);
|
||||
```
|
||||
|
||||
Shapes are automatically rotated to match the turtle's heading direction.
|
||||
|
||||
## Architecture
|
||||
|
||||
The library is designed for easy extension and potential multi-threading support:
|
||||
|
||||
- **State Management**: Clean separation between turtle state and world state
|
||||
- **Command Queue**: Commands are queued and can be executed immediately or with tweening
|
||||
- **Tweening System**: Smooth interpolation between states with easing functions
|
||||
- **Rendering**: Direct Macroquad drawing calls with earcutr polygon triangulation
|
||||
- **Shape System**: Extensible turtle shapes with support for both convex and concave polygons
|
||||
|
||||
### Module Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── lib.rs - Public API and TurtleApp
|
||||
├── state.rs - TurtleState and TurtleWorld
|
||||
├── commands.rs - TurtleCommand enum and CommandQueue
|
||||
├── builders.rs - Builder traits (DirectionalMovement, Turnable, etc.)
|
||||
├── execution.rs - Command execution logic
|
||||
├── tweening.rs - Animation/tweening controller
|
||||
├── drawing.rs - Macroquad rendering
|
||||
├── shapes.rs - Turtle shape definitions
|
||||
└── general/ - Type definitions (Angle, Length, etc.)
|
||||
```
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Multi-threading Ready
|
||||
|
||||
While multi-threading is not implemented yet, the architecture supports future additions:
|
||||
- State and world are separated
|
||||
- Commands can be generated on separate threads
|
||||
- Rendering happens on main thread (Macroquad requirement)
|
||||
|
||||
### Tweening vs Interpolation
|
||||
|
||||
We use "tweening" terminology throughout the codebase for clarity and game development conventions.
|
||||
|
||||
### No API Compatibility Constraints
|
||||
|
||||
This library is designed from scratch without backwards compatibility requirements, allowing for optimal design choices.
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential additions (not yet implemented):
|
||||
- Multi-threading support for interactive games
|
||||
- Filled shapes and polygons
|
||||
- Text rendering
|
||||
- Image stamps
|
||||
|
||||
## License
|
||||
|
||||
MIT OR Apache-2.0
|
||||
See [../README.md](../README.md) for full API documentation and project status.
|
||||
|
||||
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);
|
||||
|
||||
// Draw some circles
|
||||
plan.set_color(RED);
|
||||
plan.set_pen_color(RED);
|
||||
plan.set_pen_width(0.5);
|
||||
plan.left(90.0);
|
||||
plan.set_speed(999);
|
||||
@ -18,14 +18,16 @@ async fn main() {
|
||||
|
||||
plan.forward(150.0);
|
||||
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
|
||||
// Set animation speed
|
||||
plan.set_speed(20);
|
||||
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_color(GREEN);
|
||||
plan.set_pen_color(GREEN);
|
||||
plan.circle_left(50.0, 180.0, 36); // Half circle to the left
|
||||
|
||||
// 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() {
|
||||
// Create a turtle plan
|
||||
let mut t = create_turtle();
|
||||
t.set_speed(900);
|
||||
|
||||
t.circle_left(90.0, 180.0, 36);
|
||||
t.begin_fill();
|
||||
t.circle_left(90.0, 180.0, 36);
|
||||
t.circle_left(45.0, 180.0, 26);
|
||||
t.circle_right(45.0, 180.0, 26);
|
||||
@ -24,6 +26,7 @@ async fn main() {
|
||||
t.left(90.0);
|
||||
t.pen_down();
|
||||
t.circle_right(8.0, 360.0, 12);
|
||||
t.end_fill();
|
||||
|
||||
// Set animation speed
|
||||
t.set_speed(1000);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
//! Builder pattern traits for creating turtle command sequences
|
||||
|
||||
use crate::commands::{CommandQueue, TurtleCommand};
|
||||
use crate::general::{AnimationSpeed, Color, Precision};
|
||||
use crate::general::{AnimationSpeed, Color, Coordinate, Precision};
|
||||
use crate::shapes::{ShapeType, TurtleShape};
|
||||
|
||||
/// Trait for adding commands to a queue
|
||||
@ -114,7 +114,7 @@ impl TurtlePlan {
|
||||
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
|
||||
}
|
||||
@ -158,6 +158,27 @@ impl TurtlePlan {
|
||||
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 {
|
||||
self.queue
|
||||
}
|
||||
|
||||
@ -38,6 +38,10 @@ pub enum TurtleCommand {
|
||||
// Visibility
|
||||
ShowTurtle,
|
||||
HideTurtle,
|
||||
|
||||
// Fill operations
|
||||
BeginFill,
|
||||
EndFill,
|
||||
}
|
||||
|
||||
/// 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::state::{DrawCommand, TurtleState, TurtleWorld};
|
||||
use crate::tessellation;
|
||||
use macroquad::prelude::*;
|
||||
|
||||
// Import the easing function from the tween crate
|
||||
// To change the easing, change both this import and the usage in the draw_tween_arc_* 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,
|
||||
// ExpoInOut, CircInOut, BackInOut, ElasticInOut, BounceInOut, etc.
|
||||
// See https://easings.net/ for visual demonstrations
|
||||
@ -26,43 +27,8 @@ pub fn render_world(world: &TurtleWorld) {
|
||||
// Draw all accumulated commands
|
||||
for cmd in &world.commands {
|
||||
match cmd {
|
||||
DrawCommand::Line {
|
||||
start,
|
||||
end,
|
||||
color,
|
||||
width,
|
||||
} => {
|
||||
draw_line(start.x, start.y, end.x, end.y, *width, *color);
|
||||
}
|
||||
DrawCommand::Circle {
|
||||
center,
|
||||
radius,
|
||||
color,
|
||||
filled,
|
||||
} => {
|
||||
if *filled {
|
||||
draw_circle(center.x, center.y, *radius, *color);
|
||||
} else {
|
||||
draw_circle_lines(center.x, center.y, *radius, 2.0, *color);
|
||||
}
|
||||
}
|
||||
DrawCommand::Arc {
|
||||
center,
|
||||
radius,
|
||||
rotation,
|
||||
arc,
|
||||
color,
|
||||
width,
|
||||
sides,
|
||||
} => {
|
||||
draw_arc(
|
||||
center.x, center.y, *sides, *radius, *rotation, *width, *arc, *color,
|
||||
);
|
||||
}
|
||||
DrawCommand::FilledPolygon { vertices, color } => {
|
||||
if vertices.len() >= 3 {
|
||||
draw_filled_polygon(vertices, *color);
|
||||
}
|
||||
DrawCommand::Mesh(mesh_data) => {
|
||||
draw_mesh(&mesh_data.to_mesh());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -99,43 +65,8 @@ pub(crate) fn render_world_with_tween(
|
||||
// Draw all accumulated commands
|
||||
for cmd in &world.commands {
|
||||
match cmd {
|
||||
DrawCommand::Line {
|
||||
start,
|
||||
end,
|
||||
color,
|
||||
width,
|
||||
} => {
|
||||
draw_line(start.x, start.y, end.x, end.y, *width, *color);
|
||||
}
|
||||
DrawCommand::Circle {
|
||||
center,
|
||||
radius,
|
||||
color,
|
||||
filled,
|
||||
} => {
|
||||
if *filled {
|
||||
draw_circle(center.x, center.y, *radius, *color);
|
||||
} else {
|
||||
draw_circle_lines(center.x, center.y, *radius, 2.0, *color);
|
||||
}
|
||||
}
|
||||
DrawCommand::Arc {
|
||||
center,
|
||||
radius,
|
||||
rotation,
|
||||
arc,
|
||||
color,
|
||||
width,
|
||||
sides,
|
||||
} => {
|
||||
draw_arc(
|
||||
center.x, center.y, *sides, *radius, *rotation, *width, *arc, *color,
|
||||
);
|
||||
}
|
||||
DrawCommand::FilledPolygon { vertices, color } => {
|
||||
if vertices.len() >= 3 {
|
||||
draw_filled_polygon(vertices, *color);
|
||||
}
|
||||
DrawCommand::Mesh(mesh_data) => {
|
||||
draw_mesh(&mesh_data.to_mesh());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
if world.turtle.visible {
|
||||
draw_turtle(&world.turtle);
|
||||
@ -237,14 +299,32 @@ pub fn draw_turtle(turtle: &TurtleState) {
|
||||
let rotated_vertices = turtle.shape.rotated_vertices(turtle.heading);
|
||||
|
||||
if turtle.shape.filled {
|
||||
// Draw filled polygon (now supports concave shapes via ear clipping)
|
||||
// Draw filled polygon using Lyon tessellation
|
||||
if rotated_vertices.len() >= 3 {
|
||||
let absolute_vertices: Vec<Vec2> = rotated_vertices
|
||||
.iter()
|
||||
.map(|v| turtle.position + *v)
|
||||
.collect();
|
||||
|
||||
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 {
|
||||
// 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::commands::TurtleCommand;
|
||||
use crate::state::{DrawCommand, TurtleState, TurtleWorld};
|
||||
use crate::tessellation;
|
||||
use macroquad::prelude::*;
|
||||
|
||||
#[cfg(test)]
|
||||
@ -17,20 +18,19 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
|
||||
let dy = distance * state.heading.sin();
|
||||
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 {
|
||||
world.add_command(DrawCommand::Line {
|
||||
start,
|
||||
end: state.position,
|
||||
color: state.color,
|
||||
width: state.pen_width,
|
||||
});
|
||||
// Add circle at end point for smooth line joins
|
||||
world.add_command(DrawCommand::Circle {
|
||||
center: state.position,
|
||||
radius: state.pen_width / 2.0,
|
||||
color: state.color,
|
||||
filled: true,
|
||||
});
|
||||
// Draw line segment with round caps (caps handled by tessellate_stroke)
|
||||
if let Ok(mesh_data) = tessellation::tessellate_stroke(
|
||||
&[start, state.position],
|
||||
state.color,
|
||||
state.pen_width,
|
||||
false, // not closed
|
||||
) {
|
||||
world.add_command(DrawCommand::Mesh(mesh_data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,15 +50,18 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
|
||||
if state.pen_down {
|
||||
let (rotation_degrees, arc_degrees) = geom.draw_arc_params(*angle);
|
||||
|
||||
world.add_command(DrawCommand::Arc {
|
||||
center: geom.center,
|
||||
radius: *radius - state.pen_width, // Adjust radius for pen width to keep arc inside
|
||||
rotation: rotation_degrees,
|
||||
arc: arc_degrees,
|
||||
color: state.color,
|
||||
width: state.pen_width,
|
||||
sides: *steps as u8,
|
||||
});
|
||||
// Use Lyon to tessellate the arc
|
||||
if let Ok(mesh_data) = tessellation::tessellate_arc(
|
||||
geom.center,
|
||||
*radius,
|
||||
rotation_degrees,
|
||||
arc_degrees,
|
||||
state.color,
|
||||
state.pen_width,
|
||||
*steps as u8,
|
||||
) {
|
||||
world.add_command(DrawCommand::Mesh(mesh_data));
|
||||
}
|
||||
}
|
||||
|
||||
// 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::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 => {
|
||||
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 => {
|
||||
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) => {
|
||||
@ -101,20 +127,19 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
|
||||
let start = state.position;
|
||||
state.position = *coord;
|
||||
|
||||
// Record vertex for fill if filling
|
||||
state.record_fill_vertex();
|
||||
|
||||
if state.pen_down {
|
||||
world.add_command(DrawCommand::Line {
|
||||
start,
|
||||
end: state.position,
|
||||
color: state.color,
|
||||
width: state.pen_width,
|
||||
});
|
||||
// Add circle at end point for smooth line joins
|
||||
world.add_command(DrawCommand::Circle {
|
||||
center: state.position,
|
||||
radius: state.pen_width / 2.0,
|
||||
color: state.color,
|
||||
filled: true,
|
||||
});
|
||||
// Draw line segment with round caps
|
||||
if let Ok(mesh_data) = tessellation::tessellate_stroke(
|
||||
&[start, state.position],
|
||||
state.color,
|
||||
state.pen_width,
|
||||
false, // not closed
|
||||
) {
|
||||
world.add_command(DrawCommand::Mesh(mesh_data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,6 +154,53 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
|
||||
TurtleCommand::HideTurtle => {
|
||||
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 {
|
||||
TurtleCommand::Move(_) | TurtleCommand::Goto(_) => {
|
||||
if start_state.pen_down {
|
||||
world.add_command(DrawCommand::Line {
|
||||
start: start_state.position,
|
||||
end: end_state.position,
|
||||
color: start_state.color,
|
||||
width: start_state.pen_width,
|
||||
});
|
||||
// Add circle at end point for smooth line joins
|
||||
world.add_command(DrawCommand::Circle {
|
||||
center: end_state.position,
|
||||
radius: start_state.pen_width / 2.0,
|
||||
color: start_state.color,
|
||||
filled: true,
|
||||
});
|
||||
// Draw line segment with round caps
|
||||
if let Ok(mesh_data) = tessellation::tessellate_stroke(
|
||||
&[start_state.position, end_state.position],
|
||||
start_state.color,
|
||||
start_state.pen_width,
|
||||
false, // not closed
|
||||
) {
|
||||
world.add_command(DrawCommand::Mesh(mesh_data));
|
||||
}
|
||||
}
|
||||
}
|
||||
TurtleCommand::Circle {
|
||||
@ -172,31 +240,23 @@ pub fn add_draw_for_completed_tween(
|
||||
);
|
||||
let (rotation_degrees, arc_degrees) = geom.draw_arc_params(*angle);
|
||||
|
||||
world.add_command(DrawCommand::Arc {
|
||||
center: geom.center,
|
||||
radius: *radius - start_state.pen_width / 2.0,
|
||||
rotation: rotation_degrees,
|
||||
arc: arc_degrees,
|
||||
color: start_state.color,
|
||||
width: start_state.pen_width,
|
||||
sides: *steps as u8,
|
||||
});
|
||||
|
||||
// Add endpoint circles for smooth joins
|
||||
world.add_command(DrawCommand::Circle {
|
||||
center: start_state.position,
|
||||
radius: start_state.pen_width / 2.0,
|
||||
color: start_state.color,
|
||||
filled: true,
|
||||
});
|
||||
world.add_command(DrawCommand::Circle {
|
||||
center: end_state.position,
|
||||
radius: start_state.pen_width / 2.0,
|
||||
color: start_state.color,
|
||||
filled: true,
|
||||
});
|
||||
// Use Lyon to tessellate the arc
|
||||
if let Ok(mesh_data) = tessellation::tessellate_arc(
|
||||
geom.center,
|
||||
*radius,
|
||||
rotation_degrees,
|
||||
arc_degrees,
|
||||
start_state.color,
|
||||
start_state.pen_width,
|
||||
*steps as u8,
|
||||
) {
|
||||
world.add_command(DrawCommand::Mesh(mesh_data));
|
||||
}
|
||||
}
|
||||
}
|
||||
TurtleCommand::BeginFill | TurtleCommand::EndFill => {
|
||||
// No immediate drawing for fill commands, handled in execute_command
|
||||
}
|
||||
_ => {
|
||||
// Other commands don't create drawing
|
||||
}
|
||||
@ -223,6 +283,7 @@ mod tests {
|
||||
speed: AnimationSpeed::Animated(100.0),
|
||||
visible: true,
|
||||
shape: TurtleShape::turtle(),
|
||||
filling: None,
|
||||
};
|
||||
|
||||
// We'll use a dummy world but won't actually call drawing commands
|
||||
|
||||
@ -32,6 +32,7 @@ pub mod execution;
|
||||
pub mod general;
|
||||
pub mod shapes;
|
||||
pub mod state;
|
||||
pub mod tessellation;
|
||||
pub mod tweening;
|
||||
|
||||
// Re-export commonly used types
|
||||
@ -91,7 +92,8 @@ impl TurtleApp {
|
||||
self.handle_mouse_zoom();
|
||||
|
||||
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)
|
||||
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 macroquad::prelude::*;
|
||||
|
||||
/// State during active fill operation
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FillState {
|
||||
/// Starting position of the fill
|
||||
pub start_position: Coordinate,
|
||||
|
||||
/// All contours collected so far. Each contour is a separate closed path.
|
||||
/// The first contour is the outer boundary, subsequent contours are holes.
|
||||
pub contours: Vec<Vec<Coordinate>>,
|
||||
|
||||
/// Current contour being built (vertices for the active pen_down segment)
|
||||
pub current_contour: Vec<Coordinate>,
|
||||
|
||||
/// Fill color (cached from when begin_fill was called)
|
||||
pub fill_color: Color,
|
||||
}
|
||||
|
||||
/// State of a single turtle
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TurtleState {
|
||||
@ -16,6 +33,9 @@ pub struct TurtleState {
|
||||
pub speed: AnimationSpeed,
|
||||
pub visible: bool,
|
||||
pub shape: TurtleShape,
|
||||
|
||||
// Fill tracking
|
||||
pub filling: Option<FillState>,
|
||||
}
|
||||
|
||||
impl Default for TurtleState {
|
||||
@ -30,6 +50,7 @@ impl Default for TurtleState {
|
||||
speed: AnimationSpeed::default(),
|
||||
visible: true,
|
||||
shape: TurtleShape::turtle(),
|
||||
filling: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -42,36 +63,168 @@ impl TurtleState {
|
||||
pub fn heading_angle(&self) -> Angle {
|
||||
Angle::radians(self.heading)
|
||||
}
|
||||
|
||||
/// Start recording fill vertices
|
||||
pub fn begin_fill(&mut self, fill_color: Color) {
|
||||
self.filling = Some(FillState {
|
||||
start_position: self.position,
|
||||
contours: Vec::new(),
|
||||
current_contour: vec![self.position],
|
||||
fill_color,
|
||||
});
|
||||
}
|
||||
|
||||
/// Record current position if filling and pen is down
|
||||
pub fn record_fill_vertex(&mut self) {
|
||||
if let Some(ref mut fill_state) = self.filling {
|
||||
if self.pen_down {
|
||||
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
|
||||
/// All drawing is done via Lyon-tessellated meshes for consistency and quality
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DrawCommand {
|
||||
Line {
|
||||
start: Coordinate,
|
||||
end: Coordinate,
|
||||
color: Color,
|
||||
width: Precision,
|
||||
},
|
||||
Circle {
|
||||
center: Coordinate,
|
||||
radius: Precision,
|
||||
color: Color,
|
||||
filled: bool,
|
||||
},
|
||||
Arc {
|
||||
center: Coordinate,
|
||||
radius: Precision,
|
||||
rotation: Precision, // Start angle in degrees
|
||||
arc: Precision, // Arc extent in degrees
|
||||
color: Color,
|
||||
width: Precision,
|
||||
sides: u8, // Number of segments for quality
|
||||
},
|
||||
FilledPolygon {
|
||||
vertices: Vec<Coordinate>,
|
||||
color: Color,
|
||||
},
|
||||
/// Pre-tessellated mesh data (lines, arcs, circles, polygons - all use this)
|
||||
Mesh(MeshData),
|
||||
}
|
||||
|
||||
/// 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::commands::{CommandQueue, TurtleCommand};
|
||||
use crate::general::AnimationSpeed;
|
||||
use crate::state::TurtleState;
|
||||
use crate::state::{DrawCommand, TurtleState};
|
||||
use crate::tessellation;
|
||||
use macroquad::prelude::*;
|
||||
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
|
||||
/// Also takes commands vec to handle side effects like fill operations
|
||||
/// Each command has its own start_state and end_state pair
|
||||
pub fn update(
|
||||
&mut self,
|
||||
state: &mut TurtleState,
|
||||
commands: &mut Vec<crate::state::DrawCommand>,
|
||||
) -> Vec<(TurtleCommand, TurtleState, TurtleState)> {
|
||||
// In instant mode, execute commands up to the draw calls per frame limit
|
||||
if let AnimationSpeed::Instant(max_draw_calls) = self.speed {
|
||||
@ -105,15 +108,89 @@ impl TweenController {
|
||||
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
|
||||
let target_state = self.calculate_target_state(state, &command);
|
||||
*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
|
||||
let end_state = state.clone();
|
||||
|
||||
// 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));
|
||||
draw_call_count += 1;
|
||||
|
||||
@ -197,13 +274,92 @@ impl TweenController {
|
||||
*state = tween.target_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();
|
||||
self.current_tween = None;
|
||||
|
||||
// Only return command if it creates drawable elements
|
||||
if Self::command_creates_drawing(&completed_command) {
|
||||
return vec![(completed_command, start_state, end_state)];
|
||||
// Handle fill commands that have side effects
|
||||
match &completed_command {
|
||||
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() {
|
||||
let command_clone = command.clone();
|
||||
|
||||
// Handle SetSpeed command specially
|
||||
if let TurtleCommand::SetSpeed(new_speed) = &command_clone {
|
||||
state.set_speed(*new_speed);
|
||||
self.speed = *new_speed;
|
||||
// If switched to instant mode, process commands immediately
|
||||
if matches!(self.speed, AnimationSpeed::Instant(_)) {
|
||||
return self.update(state); // Recursively process in instant mode
|
||||
// Handle commands that should execute immediately (no animation)
|
||||
match &command_clone {
|
||||
TurtleCommand::SetSpeed(new_speed) => {
|
||||
state.set_speed(*new_speed);
|
||||
self.speed = *new_speed;
|
||||
// If switched to instant mode, process commands immediately
|
||||
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
|
||||
return self.update(state);
|
||||
TurtleCommand::PenUp => {
|
||||
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
|
||||
@ -375,6 +567,10 @@ impl TweenController {
|
||||
TurtleCommand::SetFillColor(color) => {
|
||||
target.fill_color = *color;
|
||||
}
|
||||
TurtleCommand::BeginFill | TurtleCommand::EndFill => {
|
||||
// Fill commands don't change turtle state for tweening purposes
|
||||
// They're handled directly in execution
|
||||
}
|
||||
}
|
||||
|
||||
target
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user