diff --git a/turtle-lib/src/circle_geometry.rs b/turtle-lib/src/circle_geometry.rs index c9d31a2..7f7896a 100644 --- a/turtle-lib/src/circle_geometry.rs +++ b/turtle-lib/src/circle_geometry.rs @@ -111,74 +111,7 @@ impl CircleGeometry { ) } - /// 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); - - let mut angle_diff = match self.direction { - CircleDirection::Left => self.start_angle_from_center - current_angle, - CircleDirection::Right => current_angle - self.start_angle_from_center, - }; - - // Normalize to [0, 2π) - if angle_diff < 0.0 { - angle_diff += 2.0 * std::f32::consts::PI; - } - - angle_diff - } - - /// 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 => { - // For left (counter-clockwise), we need to draw counter-clockwise from end back to start - // so we start at (start - total_angle) and draw total_angle counter-clockwise - let end_angle = self.start_angle_from_center - total_angle_degrees.to_radians(); - (end_angle.to_degrees(), total_angle_degrees) - } - CircleDirection::Right => { - // For right (clockwise), draw from start - ( - self.start_angle_from_center.to_degrees(), - total_angle_degrees, - ) - } - } - } - - /// 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(); - - match self.direction { - CircleDirection::Left => { - // Draw from current position backwards (counter-clockwise) to start - let current_angle = self.start_angle_from_center - angle_traveled; - (current_angle.to_degrees(), angle_traveled_degrees) - } - CircleDirection::Right => { - // Draw from start, counter-clockwise - ( - self.start_angle_from_center.to_degrees(), - angle_traveled_degrees, - ) - } - } - } } #[cfg(test)] diff --git a/turtle-lib/src/commands_channel.rs b/turtle-lib/src/commands_channel.rs index 627aa60..fd5873c 100644 --- a/turtle-lib/src/commands_channel.rs +++ b/turtle-lib/src/commands_channel.rs @@ -77,7 +77,6 @@ pub struct TurtleCommandSender { /// Paired with `TurtleCommandSender` via `turtle_command_channel()`. /// Automatically managed by `TurtleApp::process_commands()`. pub(crate) struct TurtleCommandReceiver { - turtle_id: usize, rx: Receiver, } @@ -142,12 +141,6 @@ impl TurtleCommandSender { } impl TurtleCommandReceiver { - /// Get the turtle ID this receiver is bound to - #[must_use] - pub fn turtle_id(&self) -> usize { - self.turtle_id - } - /// Drain all pending commands for this turtle (non-blocking) /// /// # Examples @@ -169,24 +162,6 @@ impl TurtleCommandReceiver { pub fn recv_all(&self) -> Vec { self.rx.try_iter().collect() } - - /// Try to receive one command batch (non-blocking) - #[must_use] - pub fn try_recv(&self) -> Option { - self.rx.try_recv().ok() - } - - /// Check if this receiver's queue is empty - #[must_use] - pub fn is_empty(&self) -> bool { - self.rx.is_empty() - } - - /// Get the number of pending command batches - #[must_use] - pub fn len(&self) -> usize { - self.rx.len() - } } /// Create a command channel for a specific turtle @@ -217,6 +192,6 @@ pub(crate) fn turtle_command_channel( let (tx, rx) = bounded(buffer_size); ( TurtleCommandSender { turtle_id, tx }, - TurtleCommandReceiver { turtle_id, rx }, + TurtleCommandReceiver { rx }, ) } diff --git a/turtle-lib/src/execution.rs b/turtle-lib/src/execution.rs index 503d5f7..e65d477 100644 --- a/turtle-lib/src/execution.rs +++ b/turtle-lib/src/execution.rs @@ -93,7 +93,6 @@ pub(crate) fn execute_command_side_effects( BLACK }); *filling = Some(FillState { - start_position: params.position, contours: Vec::new(), current_contour: vec![params.position], fill_color, diff --git a/turtle-lib/src/export.rs b/turtle-lib/src/export.rs index 3fa2f3d..0baf558 100644 --- a/turtle-lib/src/export.rs +++ b/turtle-lib/src/export.rs @@ -1,5 +1,5 @@ //! Export backend trait and core export types. - +#[cfg(feature = "svg")] use crate::state::TurtleWorld; use crate::TurtlePlan; @@ -17,6 +17,7 @@ pub enum DrawingFormat { // Additional formats: Png, Pdf, … } +#[cfg(feature = "svg")] pub(crate) trait DrawingExporter { /// Export the drawing to the specified format and filename /// @@ -55,12 +56,8 @@ where let mut turtle = crate::create_turtle_plan(); build_commands(&mut turtle); - let mut app = crate::TurtleApp::new().with_commands(turtle.build()); - app.set_all_turtles_speed(crate::AnimationSpeed::Instant(1000)); - - while !app.is_complete() { - app.step_animations(); - } + let mut app = crate::TurtleApp::new(); + app.execute_immediate(0, turtle); app.export_drawing(filename, crate::export::DrawingFormat::Svg) } diff --git a/turtle-lib/src/export_svg.rs b/turtle-lib/src/export_svg.rs index 71c1c79..5579b3b 100644 --- a/turtle-lib/src/export_svg.rs +++ b/turtle-lib/src/export_svg.rs @@ -4,15 +4,31 @@ pub mod svg_export { use crate::export::{DrawingExporter, ExportError}; use crate::state::{SvgRecord, TurtleWorld}; + use std::fmt::Write; use std::fs::File; use svg::{ node::element::{Circle, Line, Text as SvgText}, Document, }; + fn update_bounds( + min_x: &mut f32, + max_x: &mut f32, + min_y: &mut f32, + max_y: &mut f32, + x: f32, + y: f32, + ) { + *min_x = min_x.min(x); + *max_x = max_x.max(x); + *min_y = min_y.min(y); + *max_y = max_y.max(y); + } + pub struct SvgExporter; impl DrawingExporter for SvgExporter { + #[allow(clippy::too_many_lines)] fn export(&self, world: &TurtleWorld, filename: &str) -> Result<(), ExportError> { let mut doc = Document::new(); @@ -21,20 +37,6 @@ pub mod svg_export { let mut min_y = f32::INFINITY; let mut max_y = f32::NEG_INFINITY; - fn update_bounds( - min_x: &mut f32, - max_x: &mut f32, - min_y: &mut f32, - max_y: &mut f32, - x: f32, - y: f32, - ) { - *min_x = min_x.min(x); - *max_x = max_x.max(x); - *min_y = min_y.min(y); - *max_y = max_y.max(y); - } - for turtle in &world.turtles { for record in &turtle.svg_log.records { match record { @@ -110,7 +112,7 @@ pub mod svg_export { } else { // Partial arc — emit as let end = geom.position_at_angle(angle.as_radians().value()); - let large_arc = if angle.value() > 180.0 { 1 } else { 0 }; + let large_arc = i32::from(angle.value() > 180.0); let sweep = match direction { crate::circle_geometry::CircleDirection::Left => 0, crate::circle_geometry::CircleDirection::Right => 1, @@ -154,9 +156,9 @@ pub mod svg_export { if i > 0 { d.push(' '); } - d.push_str(&format!("M {} {}", contour[0].x, contour[0].y)); + let _ = write!(d, "M {} {}", contour[0].x, contour[0].y); for point in contour.iter().skip(1) { - d.push_str(&format!(" L {} {}", point.x, point.y)); + let _ = write!(d, " L {} {}", point.x, point.y); } d.push_str(" Z"); } @@ -227,9 +229,9 @@ pub mod svg_export { let g = (color.g * 255.0) as u8; let b = (color.b * 255.0) as u8; if color.a < 1.0 { - format!("rgba({},{},{},{})", r, g, b, color.a) + format!("rgba({r},{g},{b},{})", color.a) } else { - format!("rgb({},{},{})", r, g, b) + format!("rgb({r},{g},{b})") } } } diff --git a/turtle-lib/src/general.rs b/turtle-lib/src/general.rs index 463f9fd..015cd45 100644 --- a/turtle-lib/src/general.rs +++ b/turtle-lib/src/general.rs @@ -20,8 +20,7 @@ pub type Precision = f32; /// - internal render-space state uses Macroquad-style Y-down coordinates pub type Coordinate = Vec2; -/// Visibility flag for turtle -pub type Visibility = bool; + /// Execution speed setting /// - `Instant(draw_calls)`: Fast execution with limited draw calls per frame (speed - 1000, minimum 1) diff --git a/turtle-lib/src/lib.rs b/turtle-lib/src/lib.rs index 640417f..9b456d9 100644 --- a/turtle-lib/src/lib.rs +++ b/turtle-lib/src/lib.rs @@ -253,6 +253,11 @@ impl TurtleApp { /// Execute a plan immediately on a specific turtle (no animation) pub fn execute_immediate(&mut self, turtle_id: usize, plan: TurtlePlan) { + // Ensure turtle exists + while self.world.turtles.len() <= turtle_id { + self.world.add_turtle(); + } + for ref cmd in plan.build() { execution::execute_command_with_id(cmd, turtle_id, &mut self.world); } @@ -393,16 +398,7 @@ impl TurtleApp { } } - /// Get reference to the world state - #[must_use] - pub(crate) fn world(&self) -> &TurtleWorld { - &self.world - } - /// Get mutable reference to the world state - pub(crate) fn world_mut(&mut self) -> &mut TurtleWorld { - &mut self.world - } } impl Default for TurtleApp { diff --git a/turtle-lib/src/state.rs b/turtle-lib/src/state.rs index fe4c13d..24da2fc 100644 --- a/turtle-lib/src/state.rs +++ b/turtle-lib/src/state.rs @@ -9,9 +9,6 @@ use macroquad::prelude::*; /// State during active fill operation #[derive(Clone, Debug)] pub(crate) struct FillState { - /// Starting position of the fill - pub(crate) start_position: Coordinate, - /// All contours collected so far. Each contour is a separate closed path. /// The first contour is the outer boundary, subsequent contours are holes. pub(crate) contours: Vec>, @@ -56,6 +53,7 @@ impl Default for TurtleParams { /// State of a single turtle pub(crate) struct Turtle { + #[allow(clippy::struct_field_names)] pub(crate) turtle_id: usize, pub(crate) params: TurtleParams, @@ -90,26 +88,6 @@ impl Turtle { self.params.speed = speed; } - #[must_use] - pub fn heading_angle(&self) -> crate::general::Radians { - crate::general::Radians::new(self.params.heading) - } - - /// Reset turtle to default state (preserves `turtle_id` and queued commands) - pub fn reset(&mut self) { - // Clear all drawings - self.commands.clear(); - self.svg_log.clear(); - - // Clear fill state - self.filling = None; - - // Reset parameters to defaults - self.params = TurtleParams::default(); - - // Keep turtle_id and tween_controller (preserves queued commands) - } - /// Drive the animation controller for one frame. /// /// Returns `(command, start_params, end_params)` for every command that @@ -130,156 +108,6 @@ impl Turtle { &mut self.svg_log, ) } - - /// Start recording fill vertices - pub fn begin_fill(&mut self, fill_color: Color) { - self.filling = Some(FillState { - start_position: self.params.position, - contours: Vec::new(), - current_contour: vec![self.params.position], - fill_color, - }); - } - - /// Record current position if filling and pen is down - pub fn record_fill_vertex(&mut self) { - if let Some(ref mut fill_state) = self.filling { - if self.params.pen_down { - tracing::trace!( - turtle_id = self.turtle_id, - x = self.params.position.x, - y = self.params.position.y, - vertices = fill_state.current_contour.len() + 1, - "Adding vertex to current contour" - ); - fill_state.current_contour.push(self.params.position); - } else { - tracing::trace!(turtle_id = self.turtle_id, "Skipping vertex (pen is 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!( - turtle_id = self.turtle_id, - vertices = fill_state.current_contour.len(), - "close_fill_contour called" - ); - // Only close if we have vertices in current contour - if fill_state.current_contour.len() >= 2 { - tracing::debug!( - turtle_id = self.turtle_id, - vertices = fill_state.current_contour.len(), - first_x = fill_state.current_contour[0].x, - first_y = fill_state.current_contour[0].y, - last_x = fill_state.current_contour[fill_state.current_contour.len() - 1].x, - last_y = fill_state.current_contour[fill_state.current_contour.len() - 1].y, - "Closing contour" - ); - // Move current contour to completed contours - let contour = std::mem::take(&mut fill_state.current_contour); - fill_state.contours.push(contour); - tracing::debug!( - turtle_id = self.turtle_id, - completed_contours = fill_state.contours.len(), - "Contour moved to completed list" - ); - } else if !fill_state.current_contour.is_empty() { - tracing::warn!( - turtle_id = self.turtle_id, - vertices = fill_state.current_contour.len(), - "Current contour has insufficient vertices, not closing" - ); - } else { - tracing::warn!( - turtle_id = self.turtle_id, - "Current contour is empty, nothing to close" - ); - } - } else { - tracing::warn!( - turtle_id = self.turtle_id, - "close_fill_contour called but no active fill state" - ); - } - } - - /// 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 - tracing::debug!( - x = self.params.position.x, - y = self.params.position.y, - completed_contours = fill_state.contours.len(), - self.turtle_id = self.turtle_id, - "Starting new contour" - ); - fill_state.current_contour = vec![self.params.position]; - } - } - - /// Record multiple vertices along a circle arc for filling - /// This ensures circles are properly filled by sampling points along the arc - pub fn record_fill_vertices_for_arc( - &mut self, - center: Coordinate, - radius: f32, - start_angle: f32, - angle_traveled: f32, - direction: crate::circle_geometry::CircleDirection, - steps: u32, - ) { - if let Some(ref mut fill_state) = self.filling { - if self.params.pen_down { - // Sample points along the arc based on steps - let num_samples = steps.max(1); - - tracing::trace!( - turtle_id = self.turtle_id, - center_x = center.x, - center_y = center.y, - radius = radius, - steps = steps, - num_samples = num_samples, - "Recording arc vertices" - ); - - for i in 1..=num_samples { - let progress = i as f32 / num_samples as f32; - let current_angle = match direction { - crate::circle_geometry::CircleDirection::Left => { - start_angle - angle_traveled * progress - } - crate::circle_geometry::CircleDirection::Right => { - start_angle + angle_traveled * progress - } - }; - - let vertex = Coordinate::new( - center.x + radius * current_angle.cos(), - center.y + radius * current_angle.sin(), - ); - tracing::trace!( - turtle_id = self.turtle_id, - vertex_idx = i, - x = vertex.x, - y = vertex.y, - angle_degrees = current_angle.to_degrees(), - "Arc vertex" - ); - fill_state.current_contour.push(vertex); - } - } - } - } - - /// Clear fill state (called after `end_fill`) - pub fn reset_fill(&mut self) { - self.filling = None; - } } /// The draw-event log for SVG export. @@ -368,7 +196,6 @@ pub(crate) struct TurtleWorld { /// All turtles in the world (indexed by turtle ID) pub(crate) turtles: Vec, pub(crate) camera: Camera2D, - pub(crate) background_color: Color, } impl TurtleWorld { @@ -377,7 +204,6 @@ impl TurtleWorld { Self { turtles: vec![], // Start with no turtles camera: Camera2D::default(), - background_color: WHITE, } } @@ -392,32 +218,10 @@ impl TurtleWorld { turtle_id } - /// Get turtle by ID - #[must_use] - pub fn get_turtle(&self, id: usize) -> Option<&Turtle> { - self.turtles.get(id) - } - /// Get mutable turtle by ID pub fn get_turtle_mut(&mut self, id: usize) -> Option<&mut Turtle> { self.turtles.get_mut(id) } - - /// Reset a specific turtle to default state and remove all its drawings - pub fn reset_turtle(&mut self, turtle_id: usize) { - if let Some(turtle) = self.get_turtle_mut(turtle_id) { - turtle.reset(); - turtle.turtle_id = turtle_id; // Preserve turtle_id after reset - } - } - - /// Clear all drawings and reset all turtle states - pub fn clear(&mut self) { - for (id, turtle) in self.turtles.iter_mut().enumerate() { - turtle.reset(); - turtle.turtle_id = id; // Preserve turtle_id after reset - } - } } impl Default for TurtleWorld { diff --git a/turtle-lib/src/tweening.rs b/turtle-lib/src/tweening.rs index a4b2a70..6e124e2 100644 --- a/turtle-lib/src/tweening.rs +++ b/turtle-lib/src/tweening.rs @@ -54,7 +54,6 @@ pub(crate) struct TweenController { #[derive(Clone, Debug)] pub(crate) struct CommandTween { - pub(crate) turtle_id: usize, pub(crate) command: TurtleCommand, pub(crate) start_time: f64, pub(crate) duration: f64, @@ -316,7 +315,6 @@ impl TweenController { ); self.current_tween = Some(CommandTween { - turtle_id, command, start_time: current_time(), duration, @@ -396,14 +394,14 @@ pub(crate) fn normalize_angle(angle: f32) -> f32 { #[inline] fn current_time() -> f64 { - #[cfg(test)] + #[cfg(not(target_arch = "wasm32"))] { use std::sync::OnceLock; use std::time::Instant; static START: OnceLock = OnceLock::new(); START.get_or_init(Instant::now).elapsed().as_secs_f64() } - #[cfg(not(test))] + #[cfg(target_arch = "wasm32")] { macroquad::time::get_time() }