From 2b64be29a8f240d7071f6a8abf9b52c81b1cabd4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 1 Jan 2026 20:27:41 +0000 Subject: [PATCH 1/8] Initial plan From d85a9c7d262277098cb64b2c3d6a02e6add28904 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 1 Jan 2026 20:31:58 +0000 Subject: [PATCH 2/8] Add --export-svg CLI parameter support to turtle_main macro Co-authored-by: enaut <290005+enaut@users.noreply.github.com> --- turtle-lib-macros/src/lib.rs | 113 +++++++++++++++++++++++++++++++++++ turtle-lib/src/lib.rs | 17 ++++++ 2 files changed, 130 insertions(+) diff --git a/turtle-lib-macros/src/lib.rs b/turtle-lib-macros/src/lib.rs index 6a292b3..e1086fe 100644 --- a/turtle-lib-macros/src/lib.rs +++ b/turtle-lib-macros/src/lib.rs @@ -16,6 +16,14 @@ use syn::{parse_macro_input, ItemFn}; /// - Creates a turtle instance (`turtle`) /// - Sets up the `TurtleApp` with your drawing commands /// - Provides a main loop with rendering and quit handling (ESC or Q) +/// - Adds command-line parameter support for SVG export (when `svg` feature is enabled) +/// +/// # Command-Line Parameters +/// +/// When the `svg` feature is enabled, the following command-line parameter is available: +/// +/// * `--export-svg ` - Exports the drawing to an SVG file and exits immediately +/// without opening the window. Example: `cargo run --features svg -- --export-svg output.svg` /// /// # Example /// @@ -45,6 +53,13 @@ use syn::{parse_macro_input, ItemFn}; /// } /// ``` /// +/// # SVG Export Example +/// +/// ```bash +/// # Run with SVG export (requires svg feature) +/// cargo run --package turtle-lib --example macro_demo --features svg -- --export-svg output.svg +/// ``` +/// /// This expands to approximately: /// /// ```ignore @@ -53,6 +68,10 @@ use syn::{parse_macro_input, ItemFn}; /// /// #[macroquad::main("My Turtle Drawing")] /// async fn main() { +/// // Parse CLI args for --export-svg flag +/// let args: Vec = std::env::args().collect(); +/// // ... (argument parsing logic) +/// /// let mut turtle = create_turtle_plan(); /// /// // Your drawing code here @@ -63,6 +82,8 @@ use syn::{parse_macro_input, ItemFn}; /// /// let mut app = TurtleApp::new().with_commands(turtle.build()); /// +/// // If --export-svg flag is present, export and exit +/// // Otherwise, enter normal rendering loop /// loop { /// clear_background(WHITE); /// app.update(); @@ -102,6 +123,19 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream { quote! { #[macroquad::main(#window_title)] async fn main() { + // Parse command-line arguments for SVG export + let args: Vec = std::env::args().collect(); + let mut export_svg_path: Option = None; + + let mut i = 1; + while i < args.len() { + if args[i] == "--export-svg" && i + 1 < args.len() { + export_svg_path = Some(args[i + 1].clone()); + break; + } + i += 1; + } + let mut turtle = turtle_lib::create_turtle_plan(); // Call the user's function with the turtle @@ -110,6 +144,39 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream { let mut app = turtle_lib::TurtleApp::new() .with_commands(turtle.build()); + // Handle SVG export if requested + if let Some(filename) = export_svg_path { + #[cfg(feature = "svg")] + { + // Set instant speed to execute all commands immediately + app.set_all_turtles_speed(turtle_lib::AnimationSpeed::Instant(1000)); + + // Execute all commands instantly + while !app.all_animations_complete() { + app.update(); + } + + // Export to SVG + match app.export_drawing(&filename, turtle_lib::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"))] + { + eprintln!("Error: SVG export feature is not enabled."); + eprintln!("Please rebuild with --features svg"); + std::process::exit(1); + } + } + + // Normal rendering loop loop { macroquad::prelude::clear_background(macroquad::prelude::WHITE); app.update(); @@ -139,6 +206,19 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream { quote! { #[macroquad::main(#window_title)] async fn main() { + // Parse command-line arguments for SVG export + let args: Vec = std::env::args().collect(); + let mut export_svg_path: Option = None; + + let mut i = 1; + while i < args.len() { + if args[i] == "--export-svg" && i + 1 < args.len() { + export_svg_path = Some(args[i + 1].clone()); + break; + } + i += 1; + } + let mut turtle = turtle_lib::create_turtle_plan(); // Inline the user's code @@ -147,6 +227,39 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream { let mut app = turtle_lib::TurtleApp::new() .with_commands(turtle.build()); + // Handle SVG export if requested + if let Some(filename) = export_svg_path { + #[cfg(feature = "svg")] + { + // Set instant speed to execute all commands immediately + app.set_all_turtles_speed(turtle_lib::AnimationSpeed::Instant(1000)); + + // Execute all commands instantly + while !app.all_animations_complete() { + app.update(); + } + + // Export to SVG + match app.export_drawing(&filename, turtle_lib::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"))] + { + eprintln!("Error: SVG export feature is not enabled."); + eprintln!("Please rebuild with --features svg"); + std::process::exit(1); + } + } + + // Normal rendering loop loop { macroquad::prelude::clear_background(macroquad::prelude::WHITE); app.update(); diff --git a/turtle-lib/src/lib.rs b/turtle-lib/src/lib.rs index fa8045d..8b4cc94 100644 --- a/turtle-lib/src/lib.rs +++ b/turtle-lib/src/lib.rs @@ -367,6 +367,23 @@ impl TurtleApp { .all(|turtle| turtle.tween_controller.is_complete()) } + /// Check if all animations are complete (alias for is_complete) + #[must_use] + pub fn all_animations_complete(&self) -> bool { + self.is_complete() + } + + /// Set the animation speed for all turtles + /// + /// # Arguments + /// * `speed` - The animation speed to set for all turtles + pub fn set_all_turtles_speed(&mut self, speed: AnimationSpeed) { + for turtle in &mut self.world.turtles { + turtle.set_speed(speed); + turtle.tween_controller.set_speed(speed); + } + } + /// Get reference to the world state #[must_use] pub fn world(&self) -> &TurtleWorld { From c806570156c473cb76bf90f49dbc44010fcb695c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 1 Jan 2026 20:40:06 +0000 Subject: [PATCH 3/8] Implement CLI --export-svg parameter for instant SVG export Co-authored-by: enaut <290005+enaut@users.noreply.github.com> --- turtle-lib-macros/src/lib.rs | 60 +++++++++++++++----------- turtle-lib/examples/test_svg_export.rs | 36 ++++++++++++++++ 2 files changed, 72 insertions(+), 24 deletions(-) create mode 100644 turtle-lib/examples/test_svg_export.rs diff --git a/turtle-lib-macros/src/lib.rs b/turtle-lib-macros/src/lib.rs index e1086fe..c630ea7 100644 --- a/turtle-lib-macros/src/lib.rs +++ b/turtle-lib-macros/src/lib.rs @@ -123,7 +123,7 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream { quote! { #[macroquad::main(#window_title)] async fn main() { - // Parse command-line arguments for SVG export + // Parse command-line arguments for SVG export FIRST (before any graphics init) let args: Vec = std::env::args().collect(); let mut export_svg_path: Option = None; @@ -136,22 +136,21 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream { i += 1; } - let mut turtle = turtle_lib::create_turtle_plan(); - - // Call the user's function with the turtle - #fn_name(&mut turtle); - - let mut app = turtle_lib::TurtleApp::new() - .with_commands(turtle.build()); - - // Handle SVG export if requested + // Handle SVG export mode (execute instantly without rendering) if let Some(filename) = export_svg_path { #[cfg(feature = "svg")] { + let mut turtle = turtle_lib::create_turtle_plan(); + #fn_name(&mut turtle); + + // Create app and execute instantly + let mut app = turtle_lib::TurtleApp::new() + .with_commands(turtle.build()); + // Set instant speed to execute all commands immediately app.set_all_turtles_speed(turtle_lib::AnimationSpeed::Instant(1000)); - // Execute all commands instantly + // Execute all commands instantly (no rendering needed) while !app.all_animations_complete() { app.update(); } @@ -176,7 +175,15 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream { } } - // Normal rendering loop + // Normal rendering mode (with window) + let mut turtle = turtle_lib::create_turtle_plan(); + + // Call the user's function with the turtle + #fn_name(&mut turtle); + + let mut app = turtle_lib::TurtleApp::new() + .with_commands(turtle.build()); + loop { macroquad::prelude::clear_background(macroquad::prelude::WHITE); app.update(); @@ -206,7 +213,7 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream { quote! { #[macroquad::main(#window_title)] async fn main() { - // Parse command-line arguments for SVG export + // Parse command-line arguments for SVG export FIRST (before any graphics init) let args: Vec = std::env::args().collect(); let mut export_svg_path: Option = None; @@ -219,22 +226,21 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream { i += 1; } - let mut turtle = turtle_lib::create_turtle_plan(); - - // Inline the user's code - #fn_block - - let mut app = turtle_lib::TurtleApp::new() - .with_commands(turtle.build()); - - // Handle SVG export if requested + // Handle SVG export mode (execute instantly without rendering) if let Some(filename) = export_svg_path { #[cfg(feature = "svg")] { + let mut turtle = turtle_lib::create_turtle_plan(); + #fn_block + + // Create app and execute instantly + let mut app = turtle_lib::TurtleApp::new() + .with_commands(turtle.build()); + // Set instant speed to execute all commands immediately app.set_all_turtles_speed(turtle_lib::AnimationSpeed::Instant(1000)); - // Execute all commands instantly + // Execute all commands instantly (no rendering needed) while !app.all_animations_complete() { app.update(); } @@ -259,7 +265,13 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream { } } - // Normal rendering loop + // Normal rendering mode (with window) + let mut turtle = turtle_lib::create_turtle_plan(); + #fn_block + + let mut app = turtle_lib::TurtleApp::new() + .with_commands(turtle.build()); + loop { macroquad::prelude::clear_background(macroquad::prelude::WHITE); app.update(); diff --git a/turtle-lib/examples/test_svg_export.rs b/turtle-lib/examples/test_svg_export.rs new file mode 100644 index 0000000..dbeba35 --- /dev/null +++ b/turtle-lib/examples/test_svg_export.rs @@ -0,0 +1,36 @@ +//! Test example for CLI SVG export feature +//! +//! Run this with: cargo run --package turtle-lib --example test_svg_export --features svg -- --export-svg test_output.svg + +use turtle_lib::*; + +#[turtle_main("SVG Export Test")] +fn draw_test(turtle: &mut TurtlePlan) { + turtle.set_pen_color(RED); + turtle.set_pen_width(3.0); + + // Draw a square + for _ in 0..4 { + turtle.forward(100.0); + turtle.right(90.0); + } + + // Draw a circle + turtle.set_pen_color(BLUE); + turtle.pen_up(); + turtle.forward(150.0); + turtle.pen_down(); + turtle.circle_left(50.0, 360.0, 36); + + // Draw a filled triangle + turtle.set_fill_color(GREEN); + turtle.pen_up(); + turtle.go_to(vec2(-50.0, 100.0)); + turtle.pen_down(); + turtle.begin_fill(); + for _ in 0..3 { + turtle.forward(80.0); + turtle.right(120.0); + } + turtle.end_fill(); +} From f5140361d5b5af70f069b78c748187c0e2efb9a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 1 Jan 2026 20:41:52 +0000 Subject: [PATCH 4/8] Add documentation comment explaining magic number in instant speed Co-authored-by: enaut <290005+enaut@users.noreply.github.com> --- turtle-lib-macros/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/turtle-lib-macros/src/lib.rs b/turtle-lib-macros/src/lib.rs index c630ea7..5ed3835 100644 --- a/turtle-lib-macros/src/lib.rs +++ b/turtle-lib-macros/src/lib.rs @@ -148,6 +148,7 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream { .with_commands(turtle.build()); // Set instant speed to execute all commands immediately + // Use a high draw call limit (1000) to ensure all commands execute in one frame app.set_all_turtles_speed(turtle_lib::AnimationSpeed::Instant(1000)); // Execute all commands instantly (no rendering needed) @@ -238,6 +239,7 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream { .with_commands(turtle.build()); // Set instant speed to execute all commands immediately + // Use a high draw call limit (1000) to ensure all commands execute in one frame app.set_all_turtles_speed(turtle_lib::AnimationSpeed::Instant(1000)); // Execute all commands instantly (no rendering needed) From cbe249b9b730b2c50dc89917b632182449d43124 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 1 Jan 2026 20:42:53 +0000 Subject: [PATCH 5/8] Update README with CLI SVG export documentation and examples Co-authored-by: enaut <290005+enaut@users.noreply.github.com> --- README.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d01129..4658778 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,32 @@ Add the `svg` feature to enable SVG export functionality: cargo run --example export_svg --features svg ``` -### Usage +### Command-Line SVG Export + +When using the `turtle_main` macro with the `svg` feature enabled, you can export drawings directly to SVG files using the `--export-svg` command-line parameter: + +```bash +# Export any example to SVG without showing the window +cargo run --example macro_demo --features svg -- --export-svg output.svg + +# Works with all turtle_main-based examples +cargo run --example hello_turtle --features svg -- --export-svg square.svg +``` + +This will: +- Execute all drawing commands instantly (no animation) +- Export the result to an SVG file +- Exit immediately without opening a window + +**Note**: The program still requires a display context to initialize. In headless environments, use `xvfb-run`: + +```bash +xvfb-run -a cargo run --example macro_demo --features svg -- --export-svg output.svg +``` + +### Programmatic SVG Export + +You can also export SVG programmatically from your code: ```rust use turtle_lib::*; @@ -239,6 +264,10 @@ cargo run --example nikolaus # SVG export example (requires --features svg) cargo run --example export_svg --features svg +# Export any example to SVG using CLI parameter (requires --features svg) +cargo run --example macro_demo --features svg -- --export-svg output.svg +cargo run --example hello_turtle --features svg -- --export-svg square.svg + # Logging example - shows how to enable debug output cargo run --example logging_example RUST_LOG=turtle_lib=debug cargo run --example logging_example From 9568bc10d93ed9bf27800646037bd6dfbf1d0cd3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 1 Jan 2026 20:44:27 +0000 Subject: [PATCH 6/8] Add comment explaining intentional code duplication in proc macro Co-authored-by: enaut <290005+enaut@users.noreply.github.com> --- turtle-lib-macros/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/turtle-lib-macros/src/lib.rs b/turtle-lib-macros/src/lib.rs index 5ed3835..ce33586 100644 --- a/turtle-lib-macros/src/lib.rs +++ b/turtle-lib-macros/src/lib.rs @@ -118,6 +118,11 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream { // Check if the function has the expected signature let has_turtle_param = input_fn.sig.inputs.len() == 1; + // Note: The following code has some duplication between the two branches + // (with/without turtle parameter). This is intentional in proc macros as + // we're generating different code paths, and extracting the common parts + // into helper functions would make the macro more complex without significant benefit. + let expanded = if has_turtle_param { // Function takes a turtle parameter quote! { From cadc5a6798ff24b459fcb1f606c7ce4ffec9e6f8 Mon Sep 17 00:00:00 2001 From: Franz Dietrich Date: Fri, 2 Jan 2026 11:45:20 +0100 Subject: [PATCH 7/8] fix cartesian axes example --- turtle-lib/examples/cartesian_axes.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/turtle-lib/examples/cartesian_axes.rs b/turtle-lib/examples/cartesian_axes.rs index 7648582..c67a43d 100644 --- a/turtle-lib/examples/cartesian_axes.rs +++ b/turtle-lib/examples/cartesian_axes.rs @@ -80,10 +80,10 @@ fn draw(turtle: &mut TurtlePlan) { // Draw and label 4 points (one per quadrant) let points = vec![ - (vec2(120.0, 100.0), "A(2|1)"), - (vec2(-120.0, 100.0), "B(-2|1)"), - (vec2(-120.0, -100.0), "C(-2|-1)"), - (vec2(120.0, -100.0), "D(2|-1)"), + (vec2(100.0, 50.0), "A(2|1)"), + (vec2(-100.0, 50.0), "B(-2|1)"), + (vec2(-100.0, -50.0), "C(-2|-1)"), + (vec2(100.0, -50.0), "D(2|-1)"), ]; for (position, label) in points { From 6f29d97bb6f1ebd05db022a2e431bd2703bedff9 Mon Sep 17 00:00:00 2001 From: Franz Dietrich Date: Fri, 2 Jan 2026 11:53:18 +0100 Subject: [PATCH 8/8] unify comandline parsing into function --- turtle-lib-macros/src/lib.rs | 24 ++---------------------- turtle-lib/src/export.rs | 12 ++++++++++++ 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/turtle-lib-macros/src/lib.rs b/turtle-lib-macros/src/lib.rs index ce33586..98b2788 100644 --- a/turtle-lib-macros/src/lib.rs +++ b/turtle-lib-macros/src/lib.rs @@ -129,17 +129,7 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream { #[macroquad::main(#window_title)] async fn main() { // Parse command-line arguments for SVG export FIRST (before any graphics init) - let args: Vec = std::env::args().collect(); - let mut export_svg_path: Option = None; - - let mut i = 1; - while i < args.len() { - if args[i] == "--export-svg" && i + 1 < args.len() { - export_svg_path = Some(args[i + 1].clone()); - break; - } - i += 1; - } + let export_svg_path = turtle_lib::export::parse_svg_export_arg(); // Handle SVG export mode (execute instantly without rendering) if let Some(filename) = export_svg_path { @@ -220,17 +210,7 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream { #[macroquad::main(#window_title)] async fn main() { // Parse command-line arguments for SVG export FIRST (before any graphics init) - let args: Vec = std::env::args().collect(); - let mut export_svg_path: Option = None; - - let mut i = 1; - while i < args.len() { - if args[i] == "--export-svg" && i + 1 < args.len() { - export_svg_path = Some(args[i + 1].clone()); - break; - } - i += 1; - } + let export_svg_path = turtle_lib::export::parse_svg_export_arg(); // Handle SVG export mode (execute instantly without rendering) if let Some(filename) = export_svg_path { diff --git a/turtle-lib/src/export.rs b/turtle-lib/src/export.rs index 0f2a79e..98086f1 100644 --- a/turtle-lib/src/export.rs +++ b/turtle-lib/src/export.rs @@ -24,3 +24,15 @@ pub trait DrawingExporter { /// Returns an error if the export fails (e.g., file I/O error) fn export(&self, world: &TurtleWorld, filename: &str) -> Result<(), ExportError>; } + +pub fn parse_svg_export_arg() -> Option { + let args: Vec = std::env::args().collect(); + let mut i = 1; + while i < args.len() { + if args[i] == "--export-svg" && i + 1 < args.len() { + return Some(args[i + 1].clone()); + } + i += 1; + } + None +}