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 {
#[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
}

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::*;
@ -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();

View File

@ -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<Item = TurtleCommand>) {
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<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
#[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,12 +309,11 @@ 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(_) => {
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 {
@ -326,7 +326,6 @@ pub fn draw_turtle(turtle: &TurtleState) {
}
}
}
}
} else {
// Draw outline
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
}

View File

@ -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)
}

View File

@ -31,7 +31,7 @@ impl Default for Angle {
impl From<i16> 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 {

View File

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

View File

@ -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()
}

View File

@ -14,11 +14,13 @@ pub struct TurtleShape {
impl TurtleShape {
/// Create a new custom shape from vertices
#[must_use]
pub fn new(vertices: Vec<Vec2>, filled: bool) -> Self {
Self { vertices, filled }
}
/// Get vertices rotated by the given angle
#[must_use]
pub fn rotated_vertices(&self, angle: f32) -> Vec<Vec2> {
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
}
}

View File

@ -14,10 +14,10 @@ pub struct FillState {
/// The first contour is the outer boundary, subsequent contours are holes.
pub contours: Vec<Vec<Coordinate>>,
/// Current contour being built (vertices for the active pen_down segment)
/// Current contour being built (vertices for the active `pen_down` segment)
pub current_contour: Vec<Coordinate>,
/// Fill color (cached from when begin_fill was called)
/// 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(),

View File

@ -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<Vertex> = 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<Vec2>],
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,

View File

@ -56,12 +56,13 @@ pub(crate) struct CommandTween {
pub duration: f64,
pub start_state: TurtleState,
pub target_state: TurtleState,
pub position_tweener: Tweener<TweenVec2, f32, CubicInOut>,
pub heading_tweener: Tweener<f32, f32, CubicInOut>,
pub pen_width_tweener: Tweener<f32, f32, CubicInOut>,
pub position_tweener: Tweener<TweenVec2, f64, CubicInOut>,
pub heading_tweener: Tweener<f32, f64, CubicInOut>,
pub pen_width_tweener: Tweener<f32, f64, CubicInOut>,
}
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 {