Consistency Refactoring
All 6 consistency review items have been resolved, verified with new
unit tests, automated test suites, and clean example builds.
- **Fixed `Goto` Duration Bug**: In
[`turtle-lib/src/command_behavior.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/command_behavior.rs),
mapped Cartesian target coordinates to screen space (`vec2(target.x,
-target.y)`) before calculating $\Delta x$ and $\Delta y$ in
`animation_duration`.
- **Unit Test**: Added `test_goto_duration_cartesian_inversion` testing
that moving from screen $(0, 100)$ (Cartesian $(0, -100)$) to
Cartesian $(0, 100)$ computes duration based on the actual 200px
Euclidean distance ($2.0\text{s}$ at $100\text{ px/s}$).
- **Stored `Degrees`**: Updated `TurtleCommand::SetHeading(Degrees)` in
[`turtle-lib/src/commands.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/commands.rs)
to store user degrees directly ($0^\circ = \text{East}$, $90^\circ =
\text{North}$), matching `Turn(Degrees)` and `Circle { angle: Degrees
}`.
- **Deferred Screen-Space Conversion**: Converted to internal screen
radians (`normalize_angle(-heading.as_radians().value())`) inside
`apply_to_params` in
[`turtle-lib/src/command_behavior.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/command_behavior.rs).
- **Instant Duration**: Maintained instant transition ($0.01\text{s}$
minimum) in `animation_duration`.
- **Unit Test**: Added `test_set_heading_degrees_and_instant_duration`
verifying $90^\circ$ maps to North ($-\frac{\pi}{2}$ in screen space),
$0^\circ$ to East ($0$), $270^\circ$ to South ($+\frac{\pi}{2}$), and
duration is $0.01\text{s}$.
- **Enhanced `Length` Type**: Expanded
[`turtle-lib/src/general/length.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/general/length.rs)
with `new()`, `value()`, `Neg`, `PartialOrd`, and conversion
implementations (`From<f32>`, `From<f64>`, `From<i32>`, `From<i16>`,
`From<usize>`).
- **Adopted Across API**:
- `TurtleCommand::Move(Length)` in `commands.rs`.
- `TurtleCommand::Circle { radius: Length, ... }` in `commands.rs`.
- `forward<T: Into<Length>>` and `backward<T: Into<Length>>` in
[`turtle-lib/src/builders.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/builders.rs).
- `circle_left`, `circle_right` accept `radius: impl Into<Length>`.
- Kept stroke attribute `pen_width` as `Precision` (`f32`).
- **Unit Test**: Added `test_length_conversions_and_negation` in
`general::length`.
- **Removed Duplicate Alias**: Removed `all_animations_complete(&self)`
from `TurtleApp` in
[`turtle-lib/src/lib.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/lib.rs);
updated call in
[`turtle-lib/src/export.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/export.rs)
to use canonical `is_complete(&self)`.
- **Corrected
[`README.md`](file:///home/dietrich/Projekte/Source/turtlers/README.md)**:
- `plan.goto(...)` $\rightarrow$ `plan.go_to(...)`
- `plan.set_color(...)` $\rightarrow$ `plan.set_pen_color(...)`
- `create_turtle()` $\rightarrow$ `create_turtle_plan()`
- **Corrected
[`AGENTS.md`](file:///home/dietrich/Projekte/Source/turtlers/AGENTS.md)**:
- `create_turtle()` $\rightarrow$ `create_turtle_plan()` in Threading
Pattern.
- **Unified on `1000.0`**:
- Updated
[`README.md`](file:///home/dietrich/Projekte/Source/turtlers/README.md)
lines 9, 108, 109 to state `speed >= 1000` is Instant and `speed <
1000` is Animated.
- Updated
[`turtle-lib/examples/circle_test.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/examples/circle_test.rs)
to use `turtle.set_speed(1000)`.
- **Renamed Examples**:
- `turtle-lib/examples/stern.rs` $\rightarrow$
[`turtle-lib/examples/star.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/examples/star.rs)
- `turtle-lib/examples/nikolaus.rs` $\rightarrow$
[`turtle-lib/examples/house_of_nikolaus.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/examples/house_of_nikolaus.rs)
- **Translated Functions & Parameters**:
- `house_of_nikolaus.rs`: Translated `nikolausquadrat` $\rightarrow$
`house_square`, `nikolausdiag` $\rightarrow$ `house_diagonal`,
`nikolausdach2` $\rightarrow$ `house_roof`, parameter `groesse`
$\rightarrow$ `size`. Added doc comment explaining the Eulerian path
puzzle.
- `breadboard.rs`: Translated `pin_reihe` $\rightarrow$ `pin_row`,
`pin_spalte` $\rightarrow$ `pin_column`, `pin_seite` $\rightarrow$
`pin_side`, parameter `anzahl` $\rightarrow$ `count`,
`anzahl_reihen` $\rightarrow$ `row_count`. Added `#[cfg(feature =
"svg")]` to helper functions to eliminate dead-code warnings when
SVG feature is not enabled.
- **Translated UI Text & Print Messages**:
- `"Drücke E für SVG-Export"` $\rightarrow$ `"Press E for SVG export"`
- `"SVG exportiert nach test.svg"` $\rightarrow$ `"SVG exported to
test.svg"`
- `"Fehler beim Export: {:?}"` $\rightarrow$ `"Export error: {:?}"`
- `"SVG-Export ist nicht aktiviert..."` $\rightarrow$ `"SVG export is
not enabled..."`
- Translated module doc in `export_svg.rs`.
- **Updated
[`README.md`](file:///home/dietrich/Projekte/Source/turtlers/README.md)**
example lists and CLI commands to reference `star` and
`house_of_nikolaus`.
---
- `cargo test --package turtle-lib`:
- 13 unit tests passed (including 3 new targeted unit tests)
- 34 doctests passed (1 ignored internal helper)
- `cargo check --workspace --all-targets --all-features`: Passed with 0
errors.
- `cargo check --examples --package turtle-lib --all-features`: All 30
examples compiled cleanly with 0 errors.
- `cargo clippy --package turtle-lib -- -Wclippy::pedantic
-Aclippy::cast_precision_loss -Aclippy::cast_sign_loss
-Aclippy::cast_possible_truncation`: Passed with 0 errors.
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
use turtle_lib::*;
|
||||
|
||||
#[cfg(feature = "svg")]
|
||||
#[macroquad::main("Export SVG")]
|
||||
|
||||
async fn main() {
|
||||
// Create turtle plan
|
||||
let mut turtle = create_turtle_plan();
|
||||
|
||||
// Set instant mode so commands execute imqmediately
|
||||
// Set instant mode so commands execute immediately
|
||||
turtle.set_speed(1200).set_pen_width(0.5);
|
||||
|
||||
breadboard(&mut turtle, 65);
|
||||
@@ -24,12 +24,12 @@ async fn main() {
|
||||
app.update();
|
||||
app.render();
|
||||
|
||||
draw_text("Drücke E für SVG-Export", 20.0, 40.0, 32.0, BLACK);
|
||||
draw_text("Press E for SVG export", 20.0, 40.0, 32.0, BLACK);
|
||||
|
||||
if is_key_pressed(KeyCode::E) {
|
||||
match app.export_drawing("test.svg", export::DrawingFormat::Svg) {
|
||||
Ok(_) => println!("SVG exportiert nach test.svg"),
|
||||
Err(e) => println!("Fehler beim Export: {:?}", e),
|
||||
Ok(_) => println!("SVG exported to test.svg"),
|
||||
Err(e) => eprintln!("Export error: {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,10 @@ async fn main() {
|
||||
|
||||
#[cfg(not(feature = "svg"))]
|
||||
fn main() {
|
||||
println!("SVG-Export ist nicht aktiviert. Baue mit --features svg");
|
||||
println!("SVG export is not enabled. Build with --features svg");
|
||||
}
|
||||
|
||||
#[cfg(feature = "svg")]
|
||||
fn pin(t: &mut TurtlePlan, size: f32) {
|
||||
t.left(90.0).forward(size / 2.0);
|
||||
for _ in 0..5 {
|
||||
@@ -50,52 +51,56 @@ fn pin(t: &mut TurtlePlan, size: f32) {
|
||||
t.right(90.0).forward(size / 2.0).left(90.0);
|
||||
}
|
||||
|
||||
fn pin_reihe(t: &mut TurtlePlan, anzahl: usize) {
|
||||
for x in 0..anzahl {
|
||||
#[cfg(feature = "svg")]
|
||||
fn pin_row(t: &mut TurtlePlan, count: usize) {
|
||||
for x in 0..count {
|
||||
pin(t, 5.0);
|
||||
if x < anzahl - 1 {
|
||||
if x < count - 1 {
|
||||
t.forward(5.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pin_spalte(t: &mut TurtlePlan, anzahl: usize, x_coord: f32) {
|
||||
for x in 0..anzahl {
|
||||
#[cfg(feature = "svg")]
|
||||
fn pin_column(t: &mut TurtlePlan, count: usize, x_coord: f32) {
|
||||
for x in 0..count {
|
||||
t.pen_up().go_to(vec2(x_coord, x as f32 * 10.0)).pen_down();
|
||||
pin_reihe(t, 5);
|
||||
pin_row(t, 5);
|
||||
}
|
||||
}
|
||||
|
||||
fn pin_seite(t: &mut TurtlePlan, anzahl: usize, x_coord: f32, color: Color) {
|
||||
#[cfg(feature = "svg")]
|
||||
fn pin_side(t: &mut TurtlePlan, count: usize, x_coord: f32, color: Color) {
|
||||
t.pen_up()
|
||||
.go_to(vec2(x_coord, -2.5))
|
||||
.pen_down()
|
||||
.set_pen_color(color)
|
||||
.set_heading(90.0);
|
||||
for x in 0..anzahl {
|
||||
for x in 0..count {
|
||||
pin(t, 5.0);
|
||||
if x < anzahl - 1 {
|
||||
if x < count - 1 {
|
||||
t.forward(5.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn breadboard(t: &mut TurtlePlan, anzahl_reihen: usize) {
|
||||
pin_spalte(t, anzahl_reihen, 0.0);
|
||||
pin_spalte(t, anzahl_reihen, 65.0);
|
||||
pin_seite(t, anzahl_reihen, -15.0, BLUE);
|
||||
pin_seite(t, anzahl_reihen, -25.0, RED);
|
||||
pin_seite(t, anzahl_reihen, 125.0, BLUE);
|
||||
pin_seite(t, anzahl_reihen, 135.0, RED);
|
||||
#[cfg(feature = "svg")]
|
||||
fn breadboard(t: &mut TurtlePlan, row_count: usize) {
|
||||
pin_column(t, row_count, 0.0);
|
||||
pin_column(t, row_count, 65.0);
|
||||
pin_side(t, row_count, -15.0, BLUE);
|
||||
pin_side(t, row_count, -25.0, RED);
|
||||
pin_side(t, row_count, 125.0, BLUE);
|
||||
pin_side(t, row_count, 135.0, RED);
|
||||
|
||||
// draw outline
|
||||
t.pen_up().go_to(vec2(-30.0, -5.0)).pen_down();
|
||||
t.set_pen_color(BLACK)
|
||||
.forward(anzahl_reihen as f32 * 10.0 + 10.0)
|
||||
.forward(row_count as f32 * 10.0 + 10.0)
|
||||
.right(90.0)
|
||||
.forward(170.0)
|
||||
.right(90.0)
|
||||
.forward(anzahl_reihen as f32 * 10.0 + 10.0)
|
||||
.forward(row_count as f32 * 10.0 + 10.0)
|
||||
.right(90.0)
|
||||
.forward(170.0)
|
||||
.right(90.0);
|
||||
|
||||
@@ -10,7 +10,7 @@ fn draw(turtle: &mut TurtlePlan) {
|
||||
turtle.set_pen_color(RED);
|
||||
turtle.set_pen_width(0.5);
|
||||
turtle.left(90.0);
|
||||
turtle.set_speed(999);
|
||||
turtle.set_speed(1000);
|
||||
turtle.circle_left(100.0, 540.0, 72); // partial circle to the left
|
||||
|
||||
turtle.begin_fill();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Beispiel: Exportiere ein SVG aus einer einfachen Zeichnung
|
||||
//! Example: Export an SVG from a simple drawing
|
||||
|
||||
#[cfg(feature = "svg")]
|
||||
use turtle_lib::*;
|
||||
@@ -52,12 +52,12 @@ async fn main() {
|
||||
app.update();
|
||||
app.render();
|
||||
|
||||
draw_text("Drücke E für SVG-Export", 20.0, 40.0, 32.0, BLACK);
|
||||
draw_text("Press E for SVG export", 20.0, 40.0, 32.0, BLACK);
|
||||
|
||||
if is_key_pressed(KeyCode::E) {
|
||||
match app.export_drawing("test.svg", export::DrawingFormat::Svg) {
|
||||
Ok(_) => println!("SVG exportiert nach test.svg"),
|
||||
Err(e) => println!("Fehler beim Export: {:?}", e),
|
||||
Ok(_) => println!("SVG exported to test.svg"),
|
||||
Err(e) => eprintln!("Export error: {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,5 +67,5 @@ async fn main() {
|
||||
|
||||
#[cfg(not(feature = "svg"))]
|
||||
fn main() {
|
||||
println!("SVG-Export ist nicht aktiviert. Baue mit --features svg");
|
||||
println!("SVG export is not enabled. Build with --features svg");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
//! House of Nikolaus example - draws the classic house figure (Eulerian path puzzle)
|
||||
|
||||
use turtle_lib::*;
|
||||
|
||||
fn house_square(turtle: &mut TurtlePlan, size: f32) {
|
||||
turtle.forward(size);
|
||||
turtle.left(90.0);
|
||||
turtle.forward(size);
|
||||
turtle.left(90.0);
|
||||
turtle.forward(size);
|
||||
turtle.left(90.0);
|
||||
turtle.forward(size);
|
||||
turtle.left(90.0);
|
||||
}
|
||||
|
||||
fn house_diagonal(turtle: &mut TurtlePlan, size: f32) {
|
||||
let square = size * size;
|
||||
let diag = (square + square).sqrt();
|
||||
|
||||
turtle.left(45.0);
|
||||
turtle.forward(diag);
|
||||
turtle.left(45.0);
|
||||
house_roof(turtle, size);
|
||||
turtle.left(45.0);
|
||||
turtle.forward(diag);
|
||||
turtle.left(45.0);
|
||||
}
|
||||
|
||||
fn house_roof(turtle: &mut TurtlePlan, size: f32) {
|
||||
let square = size * size;
|
||||
let diag = (square + square).sqrt();
|
||||
turtle.left(45.0);
|
||||
turtle.forward(diag / 2.0);
|
||||
turtle.left(90.0);
|
||||
turtle.forward(diag / 2.0);
|
||||
turtle.left(45.0);
|
||||
}
|
||||
|
||||
fn house_of_nikolaus(turtle: &mut TurtlePlan, size: f32) {
|
||||
house_square(turtle, size);
|
||||
house_diagonal(turtle, size);
|
||||
}
|
||||
|
||||
#[turtle_main("House of Nikolaus")]
|
||||
fn draw(turtle: &mut TurtlePlan) {
|
||||
turtle.shape(ShapeType::Turtle);
|
||||
|
||||
// Position the turtle (pen up, move, pen down)
|
||||
turtle.pen_up();
|
||||
turtle.backward(80.0);
|
||||
turtle.left(90.0);
|
||||
turtle.backward(50.0);
|
||||
turtle.right(90.0);
|
||||
turtle.pen_down();
|
||||
|
||||
house_of_nikolaus(turtle, 100.0);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
//! Nikolaus example - draws a house-like figure
|
||||
|
||||
use turtle_lib::*;
|
||||
|
||||
fn nikolausquadrat(turtle: &mut TurtlePlan, groesse: f32) {
|
||||
turtle.forward(groesse);
|
||||
turtle.left(90.0);
|
||||
turtle.forward(groesse);
|
||||
turtle.left(90.0);
|
||||
turtle.forward(groesse);
|
||||
turtle.left(90.0);
|
||||
turtle.forward(groesse);
|
||||
turtle.left(90.0);
|
||||
}
|
||||
|
||||
fn nikolausdiag(turtle: &mut TurtlePlan, groesse: f32) {
|
||||
let quadrat = groesse * groesse;
|
||||
let diag = (quadrat + quadrat).sqrt();
|
||||
|
||||
turtle.left(45.0);
|
||||
turtle.forward(diag);
|
||||
turtle.left(45.0);
|
||||
nikolausdach2(turtle, groesse);
|
||||
turtle.left(45.0);
|
||||
turtle.forward(diag);
|
||||
turtle.left(45.0);
|
||||
}
|
||||
|
||||
fn nikolausdach2(turtle: &mut TurtlePlan, groesse: f32) {
|
||||
let quadrat = groesse * groesse;
|
||||
let diag = (quadrat + quadrat).sqrt();
|
||||
turtle.left(45.0);
|
||||
turtle.forward(diag / 2.0);
|
||||
turtle.left(90.0);
|
||||
turtle.forward(diag / 2.0);
|
||||
turtle.left(45.0);
|
||||
}
|
||||
|
||||
fn nikolaus(turtle: &mut TurtlePlan, groesse: f32) {
|
||||
nikolausquadrat(turtle, groesse);
|
||||
nikolausdiag(turtle, groesse);
|
||||
}
|
||||
|
||||
#[turtle_main("Nikolaus")]
|
||||
fn draw(turtle: &mut TurtlePlan) {
|
||||
turtle.shape(ShapeType::Turtle);
|
||||
|
||||
// Position the turtle (pen up, move, pen down)
|
||||
turtle.pen_up();
|
||||
turtle.backward(80.0);
|
||||
turtle.left(90.0);
|
||||
turtle.backward(50.0);
|
||||
turtle.right(90.0);
|
||||
turtle.pen_down();
|
||||
|
||||
nikolaus(turtle, 100.0);
|
||||
}
|
||||
+10
-13
@@ -1,7 +1,7 @@
|
||||
//! Builder pattern traits for creating turtle command sequences
|
||||
|
||||
use crate::commands::{CommandQueue, TurtleCommand};
|
||||
use crate::general::{AnimationSpeed, Color, Coordinate, Degrees, FontSize, Precision};
|
||||
use crate::general::{AnimationSpeed, Color, Coordinate, Degrees, FontSize, Length, Precision};
|
||||
use crate::shapes::{ShapeType, TurtleShape};
|
||||
|
||||
/// Trait for adding commands to a queue
|
||||
@@ -33,9 +33,9 @@ pub trait DirectionalMovement: WithCommands {
|
||||
/// ```
|
||||
fn forward<T>(&mut self, distance: T) -> &mut Self
|
||||
where
|
||||
T: Into<Precision>,
|
||||
T: Into<Length>,
|
||||
{
|
||||
let dist: Precision = distance.into();
|
||||
let dist: Length = distance.into();
|
||||
self.get_commands_mut().push(TurtleCommand::Move(dist));
|
||||
self
|
||||
}
|
||||
@@ -61,9 +61,9 @@ pub trait DirectionalMovement: WithCommands {
|
||||
/// ```
|
||||
fn backward<T>(&mut self, distance: T) -> &mut Self
|
||||
where
|
||||
T: Into<Precision>,
|
||||
T: Into<Length>,
|
||||
{
|
||||
let dist: Precision = distance.into();
|
||||
let dist: Length = distance.into();
|
||||
self.get_commands_mut().push(TurtleCommand::Move(-dist));
|
||||
self
|
||||
}
|
||||
@@ -159,10 +159,10 @@ pub trait CurvedMovement: WithCommands {
|
||||
/// ```
|
||||
fn circle_left<R, A>(&mut self, radius: R, angle: A, steps: usize) -> &mut Self
|
||||
where
|
||||
R: Into<Precision>,
|
||||
R: Into<Length>,
|
||||
A: Into<Degrees>,
|
||||
{
|
||||
let r: Precision = radius.into();
|
||||
let r: Length = radius.into();
|
||||
self.get_commands_mut().push(TurtleCommand::Circle {
|
||||
radius: r,
|
||||
angle: angle.into(),
|
||||
@@ -205,10 +205,10 @@ pub trait CurvedMovement: WithCommands {
|
||||
/// ```
|
||||
fn circle_right<R, A>(&mut self, radius: R, angle: A, steps: usize) -> &mut Self
|
||||
where
|
||||
R: Into<Precision>,
|
||||
R: Into<Length>,
|
||||
A: Into<Degrees>,
|
||||
{
|
||||
let r: Precision = radius.into();
|
||||
let r: Length = radius.into();
|
||||
self.get_commands_mut().push(TurtleCommand::Circle {
|
||||
radius: r,
|
||||
angle: angle.into(),
|
||||
@@ -370,10 +370,7 @@ impl TurtlePlan {
|
||||
/// }
|
||||
/// ```
|
||||
pub fn set_heading<T: Into<Degrees>>(&mut self, heading: T) -> &mut Self {
|
||||
// Convert user-facing turtle heading (degrees, Y-up mental model)
|
||||
// to internal radians used by the render-space pipeline.
|
||||
self.queue
|
||||
.push(TurtleCommand::SetHeading(-heading.into().as_radians()));
|
||||
self.queue.push(TurtleCommand::SetHeading(heading.into()));
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ impl TurtleCommand {
|
||||
pub(crate) fn apply_to_params(&self, params: &mut TurtleParams) {
|
||||
match self {
|
||||
TurtleCommand::Move(dist) => {
|
||||
let dx = dist * params.heading.cos();
|
||||
let dy = dist * params.heading.sin();
|
||||
let dx = dist.value() * params.heading.cos();
|
||||
let dy = dist.value() * params.heading.sin();
|
||||
params.position = vec2(params.position.x + dx, params.position.y + dy);
|
||||
}
|
||||
TurtleCommand::Turn(angle) => {
|
||||
@@ -47,7 +47,7 @@ impl TurtleCommand {
|
||||
let geom = CircleGeometry::new(
|
||||
params.position,
|
||||
Radians::new(params.heading),
|
||||
*radius,
|
||||
radius.value(),
|
||||
*direction,
|
||||
);
|
||||
let angle_rad = angle.as_radians().value();
|
||||
@@ -62,7 +62,7 @@ impl TurtleCommand {
|
||||
params.position = vec2(coord.x, -coord.y);
|
||||
}
|
||||
TurtleCommand::SetHeading(heading) => {
|
||||
params.heading = normalize_angle(heading.value());
|
||||
params.heading = normalize_angle(-heading.as_radians().value());
|
||||
}
|
||||
TurtleCommand::SetColor(color) => {
|
||||
params.color = *color;
|
||||
@@ -119,15 +119,16 @@ impl TurtleCommand {
|
||||
}
|
||||
|
||||
let base: f32 = match self {
|
||||
TurtleCommand::Move(dist) => dist.abs() / spd,
|
||||
TurtleCommand::Move(dist) => dist.value().abs() / spd,
|
||||
TurtleCommand::Turn(angle) => angle.value().abs() / (spd * 1.8),
|
||||
TurtleCommand::Circle { radius, angle, .. } => {
|
||||
let arc_length = radius * angle.as_radians().value().abs();
|
||||
let arc_length = radius.value() * angle.as_radians().value().abs();
|
||||
arc_length / spd
|
||||
}
|
||||
TurtleCommand::Goto(target) => {
|
||||
let dx = target.x - params.position.x;
|
||||
let dy = target.y - params.position.y;
|
||||
let screen_target = vec2(target.x, -target.y);
|
||||
let dx = screen_target.x - params.position.x;
|
||||
let dy = screen_target.y - params.position.y;
|
||||
(dx * dx + dy * dy).sqrt() / spd
|
||||
}
|
||||
_ => 0.0,
|
||||
@@ -148,3 +149,77 @@ impl TurtleCommand {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::general::{AnimationSpeed, Color, Coordinate, Degrees};
|
||||
use crate::shapes::TurtleShape;
|
||||
|
||||
fn make_test_params() -> TurtleParams {
|
||||
TurtleParams {
|
||||
position: vec2(0.0, 0.0),
|
||||
heading: 0.0,
|
||||
pen_down: true,
|
||||
pen_width: 1.0,
|
||||
color: Color::new(0.0, 0.0, 0.0, 1.0),
|
||||
fill_color: None,
|
||||
visible: true,
|
||||
shape: TurtleShape::turtle(),
|
||||
speed: AnimationSpeed::Animated(100.0),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_goto_duration_cartesian_inversion() {
|
||||
let mut params = make_test_params();
|
||||
// Set screen position to (0, 100), which corresponds to Cartesian (0, -100)
|
||||
params.position = vec2(0.0, 100.0);
|
||||
|
||||
// Move to Cartesian (0, 100), which in screen space is (0, -100)
|
||||
// Distance should be 200 pixels!
|
||||
let cmd = TurtleCommand::Goto(Coordinate::new(0.0, 100.0));
|
||||
let duration = cmd.animation_duration(¶ms, AnimationSpeed::Animated(100.0));
|
||||
|
||||
// At 100 px/sec across 200 pixels, duration should be 2.0 seconds
|
||||
assert!(
|
||||
(duration - 2.0).abs() < 0.01,
|
||||
"Expected duration ~2.0s for 200px move, got {duration}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_heading_degrees_and_instant_duration() {
|
||||
let mut params = make_test_params();
|
||||
|
||||
// 90° = North (in screen coordinates: -pi/2)
|
||||
let cmd_north = TurtleCommand::SetHeading(Degrees::new(90.0));
|
||||
cmd_north.apply_to_params(&mut params);
|
||||
let expected_north = -std::f32::consts::FRAC_PI_2;
|
||||
assert!(
|
||||
(params.heading - expected_north).abs() < 0.001,
|
||||
"Heading 90° should be North (-π/2), got {}",
|
||||
params.heading
|
||||
);
|
||||
|
||||
// SetHeading should be instant (0.01 minimum duration)
|
||||
let duration = cmd_north.animation_duration(¶ms, AnimationSpeed::Animated(100.0));
|
||||
assert!((duration - 0.01).abs() < 0.001);
|
||||
|
||||
// 0° = East (0 radians)
|
||||
let cmd_east = TurtleCommand::SetHeading(Degrees::new(0.0));
|
||||
cmd_east.apply_to_params(&mut params);
|
||||
assert!((params.heading - 0.0).abs() < 0.001);
|
||||
|
||||
// 270° = South (+pi/2 radians in screen coords)
|
||||
let cmd_south = TurtleCommand::SetHeading(Degrees::new(270.0));
|
||||
cmd_south.apply_to_params(&mut params);
|
||||
let expected_south = std::f32::consts::FRAC_PI_2;
|
||||
assert!(
|
||||
(params.heading - expected_south).abs() < 0.001,
|
||||
"Heading 270° should be South (π/2), got {}",
|
||||
params.heading
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
//! Turtle commands and command queue
|
||||
|
||||
use crate::general::{AnimationSpeed, Color, Coordinate, Degrees, FontSize, Precision, Radians};
|
||||
use crate::general::{AnimationSpeed, Color, Coordinate, Degrees, FontSize, Length, Precision};
|
||||
use crate::shapes::TurtleShape;
|
||||
|
||||
/// Individual turtle commands
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum TurtleCommand {
|
||||
// Movement (positive = forward, negative = backward)
|
||||
Move(Precision),
|
||||
Move(Length),
|
||||
|
||||
// Rotation (positive = right/clockwise, negative = left/counter-clockwise)
|
||||
// Stored in degrees — the natural unit at the user-facing API boundary.
|
||||
@@ -15,7 +15,7 @@ pub enum TurtleCommand {
|
||||
|
||||
// Circle drawing
|
||||
Circle {
|
||||
radius: Precision,
|
||||
radius: Length,
|
||||
angle: Degrees, // sweep angle — degrees, as supplied by the user
|
||||
steps: usize,
|
||||
direction: crate::circle_geometry::CircleDirection,
|
||||
@@ -34,10 +34,9 @@ pub enum TurtleCommand {
|
||||
|
||||
// Position
|
||||
Goto(Coordinate),
|
||||
/// Heading stored as internal radians (Y-down render-space convention).
|
||||
/// Values passed via `TurtlePlan::set_heading` are converted from
|
||||
/// user-facing degrees before this command is enqueued.
|
||||
SetHeading(Radians),
|
||||
/// Heading stored in user degrees (Cartesian convention: 0° = East, 90° = North).
|
||||
/// Conversion to internal screen-space heading is performed when executed.
|
||||
SetHeading(Degrees),
|
||||
|
||||
// Visibility
|
||||
ShowTurtle,
|
||||
|
||||
@@ -203,13 +203,10 @@ impl TurtleCommandReceiver {
|
||||
/// Panics if `buffer_size` is 0.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```no_run
|
||||
/// # use turtle_lib::*;
|
||||
/// # fn example() {
|
||||
/// ```ignore
|
||||
/// let (tx, _rx) = turtle_command_channel(0, 100);
|
||||
/// // Sender goes to game threads
|
||||
/// // Receiver stays in render thread (or `TurtleApp`)
|
||||
/// # }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub(crate) fn turtle_command_channel(
|
||||
|
||||
@@ -62,7 +62,7 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) {
|
||||
direction,
|
||||
} => {
|
||||
// Draw arc segments from start to current position
|
||||
draw_tween_arc(tween, *radius, *angle, *steps, *direction);
|
||||
draw_tween_arc(tween, radius.value(), *angle, *steps, *direction);
|
||||
}
|
||||
_ if should_draw_tween_line(&tween.command) => {
|
||||
// Draw straight line for other movement commands (use tween's current position)
|
||||
@@ -133,7 +133,7 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) {
|
||||
let geom = CircleGeometry::new(
|
||||
tween.start_params.position,
|
||||
Radians::new(tween.start_params.heading),
|
||||
*radius,
|
||||
radius.value(),
|
||||
*direction,
|
||||
);
|
||||
let elapsed = get_time() - tween.start_time;
|
||||
@@ -147,7 +147,7 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) {
|
||||
let sweep_so_far = angle.as_radians().value() * eased_progress;
|
||||
for pt in arc_points(
|
||||
geom.center,
|
||||
*radius,
|
||||
radius.value(),
|
||||
geom.start_angle_from_center,
|
||||
sweep_so_far,
|
||||
samples_to_draw,
|
||||
|
||||
+10
-10
@@ -242,7 +242,7 @@ pub(crate) fn record_fill_vertices_after_movement(
|
||||
let geom = CircleGeometry::new(
|
||||
start_state.position,
|
||||
Radians::new(start_state.heading),
|
||||
*radius,
|
||||
radius.value(),
|
||||
*direction,
|
||||
);
|
||||
if let Some(ref mut fill_state) = filling {
|
||||
@@ -252,7 +252,7 @@ pub(crate) fn record_fill_vertices_after_movement(
|
||||
turtle_id,
|
||||
center_x = geom.center.x,
|
||||
center_y = geom.center.y,
|
||||
radius,
|
||||
radius = radius.value(),
|
||||
steps,
|
||||
num_samples,
|
||||
"Recording arc vertices"
|
||||
@@ -268,8 +268,8 @@ pub(crate) fn record_fill_vertices_after_movement(
|
||||
}
|
||||
};
|
||||
let vertex = Coordinate::new(
|
||||
geom.center.x + radius * current_angle.cos(),
|
||||
geom.center.y + radius * current_angle.sin(),
|
||||
geom.center.x + radius.value() * current_angle.cos(),
|
||||
geom.center.y + radius.value() * current_angle.sin(),
|
||||
);
|
||||
tracing::trace!(
|
||||
turtle_id,
|
||||
@@ -347,12 +347,12 @@ pub(crate) fn tessellate_command(
|
||||
let geom = CircleGeometry::new(
|
||||
start.position,
|
||||
Radians::new(start.heading),
|
||||
*radius,
|
||||
radius.value(),
|
||||
*direction,
|
||||
);
|
||||
let mesh = tessellation::tessellate_arc(
|
||||
geom.center,
|
||||
*radius,
|
||||
radius.value(),
|
||||
geom.start_angle_from_center.to_degrees(),
|
||||
angle.value(),
|
||||
start.color,
|
||||
@@ -402,7 +402,7 @@ pub(crate) fn push_svg_for_draw(
|
||||
svg_log.push(SvgRecord::Arc {
|
||||
start_position: start.position,
|
||||
start_heading: start.heading,
|
||||
radius: *radius,
|
||||
radius: radius.value(),
|
||||
angle: *angle,
|
||||
direction: *direction,
|
||||
color: start.color,
|
||||
@@ -474,7 +474,7 @@ pub(crate) fn execute_command_with_id(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::commands::TurtleCommand;
|
||||
use crate::general::Degrees;
|
||||
use crate::general::{Degrees, Length};
|
||||
use crate::shapes::TurtleShape;
|
||||
use crate::tweening::TweenController;
|
||||
|
||||
@@ -509,7 +509,7 @@ mod tests {
|
||||
assert_eq!(state.params.heading, 0.0);
|
||||
|
||||
// Forward 100 - should move to (100, 0)
|
||||
execute_command(&TurtleCommand::Move(100.0), &mut state);
|
||||
execute_command(&TurtleCommand::Move(Length::new(100.0)), &mut state);
|
||||
assert!(
|
||||
(state.params.position.x - 100.0).abs() < 0.01,
|
||||
"After forward(100): x = {}",
|
||||
@@ -544,7 +544,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Forward 50 - should move north (negative Y) to (100, -50)
|
||||
execute_command(&TurtleCommand::Move(50.0), &mut state);
|
||||
execute_command(&TurtleCommand::Move(Length::new(50.0)), &mut state);
|
||||
assert!(
|
||||
(state.params.position.x - 100.0).abs() < 0.01,
|
||||
"Final position: x = {} (expected 100.0)",
|
||||
|
||||
@@ -59,7 +59,7 @@ where
|
||||
let mut app = crate::TurtleApp::new().with_commands(turtle.build());
|
||||
app.set_all_turtles_speed(crate::AnimationSpeed::Instant(1000));
|
||||
|
||||
while !app.all_animations_complete() {
|
||||
while !app.is_complete() {
|
||||
app.update();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,33 @@
|
||||
//! Length type for distance measurements
|
||||
|
||||
use super::Precision;
|
||||
use std::ops::Neg;
|
||||
|
||||
#[derive(Default, Copy, Clone, Debug, PartialEq)]
|
||||
/// A spatial distance or length measurement.
|
||||
///
|
||||
/// Used at the public API boundary for movement distances and arc radii.
|
||||
#[derive(Default, Copy, Clone, Debug, PartialEq, PartialOrd)]
|
||||
pub struct Length(pub Precision);
|
||||
|
||||
impl From<i16> for Length {
|
||||
fn from(i: i16) -> Self {
|
||||
Self(Precision::from(i))
|
||||
impl Length {
|
||||
/// Create a new `Length` from a raw value.
|
||||
#[must_use]
|
||||
pub const fn new(v: Precision) -> Self {
|
||||
Self(v)
|
||||
}
|
||||
|
||||
/// Extract the raw `Precision` (`f32`) value.
|
||||
#[must_use]
|
||||
pub const fn value(self) -> Precision {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Neg for Length {
|
||||
type Output = Self;
|
||||
|
||||
fn neg(self) -> Self {
|
||||
Self(-self.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +37,56 @@ impl From<f32> for Length {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Length {
|
||||
fn from(f: f64) -> Self {
|
||||
Self(f as Precision)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i16> for Length {
|
||||
fn from(i: i16) -> Self {
|
||||
Self(Precision::from(i))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for Length {
|
||||
fn from(i: i32) -> Self {
|
||||
Self(i as Precision)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for Length {
|
||||
fn from(u: usize) -> Self {
|
||||
Self(u as Precision)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_length_conversions_and_negation() {
|
||||
let l_f32: Length = 42.5_f32.into();
|
||||
assert_eq!(l_f32.value(), 42.5);
|
||||
|
||||
let l_f64: Length = 100.0_f64.into();
|
||||
assert_eq!(l_f64.value(), 100.0);
|
||||
|
||||
let l_i32: Length = 50_i32.into();
|
||||
assert_eq!(l_i32.value(), 50.0);
|
||||
|
||||
let l_i16: Length = 25_i16.into();
|
||||
assert_eq!(l_i16.value(), 25.0);
|
||||
|
||||
let l_usize: Length = 10_usize.into();
|
||||
assert_eq!(l_usize.value(), 10.0);
|
||||
|
||||
let neg = -l_f32;
|
||||
assert_eq!(neg.value(), -42.5);
|
||||
|
||||
assert!(Length::new(10.0) < Length::new(20.0));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ impl TurtleApp {
|
||||
filename: &str,
|
||||
format: export::DrawingFormat,
|
||||
) -> Result<(), export::ExportError> {
|
||||
let _ = filename;
|
||||
match format {
|
||||
#[cfg(feature = "svg")]
|
||||
export::DrawingFormat::Svg => {
|
||||
@@ -371,12 +372,6 @@ impl TurtleApp {
|
||||
.all(|turtle| turtle.tween_controller.is_complete())
|
||||
}
|
||||
|
||||
/// Check if all animations are complete (alias for is_complete)
|
||||
#[must_use]
|
||||
pub fn all_animations_complete(&self) -> bool {
|
||||
self.is_complete()
|
||||
}
|
||||
|
||||
/// Set the animation speed for all turtles
|
||||
///
|
||||
/// # Arguments
|
||||
|
||||
+12
-12
@@ -180,7 +180,7 @@ impl TweenController {
|
||||
calculate_circle_position(
|
||||
tween.start_params.position,
|
||||
Radians::new(tween.start_params.heading),
|
||||
*radius,
|
||||
radius.value(),
|
||||
angle_traveled,
|
||||
*direction,
|
||||
)
|
||||
@@ -413,7 +413,7 @@ fn current_time() -> f64 {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::commands::TurtleCommand;
|
||||
use crate::general::Degrees;
|
||||
use crate::general::{Degrees, Length};
|
||||
use crate::state::TurtleParams;
|
||||
|
||||
fn make_test_params() -> TurtleParams {
|
||||
@@ -433,10 +433,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_instant_mode_drains_queue() {
|
||||
let mut queue = CommandQueue::new();
|
||||
queue.push(TurtleCommand::Move(100.0));
|
||||
queue.push(TurtleCommand::Move(Length::new(100.0)));
|
||||
queue.push(TurtleCommand::Turn(Degrees::new(90.0)));
|
||||
queue.push(TurtleCommand::PenUp);
|
||||
queue.push(TurtleCommand::Move(50.0));
|
||||
queue.push(TurtleCommand::Move(Length::new(50.0)));
|
||||
|
||||
let mut controller = TweenController::new(queue, AnimationSpeed::Instant(100));
|
||||
assert_eq!(controller.queue.len(), 4);
|
||||
@@ -467,7 +467,7 @@ mod tests {
|
||||
let mut batch = CommandQueue::new();
|
||||
batch.push(TurtleCommand::Reset);
|
||||
batch.push(TurtleCommand::PenDown);
|
||||
batch.push(TurtleCommand::Move(10.0));
|
||||
batch.push(TurtleCommand::Move(Length::new(10.0)));
|
||||
batch.push(TurtleCommand::Turn(Degrees::new(30.0)));
|
||||
|
||||
controller.append_commands(batch);
|
||||
@@ -485,11 +485,11 @@ mod tests {
|
||||
fn test_instant_mode_respects_batch_limit_and_retains_pending() {
|
||||
let mut queue = CommandQueue::new();
|
||||
// 5 drawing commands
|
||||
queue.push(TurtleCommand::Move(10.0));
|
||||
queue.push(TurtleCommand::Move(20.0));
|
||||
queue.push(TurtleCommand::Move(30.0));
|
||||
queue.push(TurtleCommand::Move(40.0));
|
||||
queue.push(TurtleCommand::Move(50.0));
|
||||
queue.push(TurtleCommand::Move(Length::new(10.0)));
|
||||
queue.push(TurtleCommand::Move(Length::new(20.0)));
|
||||
queue.push(TurtleCommand::Move(Length::new(30.0)));
|
||||
queue.push(TurtleCommand::Move(Length::new(40.0)));
|
||||
queue.push(TurtleCommand::Move(Length::new(50.0)));
|
||||
|
||||
// Limit to 2 draw calls per frame
|
||||
let mut controller = TweenController::new(queue, AnimationSpeed::Instant(2));
|
||||
@@ -520,8 +520,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_animated_mode_pops_to_current_tween() {
|
||||
let mut queue = CommandQueue::new();
|
||||
queue.push(TurtleCommand::Move(100.0));
|
||||
queue.push(TurtleCommand::Move(50.0));
|
||||
queue.push(TurtleCommand::Move(Length::new(100.0)));
|
||||
queue.push(TurtleCommand::Move(Length::new(50.0)));
|
||||
|
||||
let mut controller = TweenController::new(
|
||||
queue,
|
||||
|
||||
Reference in New Issue
Block a user