diff --git a/.copilot/instructions.md b/.copilot/instructions.md new file mode 100644 index 0000000..e739d1c --- /dev/null +++ b/.copilot/instructions.md @@ -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> +pub fn tessellate_multi_contour(contours: &[Vec], color: Color) -> Result> +pub fn tessellate_stroke(vertices: &[Vec2], color: Color, width: f32, closed: bool) -> Result> +pub fn tessellate_circle(center: Vec2, radius: f32, color: Color, filled: bool, stroke_width: f32) -> Result> +pub fn tessellate_arc(center: Vec2, radius: f32, start_angle: f32, arc_angle: f32, color: Color, stroke_width: f32, segments: usize) -> Result> +``` + +### 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) \ No newline at end of file diff --git a/turtle-lib-macroquad/src/builders.rs b/turtle-lib-macroquad/src/builders.rs index 52cf35a..308c2eb 100644 --- a/turtle-lib-macroquad/src/builders.rs +++ b/turtle-lib-macroquad/src/builders.rs @@ -94,12 +94,14 @@ pub struct TurtlePlan { } impl TurtlePlan { + #[must_use] pub fn new() -> Self { Self { queue: CommandQueue::new(), } } + #[must_use] pub fn with_capacity(capacity: usize) -> Self { Self { queue: CommandQueue::with_capacity(capacity), @@ -179,6 +181,7 @@ impl TurtlePlan { self } + #[must_use] pub fn build(self) -> CommandQueue { self.queue } diff --git a/turtle-lib-macroquad/src/circle_geometry.rs b/turtle-lib-macroquad/src/circle_geometry.rs index e64c266..59718eb 100644 --- a/turtle-lib-macroquad/src/circle_geometry.rs +++ b/turtle-lib-macroquad/src/circle_geometry.rs @@ -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::*; @@ -19,6 +19,7 @@ pub struct CircleGeometry { impl CircleGeometry { /// Create geometry for a circle command + #[must_use] pub fn new( turtle_pos: Vec2, turtle_heading: f32, @@ -58,6 +59,7 @@ impl CircleGeometry { } /// Calculate position after traveling an angle along the arc + #[must_use] pub fn position_at_angle(&self, angle_traveled: f32) -> Vec2 { let current_angle = match self.direction { 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 { let angle_traveled = total_angle * progress; self.position_at_angle(angle_traveled) } /// Get the angle traveled from start position to a given position + #[must_use] pub fn angle_to_position(&self, position: Vec2) -> f32 { let displacement = position - self.center; let current_angle = displacement.y.atan2(displacement.x); @@ -94,8 +98,9 @@ impl CircleGeometry { angle_diff } - /// Get draw_arc parameters for the full arc - /// Returns (rotation_degrees, arc_degrees) for macroquad's draw_arc + /// Get `draw_arc` parameters for the full 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) { match self.direction { CircleDirection::Left => { @@ -114,8 +119,9 @@ impl CircleGeometry { } } - /// Get draw_arc parameters for a partial arc (during tweening) - /// Returns (rotation_degrees, arc_degrees) for macroquad's draw_arc + /// Get `draw_arc` parameters for a partial arc (during tweening) + /// Returns (`rotation_degrees`, `arc_degrees`) for macroquad's `draw_arc` + #[must_use] pub fn draw_arc_params_partial(&self, angle_traveled: f32) -> (f32, f32) { let angle_traveled_degrees = angle_traveled.to_degrees(); diff --git a/turtle-lib-macroquad/src/commands.rs b/turtle-lib-macroquad/src/commands.rs index 112111b..9ccdde3 100644 --- a/turtle-lib-macroquad/src/commands.rs +++ b/turtle-lib-macroquad/src/commands.rs @@ -52,13 +52,14 @@ pub struct CommandQueue { } impl CommandQueue { + #[must_use] pub fn new() -> Self { Self { commands: Vec::new(), current_index: 0, } } - + #[must_use] pub fn with_capacity(capacity: usize) -> Self { Self { commands: Vec::with_capacity(capacity), @@ -73,33 +74,23 @@ impl CommandQueue { pub fn extend(&mut self, commands: impl IntoIterator) { self.commands.extend(commands); } - - 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 - } - } - + #[must_use] pub fn is_complete(&self) -> bool { self.current_index >= self.commands.len() } - pub fn reset(&mut self) { self.current_index = 0; } - + #[must_use] pub fn len(&self) -> usize { self.commands.len() } - + #[must_use] pub fn is_empty(&self) -> bool { self.commands.is_empty() } + #[must_use] pub fn remaining(&self) -> usize { self.commands.len().saturating_sub(self.current_index) } @@ -110,3 +101,17 @@ impl Default for CommandQueue { Self::new() } } + +impl Iterator for CommandQueue { + type Item = TurtleCommand; + + fn next(&mut self) -> Option { + if self.current_index < self.commands.len() { + let cmd = self.commands[self.current_index].clone(); + self.current_index += 1; + Some(cmd) + } else { + None + } + } +} diff --git a/turtle-lib-macroquad/src/drawing.rs b/turtle-lib-macroquad/src/drawing.rs index 05a4da3..5567bb9 100644 --- a/turtle-lib-macroquad/src/drawing.rs +++ b/turtle-lib-macroquad/src/drawing.rs @@ -43,6 +43,7 @@ pub fn render_world(world: &TurtleWorld) { } /// Render the turtle world with active tween visualization +#[allow(clippy::too_many_lines)] pub(crate) fn render_world_with_tween( world: &TurtleWorld, active_tween: Option<&crate::tweening::CommandTween>, @@ -149,12 +150,12 @@ pub(crate) fn render_world_with_tween( ); // 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); + let elapsed = get_time() - tween.start_time; + let progress = (elapsed / tween.duration).min(1.0); + let eased_progress = CubicInOut.tween(1.0, progress as f32); // 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); 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 // Use the same eased progress as the turtle position for synchronized animation - let elapsed = (get_time() - tween.start_time) as f32; - let t = (elapsed / tween.duration as f32).min(1.0); - let progress = CubicInOut.tween(1.0, t); // tween from 0 to 1 + let elapsed = get_time() - tween.start_time; + let t = (elapsed / tween.duration).min(1.0); + let progress = CubicInOut.tween(1.0, t as f32); // tween from 0 to 1 let angle_traveled = total_angle.to_radians() * progress; let (rotation_degrees, arc_degrees) = geom.draw_arc_params_partial(angle_traveled); @@ -308,22 +309,20 @@ pub fn draw_turtle(turtle: &TurtleState) { .collect(); // 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), - ); - } + if let Ok(mesh_data) = + tessellation::tessellate_polygon(&absolute_vertices, Color::new(0.0, 0.5, 1.0, 1.0)) + { + draw_mesh(&mesh_data.to_mesh()); + } else { + // 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), + ); } } } diff --git a/turtle-lib-macroquad/src/execution.rs b/turtle-lib-macroquad/src/execution.rs index 492445b..8b51d94 100644 --- a/turtle-lib-macroquad/src/execution.rs +++ b/turtle-lib-macroquad/src/execution.rs @@ -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 } diff --git a/turtle-lib-macroquad/src/general.rs b/turtle-lib-macroquad/src/general.rs index 6fc85bb..e1b2fc5 100644 --- a/turtle-lib-macroquad/src/general.rs +++ b/turtle-lib-macroquad/src/general.rs @@ -18,8 +18,8 @@ pub type Coordinate = Vec2; pub type Visibility = bool; /// Execution speed setting -/// - Instant(draw_calls): Fast execution with limited draw calls per frame (speed - 1000, minimum 1) -/// - Animated(speed): Smooth animation at specified pixels/second +/// - `Instant(draw_calls)`: Fast execution with limited draw calls per frame (speed - 1000, minimum 1) +/// - `Animated(speed)`: Smooth animation at specified pixels/second #[derive(Clone, Copy, Debug, PartialEq)] pub enum AnimationSpeed { Instant(u32), // Number of draw calls per frame (minimum 1) @@ -28,11 +28,13 @@ pub enum AnimationSpeed { impl AnimationSpeed { /// Check if this is instant mode + #[must_use] pub fn is_animating(&self) -> bool { matches!(self, AnimationSpeed::Animated(_)) } /// Get the speed value (returns encoded value for Instant) + #[must_use] pub fn value(&self) -> f32 { match self { AnimationSpeed::Instant(calls) => 1000.0 + *calls as f32, @@ -43,6 +45,7 @@ impl AnimationSpeed { /// Create from a raw speed value /// - speed >= 1000 becomes Instant with max(1, speed - 1000) draw calls per frame /// - speed < 1000 becomes Animated + #[must_use] pub fn from_value(speed: f32) -> Self { if speed >= 1000.0 { 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 + #[must_use] pub fn from_u32(speed: u32) -> Self { Self::from_value(speed as f32) } diff --git a/turtle-lib-macroquad/src/general/angle.rs b/turtle-lib-macroquad/src/general/angle.rs index 74b1076..4ff9efd 100644 --- a/turtle-lib-macroquad/src/general/angle.rs +++ b/turtle-lib-macroquad/src/general/angle.rs @@ -31,7 +31,7 @@ impl Default for Angle { impl From for Angle { fn from(i: i16) -> Self { Self { - value: AngleUnit::Degrees(i as Precision), + value: AngleUnit::Degrees(Precision::from(i)), } } } @@ -126,25 +126,28 @@ impl Sub for Angle { } impl Angle { + #[must_use] pub fn degrees(value: Precision) -> Self { Self { value: AngleUnit::Degrees(value), } } + #[must_use] pub fn radians(value: Precision) -> Self { Self { value: AngleUnit::Radians(value), } } + #[must_use] pub fn value(&self) -> Precision { match self.value { - AngleUnit::Degrees(v) => v, - AngleUnit::Radians(v) => v, + AngleUnit::Degrees(v) | AngleUnit::Radians(v) => v, } } + #[must_use] pub fn to_radians(self) -> Self { match self.value { AngleUnit::Degrees(v) => Self::radians(v.to_radians()), @@ -152,6 +155,7 @@ impl Angle { } } + #[must_use] pub fn to_degrees(self) -> Self { match self.value { AngleUnit::Degrees(_) => self, @@ -159,6 +163,7 @@ impl Angle { } } + #[must_use] pub fn limit_smaller_than_full_circle(self) -> Self { use std::f32::consts::PI; match self.value { diff --git a/turtle-lib-macroquad/src/general/length.rs b/turtle-lib-macroquad/src/general/length.rs index e884b38..4603050 100644 --- a/turtle-lib-macroquad/src/general/length.rs +++ b/turtle-lib-macroquad/src/general/length.rs @@ -7,7 +7,7 @@ pub struct Length(pub Precision); impl From for Length { fn from(i: i16) -> Self { - Self(i as Precision) + Self(Precision::from(i)) } } diff --git a/turtle-lib-macroquad/src/lib.rs b/turtle-lib-macroquad/src/lib.rs index 12a9a74..7e638e9 100644 --- a/turtle-lib-macroquad/src/lib.rs +++ b/turtle-lib-macroquad/src/lib.rs @@ -58,7 +58,8 @@ pub struct TurtleApp { } impl TurtleApp { - /// Create a new TurtleApp with default settings + /// Create a new `TurtleApp` with default settings + #[must_use] pub fn new() -> Self { Self { world: TurtleWorld::new(), @@ -72,15 +73,16 @@ impl TurtleApp { /// 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. /// Speed >= 999 = instant mode, speed < 999 = animated mode. /// /// # Arguments /// * `queue` - The command queue to execute + #[must_use] pub fn with_commands(mut self, queue: CommandQueue) -> Self { - // The TweenController will switch between instant and animated mode - // based on SetSpeed commands encountered + // The `TweenController` will switch between instant and animated mode + // based on `SetSpeed` commands encountered self.tween_controller = Some(TweenController::new(queue, self.speed)); self } @@ -164,14 +166,15 @@ impl TurtleApp { } /// Check if all commands have been executed + #[must_use] pub fn is_complete(&self) -> bool { self.tween_controller .as_ref() - .map(|c| c.is_complete()) - .unwrap_or(true) + .is_none_or(TweenController::is_complete) } /// Get reference to the world state + #[must_use] pub fn world(&self) -> &TurtleWorld { &self.world } @@ -198,11 +201,13 @@ impl Default for TurtleApp { /// turtle.forward(100.0).right(90.0).forward(50.0); /// let commands = turtle.build(); /// ``` +#[must_use] pub fn create_turtle() -> TurtlePlan { 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 { create_turtle() } diff --git a/turtle-lib-macroquad/src/shapes.rs b/turtle-lib-macroquad/src/shapes.rs index d8d8c10..d8947a9 100644 --- a/turtle-lib-macroquad/src/shapes.rs +++ b/turtle-lib-macroquad/src/shapes.rs @@ -14,11 +14,13 @@ pub struct TurtleShape { impl TurtleShape { /// Create a new custom shape from vertices + #[must_use] pub fn new(vertices: Vec, filled: bool) -> Self { Self { vertices, filled } } /// Get vertices rotated by the given angle + #[must_use] pub fn rotated_vertices(&self, angle: f32) -> Vec { self.vertices .iter() @@ -31,6 +33,7 @@ impl TurtleShape { } /// Triangle shape (simple arrow pointing right) + #[must_use] pub fn triangle() -> Self { Self { vertices: vec![ @@ -43,6 +46,7 @@ impl TurtleShape { } /// Classic turtle shape + #[must_use] pub fn turtle() -> Self { // Based on the original turtle shape from turtle-lib let polygon: &[[f32; 2]; 23] = &[ @@ -89,6 +93,7 @@ impl TurtleShape { } /// Circle shape + #[must_use] pub fn circle() -> Self { let segments = 16; let radius = 10.0; @@ -106,6 +111,7 @@ impl TurtleShape { } /// Square shape + #[must_use] pub fn square() -> Self { Self { vertices: vec![ @@ -119,6 +125,7 @@ impl TurtleShape { } /// Arrow shape (simple arrow pointing right) + #[must_use] pub fn arrow() -> Self { Self { vertices: vec![ @@ -133,9 +140,10 @@ impl TurtleShape { } /// Pre-defined shape types -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] pub enum ShapeType { Triangle, + #[default] Turtle, Circle, Square, @@ -143,7 +151,8 @@ pub enum ShapeType { } impl ShapeType { - /// Get the corresponding TurtleShape + /// Get the corresponding `TurtleShape` + #[must_use] pub fn to_shape(&self) -> TurtleShape { match self { ShapeType::Triangle => TurtleShape::triangle(), @@ -154,9 +163,3 @@ impl ShapeType { } } } - -impl Default for ShapeType { - fn default() -> Self { - ShapeType::Turtle - } -} diff --git a/turtle-lib-macroquad/src/state.rs b/turtle-lib-macroquad/src/state.rs index dad20bc..2f9f8a8 100644 --- a/turtle-lib-macroquad/src/state.rs +++ b/turtle-lib-macroquad/src/state.rs @@ -14,10 +14,10 @@ pub struct FillState { /// The first contour is the outer boundary, subsequent contours are holes. pub contours: Vec>, - /// 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, - /// Fill color (cached from when begin_fill was called) + /// Fill color (cached from when `begin_fill` was called) pub fill_color: Color, } @@ -60,6 +60,7 @@ impl TurtleState { self.speed = speed; } + #[must_use] pub fn heading_angle(&self) -> Angle { 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) { if let Some(ref mut fill_state) = self.filling { 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) { if let Some(ref mut fill_state) = self.filling { // 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) { self.filling = None; } @@ -209,6 +210,7 @@ pub struct MeshData { } impl MeshData { + #[must_use] pub fn to_mesh(&self) -> macroquad::prelude::Mesh { macroquad::prelude::Mesh { vertices: self.vertices.clone(), @@ -235,6 +237,7 @@ pub struct TurtleWorld { } impl TurtleWorld { + #[must_use] pub fn new() -> Self { Self { turtle: TurtleState::default(), diff --git a/turtle-lib-macroquad/src/tessellation.rs b/turtle-lib-macroquad/src/tessellation.rs index 4100435..6ff1f86 100644 --- a/turtle-lib-macroquad/src/tessellation.rs +++ b/turtle-lib-macroquad/src/tessellation.rs @@ -5,17 +5,22 @@ use crate::state::MeshData; use lyon::math::{point, Point}; -use lyon::path::Path; -use lyon::tessellation::*; +use lyon::path::{LineCap, LineJoin, Path}; +use lyon::tessellation::{ + BuffersBuilder, FillOptions, FillRule, FillTessellator, FillVertex, StrokeOptions, + StrokeTessellator, StrokeVertex, VertexBuffers, +}; use macroquad::prelude::*; /// Convert macroquad Vec2 to Lyon Point +#[must_use] pub fn to_lyon_point(v: Vec2) -> Point { point(v.x, v.y) } /// Convert Lyon Point to macroquad Vec2 #[allow(dead_code)] +#[must_use] pub fn to_macroquad_vec2(p: Point) -> Vec2 { vec2(p.x, p.y) } @@ -27,6 +32,7 @@ pub struct SimpleVertex { } /// Build mesh data from Lyon tessellation +#[must_use] pub fn build_mesh_data(vertices: &[SimpleVertex], indices: &[u16], color: Color) -> MeshData { let verts: Vec = vertices .iter() @@ -52,6 +58,10 @@ pub fn build_mesh_data(vertices: &[SimpleVertex], indices: &[u16], color: Color) /// Tessellate a polygon and return mesh /// /// 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( vertices: &[Vec2], color: Color, @@ -92,7 +102,11 @@ pub fn tessellate_polygon( /// 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. +/// 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( contours: &[Vec], color: Color, @@ -163,7 +177,7 @@ pub fn tessellate_multi_contour( position: vertex.position().to_array(), }), ) { - Ok(_) => { + Ok(()) => { tracing::debug!( vertices = geometry.vertices.len(), indices = geometry.indices.len(), @@ -185,6 +199,10 @@ pub fn tessellate_multi_contour( } /// Tessellate a stroked path and return mesh +/// +/// # Errors +/// +/// Returns an error if no vertices are provided or if tessellation fails. pub fn tessellate_stroke( vertices: &[Vec2], color: Color, @@ -227,6 +245,10 @@ pub fn tessellate_stroke( } /// Tessellate a circle and return mesh +/// +/// # Errors +/// +/// Returns an error if tessellation fails. pub fn tessellate_circle( center: Vec2, radius: f32, @@ -268,6 +290,10 @@ pub fn tessellate_circle( } /// Tessellate an arc (partial circle) and return mesh +/// +/// # Errors +/// +/// Returns an error if tessellation fails. pub fn tessellate_arc( center: Vec2, radius: f32, diff --git a/turtle-lib-macroquad/src/tweening.rs b/turtle-lib-macroquad/src/tweening.rs index a0b1800..b177775 100644 --- a/turtle-lib-macroquad/src/tweening.rs +++ b/turtle-lib-macroquad/src/tweening.rs @@ -56,12 +56,13 @@ pub(crate) struct CommandTween { pub duration: f64, pub start_state: TurtleState, pub target_state: TurtleState, - pub position_tweener: Tweener, - pub heading_tweener: Tweener, - pub pen_width_tweener: Tweener, + pub position_tweener: Tweener, + pub heading_tweener: Tweener, + pub pen_width_tweener: Tweener, } impl TweenController { + #[must_use] pub fn new(queue: CommandQueue, speed: AnimationSpeed) -> Self { Self { queue, @@ -74,9 +75,10 @@ impl TweenController { 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 - /// 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( &mut self, state: &mut TurtleState, @@ -87,12 +89,7 @@ impl TweenController { let mut completed_commands = Vec::new(); let mut draw_call_count = 0; - loop { - let command = match self.queue.next() { - Some(cmd) => cmd.clone(), - None => break, - }; - + for command in self.queue.by_ref() { let start_state = state.clone(); // Handle SetSpeed command to potentially switch modes @@ -111,7 +108,7 @@ impl TweenController { } // 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(); // Record fill vertices AFTER movement using centralized helper @@ -139,7 +136,7 @@ impl TweenController { // Process 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 // For circles, calculate position along the arc instead of straight line @@ -182,7 +179,7 @@ impl TweenController { TurtleCommand::Turn(angle) => { tween.start_state.heading + angle.to_radians() * progress } - TurtleCommand::SetHeading(_) | _ => { + _ => { // For other commands that change heading, lerp directly let heading_diff = tween.target_state.heading - tween.start_state.heading; tween.start_state.heading + heading_diff * progress @@ -191,7 +188,7 @@ impl TweenController { state.pen_width = tween.pen_width_tweener.move_to(elapsed); // 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 { state.pen_down = tween.target_state.pen_down; state.color = tween.target_state.color; @@ -228,9 +225,8 @@ impl TweenController { // Return drawable commands if Self::command_creates_drawing(&completed_command) && start_state.pen_down { 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(); @@ -263,30 +259,28 @@ impl TweenController { } 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 - 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 let position_tweener = Tweener::new( TweenVec2::from(state.position), TweenVec2::from(target_state.position), - duration as f32, + duration, CubicInOut, ); let heading_tweener = Tweener::new( 0.0, // We'll handle angle wrapping separately - 1.0, - duration as f32, - CubicInOut, + 1.0, duration, CubicInOut, ); let pen_width_tweener = Tweener::new( state.pen_width, target_state.pen_width, - duration as f32, + duration, CubicInOut, ); @@ -305,6 +299,7 @@ impl TweenController { Vec::new() } + #[must_use] pub fn is_complete(&self) -> bool { self.current_tween.is_none() && self.queue.is_complete() } @@ -322,7 +317,6 @@ impl TweenController { } fn calculate_duration_with_state( - &self, command: &TurtleCommand, current: &TurtleState, speed: AnimationSpeed, @@ -348,14 +342,10 @@ impl TweenController { } _ => 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( - &self, - current: &TurtleState, - command: &TurtleCommand, - ) -> TurtleState { + fn calculate_target_state(current: &TurtleState, command: &TurtleCommand) -> TurtleState { let mut target = current.clone(); match command {