apply clippy pedantic
This commit is contained in:
parent
033a1982fc
commit
453e8e39bd
105
.copilot/instructions.md
Normal file
105
.copilot/instructions.md
Normal 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)
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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,22 +309,20 @@ 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 {
|
draw_triangle(
|
||||||
draw_triangle(
|
first,
|
||||||
first,
|
absolute_vertices[i],
|
||||||
absolute_vertices[i],
|
absolute_vertices[i + 1],
|
||||||
absolute_vertices[i + 1],
|
Color::new(0.0, 0.5, 1.0, 1.0),
|
||||||
Color::new(0.0, 0.5, 1.0, 1.0),
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user