diff --git a/README.md b/README.md index 6bbe914..31b910a 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,10 @@ A modern turtle graphics library for Rust built on [Macroquad](https://macroquad - ⚡ **Smooth Animations**: Tweening support with easing functions and live fill preview - 🚀 **Instant Mode**: Execute commands immediately without animation (speed ≥ 999) - 🎯 **High-Quality Rendering**: Complete Lyon tessellation pipeline with GPU acceleration -- 🕳️ **Multi-Contour Fills**: Automatic hole detection with EvenOdd fill rule - draw cheese with holes! +- **Multi-Contour Fills**: Automatic hole detection with EvenOdd fill rule - draw cheese with holes! - 📐 **Self-Intersecting Paths**: Stars, complex shapes - all handled correctly - 🐢 **Multiple Turtle Shapes**: Triangle, classic turtle, circle, square, arrow, and custom shapes +- 🔍 **Structured Logging**: Optional `tracing` integration for debugging (zero overhead when disabled) - 💨 **Lightweight**: Fast compilation and runtime ## Quick Start @@ -149,6 +150,33 @@ loop { } ``` +## Debugging and Logging + +The library uses [`tracing`](https://docs.rs/tracing) for structured diagnostic logging. This is completely optional - if you don't set up a subscriber, there's zero overhead. + +### Enable Logging + +```rust +// Add to your Cargo.toml: +// tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } + +tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); +``` + +Control verbosity with the `RUST_LOG` environment variable: + +```bash +# Show debug output +RUST_LOG=turtle_lib_macroquad=debug cargo run + +# Very verbose trace output +RUST_LOG=turtle_lib_macroquad=trace cargo run +``` + +**See the complete example**: [`examples/logging_example.rs`](turtle-lib-macroquad/examples/logging_example.rs) demonstrates initialization, log levels, filtering, and example output. + ## Examples Run examples with: @@ -161,6 +189,10 @@ cargo run --example yinyang cargo run --example stern cargo run --example nikolaus +# Logging example - shows how to enable debug output +cargo run --example logging_example +RUST_LOG=turtle_lib_macroquad=debug cargo run --example logging_example + # Lyon proof-of-concept examples cargo run --package turtle-lyon-poc --example yinyang --release cargo run --package turtle-lyon-poc --example basic_shapes --release @@ -184,6 +216,9 @@ cargo run --package turtle-lyon-poc --example fill_comparison --release - **fill_circle_test.rs**: Circle fills with different angles - **fill_instant_test.rs**: Quick fill test in instant mode +#### Debugging +- **logging_example.rs**: Demonstrates how to enable and use tracing/logging output + ## Why Lyon? - Automatic hole detection via EvenOdd fill rule diff --git a/turtle-lib-macroquad/Cargo.toml b/turtle-lib-macroquad/Cargo.toml index 746df41..88dfbed 100644 --- a/turtle-lib-macroquad/Cargo.toml +++ b/turtle-lib-macroquad/Cargo.toml @@ -8,6 +8,8 @@ license = "MIT OR Apache-2.0" macroquad = "0.4" tween = "2.1.0" lyon = "1.0" +tracing = { version = "0.1", features = ["log"], default-features = false } [dev-dependencies] # For examples and testing +tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } diff --git a/turtle-lib-macroquad/examples/logging_example.rs b/turtle-lib-macroquad/examples/logging_example.rs new file mode 100644 index 0000000..e8b602a --- /dev/null +++ b/turtle-lib-macroquad/examples/logging_example.rs @@ -0,0 +1,97 @@ +//! Example demonstrating how to enable logging/tracing output from the turtle library +//! +//! This example shows how to use `tracing-subscriber` to see debug output from the library. +//! You can control the log level using the `RUST_LOG` environment variable: +//! +//! ```bash +//! # Show all debug output from turtle-lib-macroquad +//! RUST_LOG=turtle_lib_macroquad=debug cargo run --example logging_example +//! +//! # Show only warnings and errors +//! RUST_LOG=turtle_lib_macroquad=warn cargo run --example logging_example +//! +//! # Show trace-level output (very verbose, includes all vertices) +//! RUST_LOG=turtle_lib_macroquad=trace cargo run --example logging_example +//! +//! # Show debug output from specific modules +//! RUST_LOG=turtle_lib_macroquad::tessellation=debug cargo run --example logging_example +//! RUST_LOG=turtle_lib_macroquad::execution=debug cargo run --example logging_example +//! ``` + +use macroquad::prelude::*; +use turtle_lib_macroquad::*; + +#[macroquad::main("Turtle Logging Example")] +async fn main() { + // Initialize tracing subscriber to see debug output + // This will respect the RUST_LOG environment variable + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| { + // Default to showing info-level logs if RUST_LOG is not set + tracing_subscriber::EnvFilter::new("turtle_lib_macroquad=info") + }), + ) + .with_target(true) // Show which module the log came from + .with_thread_ids(false) + .with_line_number(true) // Show line numbers + .with_file(false) + .init(); + + tracing::info!("Starting turtle graphics example with logging enabled"); + tracing::info!("Try running with: RUST_LOG=turtle_lib_macroquad=debug cargo run --example logging_example"); + + // Create a turtle plan with fill operations to see detailed logging + let mut t = create_turtle(); + t.set_speed(900); + + // Draw a yin-yang symbol with fills (generates lots of debug output) + t.circle_left(90.0, 180.0, 36); + t.begin_fill(); + t.circle_left(90.0, 180.0, 36); + t.circle_left(45.0, 180.0, 26); + t.circle_right(45.0, 180.0, 26); + t.pen_up(); + t.right(90.0); + t.forward(37.0); + t.left(90.0); + t.pen_down(); + t.circle_right(8.0, 360.0, 12); + t.pen_up(); + t.right(90.0); + t.forward(90.0); + t.left(90.0); + t.pen_down(); + t.circle_right(8.0, 360.0, 12); + t.end_fill(); + + tracing::info!("Turtle plan created, starting animation"); + + // Set animation speed + t.set_speed(100); // Slow animation to see the logs in real-time + + // Create turtle app + let mut app = TurtleApp::new().with_commands(t.build()); + + // Main loop + loop { + clear_background(WHITE); + + // Update and render - this is where you'll see debug logs + app.update(); + app.render(); + + // Exit when animation is complete + if app.is_complete() { + tracing::info!("Animation complete, press any key to exit"); + if is_key_pressed(KeyCode::Space) { + break; + } + } + + next_frame().await + } + + tracing::info!("Example finished"); +} diff --git a/turtle-lib-macroquad/src/drawing.rs b/turtle-lib-macroquad/src/drawing.rs index 7bb0648..05a4da3 100644 --- a/turtle-lib-macroquad/src/drawing.rs +++ b/turtle-lib-macroquad/src/drawing.rs @@ -229,9 +229,9 @@ pub(crate) fn render_world_with_tween( draw_mesh(&mesh_data.to_mesh()); } Err(e) => { - eprintln!( - "#### Lyon multi-contour tessellation error for fill preview: {:?}", - e + tracing::error!( + error = ?e, + "Lyon multi-contour tessellation error for fill preview" ); } } diff --git a/turtle-lib-macroquad/src/execution.rs b/turtle-lib-macroquad/src/execution.rs index 236329d..a494c7c 100644 --- a/turtle-lib-macroquad/src/execution.rs +++ b/turtle-lib-macroquad/src/execution.rs @@ -86,7 +86,7 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world: state.pen_down = false; // Close current contour if filling if state.filling.is_some() { - eprintln!("PenUp: Closing current contour"); + tracing::debug!("PenUp: Closing current contour"); } state.close_fill_contour(); } @@ -95,9 +95,10 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world: state.pen_down = true; // Start new contour if filling if state.filling.is_some() { - eprintln!( - "PenDown: Starting new contour at position ({}, {})", - state.position.x, state.position.y + tracing::debug!( + x = state.position.x, + y = state.position.y, + "PenDown: Starting new contour" ); } state.start_fill_contour(); @@ -157,11 +158,11 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world: TurtleCommand::BeginFill => { if state.filling.is_some() { - eprintln!("Warning: begin_fill() called while already filling"); + tracing::warn!("begin_fill() called while already filling"); } let fill_color = state.fill_color.unwrap_or_else(|| { - eprintln!("Warning: No fill_color set, using black"); + tracing::warn!("No fill_color set, using black"); BLACK }); @@ -176,10 +177,11 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world: } // Debug output - eprintln!("=== EndFill Debug ==="); - eprintln!("Total contours: {}", fill_state.contours.len()); + let span = tracing::debug_span!("end_fill", contours = fill_state.contours.len()); + let _enter = span.enter(); + for (i, contour) in fill_state.contours.iter().enumerate() { - eprintln!(" Contour {}: {} vertices", i, contour.len()); + tracing::debug!(contour_idx = i, vertices = contour.len(), "Contour info"); } // Create fill command - Lyon will handle EvenOdd automatically with multiple contours @@ -188,17 +190,17 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world: &fill_state.contours, fill_state.fill_color, ) { - eprintln!( - "Successfully tessellated {} contours", - fill_state.contours.len() + tracing::debug!( + contours = fill_state.contours.len(), + "Successfully tessellated contours" ); world.add_command(DrawCommand::Mesh(mesh_data)); } else { - eprintln!("ERROR: Failed to tessellate contours!"); + tracing::error!("Failed to tessellate contours"); } } } else { - eprintln!("Warning: end_fill() called without begin_fill()"); + tracing::warn!("end_fill() called without begin_fill()"); } } } diff --git a/turtle-lib-macroquad/src/state.rs b/turtle-lib-macroquad/src/state.rs index 80d2d05..dad20bc 100644 --- a/turtle-lib-macroquad/src/state.rs +++ b/turtle-lib-macroquad/src/state.rs @@ -78,15 +78,15 @@ impl TurtleState { pub fn record_fill_vertex(&mut self) { if let Some(ref mut fill_state) = self.filling { if self.pen_down { - eprintln!( - " [FILL] Adding vertex ({:.2}, {:.2}) to current contour (now {} vertices)", - self.position.x, - self.position.y, - fill_state.current_contour.len() + 1 + tracing::trace!( + x = self.position.x, + y = self.position.y, + vertices = fill_state.current_contour.len() + 1, + "Adding vertex to current contour" ); fill_state.current_contour.push(self.position); } else { - eprintln!(" [FILL] Skipping vertex (pen is up)"); + tracing::trace!("Skipping vertex (pen is up)"); } } } @@ -94,42 +94,37 @@ impl TurtleState { /// Close the current contour and prepare for a new one (called on pen_up) pub fn close_fill_contour(&mut self) { if let Some(ref mut fill_state) = self.filling { - eprintln!( - " close_fill_contour called: current_contour has {} vertices", - fill_state.current_contour.len() + tracing::debug!( + vertices = fill_state.current_contour.len(), + "close_fill_contour called" ); // Only close if we have vertices in current contour if fill_state.current_contour.len() >= 2 { - eprintln!( - " Closing contour with {} vertices", - fill_state.current_contour.len() - ); - eprintln!( - " First: ({:.2}, {:.2})", - fill_state.current_contour[0].x, fill_state.current_contour[0].y - ); - eprintln!( - " Last: ({:.2}, {:.2})", - fill_state.current_contour[fill_state.current_contour.len() - 1].x, - fill_state.current_contour[fill_state.current_contour.len() - 1].y + tracing::debug!( + vertices = fill_state.current_contour.len(), + first_x = fill_state.current_contour[0].x, + first_y = fill_state.current_contour[0].y, + last_x = fill_state.current_contour[fill_state.current_contour.len() - 1].x, + last_y = fill_state.current_contour[fill_state.current_contour.len() - 1].y, + "Closing contour" ); // Move current contour to completed contours let contour = std::mem::take(&mut fill_state.current_contour); fill_state.contours.push(contour); - eprintln!( - " Contour moved to completed list. Total completed contours: {}", - fill_state.contours.len() + tracing::debug!( + completed_contours = fill_state.contours.len(), + "Contour moved to completed list" ); } else if !fill_state.current_contour.is_empty() { - eprintln!( - " WARNING: Current contour only has {} vertex/vertices, not closing", - fill_state.current_contour.len() + tracing::warn!( + vertices = fill_state.current_contour.len(), + "Current contour has insufficient vertices, not closing" ); } else { - eprintln!(" WARNING: Current contour is EMPTY, nothing to close"); + tracing::warn!("Current contour is empty, nothing to close"); } } else { - eprintln!(" close_fill_contour called but NO active fill state!"); + tracing::warn!("close_fill_contour called but no active fill state"); } } @@ -137,13 +132,11 @@ impl TurtleState { pub fn start_fill_contour(&mut self) { if let Some(ref mut fill_state) = self.filling { // Start new contour at current position - eprintln!( - " Starting NEW contour at ({:.2}, {:.2})", - self.position.x, self.position.y - ); - eprintln!( - " Previous contour had {} completed contours", - fill_state.contours.len() + tracing::debug!( + x = self.position.x, + y = self.position.y, + completed_contours = fill_state.contours.len(), + "Starting new contour" ); fill_state.current_contour = vec![self.position]; } @@ -165,8 +158,14 @@ impl TurtleState { // Sample points along the arc based on steps let num_samples = steps as usize; - eprintln!(" [FILL ARC] Recording arc vertices: center=({:.2}, {:.2}), radius={:.2}, steps={}, num_samples={}", - center.x, center.y, radius, steps, num_samples); + tracing::trace!( + center_x = center.x, + center_y = center.y, + radius = radius, + steps = steps, + num_samples = num_samples, + "Recording arc vertices" + ); for i in 1..=num_samples { let progress = i as f32 / num_samples as f32; @@ -183,12 +182,12 @@ impl TurtleState { center.x + radius * current_angle.cos(), center.y + radius * current_angle.sin(), ); - eprintln!( - " [FILL ARC] Vertex {}: ({:.2}, {:.2}) at angle {:.2}°", - i, - vertex.x, - vertex.y, - current_angle.to_degrees() + tracing::trace!( + vertex_idx = i, + x = vertex.x, + y = vertex.y, + angle_degrees = current_angle.to_degrees(), + "Arc vertex" ); fill_state.current_contour.push(vertex); } diff --git a/turtle-lib-macroquad/src/tessellation.rs b/turtle-lib-macroquad/src/tessellation.rs index ccf9700..4100435 100644 --- a/turtle-lib-macroquad/src/tessellation.rs +++ b/turtle-lib-macroquad/src/tessellation.rs @@ -101,25 +101,32 @@ pub fn tessellate_multi_contour( return Err("No contours provided".into()); } - eprintln!("\n=== tessellate_multi_contour Debug ==="); - eprintln!("Total contours to tessellate: {}", contours.len()); + let span = tracing::debug_span!("tessellate_multi_contour", contours = contours.len()); + let _enter = span.enter(); + + tracing::debug!("Starting multi-contour tessellation"); // Build path with multiple sub-paths (contours) let mut builder = Path::builder(); for (idx, contour) in contours.iter().enumerate() { if contour.is_empty() { - eprintln!("WARNING: Contour {} is empty, skipping", idx); + tracing::warn!(contour_idx = idx, "Contour is empty, skipping"); continue; } - eprintln!("\nContour {}: {} vertices", idx, contour.len()); - eprintln!(" First vertex: ({:.2}, {:.2})", contour[0].x, contour[0].y); + tracing::trace!( + contour_idx = idx, + vertices = contour.len(), + first_x = contour[0].x, + first_y = contour[0].y, + "Processing contour" + ); if contour.len() > 1 { - eprintln!( - " Last vertex: ({:.2}, {:.2})", - contour[contour.len() - 1].x, - contour[contour.len() - 1].y + tracing::trace!( + last_x = contour[contour.len() - 1].x, + last_y = contour[contour.len() - 1].y, + "Contour end vertex" ); } @@ -128,24 +135,27 @@ pub fn tessellate_multi_contour( for (i, v) in contour[1..].iter().enumerate() { builder.line_to(to_lyon_point(*v)); if i < 3 || i >= contour.len() - 4 { - eprintln!(" Vertex {}: ({:.2}, {:.2})", i + 1, v.x, v.y); + tracing::trace!(vertex_idx = i + 1, x = v.x, y = v.y, "Contour vertex"); } else if i == 3 { - eprintln!(" ... ({} more vertices)", contour.len() - 7); + tracing::trace!( + omitted = contour.len() - 7, + "Additional vertices omitted from trace" + ); } } builder.end(true); // Close this contour - eprintln!(" Contour closed"); + tracing::trace!(contour_idx = idx, "Contour closed"); } - eprintln!("\nBuilding Lyon path..."); + tracing::debug!("Building Lyon path"); let path = builder.build(); - eprintln!("Path built successfully"); + tracing::debug!("Path built successfully"); // Tessellate with EvenOdd fill rule - overlapping areas become holes let mut geometry: VertexBuffers = VertexBuffers::new(); let mut tessellator = FillTessellator::new(); - eprintln!("Starting tessellation with EvenOdd fill rule..."); + tracing::debug!("Starting tessellation with EvenOdd fill rule"); match tessellator.tessellate_path( &path, &FillOptions::default().with_fill_rule(FillRule::EvenOdd), @@ -154,16 +164,15 @@ pub fn tessellate_multi_contour( }), ) { Ok(_) => { - eprintln!("Tessellation successful!"); - eprintln!( - " Generated {} vertices, {} indices", - geometry.vertices.len(), - geometry.indices.len() + tracing::debug!( + vertices = geometry.vertices.len(), + indices = geometry.indices.len(), + triangles = geometry.indices.len() / 3, + "Tessellation successful" ); - eprintln!(" Triangles: {}", geometry.indices.len() / 3); } Err(e) => { - eprintln!("ERROR: Tessellation failed: {}", e); + tracing::error!(error = %e, "Tessellation failed"); return Err(Box::new(e)); } }