Compare commits

...

2 Commits

Author SHA1 Message Date
14b93f657b improve hangman example 2025-10-18 08:28:36 +02:00
16430f3958 add turtle.reset 2025-10-18 08:28:17 +02:00
7 changed files with 243 additions and 189 deletions

View File

@ -2,59 +2,73 @@
## Project Overview ## Project Overview
Rust workspace with turtle graphics implementations. **Primary focus: `turtle-lib`** - lightweight library using Macroquad + Lyon for GPU-accelerated rendering. Rust workspace with turtle graphics implementations. **Primary focus: `turtle-lib`** - lightweight library using Macroquad + Lyon for GPU-accelerated rendering with multi-turtle support and optional threading.
### Workspace Structure ### Workspace Structure
``` ```
turtlers/ turtlers/
├── turtle-lib/ # MAIN LIBRARY - Macroquad + Lyon (focus here) ├── turtle-lib/ # MAIN LIBRARY - Macroquad + Lyon (focus here)
└── turtle-lib-macros/ # Proc macro for turtle_main ├── turtle-lib-macros/ # Proc macro for turtle_main
└── examples/ # 15+ examples including threading patterns
``` ```
## Architecture (`turtle-lib`) ## Architecture (`turtle-lib`)
### Core Design Pattern: Command Queue + Tweening ### Core Design Pattern: Persistent Controllers + Command Queues
- **Builder API** (`TurtlePlan`) accumulates commands - **Builder API** (`TurtlePlan`) accumulates commands into immutable `CommandQueue`
- **Command Queue** stores execution plan - **TurtleApp** maintains persistent `Vec<TweenController>` (one per turtle with embedded turtle_id)
- **Tween Controller** interpolates between states for animation - **TweenController** manages command execution and animation state
- **Lyon Tessellation** converts all primitives to GPU meshes - **Lyon Tessellation** converts all primitives to GPU meshes
- **Multi-Turtle** support: Create multiple turtles with `add_turtle()` or threading channels
### Key Architectural Decision: Turtle ID Storage
**Critical**: After recent refactoring, `turtle_id` is now **stored in TweenController** (not derived from Vec index). This makes rendering robust when turtles/controllers are sparse or deleted.
### Key Files ### Key Files
``` ```
src/ src/
├── lib.rs - Public API, TurtleApp (main loop), re-exports ├── lib.rs - TurtleApp, multi-turtle API, channel integration
├── builders.rs - Fluent API traits (forward/right/etc chain) ├── builders.rs - Fluent API traits (forward/right/circle/reset/etc)
├── commands.rs - TurtleCommand enum (Move/Turn/Circle/etc) ├── commands.rs - TurtleCommand enum (Move/Turn/Circle/Reset/etc)
├── execution.rs - Execute commands, update state ├── execution.rs - Command execution (immediate) + state updates
├── tweening.rs - Animation interpolation, speed control ├── tweening.rs - Animation + tween interpolation (CommandTween embeds turtle_id)
├── drawing.rs - Render Lyon meshes with Macroquad ├── drawing.rs - Lyon mesh rendering with Macroquad
├── tessellation.rs - Lyon integration (polygons/strokes/fills/arcs) ├── state.rs - Turtle, TurtleParams, TurtleWorld (persistent state)
├── state.rs - TurtleState, TurtleWorld, FillState ├── tessellation.rs - Lyon integration (polygons/strokes/fills/arcs)
└── circle_geometry.rs - Arc/circle math ├── circle_geometry.rs - Arc/circle math helpers
└── commands_channel.rs - Async channels for threading patterns
``` ```
### Critical Concepts ### Critical Concepts
**1. Consolidated Commands** (reduces duplication): **1. Consolidated Move Commands**:
- `Move(distance)` - negative = backward - `Move(distance)` - negative = backward (no separate Backward)
- `Turn(angle)` - positive = right, negative = left (degrees) - `Turn(angle)` - positive = right, negative = left (degrees)
- `Circle{radius, angle, steps, direction}` - unified left/right - `Circle{radius, angle, steps, direction}` - unified left/right via CircleDirection
- `Reset` - clears drawings, animations, fill state; resets params to defaults
**2. Fill System** (multi-contour with holes): **2. Fill System (Multi-Contour with Holes)**:
- `FillState` tracks `Vec<Vec<Vec2>>` (multiple contours) - `FillState` tracks `Vec<Vec<Coordinate>>` (multiple closed contours)
- `pen_up()` closes current contour, `pen_down()` opens new - `pen_up()` closes current contour, `pen_down()` opens new one
- Lyon's EvenOdd fill rule auto-detects holes - Lyon's EvenOdd fill rule auto-detects holes (inner contours with opposite winding)
- Example: Donut = outer circle + inner circle (2 contours) - Example: Donut = outer circle (pen_down) → pen_up → inner circle → end_fill
**3. Speed Modes**: **3. Animation Modes**:
- `< 999`: Animated with tweening - Speed `>= 999`: Instant mode (no tweening, executes immediately)
- `>= 999`: Instant execution - Speed `< 999`: Animated mode (tweens with CubicInOut easing, ~duration based on distance/speed)
- Controlled via `SetSpeed` commands (dynamic switching) - Dynamic switching via `SetSpeed` command mid-animation
**4. Lyon Tessellation Pipeline**: **4. Multi-Turtle Architecture**:
All drawing → Lyon → GPU mesh → Macroquad rendering - Each turtle owns a persistent `TweenController` with embedded `turtle_id`
- ~410 lines eliminated vs manual triangulation - Rendering finds active tween by checking `controller.current_tween().turtle_id` (not Vec index)
- Functions: `tessellate_polygon/stroke/circle/arc/multi_contour` - Supports concurrent animation of multiple turtles
- Example: Hangman uses `turtle_command_channel()` for blocking stdin on separate thread
**5. Threading Pattern** (for interactive apps like Hangman):
- `create_turtle_channel(buffer_size)` returns `TurtleCommandSender` (clonable, Send)
- Spawn game logic on thread, send `CommandQueue` batches via channel
- Main render loop calls `app.process_commands()` before `update()` to drain channels
- Example: `hangman_threaded.rs` spawns stdin reader, sends drawing commands when player guesses
## Developer Workflows ## Developer Workflows
@ -66,131 +80,149 @@ cargo test --package turtle-lib
cargo clippy --package turtle-lib -- -Wclippy::pedantic \ cargo clippy --package turtle-lib -- -Wclippy::pedantic \
-Aclippy::cast_precision_loss -Aclippy::cast_sign_loss -Aclippy::cast_possible_truncation -Aclippy::cast_precision_loss -Aclippy::cast_sign_loss -Aclippy::cast_possible_truncation
# Run examples (15+ examples available) # Run examples
cargo run --package turtle-lib --example hello_turtle cargo run --package turtle-lib --example hello_turtle
cargo run --package turtle-lib --example yinyang cargo run --package turtle-lib --example yinyang
cargo run --package turtle-lib --example cheese_macro cargo run --package turtle-lib --example hangman_threaded
```
### Macro Crate
```bash
cargo build --package turtle-lib-macros
``` ```
### Code Quality Standards ### Code Quality Standards
- Clippy pedantic mode enabled - Clippy pedantic enabled (graphics math casts allowed)
- Cast warnings allowed for graphics math - Examples must build warning-free
- All examples must build warning-free
- Use `#[must_use]` on builder methods - Use `#[must_use]` on builder methods
- Builder methods return `&mut Self` (never owned Self) for chaining
## Project-Specific Patterns ## Project-Specific Patterns
### 1. The `turtle_main` Macro (PREFERRED for examples) ### 1. Single Turtle (Default)
Simplest way to create turtle programs:
```rust ```rust
use turtle_lib::*; #[turtle_main("Simple")]
#[turtle_main("Window Title")]
fn draw(turtle: &mut TurtlePlan) { fn draw(turtle: &mut TurtlePlan) {
turtle.forward(100.0).right(90.0); turtle.forward(100.0).right(90.0);
} }
``` ```
Generates: window setup + render loop + quit handling (ESC/Q) ### 2. Multi-Turtle Direct Setup
### 2. Import Convention
Only need: `use turtle_lib::*;`
- Re-exports: `vec2`, `RED/BLUE/GREEN/etc`, all turtle types
- No `use macroquad::prelude::*` needed (causes unused warnings)
### 3. Builder Chain Pattern
```rust ```rust
let mut t = create_turtle(); let mut app = TurtleApp::new();
t.forward(100).right(90) let t0_id = app.add_turtle(); // Default setup
.set_pen_color(BLUE) let t1_id = app.add_turtle();
.circle_left(50.0, 360.0, 36)
.begin_fill() app.append_commands(t0_id, turtle1_plan.build());
.end_fill(); app.append_commands(t1_id, turtle2_plan.build());
let app = TurtleApp::new().with_commands(t.build());
``` ```
### 4. Multi-Contour Fill Example ### 3. Threading Pattern (Hangman Example)
```rust ```rust
turtle.begin_fill(); let mut app = TurtleApp::new();
turtle.circle_left(100.0, 360.0, 72); // Outer circle let turtle_tx = app.create_turtle_channel(100);
turtle.pen_up(); // Closes contour
turtle.goto(vec2(0.0, -30.0));
turtle.pen_down(); // Opens new contour
turtle.circle_left(30.0, 360.0, 36); // Inner (becomes hole)
turtle.end_fill(); // EvenOdd rule creates donut
```
### 5. Manual Setup (advanced control)
```rust
#[macroquad::main("Custom")]
async fn main() {
let mut turtle = create_turtle();
// ... drawing code ...
let mut app = TurtleApp::new().with_commands(turtle.build());
// Spawn game thread
let tx = turtle_tx.clone();
std::thread::spawn(move || {
loop { loop {
clear_background(WHITE); let letter = get_input(); // Blocks
app.update(); let mut plan = create_turtle();
app.render(); plan.forward(50.0);
next_frame().await; tx.send(plan.build()).ok();
} }
});
// Main loop
loop {
clear_background(WHITE);
app.process_commands(); // ← Drains channel
app.update();
app.render();
next_frame().await;
} }
``` ```
### 4. Multi-Contour Fills (Donut)
```rust
turtle.set_fill_color(BLUE)
.begin_fill()
.circle_left(100.0, 360.0, 72); // Outer
turtle.pen_up()
.go_to(vec2(0.0, -30.0))
.pen_down()
.circle_left(30.0, 360.0, 36); // Inner (hole)
turtle.end_fill(); // EvenOdd creates hole
```
### 5. Reset Turtle
```rust
turtle.forward(100.0)
.reset() // Clears drawings, resets to defaults
.forward(50.0); // Fresh start
```
## Common Tasks ## Common Tasks
### Adding New Turtle Command ### Adding New Turtle Command
1. Add variant to `TurtleCommand` enum in `commands.rs` 1. Add variant to `TurtleCommand` enum in `commands.rs`
2. Implement builder method in `builders.rs` (chain with `self`) 2. Implement builder method in `builders.rs` (always return `&mut Self`)
3. Add execution logic in `execution.rs` 3. Add execution in `execution.rs` (for immediate state changes) or `tweening.rs` (for animated state)
4. Update tessellation/rendering if needed 4. If animated, implement in `calculate_target_state()` in `tweening.rs`
5. Update drawing/tessellation if needed
### Adding Example ### Adding an Example
- Prefer `turtle_main` macro for simplicity - Use `turtle_main` macro for simplicity
- Use only `use turtle_lib::*;` - Import only `use turtle_lib::*;` (all exports included)
- Keep examples focused (one concept each) - For threading: use `create_turtle_channel()` + `process_commands()`
- See `examples/hello_turtle.rs` for minimal template - Place in `turtle-lib/examples/` and update README examples
### Debugging Lyon Issues ### Debugging Animation Issues
- Enable tracing: `RUST_LOG=turtle_lib=debug cargo run` ```bash
- Check `tessellation.rs` for Lyon API usage RUST_LOG=turtle_lib=debug cargo run --example yinyang
- EvenOdd fill rule: holes must have opposite winding ```
- Check `tweening.rs` for state transitions
- Verify `command_creates_drawing()` includes your command type
- Circle direction: Left = counter-clockwise, Right = clockwise
## Critical Implementation Details
### TurtleCommand::Reset Behavior
- Clears `Turtle::commands` (all drawings)
- Clears `Turtle::filling` (ongoing fill operations)
- Resets `TurtleParams` to defaults (position 0,0, heading 0, pen down, etc.)
- Preserves `turtle_id` after reset
- Called via `execute_command()` in both instant and animated modes
### Turtle ID Robustness
- **Before**: `turtle_id` derived from Vec index (fragile if controllers deleted)
- **After**: `turtle_id` embedded in `TweenController` and `CommandTween`
- Rendering finds active tween via `find_map(|c| c.current_tween())` → uses `tween.turtle_id` directly
- Safe for sparse/dynamic turtle creation
### Lyon Tessellation
- All drawing → `tessellate_arc/stroke/circle/multi_contour``MeshData` → Macroquad `Mesh`
- Circle direction affects angle stepping: Left subtracts, Right adds
- Start angle for circles: `geom.start_angle_from_center.to_degrees()` (NOT rotation)
- EvenOdd fill rule: holes detected by contour winding (no explicit flag needed)
## Dependencies & Integration ## Dependencies & Integration
### Main Dependencies ### Main Dependencies
- `macroquad = "0.4"` - Window/rendering framework - `macroquad = "0.4"` - Window/rendering framework
- `lyon = "1.0"` - Tessellation (fills, strokes, circles) - `lyon = "1.0"` - Tessellation (fills, strokes, circles)
- `tween = "2.1.0"` - Animation easing - `tween = "2.1.0"` - Animation easing (CubicInOut)
- `tracing = "0.1"` - Optional logging (zero overhead when unused) - `tracing = "0.1"` - Optional logging (zero cost when unused)
- `crossbeam-channel` - Threading pattern support (if used)
### Proc Macro Crate
- Separate crate required by Rust (proc-macro = true)
- Uses `syn`, `quote`, `proc-macro2`
- Generates full macroquad app boilerplate
## What NOT to Do ## What NOT to Do
- Don't add `use macroquad::prelude::*` in examples when not required - Don't derive `turtle_id` from Vec index for rendering (use embedded id)
- Don't manually triangulate - use Lyon functions - Don't add `use macroquad::prelude::*` without explicit need (causes unused imports)
- Don't add commands for Forward/Backward separately (use Move) - Don't manually triangulate—always use Lyon `tessellate_*` functions
- Don't create summary/comparison docs unless requested - Don't separate Forward/Backward—use negative `Move` values
- Don't call `reset()` expecting to preserve drawing state—it clears everything
## Key Documentation Files
- `README.md` - Main API docs
- `turtle-lib/README.md` - Library-specific docs
- `turtle-lib-macros/README.md` - Macro docs
## Response Style ## Response Style
- Be concise, no extensive summaries - Be concise, actionable, focused on code
- No emojis in technical responses - Reference specific files/lines when helpful
- Focus on code solutions over explanations - Use examples from `examples/` directory
- Use bullet points for lists - No generic advice; focus on THIS project's patterns
- Reference specific files when helpful

View File

@ -34,23 +34,18 @@ async fn main() {
let lines_tx = app.create_turtle_channel(100); let lines_tx = app.create_turtle_channel(100);
let smiley_tx = app.create_turtle_channel(100); let smiley_tx = app.create_turtle_channel(100);
// Channel for game logic to communicate with render thread
let (tx, rx) = mpsc::channel();
// Spawn game logic thread // Spawn game logic thread
let game_thread = thread::spawn({ let game_thread = thread::spawn({
let hangman = hangman_tx.clone(); let hangman = hangman_tx.clone();
let lines = lines_tx.clone(); let lines = lines_tx.clone();
let smiley = smiley_tx.clone(); let smiley = smiley_tx.clone();
let tx = tx.clone();
move || { move || {
run_game_logic(hangman, lines, smiley, tx); run_game_logic(hangman, lines, smiley);
} }
}); });
// Main render loop // Main render loop
let mut frame = 0;
loop { loop {
// Check for quit // Check for quit
if macroquad::prelude::is_key_pressed(macroquad::prelude::KeyCode::Escape) if macroquad::prelude::is_key_pressed(macroquad::prelude::KeyCode::Escape)
@ -59,31 +54,12 @@ async fn main() {
break; break;
} }
// Process incoming commands from game thread
while let Ok(msg) = rx.try_recv() {
match msg {
GameMessage::GameOver { won, word } => {
if won {
println!("🎉 You Won! The word was: {}", word);
} else {
println!("💀 You Lost! The word was: {}", word);
}
break;
}
}
}
// Clear and render // Clear and render
macroquad::prelude::clear_background(WHITE); macroquad::prelude::clear_background(WHITE);
app.process_commands(); app.process_commands();
app.update(); app.update();
app.render(); app.render();
frame += 1;
if frame % 60 == 0 {
println!("Rendered {} frames", frame / 60);
}
macroquad::prelude::next_frame().await; macroquad::prelude::next_frame().await;
} }
@ -100,13 +76,12 @@ fn run_game_logic(
hangman_tx: TurtleCommandSender, hangman_tx: TurtleCommandSender,
lines_tx: TurtleCommandSender, lines_tx: TurtleCommandSender,
smiley_tx: TurtleCommandSender, smiley_tx: TurtleCommandSender,
tx: mpsc::Sender<GameMessage>,
) { ) {
let secret = choose_word(); let secret = choose_word();
println!("Starting hangman game..."); println!("Starting hangman game...");
println!("Secret word has {} letters", secret.len()); println!("Secret word has {} letters", secret.len());
// Setup: Position hangman turtle and draw base (hill + mast) // Setup: Position hangman turtle and draw base (hill)
{ {
let mut plan = create_turtle_plan(); let mut plan = create_turtle_plan();
setup_hangman(&mut plan); setup_hangman(&mut plan);
@ -114,37 +89,23 @@ fn run_game_logic(
hangman_tx.send(plan.build()).ok(); hangman_tx.send(plan.build()).ok();
} }
// Give render thread time to process
std::thread::sleep(std::time::Duration::from_millis(100));
let mut all_guesses = String::new(); let mut all_guesses = String::new();
let mut wrong_guesses = 0; let mut wrong_guesses = 0;
const MAX_WRONG: usize = 8; // 8 body parts after base const MAX_WRONG: usize = 8; // 8 body parts after base
// Draw current state of lines
draw_lines_state(&lines_tx, &secret, &all_guesses);
// Main game loop // Main game loop
loop { loop {
// Draw current state of lines
draw_lines_state(&lines_tx, &secret, &all_guesses);
// Check if won // Check if won
if secret.chars().all(|c| all_guesses.contains(c)) { if secret.chars().all(|c| all_guesses.contains(c)) {
draw_smiley(&smiley_tx, true); draw_smiley(&smiley_tx, true);
tx.send(GameMessage::GameOver {
won: true,
word: secret.to_string(),
})
.ok();
break; break;
} }
// Check if lost // Check if lost
if wrong_guesses >= MAX_WRONG { if wrong_guesses >= MAX_WRONG {
draw_smiley(&smiley_tx, false); draw_smiley(&smiley_tx, false);
tx.send(GameMessage::GameOver {
won: false,
word: secret.to_string(),
})
.ok();
break; break;
} }
@ -152,16 +113,12 @@ fn run_game_logic(
let guess = ask_for_letter(); let guess = ask_for_letter();
let guess_lower = guess.to_lowercase(); let guess_lower = guess.to_lowercase();
// Check if already guessed
if all_guesses.contains(&guess_lower) {
println!("You already guessed '{}'", guess_lower);
continue;
}
all_guesses.push_str(&guess_lower); all_guesses.push_str(&guess_lower);
if secret.contains(&guess_lower) { if secret.contains(&guess_lower) {
println!("✓ Correct! '{}' is in the word", guess_lower); println!("✓ Correct! '{}' is in the word", guess_lower);
// Draw current state of lines
draw_lines_state(&lines_tx, &secret, &all_guesses);
} else { } else {
println!("✗ Wrong! '{}' is NOT in the word", guess_lower); println!("✗ Wrong! '{}' is NOT in the word", guess_lower);
wrong_guesses += 1; wrong_guesses += 1;
@ -221,9 +178,9 @@ fn draw_hangman_step(tx: &TurtleCommandSender, step: usize) {
// Hangman drawing functions (scaled down for visibility) // Hangman drawing functions (scaled down for visibility)
fn draw_hill(plan: &mut TurtlePlan) { fn draw_hill(plan: &mut TurtlePlan) {
plan.circle_left(50.0, 180.0, 36) plan.left(135.0)
.left(180.0) .circle_left(100.0, 90.0, 36)
.circle_right(50.0, 90.0, 36) .circle_left(100.0, -45.0, 36)
.right(90.0); .right(90.0);
} }
@ -278,24 +235,21 @@ fn draw_legs(plan: &mut TurtlePlan) {
fn draw_lines_state(tx: &TurtleCommandSender, secret: &str, all_guesses: &str) { fn draw_lines_state(tx: &TurtleCommandSender, secret: &str, all_guesses: &str) {
let mut plan = create_turtle_plan(); let mut plan = create_turtle_plan();
plan.hide() plan.reset()
.set_speed(1001) // Instant mode //.hide()
.set_pen_color(BLACK) .set_pen_color(BLACK)
.set_pen_width(2.0) .set_pen_width(2.0)
.pen_up() .pen_up()
.go_to(vec2(-100.0, 100.0)) // Top of screen .go_to(vec2(-100.0, 100.0))
.pen_down() .pen_down();
.right(90.0);
// Print word state in console // Print word state in console
print!("Word: "); print!("Word: ");
for letter in secret.chars() { for letter in secret.chars() {
if all_guesses.contains(letter) { if all_guesses.contains(letter) {
print!("{} ", letter); print!("{} ", letter);
plan.forward(20.0);
} else { } else {
print!("_ "); print!("_ ");
plan.forward(20.0);
} }
} }
println!(); println!();
@ -305,20 +259,21 @@ fn draw_lines_state(tx: &TurtleCommandSender, secret: &str, all_guesses: &str) {
if all_guesses.contains(letter) { if all_guesses.contains(letter) {
// Draw green circle for revealed letter // Draw green circle for revealed letter
plan.pen_up() plan.pen_up()
.forward(2.5)
.right(90.0) .right(90.0)
.forward(2.5)
.set_pen_color(GREEN) .set_pen_color(GREEN)
.pen_down() .pen_down()
.circle_left(7.5, 360.0, 24) .circle_left(7.5, 360.0, 24)
.set_pen_color(BLACK) .pen_up()
.left(90.0)
.backward(2.5) .backward(2.5)
.pen_up(); .left(90.0)
.set_pen_color(BLACK)
.pen_down();
} else { } else {
// Draw black underscore // Draw black underscore
plan.forward(5.0); plan.forward(15.0);
} }
plan.forward(15.0).pen_down(); plan.pen_up().forward(15.0).pen_down();
} }
tx.send(plan.build()).ok(); tx.send(plan.build()).ok();

View File

@ -635,6 +635,41 @@ impl TurtlePlan {
self self
} }
/// Resets the turtle to its default state.
///
/// This clears all drawings, clears the animation queue, and resets all turtle parameters:
/// - Position: (0, 0)
/// - Heading: 0° (facing right)
/// - Pen: down
/// - Pen width: 2.0
/// - Pen color: black
/// - Fill color: none
/// - Visibility: visible
/// - Shape: arrow
/// - Speed: default
///
/// # Examples
///
/// ```no_run
/// # use turtle_lib::*;
/// #
/// #[turtle_main("Reset Example")]
/// fn draw(turtle: &mut TurtlePlan) {
/// // Draw something
/// turtle.forward(100.0);
///
/// // Reset everything back to default
/// turtle.reset();
///
/// // Start fresh
/// turtle.forward(50.0);
/// }
/// ```
pub fn reset(&mut self) -> &mut Self {
self.queue.push(TurtleCommand::Reset);
self
}
/// Consumes the `TurtlePlan` and returns the command queue. /// Consumes the `TurtlePlan` and returns the command queue.
/// ///
/// Use this to finalize the turtle commands and pass them to `TurtleApp`. /// Use this to finalize the turtle commands and pass them to `TurtleApp`.

View File

@ -42,6 +42,9 @@ pub enum TurtleCommand {
// Fill operations // Fill operations
BeginFill, BeginFill,
EndFill, EndFill,
// Reset
Reset,
} }
/// Queue of turtle commands with execution state /// Queue of turtle commands with execution state

View File

@ -30,7 +30,6 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle)
state.begin_fill(fill_color); state.begin_fill(fill_color);
true true
} }
TurtleCommand::EndFill => { TurtleCommand::EndFill => {
if let Some(mut fill_state) = state.filling.take() { if let Some(mut fill_state) = state.filling.take() {
if !fill_state.current_contour.is_empty() { if !fill_state.current_contour.is_empty() {
@ -79,7 +78,6 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle)
} }
true true
} }
TurtleCommand::PenUp => { TurtleCommand::PenUp => {
state.params.pen_down = false; state.params.pen_down = false;
if state.filling.is_some() { if state.filling.is_some() {
@ -91,7 +89,6 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle)
state.close_fill_contour(); state.close_fill_contour();
true true
} }
TurtleCommand::PenDown => { TurtleCommand::PenDown => {
state.params.pen_down = true; state.params.pen_down = true;
if state.filling.is_some() { if state.filling.is_some() {
@ -106,7 +103,23 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle)
true true
} }
_ => false, // Not a side-effect-only command TurtleCommand::Reset => {
state.reset();
true
}
TurtleCommand::Move(_)
| TurtleCommand::Turn(_)
| TurtleCommand::Circle { .. }
| TurtleCommand::Goto(_)
| TurtleCommand::SetColor(_)
| TurtleCommand::SetFillColor(_)
| TurtleCommand::SetPenWidth(_)
| TurtleCommand::SetSpeed(_)
| TurtleCommand::SetShape(_)
| TurtleCommand::SetHeading(_)
| TurtleCommand::ShowTurtle
| TurtleCommand::HideTurtle => false,
} }
} }
@ -248,6 +261,11 @@ pub fn execute_command(command: &TurtleCommand, state: &mut Turtle) {
TurtleCommand::ShowTurtle => state.params.visible = true, TurtleCommand::ShowTurtle => state.params.visible = true,
TurtleCommand::HideTurtle => state.params.visible = false, TurtleCommand::HideTurtle => state.params.visible = false,
// Reset
TurtleCommand::Reset => {
state.reset();
}
_ => {} // Already handled by execute_command_side_effects _ => {} // Already handled by execute_command_side_effects
} }

View File

@ -92,11 +92,18 @@ impl Turtle {
Angle::radians(self.params.heading) Angle::radians(self.params.heading)
} }
/// Reset turtle to default state (preserves turtle_id) /// Reset turtle to default state (preserves turtle_id and queued commands)
pub fn reset(&mut self) { pub fn reset(&mut self) {
let id = self.turtle_id; // Clear all drawings
*self = Self::default(); self.commands.clear();
self.turtle_id = id;
// Clear fill state
self.filling = None;
// Reset parameters to defaults
self.params = TurtleParams::default();
// Keep turtle_id and tween_controller (preserves queued commands)
} }
/// Start recording fill vertices /// Start recording fill vertices

View File

@ -436,6 +436,10 @@ impl TweenController {
// Fill commands don't change turtle state for tweening purposes // Fill commands don't change turtle state for tweening purposes
// They're handled directly in execution // They're handled directly in execution
} }
TurtleCommand::Reset => {
// Reset returns to default state
target = TurtleParams::default();
}
} }
target target