When running:
```bash
cargo run --example yinyang --features svg -- --export-svg yinyang.svg
```
the application crashed with:
```text
thread 'main' panicked at macroquad-0.4.16/src/lib.rs:172:13:
assertion failed: THREAD_ID.is_some()
```
along with 10 compiler dead-code warnings in `turtle-lib`.
1. **Headless Execution Path**: When `--export-svg` is provided,
`turtle_main` runs `run_headless_svg_export` headlessly without
creating a graphics window (`macroquad::Window::new` is bypassed).
2. **Speed Overwrite in Headless Mode**: `run_headless_svg_export`
originally called `app.set_all_turtles_speed(Instant(1000))` and
stepped animations with `while !app.is_complete() {
app.step_animations(); }`. However, `yinyang.rs` contains
`turtle.set_speed(100)` in its plan. When `TweenController` processed
`SetSpeed(100)`, it switched to animated mode.
3. **Macroquad Context Assertion**: In animated mode,
`TweenController::update` creates a `CommandTween` and called
`current_time()`. In `turtle-lib/src/tweening.rs`, `current_time()`
called `macroquad::time::get_time()`, which queried Macroquad's
context (`get_context()`). Because no window was created, Macroquad
asserted `THREAD_ID.is_some()` and panicked.
4. **Dead Code Warnings**: Types previously made `pub(crate)` had dead
fields and obsolete helper methods that were never called internally
or were superseded by `execution.rs`.
---
-
**[`turtle-lib/src/export.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/export.rs)**:
- Updated `run_headless_svg_export` to use `app.execute_immediate(0,
turtle)` instead of queuing commands and stepping animations in a
loop.
- Headless SVG export now executes all commands synchronously in under
0.1s regardless of any `set_speed` in the drawing plan.
-
**[`turtle-lib/src/lib.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/lib.rs)**:
- In `TurtleApp::execute_immediate`: ensured the turtle exists in
`self.world` before executing.
- Removed unused `pub(crate) fn world` and `pub(crate) fn world_mut`.
-
**[`turtle-lib/src/tweening.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/tweening.rs)**:
- Updated `current_time()` to use monotonic `std::time::Instant` on
non-WASM targets (`#[cfg(not(target_arch = "wasm32"))]`) and
`macroquad::time::get_time()` on WASM (`#[cfg(target_arch =
"wasm32")]`).
- Removed unused `turtle_id` field on `CommandTween`.
-
**[`turtle-lib/src/circle_geometry.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/circle_geometry.rs)**:
- Removed obsolete unused methods: `position_at_progress`,
`angle_to_position`, `draw_arc_params`, and
`draw_arc_params_partial`.
-
**[`turtle-lib/src/commands_channel.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/commands_channel.rs)**:
- Removed unused `turtle_id` field from `TurtleCommandReceiver`.
- Removed unused methods `turtle_id`, `try_recv`, `is_empty`, and
`len` from `TurtleCommandReceiver`.
-
**[`turtle-lib/src/general.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/general.rs)**:
- Removed unused `Visibility` type alias.
-
**[`turtle-lib/src/state.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/state.rs)**
&
**[`turtle-lib/src/execution.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/execution.rs)**:
- Removed unused `start_position` field on `FillState`.
- Removed unused superseded methods on `Turtle`: `heading_angle`,
`reset`, `begin_fill`, `record_fill_vertex`, `close_fill_contour`,
`start_fill_contour`, `record_fill_vertices_for_arc`, `reset_fill`.
- Removed unused `background_color` field and unused methods
`get_turtle`, `reset_turtle`, `clear` from `TurtleWorld`.
- Added `#[allow(clippy::struct_field_names)]` on `Turtle::turtle_id`.
-
**[`turtle-lib/src/export_svg.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/export_svg.rs)**:
- Moved `update_bounds` outside `export`.
- Replaced `if angle.value() > 180.0 { 1 } else { 0 }` with
`i32::from(angle.value() > 180.0)`.
- Replaced `d.push_str(&format!(...))` with `write!(d, ...)`.
- Inlined format arguments in `color_to_svg`.
---
```bash
cargo run --example yinyang --features svg -- --export-svg yinyang.svg
```
Output:
```text
Finished `dev` profile [optimized + debuginfo] target(s) in 0.07s
Running `target/debug/examples/yinyang --export-svg yinyang.svg`
SVG exported successfully to: yinyang.svg
```
Completed cleanly in **0.07s** with **0 compiler warnings** and **0
errors**.
Inspected `yinyang.svg`: contains all expected paths, outer arcs, inner
S-curve, and EvenOdd fill contours.
```bash
cargo test --package turtle-lib --features svg
```
Output:
```text
test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 0 filtered
out; finished in 0.00s
test result: ok. 34 passed; 0 failed; 1 ignored; 0 measured; 0 filtered
out; finished in 0.25s
```
```bash
cargo clippy --package turtle-lib --features svg -- -Wclippy::pedantic \
-Aclippy::cast_precision_loss -Aclippy::cast_sign_loss
-Aclippy::cast_possible_truncation
```
Output:
```text
Checking turtle-lib v0.2.0
(/home/dietrich/Projekte/Source/turtlers/turtle-lib)
Finished `dev` profile [optimized + debuginfo] target(s) in 0.51s
```
**0 warnings.**
This commit is contained in:
@@ -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)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ pub struct TurtleCommandSender {
|
|||||||
/// Paired with `TurtleCommandSender` via `turtle_command_channel()`.
|
/// Paired with `TurtleCommandSender` via `turtle_command_channel()`.
|
||||||
/// Automatically managed by `TurtleApp::process_commands()`.
|
/// Automatically managed by `TurtleApp::process_commands()`.
|
||||||
pub(crate) struct TurtleCommandReceiver {
|
pub(crate) struct TurtleCommandReceiver {
|
||||||
turtle_id: usize,
|
|
||||||
rx: Receiver<CommandQueue>,
|
rx: Receiver<CommandQueue>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,12 +141,6 @@ impl TurtleCommandSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TurtleCommandReceiver {
|
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)
|
/// Drain all pending commands for this turtle (non-blocking)
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
@@ -169,24 +162,6 @@ impl TurtleCommandReceiver {
|
|||||||
pub fn recv_all(&self) -> Vec<CommandQueue> {
|
pub fn recv_all(&self) -> Vec<CommandQueue> {
|
||||||
self.rx.try_iter().collect()
|
self.rx.try_iter().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to receive one command batch (non-blocking)
|
|
||||||
#[must_use]
|
|
||||||
pub fn try_recv(&self) -> Option<CommandQueue> {
|
|
||||||
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
|
/// Create a command channel for a specific turtle
|
||||||
@@ -217,6 +192,6 @@ pub(crate) fn turtle_command_channel(
|
|||||||
let (tx, rx) = bounded(buffer_size);
|
let (tx, rx) = bounded(buffer_size);
|
||||||
(
|
(
|
||||||
TurtleCommandSender { turtle_id, tx },
|
TurtleCommandSender { turtle_id, tx },
|
||||||
TurtleCommandReceiver { turtle_id, rx },
|
TurtleCommandReceiver { rx },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ pub(crate) fn execute_command_side_effects(
|
|||||||
BLACK
|
BLACK
|
||||||
});
|
});
|
||||||
*filling = Some(FillState {
|
*filling = Some(FillState {
|
||||||
start_position: params.position,
|
|
||||||
contours: Vec::new(),
|
contours: Vec::new(),
|
||||||
current_contour: vec![params.position],
|
current_contour: vec![params.position],
|
||||||
fill_color,
|
fill_color,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//! Export backend trait and core export types.
|
//! Export backend trait and core export types.
|
||||||
|
#[cfg(feature = "svg")]
|
||||||
use crate::state::TurtleWorld;
|
use crate::state::TurtleWorld;
|
||||||
use crate::TurtlePlan;
|
use crate::TurtlePlan;
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ pub enum DrawingFormat {
|
|||||||
// Additional formats: Png, Pdf, …
|
// Additional formats: Png, Pdf, …
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "svg")]
|
||||||
pub(crate) trait DrawingExporter {
|
pub(crate) trait DrawingExporter {
|
||||||
/// Export the drawing to the specified format and filename
|
/// Export the drawing to the specified format and filename
|
||||||
///
|
///
|
||||||
@@ -55,12 +56,8 @@ where
|
|||||||
let mut turtle = crate::create_turtle_plan();
|
let mut turtle = crate::create_turtle_plan();
|
||||||
build_commands(&mut turtle);
|
build_commands(&mut turtle);
|
||||||
|
|
||||||
let mut app = crate::TurtleApp::new().with_commands(turtle.build());
|
let mut app = crate::TurtleApp::new();
|
||||||
app.set_all_turtles_speed(crate::AnimationSpeed::Instant(1000));
|
app.execute_immediate(0, turtle);
|
||||||
|
|
||||||
while !app.is_complete() {
|
|
||||||
app.step_animations();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.export_drawing(filename, crate::export::DrawingFormat::Svg)
|
app.export_drawing(filename, crate::export::DrawingFormat::Svg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,23 +4,13 @@
|
|||||||
pub mod svg_export {
|
pub mod svg_export {
|
||||||
use crate::export::{DrawingExporter, ExportError};
|
use crate::export::{DrawingExporter, ExportError};
|
||||||
use crate::state::{SvgRecord, TurtleWorld};
|
use crate::state::{SvgRecord, TurtleWorld};
|
||||||
|
use std::fmt::Write;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use svg::{
|
use svg::{
|
||||||
node::element::{Circle, Line, Text as SvgText},
|
node::element::{Circle, Line, Text as SvgText},
|
||||||
Document,
|
Document,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct SvgExporter;
|
|
||||||
|
|
||||||
impl DrawingExporter for SvgExporter {
|
|
||||||
fn export(&self, world: &TurtleWorld, filename: &str) -> Result<(), ExportError> {
|
|
||||||
let mut doc = Document::new();
|
|
||||||
|
|
||||||
let mut min_x = f32::INFINITY;
|
|
||||||
let mut max_x = f32::NEG_INFINITY;
|
|
||||||
let mut min_y = f32::INFINITY;
|
|
||||||
let mut max_y = f32::NEG_INFINITY;
|
|
||||||
|
|
||||||
fn update_bounds(
|
fn update_bounds(
|
||||||
min_x: &mut f32,
|
min_x: &mut f32,
|
||||||
max_x: &mut f32,
|
max_x: &mut f32,
|
||||||
@@ -35,6 +25,18 @@ pub mod svg_export {
|
|||||||
*max_y = max_y.max(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();
|
||||||
|
|
||||||
|
let mut min_x = f32::INFINITY;
|
||||||
|
let mut max_x = f32::NEG_INFINITY;
|
||||||
|
let mut min_y = f32::INFINITY;
|
||||||
|
let mut max_y = f32::NEG_INFINITY;
|
||||||
|
|
||||||
for turtle in &world.turtles {
|
for turtle in &world.turtles {
|
||||||
for record in &turtle.svg_log.records {
|
for record in &turtle.svg_log.records {
|
||||||
match record {
|
match record {
|
||||||
@@ -110,7 +112,7 @@ pub mod svg_export {
|
|||||||
} else {
|
} else {
|
||||||
// Partial arc — emit as <path A …>
|
// Partial arc — emit as <path A …>
|
||||||
let end = geom.position_at_angle(angle.as_radians().value());
|
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 {
|
let sweep = match direction {
|
||||||
crate::circle_geometry::CircleDirection::Left => 0,
|
crate::circle_geometry::CircleDirection::Left => 0,
|
||||||
crate::circle_geometry::CircleDirection::Right => 1,
|
crate::circle_geometry::CircleDirection::Right => 1,
|
||||||
@@ -154,9 +156,9 @@ pub mod svg_export {
|
|||||||
if i > 0 {
|
if i > 0 {
|
||||||
d.push(' ');
|
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) {
|
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");
|
d.push_str(" Z");
|
||||||
}
|
}
|
||||||
@@ -227,9 +229,9 @@ pub mod svg_export {
|
|||||||
let g = (color.g * 255.0) as u8;
|
let g = (color.g * 255.0) as u8;
|
||||||
let b = (color.b * 255.0) as u8;
|
let b = (color.b * 255.0) as u8;
|
||||||
if color.a < 1.0 {
|
if color.a < 1.0 {
|
||||||
format!("rgba({},{},{},{})", r, g, b, color.a)
|
format!("rgba({r},{g},{b},{})", color.a)
|
||||||
} else {
|
} else {
|
||||||
format!("rgb({},{},{})", r, g, b)
|
format!("rgb({r},{g},{b})")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ pub type Precision = f32;
|
|||||||
/// - internal render-space state uses Macroquad-style Y-down coordinates
|
/// - internal render-space state uses Macroquad-style Y-down coordinates
|
||||||
pub type Coordinate = Vec2;
|
pub type Coordinate = Vec2;
|
||||||
|
|
||||||
/// Visibility flag for turtle
|
|
||||||
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)
|
||||||
|
|||||||
@@ -253,6 +253,11 @@ impl TurtleApp {
|
|||||||
|
|
||||||
/// Execute a plan immediately on a specific turtle (no animation)
|
/// Execute a plan immediately on a specific turtle (no animation)
|
||||||
pub fn execute_immediate(&mut self, turtle_id: usize, plan: TurtlePlan) {
|
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() {
|
for ref cmd in plan.build() {
|
||||||
execution::execute_command_with_id(cmd, turtle_id, &mut self.world);
|
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 {
|
impl Default for TurtleApp {
|
||||||
|
|||||||
+1
-197
@@ -9,9 +9,6 @@ use macroquad::prelude::*;
|
|||||||
/// State during active fill operation
|
/// State during active fill operation
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct FillState {
|
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.
|
/// All contours collected so far. Each contour is a separate closed path.
|
||||||
/// The first contour is the outer boundary, subsequent contours are holes.
|
/// The first contour is the outer boundary, subsequent contours are holes.
|
||||||
pub(crate) contours: Vec<Vec<Coordinate>>,
|
pub(crate) contours: Vec<Vec<Coordinate>>,
|
||||||
@@ -56,6 +53,7 @@ impl Default for TurtleParams {
|
|||||||
|
|
||||||
/// State of a single turtle
|
/// State of a single turtle
|
||||||
pub(crate) struct Turtle {
|
pub(crate) struct Turtle {
|
||||||
|
#[allow(clippy::struct_field_names)]
|
||||||
pub(crate) turtle_id: usize,
|
pub(crate) turtle_id: usize,
|
||||||
pub(crate) params: TurtleParams,
|
pub(crate) params: TurtleParams,
|
||||||
|
|
||||||
@@ -90,26 +88,6 @@ impl Turtle {
|
|||||||
self.params.speed = speed;
|
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.
|
/// Drive the animation controller for one frame.
|
||||||
///
|
///
|
||||||
/// Returns `(command, start_params, end_params)` for every command that
|
/// Returns `(command, start_params, end_params)` for every command that
|
||||||
@@ -130,156 +108,6 @@ impl Turtle {
|
|||||||
&mut self.svg_log,
|
&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.
|
/// The draw-event log for SVG export.
|
||||||
@@ -368,7 +196,6 @@ pub(crate) struct TurtleWorld {
|
|||||||
/// All turtles in the world (indexed by turtle ID)
|
/// All turtles in the world (indexed by turtle ID)
|
||||||
pub(crate) turtles: Vec<Turtle>,
|
pub(crate) turtles: Vec<Turtle>,
|
||||||
pub(crate) camera: Camera2D,
|
pub(crate) camera: Camera2D,
|
||||||
pub(crate) background_color: Color,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TurtleWorld {
|
impl TurtleWorld {
|
||||||
@@ -377,7 +204,6 @@ impl TurtleWorld {
|
|||||||
Self {
|
Self {
|
||||||
turtles: vec![], // Start with no turtles
|
turtles: vec![], // Start with no turtles
|
||||||
camera: Camera2D::default(),
|
camera: Camera2D::default(),
|
||||||
background_color: WHITE,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,32 +218,10 @@ impl TurtleWorld {
|
|||||||
turtle_id
|
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
|
/// Get mutable turtle by ID
|
||||||
pub fn get_turtle_mut(&mut self, id: usize) -> Option<&mut Turtle> {
|
pub fn get_turtle_mut(&mut self, id: usize) -> Option<&mut Turtle> {
|
||||||
self.turtles.get_mut(id)
|
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 {
|
impl Default for TurtleWorld {
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ pub(crate) struct TweenController {
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct CommandTween {
|
pub(crate) struct CommandTween {
|
||||||
pub(crate) turtle_id: usize,
|
|
||||||
pub(crate) command: TurtleCommand,
|
pub(crate) command: TurtleCommand,
|
||||||
pub(crate) start_time: f64,
|
pub(crate) start_time: f64,
|
||||||
pub(crate) duration: f64,
|
pub(crate) duration: f64,
|
||||||
@@ -316,7 +315,6 @@ impl TweenController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
self.current_tween = Some(CommandTween {
|
self.current_tween = Some(CommandTween {
|
||||||
turtle_id,
|
|
||||||
command,
|
command,
|
||||||
start_time: current_time(),
|
start_time: current_time(),
|
||||||
duration,
|
duration,
|
||||||
@@ -396,14 +394,14 @@ pub(crate) fn normalize_angle(angle: f32) -> f32 {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn current_time() -> f64 {
|
fn current_time() -> f64 {
|
||||||
#[cfg(test)]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
{
|
{
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
static START: OnceLock<Instant> = OnceLock::new();
|
static START: OnceLock<Instant> = OnceLock::new();
|
||||||
START.get_or_init(Instant::now).elapsed().as_secs_f64()
|
START.get_or_init(Instant::now).elapsed().as_secs_f64()
|
||||||
}
|
}
|
||||||
#[cfg(not(test))]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
macroquad::time::get_time()
|
macroquad::time::get_time()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user