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:
@@ -124,7 +124,7 @@ let tx = turtle_tx.clone();
|
|||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
loop {
|
loop {
|
||||||
let letter = get_input(); // Blocks
|
let letter = get_input(); // Blocks
|
||||||
let mut plan = create_turtle();
|
let mut plan = create_turtle_plan();
|
||||||
plan.forward(50.0);
|
plan.forward(50.0);
|
||||||
tx.send(plan.build()).ok();
|
tx.send(plan.build()).ok();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ A modern turtle graphics library for Rust built on [Macroquad](https://macroquad
|
|||||||
|
|
||||||
- 🎨 **Simple Builder API**: Chain commands like `forward(100).right(90)`
|
- 🎨 **Simple Builder API**: Chain commands like `forward(100).right(90)`
|
||||||
- ⚡ **Smooth Animations**: Tweening support with easing functions and live fill preview
|
- ⚡ **Smooth Animations**: Tweening support with easing functions and live fill preview
|
||||||
- 🚀 **Instant Mode**: Execute commands immediately without animation (speed ≥ 999)
|
- 🚀 **Instant Mode**: Execute commands immediately without animation (speed ≥ 1000)
|
||||||
- 🎯 **High-Quality Rendering**: Complete Lyon tessellation pipeline with GPU acceleration
|
- 🎯 **High-Quality Rendering**: Complete Lyon tessellation pipeline with GPU acceleration
|
||||||
- **Multi-Contour Fills**: Automatic hole detection with EvenOdd fill rule - draw cheese with holes!
|
- **Multi-Contour Fills**: Automatic hole detection with EvenOdd fill rule - draw cheese with holes!
|
||||||
- 📐 **Self-Intersecting Paths**: Stars, complex shapes - all handled correctly
|
- 📐 **Self-Intersecting Paths**: Stars, complex shapes - all handled correctly
|
||||||
@@ -99,14 +99,14 @@ plan.begin_fill();
|
|||||||
plan.end_fill(); // Auto-closes and applies fill
|
plan.end_fill(); // Auto-closes and applies fill
|
||||||
|
|
||||||
// Appearance
|
// Appearance
|
||||||
plan.set_color(RED);
|
plan.set_pen_color(RED);
|
||||||
plan.set_pen_width(5.0);
|
plan.set_pen_width(5.0);
|
||||||
plan.hide();
|
plan.hide();
|
||||||
plan.show();
|
plan.show();
|
||||||
|
|
||||||
// Speed control (dynamic)
|
// Speed control (dynamic)
|
||||||
plan.set_speed(100); // Animated mode (< 999)
|
plan.set_speed(100); // Animated mode (< 1000)
|
||||||
plan.set_speed(1000); // Instant mode (>= 999)
|
plan.set_speed(1000); // Instant mode (>= 1000)
|
||||||
|
|
||||||
// Turtle shapes
|
// Turtle shapes
|
||||||
plan.shape(ShapeType::Triangle);
|
plan.shape(ShapeType::Triangle);
|
||||||
@@ -136,7 +136,7 @@ let mut plan = create_turtle_plan();
|
|||||||
// Fast initial positioning (instant mode)
|
// Fast initial positioning (instant mode)
|
||||||
plan.set_speed(1000);
|
plan.set_speed(1000);
|
||||||
plan.pen_up();
|
plan.pen_up();
|
||||||
plan.goto(vec2(-100.0, -100.0));
|
plan.go_to(vec2(-100.0, -100.0));
|
||||||
|
|
||||||
// Slow animated drawing
|
// Slow animated drawing
|
||||||
plan.set_speed(50);
|
plan.set_speed(50);
|
||||||
@@ -219,7 +219,7 @@ You can also export SVG programmatically from your code:
|
|||||||
use turtle_lib::*;
|
use turtle_lib::*;
|
||||||
|
|
||||||
// Create your drawing
|
// Create your drawing
|
||||||
let mut plan = create_turtle();
|
let mut plan = create_turtle_plan();
|
||||||
plan.forward(100).right(90).forward(100);
|
plan.forward(100).right(90).forward(100);
|
||||||
|
|
||||||
// Create app
|
// Create app
|
||||||
@@ -252,8 +252,8 @@ cargo run --example square
|
|||||||
cargo run --example koch
|
cargo run --example koch
|
||||||
cargo run --example shapes
|
cargo run --example shapes
|
||||||
cargo run --example yinyang
|
cargo run --example yinyang
|
||||||
cargo run --example stern
|
cargo run --example star
|
||||||
cargo run --example nikolaus
|
cargo run --example house_of_nikolaus
|
||||||
|
|
||||||
# SVG export example (requires --features svg)
|
# SVG export example (requires --features svg)
|
||||||
cargo run --example export_svg --features svg
|
cargo run --example export_svg --features svg
|
||||||
@@ -274,8 +274,8 @@ RUST_LOG=turtle_lib=debug cargo run --example logging_example
|
|||||||
- **square.rs**: Basic square drawing
|
- **square.rs**: Basic square drawing
|
||||||
- **koch.rs**: Koch snowflake fractal
|
- **koch.rs**: Koch snowflake fractal
|
||||||
- **shapes.rs**: Demonstrates different turtle shapes
|
- **shapes.rs**: Demonstrates different turtle shapes
|
||||||
- **stern.rs**: Star pattern drawing
|
- **star.rs**: Star pattern drawing
|
||||||
- **nikolaus.rs**: Nikolaus (Santa) drawing
|
- **house_of_nikolaus.rs**: House of Nikolaus (Eulerian path puzzle)
|
||||||
|
|
||||||
#### Fill Examples
|
#### Fill Examples
|
||||||
|
|
||||||
@@ -297,7 +297,7 @@ RUST_LOG=turtle_lib=debug cargo run --example logging_example
|
|||||||
### Basic Fill
|
### Basic Fill
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let mut plan = create_turtle();
|
let mut plan = create_turtle_plan();
|
||||||
plan.set_fill_color(RED);
|
plan.set_fill_color(RED);
|
||||||
plan.begin_fill();
|
plan.begin_fill();
|
||||||
|
|
||||||
@@ -321,7 +321,7 @@ plan.circle_left(90.0, 360.0, 72);
|
|||||||
|
|
||||||
// pen_up() closes current contour
|
// pen_up() closes current contour
|
||||||
plan.pen_up();
|
plan.pen_up();
|
||||||
plan.goto(vec2(0.0, -30.0));
|
plan.go_to(vec2(0.0, -30.0));
|
||||||
|
|
||||||
// pen_down() starts new contour
|
// pen_down() starts new contour
|
||||||
plan.pen_down();
|
plan.pen_down();
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use turtle_lib::*;
|
use turtle_lib::*;
|
||||||
|
|
||||||
#[cfg(feature = "svg")]
|
#[cfg(feature = "svg")]
|
||||||
#[macroquad::main("Export SVG")]
|
#[macroquad::main("Export SVG")]
|
||||||
|
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// Create turtle plan
|
// Create turtle plan
|
||||||
let mut turtle = 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);
|
turtle.set_speed(1200).set_pen_width(0.5);
|
||||||
|
|
||||||
breadboard(&mut turtle, 65);
|
breadboard(&mut turtle, 65);
|
||||||
@@ -24,12 +24,12 @@ async fn main() {
|
|||||||
app.update();
|
app.update();
|
||||||
app.render();
|
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) {
|
if is_key_pressed(KeyCode::E) {
|
||||||
match app.export_drawing("test.svg", export::DrawingFormat::Svg) {
|
match app.export_drawing("test.svg", export::DrawingFormat::Svg) {
|
||||||
Ok(_) => println!("SVG exportiert nach test.svg"),
|
Ok(_) => println!("SVG exported to test.svg"),
|
||||||
Err(e) => println!("Fehler beim Export: {:?}", e),
|
Err(e) => eprintln!("Export error: {:?}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,9 +39,10 @@ async fn main() {
|
|||||||
|
|
||||||
#[cfg(not(feature = "svg"))]
|
#[cfg(not(feature = "svg"))]
|
||||||
fn main() {
|
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) {
|
fn pin(t: &mut TurtlePlan, size: f32) {
|
||||||
t.left(90.0).forward(size / 2.0);
|
t.left(90.0).forward(size / 2.0);
|
||||||
for _ in 0..5 {
|
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);
|
t.right(90.0).forward(size / 2.0).left(90.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pin_reihe(t: &mut TurtlePlan, anzahl: usize) {
|
#[cfg(feature = "svg")]
|
||||||
for x in 0..anzahl {
|
fn pin_row(t: &mut TurtlePlan, count: usize) {
|
||||||
|
for x in 0..count {
|
||||||
pin(t, 5.0);
|
pin(t, 5.0);
|
||||||
if x < anzahl - 1 {
|
if x < count - 1 {
|
||||||
t.forward(5.0);
|
t.forward(5.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pin_spalte(t: &mut TurtlePlan, anzahl: usize, x_coord: f32) {
|
#[cfg(feature = "svg")]
|
||||||
for x in 0..anzahl {
|
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();
|
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()
|
t.pen_up()
|
||||||
.go_to(vec2(x_coord, -2.5))
|
.go_to(vec2(x_coord, -2.5))
|
||||||
.pen_down()
|
.pen_down()
|
||||||
.set_pen_color(color)
|
.set_pen_color(color)
|
||||||
.set_heading(90.0);
|
.set_heading(90.0);
|
||||||
for x in 0..anzahl {
|
for x in 0..count {
|
||||||
pin(t, 5.0);
|
pin(t, 5.0);
|
||||||
if x < anzahl - 1 {
|
if x < count - 1 {
|
||||||
t.forward(5.0);
|
t.forward(5.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn breadboard(t: &mut TurtlePlan, anzahl_reihen: usize) {
|
#[cfg(feature = "svg")]
|
||||||
pin_spalte(t, anzahl_reihen, 0.0);
|
fn breadboard(t: &mut TurtlePlan, row_count: usize) {
|
||||||
pin_spalte(t, anzahl_reihen, 65.0);
|
pin_column(t, row_count, 0.0);
|
||||||
pin_seite(t, anzahl_reihen, -15.0, BLUE);
|
pin_column(t, row_count, 65.0);
|
||||||
pin_seite(t, anzahl_reihen, -25.0, RED);
|
pin_side(t, row_count, -15.0, BLUE);
|
||||||
pin_seite(t, anzahl_reihen, 125.0, BLUE);
|
pin_side(t, row_count, -25.0, RED);
|
||||||
pin_seite(t, anzahl_reihen, 135.0, RED);
|
pin_side(t, row_count, 125.0, BLUE);
|
||||||
|
pin_side(t, row_count, 135.0, RED);
|
||||||
|
|
||||||
// draw outline
|
// draw outline
|
||||||
t.pen_up().go_to(vec2(-30.0, -5.0)).pen_down();
|
t.pen_up().go_to(vec2(-30.0, -5.0)).pen_down();
|
||||||
t.set_pen_color(BLACK)
|
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)
|
.right(90.0)
|
||||||
.forward(170.0)
|
.forward(170.0)
|
||||||
.right(90.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)
|
.right(90.0)
|
||||||
.forward(170.0)
|
.forward(170.0)
|
||||||
.right(90.0);
|
.right(90.0);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ fn draw(turtle: &mut TurtlePlan) {
|
|||||||
turtle.set_pen_color(RED);
|
turtle.set_pen_color(RED);
|
||||||
turtle.set_pen_width(0.5);
|
turtle.set_pen_width(0.5);
|
||||||
turtle.left(90.0);
|
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.circle_left(100.0, 540.0, 72); // partial circle to the left
|
||||||
|
|
||||||
turtle.begin_fill();
|
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")]
|
#[cfg(feature = "svg")]
|
||||||
use turtle_lib::*;
|
use turtle_lib::*;
|
||||||
@@ -52,12 +52,12 @@ async fn main() {
|
|||||||
app.update();
|
app.update();
|
||||||
app.render();
|
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) {
|
if is_key_pressed(KeyCode::E) {
|
||||||
match app.export_drawing("test.svg", export::DrawingFormat::Svg) {
|
match app.export_drawing("test.svg", export::DrawingFormat::Svg) {
|
||||||
Ok(_) => println!("SVG exportiert nach test.svg"),
|
Ok(_) => println!("SVG exported to test.svg"),
|
||||||
Err(e) => println!("Fehler beim Export: {:?}", e),
|
Err(e) => eprintln!("Export error: {:?}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,5 +67,5 @@ async fn main() {
|
|||||||
|
|
||||||
#[cfg(not(feature = "svg"))]
|
#[cfg(not(feature = "svg"))]
|
||||||
fn main() {
|
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
|
//! Builder pattern traits for creating turtle command sequences
|
||||||
|
|
||||||
use crate::commands::{CommandQueue, TurtleCommand};
|
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};
|
use crate::shapes::{ShapeType, TurtleShape};
|
||||||
|
|
||||||
/// Trait for adding commands to a queue
|
/// Trait for adding commands to a queue
|
||||||
@@ -33,9 +33,9 @@ pub trait DirectionalMovement: WithCommands {
|
|||||||
/// ```
|
/// ```
|
||||||
fn forward<T>(&mut self, distance: T) -> &mut Self
|
fn forward<T>(&mut self, distance: T) -> &mut Self
|
||||||
where
|
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.get_commands_mut().push(TurtleCommand::Move(dist));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -61,9 +61,9 @@ pub trait DirectionalMovement: WithCommands {
|
|||||||
/// ```
|
/// ```
|
||||||
fn backward<T>(&mut self, distance: T) -> &mut Self
|
fn backward<T>(&mut self, distance: T) -> &mut Self
|
||||||
where
|
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.get_commands_mut().push(TurtleCommand::Move(-dist));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -159,10 +159,10 @@ pub trait CurvedMovement: WithCommands {
|
|||||||
/// ```
|
/// ```
|
||||||
fn circle_left<R, A>(&mut self, radius: R, angle: A, steps: usize) -> &mut Self
|
fn circle_left<R, A>(&mut self, radius: R, angle: A, steps: usize) -> &mut Self
|
||||||
where
|
where
|
||||||
R: Into<Precision>,
|
R: Into<Length>,
|
||||||
A: Into<Degrees>,
|
A: Into<Degrees>,
|
||||||
{
|
{
|
||||||
let r: Precision = radius.into();
|
let r: Length = radius.into();
|
||||||
self.get_commands_mut().push(TurtleCommand::Circle {
|
self.get_commands_mut().push(TurtleCommand::Circle {
|
||||||
radius: r,
|
radius: r,
|
||||||
angle: angle.into(),
|
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
|
fn circle_right<R, A>(&mut self, radius: R, angle: A, steps: usize) -> &mut Self
|
||||||
where
|
where
|
||||||
R: Into<Precision>,
|
R: Into<Length>,
|
||||||
A: Into<Degrees>,
|
A: Into<Degrees>,
|
||||||
{
|
{
|
||||||
let r: Precision = radius.into();
|
let r: Length = radius.into();
|
||||||
self.get_commands_mut().push(TurtleCommand::Circle {
|
self.get_commands_mut().push(TurtleCommand::Circle {
|
||||||
radius: r,
|
radius: r,
|
||||||
angle: angle.into(),
|
angle: angle.into(),
|
||||||
@@ -370,10 +370,7 @@ impl TurtlePlan {
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn set_heading<T: Into<Degrees>>(&mut self, heading: T) -> &mut Self {
|
pub fn set_heading<T: Into<Degrees>>(&mut self, heading: T) -> &mut Self {
|
||||||
// Convert user-facing turtle heading (degrees, Y-up mental model)
|
self.queue.push(TurtleCommand::SetHeading(heading.into()));
|
||||||
// to internal radians used by the render-space pipeline.
|
|
||||||
self.queue
|
|
||||||
.push(TurtleCommand::SetHeading(-heading.into().as_radians()));
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ impl TurtleCommand {
|
|||||||
pub(crate) fn apply_to_params(&self, params: &mut TurtleParams) {
|
pub(crate) fn apply_to_params(&self, params: &mut TurtleParams) {
|
||||||
match self {
|
match self {
|
||||||
TurtleCommand::Move(dist) => {
|
TurtleCommand::Move(dist) => {
|
||||||
let dx = dist * params.heading.cos();
|
let dx = dist.value() * params.heading.cos();
|
||||||
let dy = dist * params.heading.sin();
|
let dy = dist.value() * params.heading.sin();
|
||||||
params.position = vec2(params.position.x + dx, params.position.y + dy);
|
params.position = vec2(params.position.x + dx, params.position.y + dy);
|
||||||
}
|
}
|
||||||
TurtleCommand::Turn(angle) => {
|
TurtleCommand::Turn(angle) => {
|
||||||
@@ -47,7 +47,7 @@ impl TurtleCommand {
|
|||||||
let geom = CircleGeometry::new(
|
let geom = CircleGeometry::new(
|
||||||
params.position,
|
params.position,
|
||||||
Radians::new(params.heading),
|
Radians::new(params.heading),
|
||||||
*radius,
|
radius.value(),
|
||||||
*direction,
|
*direction,
|
||||||
);
|
);
|
||||||
let angle_rad = angle.as_radians().value();
|
let angle_rad = angle.as_radians().value();
|
||||||
@@ -62,7 +62,7 @@ impl TurtleCommand {
|
|||||||
params.position = vec2(coord.x, -coord.y);
|
params.position = vec2(coord.x, -coord.y);
|
||||||
}
|
}
|
||||||
TurtleCommand::SetHeading(heading) => {
|
TurtleCommand::SetHeading(heading) => {
|
||||||
params.heading = normalize_angle(heading.value());
|
params.heading = normalize_angle(-heading.as_radians().value());
|
||||||
}
|
}
|
||||||
TurtleCommand::SetColor(color) => {
|
TurtleCommand::SetColor(color) => {
|
||||||
params.color = *color;
|
params.color = *color;
|
||||||
@@ -119,15 +119,16 @@ impl TurtleCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let base: f32 = match self {
|
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::Turn(angle) => angle.value().abs() / (spd * 1.8),
|
||||||
TurtleCommand::Circle { radius, angle, .. } => {
|
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
|
arc_length / spd
|
||||||
}
|
}
|
||||||
TurtleCommand::Goto(target) => {
|
TurtleCommand::Goto(target) => {
|
||||||
let dx = target.x - params.position.x;
|
let screen_target = vec2(target.x, -target.y);
|
||||||
let dy = target.y - params.position.y;
|
let dx = screen_target.x - params.position.x;
|
||||||
|
let dy = screen_target.y - params.position.y;
|
||||||
(dx * dx + dy * dy).sqrt() / spd
|
(dx * dx + dy * dy).sqrt() / spd
|
||||||
}
|
}
|
||||||
_ => 0.0,
|
_ => 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
|
//! 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;
|
use crate::shapes::TurtleShape;
|
||||||
|
|
||||||
/// Individual turtle commands
|
/// Individual turtle commands
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum TurtleCommand {
|
pub enum TurtleCommand {
|
||||||
// Movement (positive = forward, negative = backward)
|
// Movement (positive = forward, negative = backward)
|
||||||
Move(Precision),
|
Move(Length),
|
||||||
|
|
||||||
// Rotation (positive = right/clockwise, negative = left/counter-clockwise)
|
// Rotation (positive = right/clockwise, negative = left/counter-clockwise)
|
||||||
// Stored in degrees — the natural unit at the user-facing API boundary.
|
// Stored in degrees — the natural unit at the user-facing API boundary.
|
||||||
@@ -15,7 +15,7 @@ pub enum TurtleCommand {
|
|||||||
|
|
||||||
// Circle drawing
|
// Circle drawing
|
||||||
Circle {
|
Circle {
|
||||||
radius: Precision,
|
radius: Length,
|
||||||
angle: Degrees, // sweep angle — degrees, as supplied by the user
|
angle: Degrees, // sweep angle — degrees, as supplied by the user
|
||||||
steps: usize,
|
steps: usize,
|
||||||
direction: crate::circle_geometry::CircleDirection,
|
direction: crate::circle_geometry::CircleDirection,
|
||||||
@@ -34,10 +34,9 @@ pub enum TurtleCommand {
|
|||||||
|
|
||||||
// Position
|
// Position
|
||||||
Goto(Coordinate),
|
Goto(Coordinate),
|
||||||
/// Heading stored as internal radians (Y-down render-space convention).
|
/// Heading stored in user degrees (Cartesian convention: 0° = East, 90° = North).
|
||||||
/// Values passed via `TurtlePlan::set_heading` are converted from
|
/// Conversion to internal screen-space heading is performed when executed.
|
||||||
/// user-facing degrees before this command is enqueued.
|
SetHeading(Degrees),
|
||||||
SetHeading(Radians),
|
|
||||||
|
|
||||||
// Visibility
|
// Visibility
|
||||||
ShowTurtle,
|
ShowTurtle,
|
||||||
|
|||||||
@@ -203,13 +203,10 @@ impl TurtleCommandReceiver {
|
|||||||
/// Panics if `buffer_size` is 0.
|
/// Panics if `buffer_size` is 0.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```no_run
|
/// ```ignore
|
||||||
/// # use turtle_lib::*;
|
|
||||||
/// # fn example() {
|
|
||||||
/// let (tx, _rx) = turtle_command_channel(0, 100);
|
/// let (tx, _rx) = turtle_command_channel(0, 100);
|
||||||
/// // Sender goes to game threads
|
/// // Sender goes to game threads
|
||||||
/// // Receiver stays in render thread (or `TurtleApp`)
|
/// // Receiver stays in render thread (or `TurtleApp`)
|
||||||
/// # }
|
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub(crate) fn turtle_command_channel(
|
pub(crate) fn turtle_command_channel(
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ pub(crate) fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) {
|
|||||||
direction,
|
direction,
|
||||||
} => {
|
} => {
|
||||||
// Draw arc segments from start to current position
|
// 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) => {
|
_ if should_draw_tween_line(&tween.command) => {
|
||||||
// Draw straight line for other movement commands (use tween's current position)
|
// 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(
|
let geom = CircleGeometry::new(
|
||||||
tween.start_params.position,
|
tween.start_params.position,
|
||||||
Radians::new(tween.start_params.heading),
|
Radians::new(tween.start_params.heading),
|
||||||
*radius,
|
radius.value(),
|
||||||
*direction,
|
*direction,
|
||||||
);
|
);
|
||||||
let elapsed = get_time() - tween.start_time;
|
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;
|
let sweep_so_far = angle.as_radians().value() * eased_progress;
|
||||||
for pt in arc_points(
|
for pt in arc_points(
|
||||||
geom.center,
|
geom.center,
|
||||||
*radius,
|
radius.value(),
|
||||||
geom.start_angle_from_center,
|
geom.start_angle_from_center,
|
||||||
sweep_so_far,
|
sweep_so_far,
|
||||||
samples_to_draw,
|
samples_to_draw,
|
||||||
|
|||||||
+10
-10
@@ -242,7 +242,7 @@ pub(crate) fn record_fill_vertices_after_movement(
|
|||||||
let geom = CircleGeometry::new(
|
let geom = CircleGeometry::new(
|
||||||
start_state.position,
|
start_state.position,
|
||||||
Radians::new(start_state.heading),
|
Radians::new(start_state.heading),
|
||||||
*radius,
|
radius.value(),
|
||||||
*direction,
|
*direction,
|
||||||
);
|
);
|
||||||
if let Some(ref mut fill_state) = filling {
|
if let Some(ref mut fill_state) = filling {
|
||||||
@@ -252,7 +252,7 @@ pub(crate) fn record_fill_vertices_after_movement(
|
|||||||
turtle_id,
|
turtle_id,
|
||||||
center_x = geom.center.x,
|
center_x = geom.center.x,
|
||||||
center_y = geom.center.y,
|
center_y = geom.center.y,
|
||||||
radius,
|
radius = radius.value(),
|
||||||
steps,
|
steps,
|
||||||
num_samples,
|
num_samples,
|
||||||
"Recording arc vertices"
|
"Recording arc vertices"
|
||||||
@@ -268,8 +268,8 @@ pub(crate) fn record_fill_vertices_after_movement(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let vertex = Coordinate::new(
|
let vertex = Coordinate::new(
|
||||||
geom.center.x + radius * current_angle.cos(),
|
geom.center.x + radius.value() * current_angle.cos(),
|
||||||
geom.center.y + radius * current_angle.sin(),
|
geom.center.y + radius.value() * current_angle.sin(),
|
||||||
);
|
);
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
turtle_id,
|
turtle_id,
|
||||||
@@ -347,12 +347,12 @@ pub(crate) fn tessellate_command(
|
|||||||
let geom = CircleGeometry::new(
|
let geom = CircleGeometry::new(
|
||||||
start.position,
|
start.position,
|
||||||
Radians::new(start.heading),
|
Radians::new(start.heading),
|
||||||
*radius,
|
radius.value(),
|
||||||
*direction,
|
*direction,
|
||||||
);
|
);
|
||||||
let mesh = tessellation::tessellate_arc(
|
let mesh = tessellation::tessellate_arc(
|
||||||
geom.center,
|
geom.center,
|
||||||
*radius,
|
radius.value(),
|
||||||
geom.start_angle_from_center.to_degrees(),
|
geom.start_angle_from_center.to_degrees(),
|
||||||
angle.value(),
|
angle.value(),
|
||||||
start.color,
|
start.color,
|
||||||
@@ -402,7 +402,7 @@ pub(crate) fn push_svg_for_draw(
|
|||||||
svg_log.push(SvgRecord::Arc {
|
svg_log.push(SvgRecord::Arc {
|
||||||
start_position: start.position,
|
start_position: start.position,
|
||||||
start_heading: start.heading,
|
start_heading: start.heading,
|
||||||
radius: *radius,
|
radius: radius.value(),
|
||||||
angle: *angle,
|
angle: *angle,
|
||||||
direction: *direction,
|
direction: *direction,
|
||||||
color: start.color,
|
color: start.color,
|
||||||
@@ -474,7 +474,7 @@ pub(crate) fn execute_command_with_id(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::commands::TurtleCommand;
|
use crate::commands::TurtleCommand;
|
||||||
use crate::general::Degrees;
|
use crate::general::{Degrees, Length};
|
||||||
use crate::shapes::TurtleShape;
|
use crate::shapes::TurtleShape;
|
||||||
use crate::tweening::TweenController;
|
use crate::tweening::TweenController;
|
||||||
|
|
||||||
@@ -509,7 +509,7 @@ mod tests {
|
|||||||
assert_eq!(state.params.heading, 0.0);
|
assert_eq!(state.params.heading, 0.0);
|
||||||
|
|
||||||
// Forward 100 - should move to (100, 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!(
|
assert!(
|
||||||
(state.params.position.x - 100.0).abs() < 0.01,
|
(state.params.position.x - 100.0).abs() < 0.01,
|
||||||
"After forward(100): x = {}",
|
"After forward(100): x = {}",
|
||||||
@@ -544,7 +544,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Forward 50 - should move north (negative Y) to (100, -50)
|
// 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!(
|
assert!(
|
||||||
(state.params.position.x - 100.0).abs() < 0.01,
|
(state.params.position.x - 100.0).abs() < 0.01,
|
||||||
"Final position: x = {} (expected 100.0)",
|
"Final position: x = {} (expected 100.0)",
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ where
|
|||||||
let mut app = crate::TurtleApp::new().with_commands(turtle.build());
|
let mut app = crate::TurtleApp::new().with_commands(turtle.build());
|
||||||
app.set_all_turtles_speed(crate::AnimationSpeed::Instant(1000));
|
app.set_all_turtles_speed(crate::AnimationSpeed::Instant(1000));
|
||||||
|
|
||||||
while !app.all_animations_complete() {
|
while !app.is_complete() {
|
||||||
app.update();
|
app.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,33 @@
|
|||||||
//! Length type for distance measurements
|
//! Length type for distance measurements
|
||||||
|
|
||||||
use super::Precision;
|
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);
|
pub struct Length(pub Precision);
|
||||||
|
|
||||||
impl From<i16> for Length {
|
impl Length {
|
||||||
fn from(i: i16) -> Self {
|
/// Create a new `Length` from a raw value.
|
||||||
Self(Precision::from(i))
|
#[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 {
|
impl From<i32> for Length {
|
||||||
fn from(i: i32) -> Self {
|
fn from(i: i32) -> Self {
|
||||||
Self(i as Precision)
|
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,
|
filename: &str,
|
||||||
format: export::DrawingFormat,
|
format: export::DrawingFormat,
|
||||||
) -> Result<(), export::ExportError> {
|
) -> Result<(), export::ExportError> {
|
||||||
|
let _ = filename;
|
||||||
match format {
|
match format {
|
||||||
#[cfg(feature = "svg")]
|
#[cfg(feature = "svg")]
|
||||||
export::DrawingFormat::Svg => {
|
export::DrawingFormat::Svg => {
|
||||||
@@ -371,12 +372,6 @@ impl TurtleApp {
|
|||||||
.all(|turtle| turtle.tween_controller.is_complete())
|
.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
|
/// Set the animation speed for all turtles
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|||||||
+12
-12
@@ -180,7 +180,7 @@ impl TweenController {
|
|||||||
calculate_circle_position(
|
calculate_circle_position(
|
||||||
tween.start_params.position,
|
tween.start_params.position,
|
||||||
Radians::new(tween.start_params.heading),
|
Radians::new(tween.start_params.heading),
|
||||||
*radius,
|
radius.value(),
|
||||||
angle_traveled,
|
angle_traveled,
|
||||||
*direction,
|
*direction,
|
||||||
)
|
)
|
||||||
@@ -413,7 +413,7 @@ fn current_time() -> f64 {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::commands::TurtleCommand;
|
use crate::commands::TurtleCommand;
|
||||||
use crate::general::Degrees;
|
use crate::general::{Degrees, Length};
|
||||||
use crate::state::TurtleParams;
|
use crate::state::TurtleParams;
|
||||||
|
|
||||||
fn make_test_params() -> TurtleParams {
|
fn make_test_params() -> TurtleParams {
|
||||||
@@ -433,10 +433,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_instant_mode_drains_queue() {
|
fn test_instant_mode_drains_queue() {
|
||||||
let mut queue = CommandQueue::new();
|
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::Turn(Degrees::new(90.0)));
|
||||||
queue.push(TurtleCommand::PenUp);
|
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));
|
let mut controller = TweenController::new(queue, AnimationSpeed::Instant(100));
|
||||||
assert_eq!(controller.queue.len(), 4);
|
assert_eq!(controller.queue.len(), 4);
|
||||||
@@ -467,7 +467,7 @@ mod tests {
|
|||||||
let mut batch = CommandQueue::new();
|
let mut batch = CommandQueue::new();
|
||||||
batch.push(TurtleCommand::Reset);
|
batch.push(TurtleCommand::Reset);
|
||||||
batch.push(TurtleCommand::PenDown);
|
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)));
|
batch.push(TurtleCommand::Turn(Degrees::new(30.0)));
|
||||||
|
|
||||||
controller.append_commands(batch);
|
controller.append_commands(batch);
|
||||||
@@ -485,11 +485,11 @@ mod tests {
|
|||||||
fn test_instant_mode_respects_batch_limit_and_retains_pending() {
|
fn test_instant_mode_respects_batch_limit_and_retains_pending() {
|
||||||
let mut queue = CommandQueue::new();
|
let mut queue = CommandQueue::new();
|
||||||
// 5 drawing commands
|
// 5 drawing commands
|
||||||
queue.push(TurtleCommand::Move(10.0));
|
queue.push(TurtleCommand::Move(Length::new(10.0)));
|
||||||
queue.push(TurtleCommand::Move(20.0));
|
queue.push(TurtleCommand::Move(Length::new(20.0)));
|
||||||
queue.push(TurtleCommand::Move(30.0));
|
queue.push(TurtleCommand::Move(Length::new(30.0)));
|
||||||
queue.push(TurtleCommand::Move(40.0));
|
queue.push(TurtleCommand::Move(Length::new(40.0)));
|
||||||
queue.push(TurtleCommand::Move(50.0));
|
queue.push(TurtleCommand::Move(Length::new(50.0)));
|
||||||
|
|
||||||
// Limit to 2 draw calls per frame
|
// Limit to 2 draw calls per frame
|
||||||
let mut controller = TweenController::new(queue, AnimationSpeed::Instant(2));
|
let mut controller = TweenController::new(queue, AnimationSpeed::Instant(2));
|
||||||
@@ -520,8 +520,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_animated_mode_pops_to_current_tween() {
|
fn test_animated_mode_pops_to_current_tween() {
|
||||||
let mut queue = CommandQueue::new();
|
let mut queue = CommandQueue::new();
|
||||||
queue.push(TurtleCommand::Move(100.0));
|
queue.push(TurtleCommand::Move(Length::new(100.0)));
|
||||||
queue.push(TurtleCommand::Move(50.0));
|
queue.push(TurtleCommand::Move(Length::new(50.0)));
|
||||||
|
|
||||||
let mut controller = TweenController::new(
|
let mut controller = TweenController::new(
|
||||||
queue,
|
queue,
|
||||||
|
|||||||
Reference in New Issue
Block a user