Usability & Ergonomics Improvements

All issues identified across Sections 3.1, 3.2, and 3.3 have been
resolved, verified with unit tests, automated compilation checks under
`-D float_literal_f32_fallback`, and headless SVG export runs.

-
  **[angle.rs](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/general/angle.rs)**:
  - Implemented `From<f64>` and `From<usize>` for `Degrees`.
  - Added unit tests `from_integer` and `from_f64` to verify conversion
    accuracy.
-
  **[fontsize.rs](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/general/fontsize.rs)**:
  - Implemented `From<f64>` for `FontSize`.
  - Refactored `FontSize::value(self)` to pass Copy type by value.
  - Added unit test `font_size_conversions`.
-
  **[general.rs](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/general.rs)**:
  - Implemented `From<f64>`, `From<i32>`, and `From<usize>` for
    `AnimationSpeed`.
  - Added unit test `animation_speed_conversions`.
  - Re-exported `macroquad` crate (`pub use macroquad;`) so downstream
    code and macro expansions have reliable direct access.

-
  **[export.rs](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/export.rs)**:
  - Made `parse_svg_export_arg()` public.
  - Implemented `run_headless_svg_export<F>(mut build_commands: F,
    filename: &str) -> Result<(), ExportError>` that executes commands
    using `app.step_animations()`, avoiding all window/GUI dependencies
    and never calling `std::process::exit`.
  - Updated `handle_svg_export` to delegate to
    `run_headless_svg_export`.
-
  **[lib.rs](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/lib.rs)**:
  - Extracted `pub fn step_animations(&mut self)` from `update(&mut
    self)`, allowing command queue draining and tween stepping
    headlessly without querying window mouse position or events.
-
  **[state.rs](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/state.rs)**:
  - Changed `TurtleWorld::new()` to initialize camera with
    `Camera2D::default()` instead of querying `screen_width()` /
    `screen_height()`, eliminating panics when running without a
    Macroquad window.
-
  **[turtle-lib-macros/src/lib.rs](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib-macros/src/lib.rs)**:
  - Added `validate_input` helper providing clean compile diagnostics
    with spans for:
    - Multiple arguments: `#[turtle_main] functions must take either 0
      arguments or a single &mut TurtlePlan`
    - Non-unit return types: `#[turtle_main] functions cannot have a
      return type`
    - Async functions: `#[turtle_main] functions cannot be async`
  - Replaced `#[macroquad::main]` wrapper expansion with a native `fn
    main()` that inspects CLI arguments first. If `--export-svg` is
    present, it runs `run_headless_svg_export` directly and returns
    cleanly without opening a window. Otherwise, it launches
    `macroquad::Window::new(#window_title, async { ... })`.
  - Added 5 unit tests in `turtle-lib-macros` testing signature
    validation.

-
  **[.vscode/launch.json](file:///home/dietrich/Projekte/Source/turtlers/.vscode/launch.json)**:
  - Removed stale references to nonexistent `turtle-example` and
    `turtle-ui`.
  - Added debug configurations for `turtle-lib` tests,
    `turtle-lib-macros` tests, `hello_turtle`, and `breadboard`.
-
  **[breadboard.rs](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/examples/breadboard.rs)**:
  - Refactored example to use `#[turtle_main("Breadboard")]`.
  - Removed `#[cfg(feature = "svg")]` and the early-exit message; the
    example now renders directly on screen by default and supports
    `--features svg -- --export-svg breadboard.svg`.
-
  **[README.md](file:///home/dietrich/Projekte/Source/turtlers/README.md)**:
  - Documented optional user-level `~/.cargo/config.toml` mold/lld
    fast-linking configuration under "Building and Running".

---

```bash
cargo test --workspace
```
- **Result**: 21 passed (16 in `turtle-lib`, 5 in `turtle-lib-macros`),
  34 doctests passed, 0 failed.

```bash
RUSTFLAGS="-D float_literal_f32_fallback" cargo check --workspace
--all-targets --all-features
RUSTFLAGS="-D float_literal_f32_fallback" cargo check --package
turtle-lib --examples --all-features
```
- **Result**: Passed with 0 errors and 0 fallback warnings across all
  workspace crates and all 30 examples.

```bash
cargo run --package turtle-lib --example hello_turtle --features svg --
--export-svg hello.svg
cargo run --package turtle-lib --example breadboard --features svg --
--export-svg breadboard.svg
```
- **Result**: Both exported SVG files successfully and exited with code
  0 without creating or flashing a graphical window.

```bash
cargo check --package turtle-lib --example breadboard
```
- **Result**: Compiled cleanly with 0 errors when SVG feature is
  disabled.
This commit is contained in:
2026-09-19 08:19:11 +02:00
parent 76d07ab009
commit 823bf13c24
10 changed files with 389 additions and 251 deletions
+46 -35
View File
@@ -26,7 +26,9 @@ pub(crate) trait DrawingExporter {
fn export(&self, world: &TurtleWorld, filename: &str) -> Result<(), ExportError>;
}
pub(crate) fn parse_svg_export_arg() -> Option<String> {
/// Check command-line arguments for the `--export-svg <filename>` flag.
#[must_use]
pub fn parse_svg_export_arg() -> Option<String> {
let args: Vec<String> = std::env::args().collect();
let mut i = 1;
while i < args.len() {
@@ -38,49 +40,58 @@ pub(crate) fn parse_svg_export_arg() -> Option<String> {
None
}
/// Headless SVG export that executes drawing commands and writes an SVG file
/// without opening a graphics window and without calling `std::process::exit`.
///
/// # Errors
///
/// Returns `ExportError` if file I/O fails or if the `svg` feature is not enabled.
pub fn run_headless_svg_export<F>(mut build_commands: F, filename: &str) -> Result<(), ExportError>
where
F: FnMut(&mut TurtlePlan),
{
#[cfg(feature = "svg")]
{
let mut turtle = crate::create_turtle_plan();
build_commands(&mut turtle);
let mut app = crate::TurtleApp::new().with_commands(turtle.build());
app.set_all_turtles_speed(crate::AnimationSpeed::Instant(1000));
while !app.is_complete() {
app.step_animations();
}
app.export_drawing(filename, crate::export::DrawingFormat::Svg)
}
#[cfg(not(feature = "svg"))]
{
let _ = &mut build_commands;
let _ = filename;
Err(ExportError::Format(
"SVG export feature is not enabled. Please rebuild with --features svg".to_string(),
))
}
}
/// Handle the optional `--export-svg` CLI flag.
///
/// The feature gating lives inside `turtle-lib`, so the `turtle_main` macro
/// no longer needs to reference cfg flags from the consuming crate.
/// Delegates to [`run_headless_svg_export`].
pub fn handle_svg_export<F>(build_commands: F)
where
F: FnMut(&mut TurtlePlan),
{
// Avoid unused warnings when the feature is disabled
let _ = &build_commands;
if let Some(filename) = parse_svg_export_arg() {
#[cfg(feature = "svg")]
{
let mut build_commands = build_commands;
let mut turtle = crate::create_turtle_plan();
build_commands(&mut turtle);
let mut app = crate::TurtleApp::new().with_commands(turtle.build());
app.set_all_turtles_speed(crate::AnimationSpeed::Instant(1000));
while !app.is_complete() {
app.update();
match run_headless_svg_export(build_commands, &filename) {
Ok(()) => {
println!("SVG exported successfully to: {filename}");
std::process::exit(0);
}
match app.export_drawing(&filename, crate::export::DrawingFormat::Svg) {
Ok(_) => {
println!("SVG exported successfully to: {}", filename);
std::process::exit(0);
}
Err(e) => {
eprintln!("Error exporting SVG: {:?}", e);
std::process::exit(1);
}
Err(e) => {
eprintln!("Error exporting SVG: {e:?}");
std::process::exit(1);
}
}
#[cfg(not(feature = "svg"))]
{
let _ = &filename;
eprintln!("Error: SVG export feature is not enabled.");
eprintln!("Please rebuild with --features svg");
std::process::exit(1);
}
}
}
+41
View File
@@ -80,11 +80,52 @@ impl From<f32> for AnimationSpeed {
}
}
impl From<f64> for AnimationSpeed {
fn from(speed: f64) -> Self {
AnimationSpeed::from_value(speed as f32)
}
}
impl From<u32> for AnimationSpeed {
fn from(speed: u32) -> Self {
AnimationSpeed::from_u32(speed)
}
}
impl From<i32> for AnimationSpeed {
fn from(speed: i32) -> Self {
AnimationSpeed::from_value(speed as f32)
}
}
impl From<usize> for AnimationSpeed {
fn from(speed: usize) -> Self {
AnimationSpeed::from_value(speed as f32)
}
}
/// Color type re-export from macroquad
pub use macroquad::color::Color;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn animation_speed_conversions() {
assert_eq!(
AnimationSpeed::from(50.0_f64),
AnimationSpeed::Animated(50.0)
);
assert_eq!(
AnimationSpeed::from(100.0_f32),
AnimationSpeed::Animated(100.0)
);
assert_eq!(AnimationSpeed::from(1000_i32), AnimationSpeed::Instant(1));
assert_eq!(AnimationSpeed::from(1200_u32), AnimationSpeed::Instant(200));
assert_eq!(
AnimationSpeed::from(1500_usize),
AnimationSpeed::Instant(500)
);
}
}
+20
View File
@@ -65,6 +65,12 @@ impl From<f32> for Degrees {
}
}
impl From<f64> for Degrees {
fn from(v: f64) -> Self {
Self(v as Precision)
}
}
impl From<i32> for Degrees {
fn from(v: i32) -> Self {
Self(v as Precision)
@@ -77,6 +83,12 @@ impl From<i16> for Degrees {
}
}
impl From<usize> for Degrees {
fn from(v: usize) -> Self {
Self(v as Precision)
}
}
// ─────────────────────────────────────────────────────────────────────────────
/// An angle measured in radians.
@@ -158,5 +170,13 @@ mod tests {
assert_eq!(d, Degrees::new(90.0));
let d2: Degrees = 45_i16.into();
assert_eq!(d2, Degrees::new(45.0));
let d3: Degrees = 180_usize.into();
assert_eq!(d3, Degrees::new(180.0));
}
#[test]
fn from_f64() {
let d: Degrees = 90.0_f64.into();
assert_eq!(d, Degrees::new(90.0));
}
}
+22 -1
View File
@@ -12,7 +12,7 @@ impl FontSize {
/// Get the inner u16 value
#[must_use]
pub const fn value(&self) -> u16 {
pub const fn value(self) -> u16 {
self.0
}
}
@@ -41,8 +41,29 @@ impl From<i16> for FontSize {
}
}
impl From<f64> for FontSize {
fn from(f: f64) -> Self {
Self(f.max(1.0) as u16)
}
}
impl From<usize> for FontSize {
fn from(size: usize) -> Self {
Self((size as u16).max(1))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn font_size_conversions() {
assert_eq!(FontSize::from(16_u16).value(), 16);
assert_eq!(FontSize::from(24_i32).value(), 24);
assert_eq!(FontSize::from(18_i16).value(), 18);
assert_eq!(FontSize::from(32_usize).value(), 32);
assert_eq!(FontSize::from(20.5_f32).value(), 20);
assert_eq!(FontSize::from(28.0_f64).value(), 28);
}
}
+11 -1
View File
@@ -73,6 +73,9 @@ pub(crate) mod export_svg;
// Re-export the turtle_main macro
pub use turtle_lib_macros::turtle_main;
// Re-export the macroquad crate so generated macro code can access it directly
pub use macroquad;
// Re-export common macroquad types and colors for convenience
pub use macroquad::prelude::{
vec2, BLACK, BLUE, DARKGRAY, GOLD, GREEN, ORANGE, PURPLE, RED, WHITE, YELLOW,
@@ -282,12 +285,19 @@ impl TurtleApp {
}
}
/// Update animation state (call every frame)
/// Update animation state and process window mouse events (call every frame in GUI loop)
pub fn update(&mut self) {
// Handle mouse panning and zoom
self.handle_mouse_panning();
self.handle_mouse_zoom();
self.step_animations();
}
/// Drive animation updates for all turtles without querying window or mouse events.
///
/// Suitable for headless execution (such as CLI SVG export) where no graphics window exists.
pub fn step_animations(&mut self) {
// Update all turtles' tween controllers
for turtle in &mut self.world.turtles {
// Drive this turtle's animation controller for one frame.
+1 -5
View File
@@ -376,11 +376,7 @@ impl TurtleWorld {
pub fn new() -> Self {
Self {
turtles: vec![], // Start with no turtles
camera: Camera2D {
zoom: vec2(1.0 / screen_width() * 2.0, 1.0 / screen_height() * 2.0),
target: vec2(0.0, 0.0),
..Default::default()
},
camera: Camera2D::default(),
background_color: WHITE,
}
}