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 diff --git a/turtle-lib-macros/src/lib.rs b/turtle-lib-macros/src/lib.rs index 6a292b3..98b2788 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(); @@ -97,11 +118,60 @@ 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! { #[macroquad::main(#window_title)] async fn main() { + // Parse command-line arguments for SVG export FIRST (before any graphics init) + 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 { + #[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 + // 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) + 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 mode (with window) let mut turtle = turtle_lib::create_turtle_plan(); // Call the user's function with the turtle @@ -139,9 +209,51 @@ pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream { quote! { #[macroquad::main(#window_title)] async fn main() { - let mut turtle = turtle_lib::create_turtle_plan(); + // Parse command-line arguments for SVG export FIRST (before any graphics init) + let export_svg_path = turtle_lib::export::parse_svg_export_arg(); - // Inline the user's code + // 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 + // 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) + 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 mode (with window) + let mut turtle = turtle_lib::create_turtle_plan(); #fn_block let mut app = turtle_lib::TurtleApp::new() 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 { 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(); +} 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 +} 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 {