diff --git a/turtle-lib-macros/src/lib.rs b/turtle-lib-macros/src/lib.rs index b9faed1..e2724ba 100644 --- a/turtle-lib-macros/src/lib.rs +++ b/turtle-lib-macros/src/lib.rs @@ -259,7 +259,7 @@ fn turtle_main_impl( return; } Err(e) => { - eprintln!("Error exporting SVG: {:?}", e); + eprintln!("Error exporting SVG: {}", e); std::process::exit(1); } } diff --git a/turtle-lib/examples/sierpinski_triangle.rs b/turtle-lib/examples/sierpinski_triangle.rs index e451f6c..7992bb8 100644 --- a/turtle-lib/examples/sierpinski_triangle.rs +++ b/turtle-lib/examples/sierpinski_triangle.rs @@ -17,6 +17,10 @@ //! That is relatively easy to implement, as long as you follow these steps and let recursion do //! the rest. Another little bonus this example provides is the ability to customize the drawing //! size: the triangle will stay correctly sized and positioned automatically. +//! +//! Note: This example queries `screen_width()` and `screen_height()` to calculate positioning +//! relative to the window. Consequently, it requires an active graphics window and does not +//! support headless SVG export (`--export-svg`). use macroquad::window::{screen_height, screen_width}; use turtle_lib::*; @@ -86,6 +90,9 @@ fn sierpinski_triangle(turtle: &mut TurtlePlan, level: u8, size: f32) { /// `level` is still required, it can't be computed automatically. However, given the used /// canvas size, it will compute the appropriate size and start point so the triangle gets /// centered and occupies as much drawing space as possible while staying in bounds. +/// +/// Note: Because this function queries window dimensions, it requires an active graphics +/// window and cannot be run in headless SVG export mode. fn sierpinski_triangle_auto(turtle: &mut TurtlePlan, level: u8) { let size = TRIANGLE_SIZE; diff --git a/turtle-lib/src/export.rs b/turtle-lib/src/export.rs index 0baf558..1a20016 100644 --- a/turtle-lib/src/export.rs +++ b/turtle-lib/src/export.rs @@ -7,9 +7,29 @@ use crate::TurtlePlan; pub enum ExportError { Io(std::io::Error), Format(String), + Execution(String), // Additional formats can be added here. } +impl std::fmt::Display for ExportError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Io(err) => write!(f, "I/O error: {err}"), + Self::Format(msg) => write!(f, "format error: {msg}"), + Self::Execution(msg) => write!(f, "execution error: {msg}"), + } + } +} + +impl std::error::Error for ExportError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Io(err) => Some(err), + Self::Format(_) | Self::Execution(_) => None, + } + } +} + #[derive(Clone, Copy, Debug)] pub enum DrawingFormat { #[cfg(feature = "svg")] @@ -41,25 +61,105 @@ pub fn parse_svg_export_arg() -> Option { None } +#[cfg(feature = "svg")] +type PanicHookFn = Box) + Sync + Send + 'static>; + +#[cfg(feature = "svg")] +struct PanicHookGuard { + prev_hook: Option>, +} + +#[cfg(feature = "svg")] +impl Drop for PanicHookGuard { + fn drop(&mut self) { + if let Some(prev) = self.prev_hook.take() { + std::panic::set_hook(Box::new(move |info| prev(info))); + } + } +} + /// 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. +/// Returns `ExportError` if file I/O fails, drawing execution panics, 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")] { + use std::panic::AssertUnwindSafe; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::Arc; + let mut turtle = crate::create_turtle_plan(); - build_commands(&mut turtle); - let mut app = crate::TurtleApp::new(); - app.execute_immediate(0, turtle); + let is_mq_panic = Arc::new(AtomicBool::new(false)); + let is_mq_clone = Arc::clone(&is_mq_panic); - app.export_drawing(filename, crate::export::DrawingFormat::Svg) + let prev_hook = Arc::new(std::panic::take_hook()); + let prev_hook_for_closure = Arc::clone(&prev_hook); + + let guard = PanicHookGuard { + prev_hook: Some(prev_hook), + }; + + std::panic::set_hook(Box::new(move |info| { + prev_hook_for_closure(info); + + let msg = if let Some(s) = info.payload().downcast_ref::<&str>() { + *s + } else if let Some(s) = info.payload().downcast_ref::() { + s.as_str() + } else { + "" + }; + + let loc_file = info.location().map_or("", std::panic::Location::file); + let is_context_panic = msg.contains("THREAD_ID.is_some()") + || (loc_file.contains("macroquad") && msg.contains("assertion failed")); + + if is_context_panic { + is_mq_clone.store(true, Ordering::SeqCst); + eprintln!("\n================================================================================"); + eprintln!("Headless SVG Export Note:"); + eprintln!("A Macroquad window/rendering function (e.g. `screen_width()`, `screen_height()`,"); + eprintln!("or input check) was called while running in headless export mode."); + eprintln!("Headless export does not initialize a graphics window. To resolve this:"); + eprintln!(" - Use relative turtle commands or fixed coordinates instead of window queries, or"); + eprintln!(" - Run the program in windowed GUI mode without the `--export-svg` flag."); + eprintln!("================================================================================\n"); + } + })); + + let result = std::panic::catch_unwind(AssertUnwindSafe(|| { + build_commands(&mut turtle); + })); + + drop(guard); + + match result { + Ok(()) => { + let mut app = crate::TurtleApp::new(); + app.execute_immediate(0, turtle); + + app.export_drawing(filename, crate::export::DrawingFormat::Svg) + } + Err(payload) => { + let err_msg = if is_mq_panic.load(Ordering::SeqCst) { + "Drawing function called Macroquad window/GUI functions (e.g. `screen_width()`, `screen_height()`) which are unavailable in headless SVG export mode.".to_string() + } else if let Some(s) = payload.downcast_ref::<&str>() { + (*s).to_string() + } else if let Some(s) = payload.downcast_ref::() { + s.clone() + } else { + "Drawing function panicked during execution".to_string() + }; + Err(ExportError::Execution(err_msg)) + } + } } #[cfg(not(feature = "svg"))] @@ -86,7 +186,7 @@ where std::process::exit(0); } Err(e) => { - eprintln!("Error exporting SVG: {e:?}"); + eprintln!("Error exporting SVG: {e}"); std::process::exit(1); } }