apply clippy pedantic

This commit is contained in:
Franz Dietrich 2025-10-12 15:30:48 +02:00
parent 033a1982fc
commit 453e8e39bd
14 changed files with 259 additions and 108 deletions

105
.copilot/instructions.md Normal file
View File

@ -0,0 +1,105 @@
## Project Context
- **turtle-lib**: Heavy Bevy-based turtle graphics (0.17.1) with ECS architecture
- **turtle-lib-macroquad**: Lightweight macroquad implementation with Lyon tessellation (current focus)
- **turtle-lyon-poc**: Proof of concept for Lyon (COMPLETED - integrated into main crate)
- **turtle-skia-poc**: Alternative tiny-skia rendering approach
- **Status**: Lyon migration complete, fill quality issues resolved
## Architecture
```
turtle-lib-macroquad/src/
├── lib.rs - Public API & TurtleApp
├── state.rs - TurtleState & TurtleWorld
├── commands.rs - TurtleCommand & CommandQueue
├── builders.rs - Builder traits (DirectionalMovement, Turnable, CurvedMovement)
├── execution.rs - Command execution
├── tweening.rs - Animation controller
├── drawing.rs - Macroquad rendering
├── tessellation.rs - Lyon tessellation (355 lines - polygons, circles, arcs, strokes)
├── circle_geometry.rs - Circle/arc geometry calculations
├── shapes.rs - Turtle shapes
└── general/ - Type definitions (Angle, Length, Color, etc.)
```
## Current Status
1. ✅ **Lyon integration complete**: Using Lyon 1.0 for all tessellation
2. ✅ **Fill quality fixed**: EvenOdd fill rule handles complex fills and holes automatically
3. ✅ **Simplified codebase**: Replaced manual triangulation with Lyon's GPU-optimized tessellation
4. ✅ **Full feature set**: Polygons, circles, arcs, strokes all using Lyon
## Key Features
- **Builder API**: Fluent interface for turtle commands
- **Animation system**: Tweening controller with configurable speeds (Instant/Animated)
- **Lyon tessellation**: Automatic hole detection, proper winding order, GPU-optimized
- **Fill support**: Multi-contour fills with automatic hole handling
- **Shapes**: Arrow, circle, square, triangle, classic turtle shapes
## Response Style Rules
- NO emoji/smileys
- NO extensive summaries
- Use bullet points for lists
- Be concise and direct
- Focus on code solutions
# Tools to use
- when in doubt you can always use #fetch to get additional docs and online information.
- when the userinput is incomplete generate a brief text and let the user confirm your understanding.
## Code Patterns
### Lyon Tessellation (Current)
```rust
// tessellation.rs - Lyon integration
pub fn tessellate_polygon(vertices: &[Vec2], color: Color) -> Result<MeshData, Box<dyn std::error::Error>>
pub fn tessellate_multi_contour(contours: &[Vec<Vec2>], color: Color) -> Result<MeshData, Box<dyn std::error::Error>>
pub fn tessellate_stroke(vertices: &[Vec2], color: Color, width: f32, closed: bool) -> Result<MeshData, Box<dyn std::error::Error>>
pub fn tessellate_circle(center: Vec2, radius: f32, color: Color, filled: bool, stroke_width: f32) -> Result<MeshData, Box<dyn std::error::Error>>
pub fn tessellate_arc(center: Vec2, radius: f32, start_angle: f32, arc_angle: f32, color: Color, stroke_width: f32, segments: usize) -> Result<MeshData, Box<dyn std::error::Error>>
```
### Fill with Holes
```rust
// Multi-contour fills automatically detect holes using EvenOdd fill rule
let contours = vec![outer_boundary, hole1, hole2];
let mesh = tessellate_multi_contour(&contours, color)?;
```
## Builder API
```rust
let mut t = create_turtle();
t.forward(100).right(90)
.circle_left(50.0, 180.0, 36)
.begin_fill()
.set_fill_color(BLACK)
.circle_left(90.0, 180.0, 36)
.end_fill();
let app = TurtleApp::new().with_commands(t.build());
```
## File Links
- Main crate: [turtle-lib-macroquad/src/lib.rs](turtle-lib-macroquad/src/lib.rs)
- Tessellation: [turtle-lib-macroquad/src/tessellation.rs](turtle-lib-macroquad/src/tessellation.rs)
- Rendering: [turtle-lib-macroquad/src/drawing.rs](turtle-lib-macroquad/src/drawing.rs)
- Animation: [turtle-lib-macroquad/src/tweening.rs](turtle-lib-macroquad/src/tweening.rs)
- Examples: [turtle-lib-macroquad/examples/](turtle-lib-macroquad/examples/)
## Testing
Run examples to verify Lyon integration:
```bash
cargo run --example yinyang
cargo run --example stern
cargo run --example nikolaus
```
## Code Quality
Run clippy with strict checks on turtle-lib-macroquad:
```bash
cargo clippy --package turtle-lib-macroquad -- -Wclippy::pedantic -Wclippy::cast_precision_loss -Wclippy::cast_sign_loss -Wclippy::cast_possible_truncation
```
Note: Cast warnings are intentionally allowed for graphics code where precision loss is acceptable.
## Dependencies
- macroquad 0.4 - Game framework and rendering
- lyon 1.0 - Tessellation (fills, strokes, circles, arcs)
- tween 2.1.0 - Animation easing
- tracing 0.1 - Logging (with log features)

View File

@ -94,12 +94,14 @@ pub struct TurtlePlan {
} }
impl TurtlePlan { impl TurtlePlan {
#[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
queue: CommandQueue::new(), queue: CommandQueue::new(),
} }
} }
#[must_use]
pub fn with_capacity(capacity: usize) -> Self { pub fn with_capacity(capacity: usize) -> Self {
Self { Self {
queue: CommandQueue::with_capacity(capacity), queue: CommandQueue::with_capacity(capacity),
@ -179,6 +181,7 @@ impl TurtlePlan {
self self
} }
#[must_use]
pub fn build(self) -> CommandQueue { pub fn build(self) -> CommandQueue {
self.queue self.queue
} }

View File

@ -1,4 +1,4 @@
//! Circle geometry calculations - single source of truth for circle_left and circle_right //! Circle geometry calculations - single source of truth for `circle_left` and `circle_right`
use macroquad::prelude::*; use macroquad::prelude::*;
@ -19,6 +19,7 @@ pub struct CircleGeometry {
impl CircleGeometry { impl CircleGeometry {
/// Create geometry for a circle command /// Create geometry for a circle command
#[must_use]
pub fn new( pub fn new(
turtle_pos: Vec2, turtle_pos: Vec2,
turtle_heading: f32, turtle_heading: f32,
@ -58,6 +59,7 @@ impl CircleGeometry {
} }
/// Calculate position after traveling an angle along the arc /// Calculate position after traveling an angle along the arc
#[must_use]
pub fn position_at_angle(&self, angle_traveled: f32) -> Vec2 { pub fn position_at_angle(&self, angle_traveled: f32) -> Vec2 {
let current_angle = match self.direction { let current_angle = match self.direction {
CircleDirection::Left => self.start_angle_from_center - angle_traveled, CircleDirection::Left => self.start_angle_from_center - angle_traveled,
@ -70,13 +72,15 @@ impl CircleGeometry {
) )
} }
/// Calculate position at a given progress (0.0 to 1.0) through total_angle /// Calculate position at a given progress (0.0 to 1.0) through `total_angle`
#[must_use]
pub fn position_at_progress(&self, total_angle: f32, progress: f32) -> Vec2 { pub fn position_at_progress(&self, total_angle: f32, progress: f32) -> Vec2 {
let angle_traveled = total_angle * progress; let angle_traveled = total_angle * progress;
self.position_at_angle(angle_traveled) self.position_at_angle(angle_traveled)
} }
/// Get the angle traveled from start position to a given position /// Get the angle traveled from start position to a given position
#[must_use]
pub fn angle_to_position(&self, position: Vec2) -> f32 { pub fn angle_to_position(&self, position: Vec2) -> f32 {
let displacement = position - self.center; let displacement = position - self.center;
let current_angle = displacement.y.atan2(displacement.x); let current_angle = displacement.y.atan2(displacement.x);
@ -94,8 +98,9 @@ impl CircleGeometry {
angle_diff angle_diff
} }
/// Get draw_arc parameters for the full arc /// Get `draw_arc` parameters for the full arc
/// Returns (rotation_degrees, arc_degrees) for macroquad's draw_arc /// Returns (`rotation_degrees`, `arc_degrees`) for macroquad's `draw_arc`
#[must_use]
pub fn draw_arc_params(&self, total_angle_degrees: f32) -> (f32, f32) { pub fn draw_arc_params(&self, total_angle_degrees: f32) -> (f32, f32) {
match self.direction { match self.direction {
CircleDirection::Left => { CircleDirection::Left => {
@ -114,8 +119,9 @@ impl CircleGeometry {
} }
} }
/// Get draw_arc parameters for a partial arc (during tweening) /// Get `draw_arc` parameters for a partial arc (during tweening)
/// Returns (rotation_degrees, arc_degrees) for macroquad's draw_arc /// Returns (`rotation_degrees`, `arc_degrees`) for macroquad's `draw_arc`
#[must_use]
pub fn draw_arc_params_partial(&self, angle_traveled: f32) -> (f32, f32) { pub fn draw_arc_params_partial(&self, angle_traveled: f32) -> (f32, f32) {
let angle_traveled_degrees = angle_traveled.to_degrees(); let angle_traveled_degrees = angle_traveled.to_degrees();

View File

@ -52,13 +52,14 @@ pub struct CommandQueue {
} }
impl CommandQueue { impl CommandQueue {
#[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
commands: Vec::new(), commands: Vec::new(),
current_index: 0, current_index: 0,
} }
} }
#[must_use]
pub fn with_capacity(capacity: usize) -> Self { pub fn with_capacity(capacity: usize) -> Self {
Self { Self {
commands: Vec::with_capacity(capacity), commands: Vec::with_capacity(capacity),
@ -73,33 +74,23 @@ impl CommandQueue {
pub fn extend(&mut self, commands: impl IntoIterator<Item = TurtleCommand>) { pub fn extend(&mut self, commands: impl IntoIterator<Item = TurtleCommand>) {
self.commands.extend(commands); self.commands.extend(commands);
} }
#[must_use]
pub fn next(&mut self) -> Option<&TurtleCommand> {
if self.current_index < self.commands.len() {
let cmd = &self.commands[self.current_index];
self.current_index += 1;
Some(cmd)
} else {
None
}
}
pub fn is_complete(&self) -> bool { pub fn is_complete(&self) -> bool {
self.current_index >= self.commands.len() self.current_index >= self.commands.len()
} }
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.current_index = 0; self.current_index = 0;
} }
#[must_use]
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.commands.len() self.commands.len()
} }
#[must_use]
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.commands.is_empty() self.commands.is_empty()
} }
#[must_use]
pub fn remaining(&self) -> usize { pub fn remaining(&self) -> usize {
self.commands.len().saturating_sub(self.current_index) self.commands.len().saturating_sub(self.current_index)
} }
@ -110,3 +101,17 @@ impl Default for CommandQueue {
Self::new() Self::new()
} }
} }
impl Iterator for CommandQueue {
type Item = TurtleCommand;
fn next(&mut self) -> Option<Self::Item> {
if self.current_index < self.commands.len() {
let cmd = self.commands[self.current_index].clone();
self.current_index += 1;
Some(cmd)
} else {
None
}
}
}

View File

@ -43,6 +43,7 @@ pub fn render_world(world: &TurtleWorld) {
} }
/// Render the turtle world with active tween visualization /// Render the turtle world with active tween visualization
#[allow(clippy::too_many_lines)]
pub(crate) fn render_world_with_tween( pub(crate) fn render_world_with_tween(
world: &TurtleWorld, world: &TurtleWorld,
active_tween: Option<&crate::tweening::CommandTween>, active_tween: Option<&crate::tweening::CommandTween>,
@ -149,12 +150,12 @@ pub(crate) fn render_world_with_tween(
); );
// Calculate progress // Calculate progress
let elapsed = (get_time() - tween.start_time) as f32; let elapsed = get_time() - tween.start_time;
let progress = (elapsed / tween.duration as f32).min(1.0); let progress = (elapsed / tween.duration).min(1.0);
let eased_progress = CubicInOut.tween(1.0, progress); let eased_progress = CubicInOut.tween(1.0, progress as f32);
// Generate arc vertices for the partial arc // Generate arc vertices for the partial arc
let num_samples = (*steps as usize).max(1); let num_samples = *steps.max(&1);
let samples_to_draw = ((num_samples as f32 * eased_progress) 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 { for i in 1..=samples_to_draw {
@ -275,9 +276,9 @@ fn draw_tween_arc(
// Calculate how much of the arc we've traveled based on tween progress // Calculate how much of the arc we've traveled based on tween progress
// Use the same eased progress as the turtle position for synchronized animation // Use the same eased progress as the turtle position for synchronized animation
let elapsed = (get_time() - tween.start_time) as f32; let elapsed = get_time() - tween.start_time;
let t = (elapsed / tween.duration as f32).min(1.0); let t = (elapsed / tween.duration).min(1.0);
let progress = CubicInOut.tween(1.0, t); // tween from 0 to 1 let progress = CubicInOut.tween(1.0, t as f32); // tween from 0 to 1
let angle_traveled = total_angle.to_radians() * progress; let angle_traveled = total_angle.to_radians() * progress;
let (rotation_degrees, arc_degrees) = geom.draw_arc_params_partial(angle_traveled); let (rotation_degrees, arc_degrees) = geom.draw_arc_params_partial(angle_traveled);
@ -308,12 +309,11 @@ pub fn draw_turtle(turtle: &TurtleState) {
.collect(); .collect();
// Use Lyon for turtle shape too // Use Lyon for turtle shape too
match tessellation::tessellate_polygon( if let Ok(mesh_data) =
&absolute_vertices, tessellation::tessellate_polygon(&absolute_vertices, Color::new(0.0, 0.5, 1.0, 1.0))
Color::new(0.0, 0.5, 1.0, 1.0), {
) { draw_mesh(&mesh_data.to_mesh());
Ok(mesh_data) => draw_mesh(&mesh_data.to_mesh()), } else {
Err(_) => {
// Fallback to simple triangle fan if Lyon fails // Fallback to simple triangle fan if Lyon fails
let first = absolute_vertices[0]; let first = absolute_vertices[0];
for i in 1..absolute_vertices.len() - 1 { for i in 1..absolute_vertices.len() - 1 {
@ -326,7 +326,6 @@ pub fn draw_turtle(turtle: &TurtleState) {
} }
} }
} }
}
} else { } else {
// Draw outline // Draw outline
if !rotated_vertices.is_empty() { if !rotated_vertices.is_empty() {

View File

@ -280,9 +280,6 @@ pub fn add_draw_for_completed_tween(
} }
} }
} }
TurtleCommand::BeginFill | TurtleCommand::EndFill => {
// No immediate drawing for fill commands, handled in execute_command
}
_ => { _ => {
// Other commands don't create drawing // Other commands don't create drawing
} }

View File

@ -18,8 +18,8 @@ pub type Coordinate = Vec2;
pub type Visibility = bool; pub type Visibility = bool;
/// Execution speed setting /// Execution speed setting
/// - Instant(draw_calls): Fast execution with limited draw calls per frame (speed - 1000, minimum 1) /// - `Instant(draw_calls)`: Fast execution with limited draw calls per frame (speed - 1000, minimum 1)
/// - Animated(speed): Smooth animation at specified pixels/second /// - `Animated(speed)`: Smooth animation at specified pixels/second
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum AnimationSpeed { pub enum AnimationSpeed {
Instant(u32), // Number of draw calls per frame (minimum 1) Instant(u32), // Number of draw calls per frame (minimum 1)
@ -28,11 +28,13 @@ pub enum AnimationSpeed {
impl AnimationSpeed { impl AnimationSpeed {
/// Check if this is instant mode /// Check if this is instant mode
#[must_use]
pub fn is_animating(&self) -> bool { pub fn is_animating(&self) -> bool {
matches!(self, AnimationSpeed::Animated(_)) matches!(self, AnimationSpeed::Animated(_))
} }
/// Get the speed value (returns encoded value for Instant) /// Get the speed value (returns encoded value for Instant)
#[must_use]
pub fn value(&self) -> f32 { pub fn value(&self) -> f32 {
match self { match self {
AnimationSpeed::Instant(calls) => 1000.0 + *calls as f32, AnimationSpeed::Instant(calls) => 1000.0 + *calls as f32,
@ -43,6 +45,7 @@ impl AnimationSpeed {
/// Create from a raw speed value /// Create from a raw speed value
/// - speed >= 1000 becomes Instant with max(1, speed - 1000) draw calls per frame /// - speed >= 1000 becomes Instant with max(1, speed - 1000) draw calls per frame
/// - speed < 1000 becomes Animated /// - speed < 1000 becomes Animated
#[must_use]
pub fn from_value(speed: f32) -> Self { pub fn from_value(speed: f32) -> Self {
if speed >= 1000.0 { if speed >= 1000.0 {
let draw_calls = (speed - 1000.0).max(1.0) as u32; // Ensure at least 1 let draw_calls = (speed - 1000.0).max(1.0) as u32; // Ensure at least 1
@ -53,6 +56,7 @@ impl AnimationSpeed {
} }
/// Create from a u32 value for backward compatibility /// Create from a u32 value for backward compatibility
#[must_use]
pub fn from_u32(speed: u32) -> Self { pub fn from_u32(speed: u32) -> Self {
Self::from_value(speed as f32) Self::from_value(speed as f32)
} }

View File

@ -31,7 +31,7 @@ impl Default for Angle {
impl From<i16> for Angle { impl From<i16> for Angle {
fn from(i: i16) -> Self { fn from(i: i16) -> Self {
Self { Self {
value: AngleUnit::Degrees(i as Precision), value: AngleUnit::Degrees(Precision::from(i)),
} }
} }
} }
@ -126,25 +126,28 @@ impl Sub for Angle {
} }
impl Angle { impl Angle {
#[must_use]
pub fn degrees(value: Precision) -> Self { pub fn degrees(value: Precision) -> Self {
Self { Self {
value: AngleUnit::Degrees(value), value: AngleUnit::Degrees(value),
} }
} }
#[must_use]
pub fn radians(value: Precision) -> Self { pub fn radians(value: Precision) -> Self {
Self { Self {
value: AngleUnit::Radians(value), value: AngleUnit::Radians(value),
} }
} }
#[must_use]
pub fn value(&self) -> Precision { pub fn value(&self) -> Precision {
match self.value { match self.value {
AngleUnit::Degrees(v) => v, AngleUnit::Degrees(v) | AngleUnit::Radians(v) => v,
AngleUnit::Radians(v) => v,
} }
} }
#[must_use]
pub fn to_radians(self) -> Self { pub fn to_radians(self) -> Self {
match self.value { match self.value {
AngleUnit::Degrees(v) => Self::radians(v.to_radians()), AngleUnit::Degrees(v) => Self::radians(v.to_radians()),
@ -152,6 +155,7 @@ impl Angle {
} }
} }
#[must_use]
pub fn to_degrees(self) -> Self { pub fn to_degrees(self) -> Self {
match self.value { match self.value {
AngleUnit::Degrees(_) => self, AngleUnit::Degrees(_) => self,
@ -159,6 +163,7 @@ impl Angle {
} }
} }
#[must_use]
pub fn limit_smaller_than_full_circle(self) -> Self { pub fn limit_smaller_than_full_circle(self) -> Self {
use std::f32::consts::PI; use std::f32::consts::PI;
match self.value { match self.value {

View File

@ -7,7 +7,7 @@ pub struct Length(pub Precision);
impl From<i16> for Length { impl From<i16> for Length {
fn from(i: i16) -> Self { fn from(i: i16) -> Self {
Self(i as Precision) Self(Precision::from(i))
} }
} }

View File

@ -58,7 +58,8 @@ pub struct TurtleApp {
} }
impl TurtleApp { impl TurtleApp {
/// Create a new TurtleApp with default settings /// Create a new `TurtleApp` with default settings
#[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
world: TurtleWorld::new(), world: TurtleWorld::new(),
@ -72,15 +73,16 @@ impl TurtleApp {
/// Add commands to the turtle /// Add commands to the turtle
/// ///
/// Speed is controlled by SetSpeed commands in the queue. /// Speed is controlled by `SetSpeed` commands in the queue.
/// Use `set_speed()` on the turtle plan to set animation speed. /// Use `set_speed()` on the turtle plan to set animation speed.
/// Speed >= 999 = instant mode, speed < 999 = animated mode. /// Speed >= 999 = instant mode, speed < 999 = animated mode.
/// ///
/// # Arguments /// # Arguments
/// * `queue` - The command queue to execute /// * `queue` - The command queue to execute
#[must_use]
pub fn with_commands(mut self, queue: CommandQueue) -> Self { pub fn with_commands(mut self, queue: CommandQueue) -> Self {
// The TweenController will switch between instant and animated mode // The `TweenController` will switch between instant and animated mode
// based on SetSpeed commands encountered // based on `SetSpeed` commands encountered
self.tween_controller = Some(TweenController::new(queue, self.speed)); self.tween_controller = Some(TweenController::new(queue, self.speed));
self self
} }
@ -164,14 +166,15 @@ impl TurtleApp {
} }
/// Check if all commands have been executed /// Check if all commands have been executed
#[must_use]
pub fn is_complete(&self) -> bool { pub fn is_complete(&self) -> bool {
self.tween_controller self.tween_controller
.as_ref() .as_ref()
.map(|c| c.is_complete()) .is_none_or(TweenController::is_complete)
.unwrap_or(true)
} }
/// Get reference to the world state /// Get reference to the world state
#[must_use]
pub fn world(&self) -> &TurtleWorld { pub fn world(&self) -> &TurtleWorld {
&self.world &self.world
} }
@ -198,11 +201,13 @@ impl Default for TurtleApp {
/// turtle.forward(100.0).right(90.0).forward(50.0); /// turtle.forward(100.0).right(90.0).forward(50.0);
/// let commands = turtle.build(); /// let commands = turtle.build();
/// ``` /// ```
#[must_use]
pub fn create_turtle() -> TurtlePlan { pub fn create_turtle() -> TurtlePlan {
TurtlePlan::new() TurtlePlan::new()
} }
/// Convenience function to get a turtle plan (alias for create_turtle) /// Convenience function to get a turtle plan (alias for `create_turtle`)
#[must_use]
pub fn get_a_turtle() -> TurtlePlan { pub fn get_a_turtle() -> TurtlePlan {
create_turtle() create_turtle()
} }

View File

@ -14,11 +14,13 @@ pub struct TurtleShape {
impl TurtleShape { impl TurtleShape {
/// Create a new custom shape from vertices /// Create a new custom shape from vertices
#[must_use]
pub fn new(vertices: Vec<Vec2>, filled: bool) -> Self { pub fn new(vertices: Vec<Vec2>, filled: bool) -> Self {
Self { vertices, filled } Self { vertices, filled }
} }
/// Get vertices rotated by the given angle /// Get vertices rotated by the given angle
#[must_use]
pub fn rotated_vertices(&self, angle: f32) -> Vec<Vec2> { pub fn rotated_vertices(&self, angle: f32) -> Vec<Vec2> {
self.vertices self.vertices
.iter() .iter()
@ -31,6 +33,7 @@ impl TurtleShape {
} }
/// Triangle shape (simple arrow pointing right) /// Triangle shape (simple arrow pointing right)
#[must_use]
pub fn triangle() -> Self { pub fn triangle() -> Self {
Self { Self {
vertices: vec![ vertices: vec![
@ -43,6 +46,7 @@ impl TurtleShape {
} }
/// Classic turtle shape /// Classic turtle shape
#[must_use]
pub fn turtle() -> Self { pub fn turtle() -> Self {
// Based on the original turtle shape from turtle-lib // Based on the original turtle shape from turtle-lib
let polygon: &[[f32; 2]; 23] = &[ let polygon: &[[f32; 2]; 23] = &[
@ -89,6 +93,7 @@ impl TurtleShape {
} }
/// Circle shape /// Circle shape
#[must_use]
pub fn circle() -> Self { pub fn circle() -> Self {
let segments = 16; let segments = 16;
let radius = 10.0; let radius = 10.0;
@ -106,6 +111,7 @@ impl TurtleShape {
} }
/// Square shape /// Square shape
#[must_use]
pub fn square() -> Self { pub fn square() -> Self {
Self { Self {
vertices: vec![ vertices: vec![
@ -119,6 +125,7 @@ impl TurtleShape {
} }
/// Arrow shape (simple arrow pointing right) /// Arrow shape (simple arrow pointing right)
#[must_use]
pub fn arrow() -> Self { pub fn arrow() -> Self {
Self { Self {
vertices: vec![ vertices: vec![
@ -133,9 +140,10 @@ impl TurtleShape {
} }
/// Pre-defined shape types /// Pre-defined shape types
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ShapeType { pub enum ShapeType {
Triangle, Triangle,
#[default]
Turtle, Turtle,
Circle, Circle,
Square, Square,
@ -143,7 +151,8 @@ pub enum ShapeType {
} }
impl ShapeType { impl ShapeType {
/// Get the corresponding TurtleShape /// Get the corresponding `TurtleShape`
#[must_use]
pub fn to_shape(&self) -> TurtleShape { pub fn to_shape(&self) -> TurtleShape {
match self { match self {
ShapeType::Triangle => TurtleShape::triangle(), ShapeType::Triangle => TurtleShape::triangle(),
@ -154,9 +163,3 @@ impl ShapeType {
} }
} }
} }
impl Default for ShapeType {
fn default() -> Self {
ShapeType::Turtle
}
}

View File

@ -14,10 +14,10 @@ pub struct FillState {
/// The first contour is the outer boundary, subsequent contours are holes. /// The first contour is the outer boundary, subsequent contours are holes.
pub contours: Vec<Vec<Coordinate>>, pub contours: Vec<Vec<Coordinate>>,
/// Current contour being built (vertices for the active pen_down segment) /// Current contour being built (vertices for the active `pen_down` segment)
pub current_contour: Vec<Coordinate>, pub current_contour: Vec<Coordinate>,
/// Fill color (cached from when begin_fill was called) /// Fill color (cached from when `begin_fill` was called)
pub fill_color: Color, pub fill_color: Color,
} }
@ -60,6 +60,7 @@ impl TurtleState {
self.speed = speed; self.speed = speed;
} }
#[must_use]
pub fn heading_angle(&self) -> Angle { pub fn heading_angle(&self) -> Angle {
Angle::radians(self.heading) Angle::radians(self.heading)
} }
@ -91,7 +92,7 @@ impl TurtleState {
} }
} }
/// Close the current contour and prepare for a new one (called on pen_up) /// Close the current contour and prepare for a new one (called on `pen_up`)
pub fn close_fill_contour(&mut self) { pub fn close_fill_contour(&mut self) {
if let Some(ref mut fill_state) = self.filling { if let Some(ref mut fill_state) = self.filling {
tracing::debug!( tracing::debug!(
@ -128,7 +129,7 @@ impl TurtleState {
} }
} }
/// Start a new contour (called on pen_down) /// Start a new contour (called on `pen_down`)
pub fn start_fill_contour(&mut self) { pub fn start_fill_contour(&mut self) {
if let Some(ref mut fill_state) = self.filling { if let Some(ref mut fill_state) = self.filling {
// Start new contour at current position // Start new contour at current position
@ -195,7 +196,7 @@ impl TurtleState {
} }
} }
/// Clear fill state (called after end_fill) /// Clear fill state (called after `end_fill`)
pub fn reset_fill(&mut self) { pub fn reset_fill(&mut self) {
self.filling = None; self.filling = None;
} }
@ -209,6 +210,7 @@ pub struct MeshData {
} }
impl MeshData { impl MeshData {
#[must_use]
pub fn to_mesh(&self) -> macroquad::prelude::Mesh { pub fn to_mesh(&self) -> macroquad::prelude::Mesh {
macroquad::prelude::Mesh { macroquad::prelude::Mesh {
vertices: self.vertices.clone(), vertices: self.vertices.clone(),
@ -235,6 +237,7 @@ pub struct TurtleWorld {
} }
impl TurtleWorld { impl TurtleWorld {
#[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
turtle: TurtleState::default(), turtle: TurtleState::default(),

View File

@ -5,17 +5,22 @@
use crate::state::MeshData; use crate::state::MeshData;
use lyon::math::{point, Point}; use lyon::math::{point, Point};
use lyon::path::Path; use lyon::path::{LineCap, LineJoin, Path};
use lyon::tessellation::*; use lyon::tessellation::{
BuffersBuilder, FillOptions, FillRule, FillTessellator, FillVertex, StrokeOptions,
StrokeTessellator, StrokeVertex, VertexBuffers,
};
use macroquad::prelude::*; use macroquad::prelude::*;
/// Convert macroquad Vec2 to Lyon Point /// Convert macroquad Vec2 to Lyon Point
#[must_use]
pub fn to_lyon_point(v: Vec2) -> Point { pub fn to_lyon_point(v: Vec2) -> Point {
point(v.x, v.y) point(v.x, v.y)
} }
/// Convert Lyon Point to macroquad Vec2 /// Convert Lyon Point to macroquad Vec2
#[allow(dead_code)] #[allow(dead_code)]
#[must_use]
pub fn to_macroquad_vec2(p: Point) -> Vec2 { pub fn to_macroquad_vec2(p: Point) -> Vec2 {
vec2(p.x, p.y) vec2(p.x, p.y)
} }
@ -27,6 +32,7 @@ pub struct SimpleVertex {
} }
/// Build mesh data from Lyon tessellation /// Build mesh data from Lyon tessellation
#[must_use]
pub fn build_mesh_data(vertices: &[SimpleVertex], indices: &[u16], color: Color) -> MeshData { pub fn build_mesh_data(vertices: &[SimpleVertex], indices: &[u16], color: Color) -> MeshData {
let verts: Vec<Vertex> = vertices let verts: Vec<Vertex> = vertices
.iter() .iter()
@ -52,6 +58,10 @@ pub fn build_mesh_data(vertices: &[SimpleVertex], indices: &[u16], color: Color)
/// Tessellate a polygon and return mesh /// Tessellate a polygon and return mesh
/// ///
/// This automatically handles holes when the path crosses itself. /// This automatically handles holes when the path crosses itself.
///
/// # Errors
///
/// Returns an error if no vertices are provided or if tessellation fails.
pub fn tessellate_polygon( pub fn tessellate_polygon(
vertices: &[Vec2], vertices: &[Vec2],
color: Color, color: Color,
@ -92,7 +102,11 @@ pub fn tessellate_polygon(
/// Tessellate multiple contours (outer boundary + holes) and return mesh /// Tessellate multiple contours (outer boundary + holes) and return mesh
/// ///
/// The first contour is the outer boundary, subsequent contours are holes. /// The first contour is the outer boundary, subsequent contours are holes.
/// Lyon's EvenOdd fill rule automatically creates holes where contours overlap. /// Lyon's `EvenOdd` fill rule automatically creates holes where contours overlap.
///
/// # Errors
///
/// Returns an error if no contours are provided or if tessellation fails.
pub fn tessellate_multi_contour( pub fn tessellate_multi_contour(
contours: &[Vec<Vec2>], contours: &[Vec<Vec2>],
color: Color, color: Color,
@ -163,7 +177,7 @@ pub fn tessellate_multi_contour(
position: vertex.position().to_array(), position: vertex.position().to_array(),
}), }),
) { ) {
Ok(_) => { Ok(()) => {
tracing::debug!( tracing::debug!(
vertices = geometry.vertices.len(), vertices = geometry.vertices.len(),
indices = geometry.indices.len(), indices = geometry.indices.len(),
@ -185,6 +199,10 @@ pub fn tessellate_multi_contour(
} }
/// Tessellate a stroked path and return mesh /// Tessellate a stroked path and return mesh
///
/// # Errors
///
/// Returns an error if no vertices are provided or if tessellation fails.
pub fn tessellate_stroke( pub fn tessellate_stroke(
vertices: &[Vec2], vertices: &[Vec2],
color: Color, color: Color,
@ -227,6 +245,10 @@ pub fn tessellate_stroke(
} }
/// Tessellate a circle and return mesh /// Tessellate a circle and return mesh
///
/// # Errors
///
/// Returns an error if tessellation fails.
pub fn tessellate_circle( pub fn tessellate_circle(
center: Vec2, center: Vec2,
radius: f32, radius: f32,
@ -268,6 +290,10 @@ pub fn tessellate_circle(
} }
/// Tessellate an arc (partial circle) and return mesh /// Tessellate an arc (partial circle) and return mesh
///
/// # Errors
///
/// Returns an error if tessellation fails.
pub fn tessellate_arc( pub fn tessellate_arc(
center: Vec2, center: Vec2,
radius: f32, radius: f32,

View File

@ -56,12 +56,13 @@ pub(crate) struct CommandTween {
pub duration: f64, pub duration: f64,
pub start_state: TurtleState, pub start_state: TurtleState,
pub target_state: TurtleState, pub target_state: TurtleState,
pub position_tweener: Tweener<TweenVec2, f32, CubicInOut>, pub position_tweener: Tweener<TweenVec2, f64, CubicInOut>,
pub heading_tweener: Tweener<f32, f32, CubicInOut>, pub heading_tweener: Tweener<f32, f64, CubicInOut>,
pub pen_width_tweener: Tweener<f32, f32, CubicInOut>, pub pen_width_tweener: Tweener<f32, f64, CubicInOut>,
} }
impl TweenController { impl TweenController {
#[must_use]
pub fn new(queue: CommandQueue, speed: AnimationSpeed) -> Self { pub fn new(queue: CommandQueue, speed: AnimationSpeed) -> Self {
Self { Self {
queue, queue,
@ -74,9 +75,10 @@ impl TweenController {
self.speed = speed; self.speed = speed;
} }
/// Update the tween, returns Vec of (command, start_state, end_state) for all completed commands this frame /// Update the tween, returns `Vec` of (`command`, `start_state`, `end_state`) for all completed commands this frame
/// Also takes commands vec to handle side effects like fill operations /// Also takes commands vec to handle side effects like fill operations
/// Each command has its own start_state and end_state pair /// Each `command` has its own `start_state` and `end_state` pair
#[allow(clippy::too_many_lines)]
pub fn update( pub fn update(
&mut self, &mut self,
state: &mut TurtleState, state: &mut TurtleState,
@ -87,12 +89,7 @@ impl TweenController {
let mut completed_commands = Vec::new(); let mut completed_commands = Vec::new();
let mut draw_call_count = 0; let mut draw_call_count = 0;
loop { for command in self.queue.by_ref() {
let command = match self.queue.next() {
Some(cmd) => cmd.clone(),
None => break,
};
let start_state = state.clone(); let start_state = state.clone();
// Handle SetSpeed command to potentially switch modes // Handle SetSpeed command to potentially switch modes
@ -111,7 +108,7 @@ impl TweenController {
} }
// Execute movement commands // Execute movement commands
let target_state = self.calculate_target_state(state, &command); let target_state = Self::calculate_target_state(state, &command);
*state = target_state.clone(); *state = target_state.clone();
// Record fill vertices AFTER movement using centralized helper // Record fill vertices AFTER movement using centralized helper
@ -139,7 +136,7 @@ impl TweenController {
// Process current tween // Process current tween
if let Some(ref mut tween) = self.current_tween { if let Some(ref mut tween) = self.current_tween {
let elapsed = (get_time() - tween.start_time) as f32; let elapsed = get_time() - tween.start_time;
// Use tweeners to calculate current values // Use tweeners to calculate current values
// For circles, calculate position along the arc instead of straight line // For circles, calculate position along the arc instead of straight line
@ -182,7 +179,7 @@ impl TweenController {
TurtleCommand::Turn(angle) => { TurtleCommand::Turn(angle) => {
tween.start_state.heading + angle.to_radians() * progress tween.start_state.heading + angle.to_radians() * progress
} }
TurtleCommand::SetHeading(_) | _ => { _ => {
// For other commands that change heading, lerp directly // For other commands that change heading, lerp directly
let heading_diff = tween.target_state.heading - tween.start_state.heading; let heading_diff = tween.target_state.heading - tween.start_state.heading;
tween.start_state.heading + heading_diff * progress tween.start_state.heading + heading_diff * progress
@ -191,7 +188,7 @@ impl TweenController {
state.pen_width = tween.pen_width_tweener.move_to(elapsed); state.pen_width = tween.pen_width_tweener.move_to(elapsed);
// Discrete properties (switch at 50% progress) // Discrete properties (switch at 50% progress)
let progress = (elapsed / tween.duration as f32).min(1.0); let progress = (elapsed / tween.duration).min(1.0);
if progress >= 0.5 { if progress >= 0.5 {
state.pen_down = tween.target_state.pen_down; state.pen_down = tween.target_state.pen_down;
state.color = tween.target_state.color; state.color = tween.target_state.color;
@ -228,9 +225,8 @@ impl TweenController {
// Return drawable commands // Return drawable commands
if Self::command_creates_drawing(&completed_command) && start_state.pen_down { if Self::command_creates_drawing(&completed_command) && start_state.pen_down {
return vec![(completed_command, start_state, end_state)]; return vec![(completed_command, start_state, end_state)];
} else {
return self.update(state, commands); // Continue to next command
} }
return self.update(state, commands); // Continue to next command
} }
return Vec::new(); return Vec::new();
@ -263,30 +259,28 @@ impl TweenController {
} }
let speed = state.speed; // Extract speed before borrowing self let speed = state.speed; // Extract speed before borrowing self
let duration = self.calculate_duration_with_state(&command_clone, state, speed); let duration = Self::calculate_duration_with_state(&command_clone, state, speed);
// Calculate target state // Calculate target state
let target_state = self.calculate_target_state(state, &command_clone); let target_state = Self::calculate_target_state(state, &command_clone);
// Create tweeners for smooth animation // Create tweeners for smooth animation
let position_tweener = Tweener::new( let position_tweener = Tweener::new(
TweenVec2::from(state.position), TweenVec2::from(state.position),
TweenVec2::from(target_state.position), TweenVec2::from(target_state.position),
duration as f32, duration,
CubicInOut, CubicInOut,
); );
let heading_tweener = Tweener::new( let heading_tweener = Tweener::new(
0.0, // We'll handle angle wrapping separately 0.0, // We'll handle angle wrapping separately
1.0, 1.0, duration, CubicInOut,
duration as f32,
CubicInOut,
); );
let pen_width_tweener = Tweener::new( let pen_width_tweener = Tweener::new(
state.pen_width, state.pen_width,
target_state.pen_width, target_state.pen_width,
duration as f32, duration,
CubicInOut, CubicInOut,
); );
@ -305,6 +299,7 @@ impl TweenController {
Vec::new() Vec::new()
} }
#[must_use]
pub fn is_complete(&self) -> bool { pub fn is_complete(&self) -> bool {
self.current_tween.is_none() && self.queue.is_complete() self.current_tween.is_none() && self.queue.is_complete()
} }
@ -322,7 +317,6 @@ impl TweenController {
} }
fn calculate_duration_with_state( fn calculate_duration_with_state(
&self,
command: &TurtleCommand, command: &TurtleCommand,
current: &TurtleState, current: &TurtleState,
speed: AnimationSpeed, speed: AnimationSpeed,
@ -348,14 +342,10 @@ impl TweenController {
} }
_ => 0.0, // Instant commands _ => 0.0, // Instant commands
}; };
base_time.max(0.01) as f64 // Minimum duration f64::from(base_time.max(0.01)) // Minimum duration
} }
fn calculate_target_state( fn calculate_target_state(current: &TurtleState, command: &TurtleCommand) -> TurtleState {
&self,
current: &TurtleState,
command: &TurtleCommand,
) -> TurtleState {
let mut target = current.clone(); let mut target = current.clone();
match command { match command {