diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 074e9c1..5251f3b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -2,59 +2,73 @@ ## 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 ``` turtlers/ ├── 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`) -### Core Design Pattern: Command Queue + Tweening -- **Builder API** (`TurtlePlan`) accumulates commands -- **Command Queue** stores execution plan -- **Tween Controller** interpolates between states for animation +### Core Design Pattern: Persistent Controllers + Command Queues +- **Builder API** (`TurtlePlan`) accumulates commands into immutable `CommandQueue` +- **TurtleApp** maintains persistent `Vec` (one per turtle with embedded turtle_id) +- **TweenController** manages command execution and animation state - **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 ``` src/ -├── lib.rs - Public API, TurtleApp (main loop), re-exports -├── builders.rs - Fluent API traits (forward/right/etc chain) -├── commands.rs - TurtleCommand enum (Move/Turn/Circle/etc) -├── execution.rs - Execute commands, update state -├── tweening.rs - Animation interpolation, speed control -├── drawing.rs - Render Lyon meshes with Macroquad -├── tessellation.rs - Lyon integration (polygons/strokes/fills/arcs) -├── state.rs - TurtleState, TurtleWorld, FillState -└── circle_geometry.rs - Arc/circle math +├── lib.rs - TurtleApp, multi-turtle API, channel integration +├── builders.rs - Fluent API traits (forward/right/circle/reset/etc) +├── commands.rs - TurtleCommand enum (Move/Turn/Circle/Reset/etc) +├── execution.rs - Command execution (immediate) + state updates +├── tweening.rs - Animation + tween interpolation (CommandTween embeds turtle_id) +├── drawing.rs - Lyon mesh rendering with Macroquad +├── state.rs - Turtle, TurtleParams, TurtleWorld (persistent state) +├── tessellation.rs - Lyon integration (polygons/strokes/fills/arcs) +├── circle_geometry.rs - Arc/circle math helpers +└── commands_channel.rs - Async channels for threading patterns ``` ### Critical Concepts -**1. Consolidated Commands** (reduces duplication): -- `Move(distance)` - negative = backward +**1. Consolidated Move Commands**: +- `Move(distance)` - negative = backward (no separate Backward) - `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): -- `FillState` tracks `Vec>` (multiple contours) -- `pen_up()` closes current contour, `pen_down()` opens new -- Lyon's EvenOdd fill rule auto-detects holes -- Example: Donut = outer circle + inner circle (2 contours) +**2. Fill System (Multi-Contour with Holes)**: +- `FillState` tracks `Vec>` (multiple closed contours) +- `pen_up()` closes current contour, `pen_down()` opens new one +- Lyon's EvenOdd fill rule auto-detects holes (inner contours with opposite winding) +- Example: Donut = outer circle (pen_down) → pen_up → inner circle → end_fill -**3. Speed Modes**: -- `< 999`: Animated with tweening -- `>= 999`: Instant execution -- Controlled via `SetSpeed` commands (dynamic switching) +**3. Animation Modes**: +- Speed `>= 999`: Instant mode (no tweening, executes immediately) +- Speed `< 999`: Animated mode (tweens with CubicInOut easing, ~duration based on distance/speed) +- Dynamic switching via `SetSpeed` command mid-animation -**4. Lyon Tessellation Pipeline**: -All drawing → Lyon → GPU mesh → Macroquad rendering -- ~410 lines eliminated vs manual triangulation -- Functions: `tessellate_polygon/stroke/circle/arc/multi_contour` +**4. Multi-Turtle Architecture**: +- Each turtle owns a persistent `TweenController` with embedded `turtle_id` +- Rendering finds active tween by checking `controller.current_tween().turtle_id` (not Vec index) +- 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 @@ -66,131 +80,149 @@ cargo test --package turtle-lib cargo clippy --package turtle-lib -- -Wclippy::pedantic \ -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 yinyang -cargo run --package turtle-lib --example cheese_macro -``` - -### Macro Crate -```bash -cargo build --package turtle-lib-macros +cargo run --package turtle-lib --example hangman_threaded ``` ### Code Quality Standards -- Clippy pedantic mode enabled -- Cast warnings allowed for graphics math -- All examples must build warning-free +- Clippy pedantic enabled (graphics math casts allowed) +- Examples must build warning-free - Use `#[must_use]` on builder methods +- Builder methods return `&mut Self` (never owned Self) for chaining ## Project-Specific Patterns -### 1. The `turtle_main` Macro (PREFERRED for examples) -Simplest way to create turtle programs: +### 1. Single Turtle (Default) ```rust -use turtle_lib::*; - -#[turtle_main("Window Title")] +#[turtle_main("Simple")] fn draw(turtle: &mut TurtlePlan) { turtle.forward(100.0).right(90.0); } ``` -Generates: window setup + render loop + quit handling (ESC/Q) - -### 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 +### 2. Multi-Turtle Direct Setup ```rust -let mut t = create_turtle(); -t.forward(100).right(90) - .set_pen_color(BLUE) - .circle_left(50.0, 360.0, 36) - .begin_fill() - .end_fill(); -let app = TurtleApp::new().with_commands(t.build()); +let mut app = TurtleApp::new(); +let t0_id = app.add_turtle(); // Default setup +let t1_id = app.add_turtle(); + +app.append_commands(t0_id, turtle1_plan.build()); +app.append_commands(t1_id, turtle2_plan.build()); ``` -### 4. Multi-Contour Fill Example +### 3. Threading Pattern (Hangman Example) ```rust -turtle.begin_fill(); -turtle.circle_left(100.0, 360.0, 72); // Outer circle -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 -``` +let mut app = TurtleApp::new(); +let turtle_tx = app.create_turtle_channel(100); -### 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 { - clear_background(WHITE); - app.update(); - app.render(); - next_frame().await; + let letter = get_input(); // Blocks + let mut plan = create_turtle(); + plan.forward(50.0); + 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 ### Adding New Turtle Command 1. Add variant to `TurtleCommand` enum in `commands.rs` -2. Implement builder method in `builders.rs` (chain with `self`) -3. Add execution logic in `execution.rs` -4. Update tessellation/rendering if needed +2. Implement builder method in `builders.rs` (always return `&mut Self`) +3. Add execution in `execution.rs` (for immediate state changes) or `tweening.rs` (for animated state) +4. If animated, implement in `calculate_target_state()` in `tweening.rs` +5. Update drawing/tessellation if needed -### Adding Example -- Prefer `turtle_main` macro for simplicity -- Use only `use turtle_lib::*;` -- Keep examples focused (one concept each) -- See `examples/hello_turtle.rs` for minimal template +### Adding an Example +- Use `turtle_main` macro for simplicity +- Import only `use turtle_lib::*;` (all exports included) +- For threading: use `create_turtle_channel()` + `process_commands()` +- Place in `turtle-lib/examples/` and update README examples -### Debugging Lyon Issues -- Enable tracing: `RUST_LOG=turtle_lib=debug cargo run` -- Check `tessellation.rs` for Lyon API usage -- EvenOdd fill rule: holes must have opposite winding +### Debugging Animation Issues +```bash +RUST_LOG=turtle_lib=debug cargo run --example yinyang +``` +- 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 ### Main Dependencies - `macroquad = "0.4"` - Window/rendering framework - `lyon = "1.0"` - Tessellation (fills, strokes, circles) -- `tween = "2.1.0"` - Animation easing -- `tracing = "0.1"` - Optional logging (zero overhead when unused) - -### Proc Macro Crate -- Separate crate required by Rust (proc-macro = true) -- Uses `syn`, `quote`, `proc-macro2` -- Generates full macroquad app boilerplate +- `tween = "2.1.0"` - Animation easing (CubicInOut) +- `tracing = "0.1"` - Optional logging (zero cost when unused) +- `crossbeam-channel` - Threading pattern support (if used) ## What NOT to Do -- Don't add `use macroquad::prelude::*` in examples when not required -- Don't manually triangulate - use Lyon functions -- Don't add commands for Forward/Backward separately (use Move) -- Don't create summary/comparison docs unless requested - -## Key Documentation Files - -- `README.md` - Main API docs -- `turtle-lib/README.md` - Library-specific docs -- `turtle-lib-macros/README.md` - Macro docs +- Don't derive `turtle_id` from Vec index for rendering (use embedded id) +- Don't add `use macroquad::prelude::*` without explicit need (causes unused imports) +- Don't manually triangulate—always use Lyon `tessellate_*` functions +- Don't separate Forward/Backward—use negative `Move` values +- Don't call `reset()` expecting to preserve drawing state—it clears everything ## Response Style -- Be concise, no extensive summaries -- No emojis in technical responses -- Focus on code solutions over explanations -- Use bullet points for lists -- Reference specific files when helpful +- Be concise, actionable, focused on code +- Reference specific files/lines when helpful +- Use examples from `examples/` directory +- No generic advice; focus on THIS project's patterns diff --git a/turtle-lib/src/builders.rs b/turtle-lib/src/builders.rs index a07e5df..50b3ac0 100644 --- a/turtle-lib/src/builders.rs +++ b/turtle-lib/src/builders.rs @@ -635,6 +635,41 @@ impl TurtlePlan { 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. /// /// Use this to finalize the turtle commands and pass them to `TurtleApp`. diff --git a/turtle-lib/src/commands.rs b/turtle-lib/src/commands.rs index 3ab3cd7..bedf417 100644 --- a/turtle-lib/src/commands.rs +++ b/turtle-lib/src/commands.rs @@ -42,6 +42,9 @@ pub enum TurtleCommand { // Fill operations BeginFill, EndFill, + + // Reset + Reset, } /// Queue of turtle commands with execution state diff --git a/turtle-lib/src/execution.rs b/turtle-lib/src/execution.rs index c9f208f..a54f3b5 100644 --- a/turtle-lib/src/execution.rs +++ b/turtle-lib/src/execution.rs @@ -30,7 +30,6 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle) state.begin_fill(fill_color); true } - TurtleCommand::EndFill => { if let Some(mut fill_state) = state.filling.take() { if !fill_state.current_contour.is_empty() { @@ -79,7 +78,6 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle) } true } - TurtleCommand::PenUp => { state.params.pen_down = false; if state.filling.is_some() { @@ -91,7 +89,6 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle) state.close_fill_contour(); true } - TurtleCommand::PenDown => { state.params.pen_down = true; if state.filling.is_some() { @@ -106,7 +103,23 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle) 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::HideTurtle => state.params.visible = false, + // Reset + TurtleCommand::Reset => { + state.reset(); + } + _ => {} // Already handled by execute_command_side_effects } diff --git a/turtle-lib/src/state.rs b/turtle-lib/src/state.rs index 4b99bea..58c740a 100644 --- a/turtle-lib/src/state.rs +++ b/turtle-lib/src/state.rs @@ -92,11 +92,18 @@ impl Turtle { 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) { - let id = self.turtle_id; - *self = Self::default(); - self.turtle_id = id; + // Clear all drawings + self.commands.clear(); + + // 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 diff --git a/turtle-lib/src/tweening.rs b/turtle-lib/src/tweening.rs index c6ce7d7..60ec351 100644 --- a/turtle-lib/src/tweening.rs +++ b/turtle-lib/src/tweening.rs @@ -436,6 +436,10 @@ impl TweenController { // Fill commands don't change turtle state for tweening purposes // They're handled directly in execution } + TurtleCommand::Reset => { + // Reset returns to default state + target = TurtleParams::default(); + } } target