diff --git a/.vscode/launch.json b/.vscode/launch.json index 8a6c427..24394b0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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", "configurations": [ { @@ -26,58 +23,57 @@ { "type": "lldb", "request": "launch", - "name": "Debug executable 'turtle-example'", - "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'", + "name": "Debug unit tests in 'turtle-lib-macros'", "cargo": { "args": [ "test", "--no-run", "--lib", - "--package=turtle-ui" + "--package=turtle-lib-macros" ], "filter": { - "name": "turtle-ui", + "name": "turtle-lib-macros", "kind": "lib" } }, "args": [], "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}" } ] } \ No newline at end of file diff --git a/README.md b/README.md index 8aa6467..c1e1ea7 100644 --- a/README.md +++ b/README.md @@ -378,6 +378,16 @@ cargo build --release 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 ### ✅ Completed diff --git a/turtle-lib-macros/src/lib.rs b/turtle-lib-macros/src/lib.rs index 6ed75ff..e433c9b 100644 --- a/turtle-lib-macros/src/lib.rs +++ b/turtle-lib-macros/src/lib.rs @@ -63,45 +63,79 @@ use syn::{parse_macro_input, ItemFn}; /// This expands to approximately: /// /// ```ignore -/// use macroquad::prelude::*; /// use turtle_lib::*; /// -/// #[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 -/// 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()); -/// -/// // If --export-svg flag is present, export and exit -/// // Otherwise, enter normal rendering loop -/// loop { -/// clear_background(WHITE); -/// app.update(); -/// app.render(); -/// draw_text("Press ESC or Q to quit", 10.0, 40.0, 16.0, DARKGRAY); -/// -/// if is_key_pressed(KeyCode::Escape) || is_key_pressed(KeyCode::Q) { -/// break; +/// fn main() { +/// // Handle optional SVG export headlessly without opening a window +/// if let Some(filename) = turtle_lib::export::parse_svg_export_arg() { +/// let mut build_commands = |turtle: &mut turtle_lib::TurtlePlan| { +/// 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); /// } -/// -/// next_frame().await; +/// return; /// } +/// +/// // Normal interactive GUI mode with window +/// turtle_lib::macroquad::Window::new("My Turtle Drawing", async { +/// let mut turtle = create_turtle_plan(); +/// my_drawing(&mut turtle); +/// +/// let mut app = TurtleApp::new().with_commands(turtle.build()); +/// +/// loop { +/// turtle_lib::macroquad::prelude::clear_background(turtle_lib::macroquad::prelude::WHITE); +/// app.update(); +/// app.render(); +/// turtle_lib::macroquad::prelude::draw_text("Press ESC or Q to quit", 10.0, 40.0, 16.0, turtle_lib::macroquad::prelude::DARKGRAY); +/// +/// 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; +/// } +/// +/// 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] pub fn turtle_main(args: TokenStream, input: TokenStream) -> TokenStream { 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") let window_title = if args.is_empty() { 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_block = &input_fn.block; - - // Check if the function has the expected signature + let fn_attrs = &input_fn.attrs; 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 helper_name = if fn_name == "main" { + quote::format_ident!("__turtle_main_draw") + } else { + fn_name.clone() + }; - let expanded = if has_turtle_param { - // Function takes a turtle parameter + let helper_fn = if has_turtle_param { 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 - turtle_lib::export::handle_svg_export(&mut build_commands); - - // Normal rendering mode (with window) - 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 + #(#fn_attrs)* + fn #helper_name(turtle: &mut turtle_lib::TurtlePlan) #fn_block } } else { - // Function takes no parameters - inline the code 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| { - let turtle = turtle; - #fn_block - }; - - // Handle optional SVG export internally in turtle-lib - turtle_lib::export::handle_svg_export(&mut build_commands); - - // Normal rendering mode (with window) - let mut turtle = turtle_lib::create_turtle_plan(); - 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_attrs)* + fn #helper_name(turtle: &mut turtle_lib::TurtlePlan) { + let turtle = turtle; + #fn_block } } }; + let expanded = quote! { + fn main() { + let mut build_commands = |turtle: &mut turtle_lib::TurtlePlan| { + #helper_name(turtle); + }; + + // 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(); + #helper_name(&mut turtle); + + let mut app = turtle_lib::TurtleApp::new() + .with_commands(turtle.build()); + + loop { + turtle_lib::macroquad::prelude::clear_background(turtle_lib::macroquad::prelude::WHITE); + app.update(); + app.render(); + turtle_lib::macroquad::prelude::draw_text( + "Press ESC or Q to quit", + 10.0, + 40.0, + 16.0, + turtle_lib::macroquad::prelude::DARKGRAY + ); + + 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; + } + + turtle_lib::macroquad::prelude::next_frame().await; + } + }); + } + + #helper_fn + }; + 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")); + } +} diff --git a/turtle-lib/examples/breadboard.rs b/turtle-lib/examples/breadboard.rs index aece5b8..c876cc1 100644 --- a/turtle-lib/examples/breadboard.rs +++ b/turtle-lib/examples/breadboard.rs @@ -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::*; -#[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) { t.left(90.0).forward(size / 2.0); 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); } -#[cfg(feature = "svg")] fn pin_row(t: &mut TurtlePlan, count: usize) { for x in 0..count { 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) { for x in 0..count { 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) { t.pen_up() .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 breadboard(t: &mut TurtlePlan, row_count: usize) { +fn draw_breadboard(t: &mut TurtlePlan, row_count: usize) { pin_column(t, row_count, 0.0); pin_column(t, row_count, 65.0); pin_side(t, row_count, -15.0, BLUE); @@ -105,3 +64,10 @@ fn breadboard(t: &mut TurtlePlan, row_count: usize) { .forward(170.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(); +} diff --git a/turtle-lib/src/export.rs b/turtle-lib/src/export.rs index e1624ba..3fa2f3d 100644 --- a/turtle-lib/src/export.rs +++ b/turtle-lib/src/export.rs @@ -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 { +/// Check command-line arguments for the `--export-svg ` flag. +#[must_use] +pub fn parse_svg_export_arg() -> Option { let args: Vec = std::env::args().collect(); let mut i = 1; while i < args.len() { @@ -38,49 +40,58 @@ pub(crate) fn parse_svg_export_arg() -> Option { 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(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(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); - } } } diff --git a/turtle-lib/src/general.rs b/turtle-lib/src/general.rs index b399104..463f9fd 100644 --- a/turtle-lib/src/general.rs +++ b/turtle-lib/src/general.rs @@ -80,11 +80,52 @@ impl From for AnimationSpeed { } } +impl From for AnimationSpeed { + fn from(speed: f64) -> Self { + AnimationSpeed::from_value(speed as f32) + } +} + impl From for AnimationSpeed { fn from(speed: u32) -> Self { AnimationSpeed::from_u32(speed) } } +impl From for AnimationSpeed { + fn from(speed: i32) -> Self { + AnimationSpeed::from_value(speed as f32) + } +} + +impl From 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) + ); + } +} diff --git a/turtle-lib/src/general/angle.rs b/turtle-lib/src/general/angle.rs index e5814f6..5e0e22a 100644 --- a/turtle-lib/src/general/angle.rs +++ b/turtle-lib/src/general/angle.rs @@ -65,6 +65,12 @@ impl From for Degrees { } } +impl From for Degrees { + fn from(v: f64) -> Self { + Self(v as Precision) + } +} + impl From for Degrees { fn from(v: i32) -> Self { Self(v as Precision) @@ -77,6 +83,12 @@ impl From for Degrees { } } +impl From 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)); } } diff --git a/turtle-lib/src/general/fontsize.rs b/turtle-lib/src/general/fontsize.rs index e702fec..42f7fe2 100644 --- a/turtle-lib/src/general/fontsize.rs +++ b/turtle-lib/src/general/fontsize.rs @@ -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 for FontSize { } } +impl From for FontSize { + fn from(f: f64) -> Self { + Self(f.max(1.0) as u16) + } +} + impl From 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); + } +} diff --git a/turtle-lib/src/lib.rs b/turtle-lib/src/lib.rs index d4c8694..640417f 100644 --- a/turtle-lib/src/lib.rs +++ b/turtle-lib/src/lib.rs @@ -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. diff --git a/turtle-lib/src/state.rs b/turtle-lib/src/state.rs index e84126c..fe4c13d 100644 --- a/turtle-lib/src/state.rs +++ b/turtle-lib/src/state.rs @@ -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, } }