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:
Vendored
+39
-43
@@ -1,7 +1,4 @@
|
|||||||
{
|
{
|
||||||
// Verwendet IntelliSense zum Ermitteln möglicher Attribute.
|
|
||||||
// Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
|
|
||||||
// Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
@@ -26,58 +23,57 @@
|
|||||||
{
|
{
|
||||||
"type": "lldb",
|
"type": "lldb",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Debug executable 'turtle-example'",
|
"name": "Debug unit tests in 'turtle-lib-macros'",
|
||||||
"cargo": {
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"--bin=turtle-example",
|
|
||||||
"--package=turtle-example"
|
|
||||||
],
|
|
||||||
"filter": {
|
|
||||||
"name": "turtle-example",
|
|
||||||
"kind": "bin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"args": [],
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Debug unit tests in executable 'turtle-example'",
|
|
||||||
"cargo": {
|
|
||||||
"args": [
|
|
||||||
"test",
|
|
||||||
"--no-run",
|
|
||||||
"--bin=turtle-example",
|
|
||||||
"--package=turtle-example"
|
|
||||||
],
|
|
||||||
"filter": {
|
|
||||||
"name": "turtle-example",
|
|
||||||
"kind": "bin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"args": [],
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Debug unit tests in library 'turtle-ui'",
|
|
||||||
"cargo": {
|
"cargo": {
|
||||||
"args": [
|
"args": [
|
||||||
"test",
|
"test",
|
||||||
"--no-run",
|
"--no-run",
|
||||||
"--lib",
|
"--lib",
|
||||||
"--package=turtle-ui"
|
"--package=turtle-lib-macros"
|
||||||
],
|
],
|
||||||
"filter": {
|
"filter": {
|
||||||
"name": "turtle-ui",
|
"name": "turtle-lib-macros",
|
||||||
"kind": "lib"
|
"kind": "lib"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"args": [],
|
"args": [],
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug example 'hello_turtle'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"--example=hello_turtle",
|
||||||
|
"--package=turtle-lib"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "hello_turtle",
|
||||||
|
"kind": "example"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug example 'breadboard'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"--example=breadboard",
|
||||||
|
"--package=turtle-lib"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "breadboard",
|
||||||
|
"kind": "example"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -378,6 +378,16 @@ cargo build --release
|
|||||||
cargo build --features svg
|
cargo build --features svg
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Optional: Faster Linker Setup
|
||||||
|
|
||||||
|
For significantly faster incremental build and linking times during development on Linux, you can optionally configure `mold` or `lld` in your user-level Cargo configuration (`~/.cargo/config.toml`):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[target.x86_64-unknown-linux-gnu]
|
||||||
|
linker = "clang"
|
||||||
|
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||||
|
```
|
||||||
|
|
||||||
## Development Status
|
## Development Status
|
||||||
|
|
||||||
### ✅ Completed
|
### ✅ Completed
|
||||||
|
|||||||
+151
-84
@@ -63,45 +63,79 @@ use syn::{parse_macro_input, ItemFn};
|
|||||||
/// This expands to approximately:
|
/// This expands to approximately:
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// use macroquad::prelude::*;
|
|
||||||
/// use turtle_lib::*;
|
/// use turtle_lib::*;
|
||||||
///
|
///
|
||||||
/// #[macroquad::main("My Turtle Drawing")]
|
/// fn main() {
|
||||||
/// async fn main() {
|
/// // Handle optional SVG export headlessly without opening a window
|
||||||
/// // Parse CLI args for --export-svg flag
|
/// if let Some(filename) = turtle_lib::export::parse_svg_export_arg() {
|
||||||
/// let args: Vec<String> = std::env::args().collect();
|
/// let mut build_commands = |turtle: &mut turtle_lib::TurtlePlan| {
|
||||||
/// // ... (argument parsing logic)
|
/// my_drawing(turtle);
|
||||||
|
/// };
|
||||||
|
/// if let Err(e) = turtle_lib::export::run_headless_svg_export(&mut build_commands, &filename) {
|
||||||
|
/// eprintln!("Error exporting SVG: {:?}", e);
|
||||||
|
/// std::process::exit(1);
|
||||||
|
/// }
|
||||||
|
/// return;
|
||||||
|
/// }
|
||||||
///
|
///
|
||||||
|
/// // Normal interactive GUI mode with window
|
||||||
|
/// turtle_lib::macroquad::Window::new("My Turtle Drawing", async {
|
||||||
/// let mut turtle = create_turtle_plan();
|
/// let mut turtle = create_turtle_plan();
|
||||||
///
|
/// my_drawing(&mut turtle);
|
||||||
/// // Your drawing code here
|
|
||||||
/// turtle.set_pen_color(RED);
|
|
||||||
/// turtle.forward(100.0);
|
|
||||||
/// turtle.right(90.0);
|
|
||||||
/// turtle.forward(100.0);
|
|
||||||
///
|
///
|
||||||
/// let mut app = TurtleApp::new().with_commands(turtle.build());
|
/// let mut app = TurtleApp::new().with_commands(turtle.build());
|
||||||
///
|
///
|
||||||
/// // If --export-svg flag is present, export and exit
|
|
||||||
/// // Otherwise, enter normal rendering loop
|
|
||||||
/// loop {
|
/// loop {
|
||||||
/// clear_background(WHITE);
|
/// turtle_lib::macroquad::prelude::clear_background(turtle_lib::macroquad::prelude::WHITE);
|
||||||
/// app.update();
|
/// app.update();
|
||||||
/// app.render();
|
/// app.render();
|
||||||
/// draw_text("Press ESC or Q to quit", 10.0, 40.0, 16.0, DARKGRAY);
|
/// turtle_lib::macroquad::prelude::draw_text("Press ESC or Q to quit", 10.0, 40.0, 16.0, turtle_lib::macroquad::prelude::DARKGRAY);
|
||||||
///
|
///
|
||||||
/// if is_key_pressed(KeyCode::Escape) || is_key_pressed(KeyCode::Q) {
|
/// if turtle_lib::macroquad::prelude::is_key_pressed(turtle_lib::macroquad::prelude::KeyCode::Escape)
|
||||||
|
/// || turtle_lib::macroquad::prelude::is_key_pressed(turtle_lib::macroquad::prelude::KeyCode::Q)
|
||||||
|
/// {
|
||||||
/// break;
|
/// break;
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// next_frame().await;
|
/// turtle_lib::macroquad::prelude::next_frame().await;
|
||||||
/// }
|
/// }
|
||||||
|
/// });
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
fn validate_input(input_fn: &ItemFn) -> Result<(), syn::Error> {
|
||||||
|
if input_fn.sig.asyncness.is_some() {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
input_fn.sig.fn_token,
|
||||||
|
"#[turtle_main] functions cannot be async",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if input_fn.sig.inputs.len() > 1 {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
&input_fn.sig.inputs,
|
||||||
|
"#[turtle_main] functions must take either 0 arguments or a single `&mut TurtlePlan`",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(input_fn.sig.output, syn::ReturnType::Type(..)) {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
&input_fn.sig.output,
|
||||||
|
"#[turtle_main] functions cannot have a return type",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream {
|
pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||||
let input_fn = parse_macro_input!(input as ItemFn);
|
let input_fn = parse_macro_input!(input as ItemFn);
|
||||||
|
|
||||||
|
// Validate function signature
|
||||||
|
if let Err(err) = validate_input(&input_fn) {
|
||||||
|
return err.to_compile_error().into();
|
||||||
|
}
|
||||||
|
|
||||||
// Parse the window title from args (default to "Turtle Graphics")
|
// Parse the window title from args (default to "Turtle Graphics")
|
||||||
let window_title = if args.is_empty() {
|
let window_title = if args.is_empty() {
|
||||||
quote! { "Turtle Graphics" }
|
quote! { "Turtle Graphics" }
|
||||||
@@ -114,105 +148,138 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream {
|
|||||||
|
|
||||||
let fn_name = &input_fn.sig.ident;
|
let fn_name = &input_fn.sig.ident;
|
||||||
let fn_block = &input_fn.block;
|
let fn_block = &input_fn.block;
|
||||||
|
let fn_attrs = &input_fn.attrs;
|
||||||
// Check if the function has the expected signature
|
|
||||||
let has_turtle_param = input_fn.sig.inputs.len() == 1;
|
let has_turtle_param = input_fn.sig.inputs.len() == 1;
|
||||||
|
|
||||||
// Note: The following code has some duplication between the two branches
|
let helper_name = if fn_name == "main" {
|
||||||
// (with/without turtle parameter). This is intentional in proc macros as
|
quote::format_ident!("__turtle_main_draw")
|
||||||
// we're generating different code paths, and extracting the common parts
|
} else {
|
||||||
// into helper functions would make the macro more complex without significant benefit.
|
fn_name.clone()
|
||||||
|
|
||||||
let expanded = if has_turtle_param {
|
|
||||||
// Function takes a turtle parameter
|
|
||||||
quote! {
|
|
||||||
#[macroquad::main(#window_title)]
|
|
||||||
async fn main() {
|
|
||||||
// Build function reused for both export and normal rendering
|
|
||||||
let mut build_commands = |turtle: &mut turtle_lib::TurtlePlan| {
|
|
||||||
#fn_name(turtle);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle optional SVG export internally in turtle-lib
|
let helper_fn = if has_turtle_param {
|
||||||
turtle_lib::export::handle_svg_export(&mut build_commands);
|
quote! {
|
||||||
|
#(#fn_attrs)*
|
||||||
// Normal rendering mode (with window)
|
fn #helper_name(turtle: &mut turtle_lib::TurtlePlan) #fn_block
|
||||||
let mut turtle = turtle_lib::create_turtle_plan();
|
|
||||||
|
|
||||||
// Call the user's function with the turtle
|
|
||||||
build_commands(&mut turtle);
|
|
||||||
|
|
||||||
let mut app = turtle_lib::TurtleApp::new()
|
|
||||||
.with_commands(turtle.build());
|
|
||||||
|
|
||||||
loop {
|
|
||||||
macroquad::prelude::clear_background(macroquad::prelude::WHITE);
|
|
||||||
app.update();
|
|
||||||
app.render();
|
|
||||||
macroquad::prelude::draw_text(
|
|
||||||
"Press ESC or Q to quit",
|
|
||||||
10.0,
|
|
||||||
40.0,
|
|
||||||
16.0,
|
|
||||||
macroquad::prelude::DARKGRAY
|
|
||||||
);
|
|
||||||
|
|
||||||
if macroquad::prelude::is_key_pressed(macroquad::prelude::KeyCode::Escape)
|
|
||||||
|| macroquad::prelude::is_key_pressed(macroquad::prelude::KeyCode::Q)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
macroquad::prelude::next_frame().await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn #fn_name(turtle: &mut turtle_lib::TurtlePlan) #fn_block
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Function takes no parameters - inline the code
|
|
||||||
quote! {
|
quote! {
|
||||||
#[macroquad::main(#window_title)]
|
#(#fn_attrs)*
|
||||||
async fn main() {
|
fn #helper_name(turtle: &mut turtle_lib::TurtlePlan) {
|
||||||
// Build function reused for both export and normal rendering
|
|
||||||
let mut build_commands = |turtle: &mut turtle_lib::TurtlePlan| {
|
|
||||||
let turtle = turtle;
|
let turtle = turtle;
|
||||||
#fn_block
|
#fn_block
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle optional SVG export internally in turtle-lib
|
let expanded = quote! {
|
||||||
turtle_lib::export::handle_svg_export(&mut build_commands);
|
fn main() {
|
||||||
|
let mut build_commands = |turtle: &mut turtle_lib::TurtlePlan| {
|
||||||
|
#helper_name(turtle);
|
||||||
|
};
|
||||||
|
|
||||||
// Normal rendering mode (with window)
|
// If --export-svg flag is present, export headlessly without opening a window
|
||||||
|
if let Some(filename) = turtle_lib::export::parse_svg_export_arg() {
|
||||||
|
match turtle_lib::export::run_headless_svg_export(&mut build_commands, &filename) {
|
||||||
|
Ok(()) => {
|
||||||
|
println!("SVG exported successfully to: {}", filename);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error exporting SVG: {:?}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal rendering mode (interactive window)
|
||||||
|
turtle_lib::macroquad::Window::new(#window_title, async {
|
||||||
let mut turtle = turtle_lib::create_turtle_plan();
|
let mut turtle = turtle_lib::create_turtle_plan();
|
||||||
build_commands(&mut turtle);
|
#helper_name(&mut turtle);
|
||||||
|
|
||||||
let mut app = turtle_lib::TurtleApp::new()
|
let mut app = turtle_lib::TurtleApp::new()
|
||||||
.with_commands(turtle.build());
|
.with_commands(turtle.build());
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
macroquad::prelude::clear_background(macroquad::prelude::WHITE);
|
turtle_lib::macroquad::prelude::clear_background(turtle_lib::macroquad::prelude::WHITE);
|
||||||
app.update();
|
app.update();
|
||||||
app.render();
|
app.render();
|
||||||
macroquad::prelude::draw_text(
|
turtle_lib::macroquad::prelude::draw_text(
|
||||||
"Press ESC or Q to quit",
|
"Press ESC or Q to quit",
|
||||||
10.0,
|
10.0,
|
||||||
40.0,
|
40.0,
|
||||||
16.0,
|
16.0,
|
||||||
macroquad::prelude::DARKGRAY
|
turtle_lib::macroquad::prelude::DARKGRAY
|
||||||
);
|
);
|
||||||
|
|
||||||
if macroquad::prelude::is_key_pressed(macroquad::prelude::KeyCode::Escape)
|
if turtle_lib::macroquad::prelude::is_key_pressed(turtle_lib::macroquad::prelude::KeyCode::Escape)
|
||||||
|| macroquad::prelude::is_key_pressed(macroquad::prelude::KeyCode::Q)
|
|| turtle_lib::macroquad::prelude::is_key_pressed(turtle_lib::macroquad::prelude::KeyCode::Q)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
macroquad::prelude::next_frame().await;
|
turtle_lib::macroquad::prelude::next_frame().await;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#helper_fn
|
||||||
};
|
};
|
||||||
|
|
||||||
TokenStream::from(expanded)
|
TokenStream::from(expanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use syn::parse_quote;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_valid_zero_args() {
|
||||||
|
let input: ItemFn = parse_quote! {
|
||||||
|
fn my_draw() {
|
||||||
|
turtle.forward(100.0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
assert!(validate_input(&input).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_valid_one_arg() {
|
||||||
|
let input: ItemFn = parse_quote! {
|
||||||
|
fn my_draw(t: &mut TurtlePlan) {
|
||||||
|
t.forward(100.0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
assert!(validate_input(&input).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rejects_async() {
|
||||||
|
let input: ItemFn = parse_quote! {
|
||||||
|
async fn my_draw() {}
|
||||||
|
};
|
||||||
|
let err = validate_input(&input).unwrap_err();
|
||||||
|
assert!(err.to_string().contains("cannot be async"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rejects_multiple_args() {
|
||||||
|
let input: ItemFn = parse_quote! {
|
||||||
|
fn my_draw(t: &mut TurtlePlan, extra: i32) {}
|
||||||
|
};
|
||||||
|
let err = validate_input(&input).unwrap_err();
|
||||||
|
assert!(err.to_string().contains("must take either 0 arguments or a single"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rejects_return_type() {
|
||||||
|
let input: ItemFn = parse_quote! {
|
||||||
|
fn my_draw() -> i32 {
|
||||||
|
42
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let err = validate_input(&input).unwrap_err();
|
||||||
|
assert!(err.to_string().contains("cannot have a return type"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,48 +1,11 @@
|
|||||||
|
//! Breadboard circuit diagram example.
|
||||||
|
//!
|
||||||
|
//! Demonstrates structured procedural drawing of an electronics solderless breadboard.
|
||||||
|
//! Run normally to display on screen, or export to SVG with:
|
||||||
|
//! `cargo run --package turtle-lib --example breadboard --features svg -- --export-svg breadboard.svg`
|
||||||
|
|
||||||
use turtle_lib::*;
|
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 immediately
|
|
||||||
turtle.set_speed(1200).set_pen_width(0.5);
|
|
||||||
|
|
||||||
breadboard(&mut turtle, 65);
|
|
||||||
|
|
||||||
turtle.hide();
|
|
||||||
let mut app = TurtleApp::new().with_commands(turtle.build());
|
|
||||||
use macroquad::{
|
|
||||||
input::{is_key_pressed, KeyCode},
|
|
||||||
text::draw_text,
|
|
||||||
window::{clear_background, next_frame},
|
|
||||||
};
|
|
||||||
|
|
||||||
loop {
|
|
||||||
clear_background(WHITE);
|
|
||||||
app.update();
|
|
||||||
app.render();
|
|
||||||
|
|
||||||
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 exported to test.svg"),
|
|
||||||
Err(e) => eprintln!("Export error: {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
next_frame().await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "svg"))]
|
|
||||||
fn main() {
|
|
||||||
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 {
|
||||||
@@ -51,7 +14,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "svg")]
|
|
||||||
fn pin_row(t: &mut TurtlePlan, count: usize) {
|
fn pin_row(t: &mut TurtlePlan, count: usize) {
|
||||||
for x in 0..count {
|
for x in 0..count {
|
||||||
pin(t, 5.0);
|
pin(t, 5.0);
|
||||||
@@ -61,7 +23,6 @@ fn pin_row(t: &mut TurtlePlan, count: usize) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "svg")]
|
|
||||||
fn pin_column(t: &mut TurtlePlan, count: usize, x_coord: f32) {
|
fn pin_column(t: &mut TurtlePlan, count: usize, x_coord: f32) {
|
||||||
for x in 0..count {
|
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();
|
||||||
@@ -69,7 +30,6 @@ fn pin_column(t: &mut TurtlePlan, count: usize, x_coord: f32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "svg")]
|
|
||||||
fn pin_side(t: &mut TurtlePlan, count: usize, x_coord: f32, color: Color) {
|
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))
|
||||||
@@ -84,8 +44,7 @@ fn pin_side(t: &mut TurtlePlan, count: usize, x_coord: f32, color: Color) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "svg")]
|
fn draw_breadboard(t: &mut TurtlePlan, row_count: usize) {
|
||||||
fn breadboard(t: &mut TurtlePlan, row_count: usize) {
|
|
||||||
pin_column(t, row_count, 0.0);
|
pin_column(t, row_count, 0.0);
|
||||||
pin_column(t, row_count, 65.0);
|
pin_column(t, row_count, 65.0);
|
||||||
pin_side(t, row_count, -15.0, BLUE);
|
pin_side(t, row_count, -15.0, BLUE);
|
||||||
@@ -105,3 +64,10 @@ fn breadboard(t: &mut TurtlePlan, row_count: usize) {
|
|||||||
.forward(170.0)
|
.forward(170.0)
|
||||||
.right(90.0);
|
.right(90.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[turtle_main("Breadboard")]
|
||||||
|
fn main(turtle: &mut TurtlePlan) {
|
||||||
|
turtle.set_speed(1200).set_pen_width(0.5);
|
||||||
|
draw_breadboard(turtle, 65);
|
||||||
|
turtle.hide();
|
||||||
|
}
|
||||||
|
|||||||
+35
-24
@@ -26,7 +26,9 @@ pub(crate) trait DrawingExporter {
|
|||||||
fn export(&self, world: &TurtleWorld, filename: &str) -> Result<(), ExportError>;
|
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 args: Vec<String> = std::env::args().collect();
|
||||||
let mut i = 1;
|
let mut i = 1;
|
||||||
while i < args.len() {
|
while i < args.len() {
|
||||||
@@ -38,21 +40,18 @@ pub(crate) fn parse_svg_export_arg() -> Option<String> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle the optional `--export-svg` CLI flag.
|
/// Headless SVG export that executes drawing commands and writes an SVG file
|
||||||
|
/// without opening a graphics window and without calling `std::process::exit`.
|
||||||
///
|
///
|
||||||
/// The feature gating lives inside `turtle-lib`, so the `turtle_main` macro
|
/// # Errors
|
||||||
/// no longer needs to reference cfg flags from the consuming crate.
|
///
|
||||||
pub fn handle_svg_export<F>(build_commands: F)
|
/// 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
|
where
|
||||||
F: FnMut(&mut TurtlePlan),
|
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")]
|
#[cfg(feature = "svg")]
|
||||||
{
|
{
|
||||||
let mut build_commands = build_commands;
|
|
||||||
let mut turtle = crate::create_turtle_plan();
|
let mut turtle = crate::create_turtle_plan();
|
||||||
build_commands(&mut turtle);
|
build_commands(&mut turtle);
|
||||||
|
|
||||||
@@ -60,27 +59,39 @@ where
|
|||||||
app.set_all_turtles_speed(crate::AnimationSpeed::Instant(1000));
|
app.set_all_turtles_speed(crate::AnimationSpeed::Instant(1000));
|
||||||
|
|
||||||
while !app.is_complete() {
|
while !app.is_complete() {
|
||||||
app.update();
|
app.step_animations();
|
||||||
}
|
}
|
||||||
|
|
||||||
match app.export_drawing(&filename, crate::export::DrawingFormat::Svg) {
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "svg"))]
|
#[cfg(not(feature = "svg"))]
|
||||||
{
|
{
|
||||||
let _ = &filename;
|
let _ = &mut build_commands;
|
||||||
eprintln!("Error: SVG export feature is not enabled.");
|
let _ = filename;
|
||||||
eprintln!("Please rebuild with --features svg");
|
Err(ExportError::Format(
|
||||||
|
"SVG export feature is not enabled. Please rebuild with --features svg".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle the optional `--export-svg` CLI flag.
|
||||||
|
///
|
||||||
|
/// Delegates to [`run_headless_svg_export`].
|
||||||
|
pub fn handle_svg_export<F>(build_commands: F)
|
||||||
|
where
|
||||||
|
F: FnMut(&mut TurtlePlan),
|
||||||
|
{
|
||||||
|
if let Some(filename) = parse_svg_export_arg() {
|
||||||
|
match run_headless_svg_export(build_commands, &filename) {
|
||||||
|
Ok(()) => {
|
||||||
|
println!("SVG exported successfully to: {filename}");
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error exporting SVG: {e:?}");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
impl From<u32> for AnimationSpeed {
|
||||||
fn from(speed: u32) -> Self {
|
fn from(speed: u32) -> Self {
|
||||||
AnimationSpeed::from_u32(speed)
|
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
|
/// Color type re-export from macroquad
|
||||||
pub use macroquad::color::Color;
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
impl From<i32> for Degrees {
|
||||||
fn from(v: i32) -> Self {
|
fn from(v: i32) -> Self {
|
||||||
Self(v as Precision)
|
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.
|
/// An angle measured in radians.
|
||||||
@@ -158,5 +170,13 @@ mod tests {
|
|||||||
assert_eq!(d, Degrees::new(90.0));
|
assert_eq!(d, Degrees::new(90.0));
|
||||||
let d2: Degrees = 45_i16.into();
|
let d2: Degrees = 45_i16.into();
|
||||||
assert_eq!(d2, Degrees::new(45.0));
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ impl FontSize {
|
|||||||
|
|
||||||
/// Get the inner u16 value
|
/// Get the inner u16 value
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn value(&self) -> u16 {
|
pub const fn value(self) -> u16 {
|
||||||
self.0
|
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 {
|
impl From<usize> for FontSize {
|
||||||
fn from(size: usize) -> Self {
|
fn from(size: usize) -> Self {
|
||||||
Self((size as u16).max(1))
|
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
@@ -73,6 +73,9 @@ pub(crate) mod export_svg;
|
|||||||
// Re-export the turtle_main macro
|
// Re-export the turtle_main macro
|
||||||
pub use turtle_lib_macros::turtle_main;
|
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
|
// Re-export common macroquad types and colors for convenience
|
||||||
pub use macroquad::prelude::{
|
pub use macroquad::prelude::{
|
||||||
vec2, BLACK, BLUE, DARKGRAY, GOLD, GREEN, ORANGE, PURPLE, RED, WHITE, YELLOW,
|
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) {
|
pub fn update(&mut self) {
|
||||||
// Handle mouse panning and zoom
|
// Handle mouse panning and zoom
|
||||||
self.handle_mouse_panning();
|
self.handle_mouse_panning();
|
||||||
self.handle_mouse_zoom();
|
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
|
// Update all turtles' tween controllers
|
||||||
for turtle in &mut self.world.turtles {
|
for turtle in &mut self.world.turtles {
|
||||||
// Drive this turtle's animation controller for one frame.
|
// Drive this turtle's animation controller for one frame.
|
||||||
|
|||||||
@@ -376,11 +376,7 @@ impl TurtleWorld {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
turtles: vec![], // Start with no turtles
|
turtles: vec![], // Start with no turtles
|
||||||
camera: Camera2D {
|
camera: Camera2D::default(),
|
||||||
zoom: vec2(1.0 / screen_width() * 2.0, 1.0 / screen_height() * 2.0),
|
|
||||||
target: vec2(0.0, 0.0),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
background_color: WHITE,
|
background_color: WHITE,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user