From 368497f97a0f3f858cee20da5f0b2a83304b66bb Mon Sep 17 00:00:00 2001 From: Franz Dietrich Date: Sun, 20 Sep 2026 10:33:38 +0200 Subject: [PATCH] Line caps on exported SVG lines and arcs are now configured to be round (stroke-linecap="round"), matching the on-screen Lyon tessellation. Summary of Changes turtle-lib/src/export_svg.rs: Added .set("stroke-linecap", "round") to SVG elements. Added .set("stroke-linecap", "round") to partial arc elements. Extracted SvgExporter::to_svg_document(&TurtleWorld) -> Document to allow in-memory SVG inspection and testing. Added unit tests verifying that both lines and arcs export with stroke-linecap="round". turtle-lib/src/state.rs: Added #[allow(clippy::unused_self)] to SvgLog::clear for clean clippy passes when compiling without the svg feature. --- turtle-lib/examples/star.rs | 8 ++--- turtle-lib/src/export_svg.rs | 68 ++++++++++++++++++++++++++++++++++-- turtle-lib/src/state.rs | 1 + 3 files changed, 70 insertions(+), 7 deletions(-) diff --git a/turtle-lib/examples/star.rs b/turtle-lib/examples/star.rs index 7c98d1b..bf02444 100644 --- a/turtle-lib/examples/star.rs +++ b/turtle-lib/examples/star.rs @@ -9,11 +9,11 @@ fn draw(turtle: &mut TurtlePlan) { turtle.set_pen_width(0.5); // Draw a 5-pointed star pattern repeatedly - for _i in 0..50000 { + for _i in 0..500 { turtle.forward(200.0); - turtle.circle_left(10.0, 72.0, 1000); - turtle.circle_right(5.0, 360.0, 1000); - turtle.circle_left(10.0, 72.0, 1000); + turtle.circle_left(10.0, 72.0, 100); + turtle.circle_right(5.0, 360.0, 100); + turtle.circle_left(10.0, 72.0, 100); } // Set animation speed diff --git a/turtle-lib/src/export_svg.rs b/turtle-lib/src/export_svg.rs index 5579b3b..0c6a84b 100644 --- a/turtle-lib/src/export_svg.rs +++ b/turtle-lib/src/export_svg.rs @@ -27,9 +27,11 @@ pub mod svg_export { pub struct SvgExporter; - impl DrawingExporter for SvgExporter { + impl SvgExporter { + /// Generate an SVG [`Document`] from the given [`TurtleWorld`]. + #[must_use] #[allow(clippy::too_many_lines)] - fn export(&self, world: &TurtleWorld, filename: &str) -> Result<(), ExportError> { + pub fn to_svg_document(world: &TurtleWorld) -> Document { let mut doc = Document::new(); let mut min_x = f32::INFINITY; @@ -58,7 +60,8 @@ pub mod svg_export { .set("x2", end.x) .set("y2", end.y) .set("stroke", color_to_svg(*color)) - .set("stroke-width", *pen_width); + .set("stroke-width", *pen_width) + .set("stroke-linecap", "round"); doc = doc.add(line); } @@ -132,6 +135,7 @@ pub mod svg_export { .set("d", d) .set("stroke", color_to_svg(*color)) .set("stroke-width", *pen_width) + .set("stroke-linecap", "round") .set("fill", "none"); doc = doc.add(path); } @@ -218,6 +222,13 @@ pub mod svg_export { doc = doc.set("viewBox", "0 0 400 400"); } + doc + } + } + + impl DrawingExporter for SvgExporter { + fn export(&self, world: &TurtleWorld, filename: &str) -> Result<(), ExportError> { + let doc = Self::to_svg_document(world); let mut file = File::create(filename).map_err(ExportError::Io)?; svg::write(&mut file, &doc).map_err(ExportError::Io)?; Ok(()) @@ -234,4 +245,55 @@ pub mod svg_export { format!("rgb({r},{g},{b})") } } + + #[cfg(test)] + mod tests { + use super::*; + use crate::circle_geometry::CircleDirection; + use crate::general::{Color, Coordinate, Degrees}; + use crate::state::{SvgRecord, Turtle}; + + #[test] + fn test_svg_export_line_has_round_line_caps() { + let mut world = TurtleWorld::new(); + let mut turtle = Turtle::default(); + turtle.svg_log.push(SvgRecord::Line { + start: Coordinate::new(0.0, 0.0), + end: Coordinate::new(100.0, 0.0), + color: Color::new(0.0, 0.0, 0.0, 1.0), + pen_width: 2.0, + }); + world.turtles.push(turtle); + + let doc = SvgExporter::to_svg_document(&world); + let svg_string = doc.to_string(); + assert!( + svg_string.contains(r#"stroke-linecap="round""#), + "SVG export of lines should have round line caps: {svg_string}" + ); + } + + #[test] + fn test_svg_export_arc_has_round_line_caps() { + let mut world = TurtleWorld::new(); + let mut turtle = Turtle::default(); + turtle.svg_log.push(SvgRecord::Arc { + start_position: Coordinate::new(0.0, 0.0), + start_heading: 0.0, + radius: 50.0, + angle: Degrees::new(90.0), + direction: CircleDirection::Right, + color: Color::new(0.0, 0.0, 0.0, 1.0), + pen_width: 2.0, + }); + world.turtles.push(turtle); + + let doc = SvgExporter::to_svg_document(&world); + let svg_string = doc.to_string(); + assert!( + svg_string.contains(r#"stroke-linecap="round""#), + "SVG export of partial arcs should have round line caps: {svg_string}" + ); + } + } } diff --git a/turtle-lib/src/state.rs b/turtle-lib/src/state.rs index 24da2fc..b293ecc 100644 --- a/turtle-lib/src/state.rs +++ b/turtle-lib/src/state.rs @@ -126,6 +126,7 @@ pub(crate) struct SvgLog { } impl SvgLog { + #[allow(clippy::unused_self)] pub(crate) fn clear(&mut self) { #[cfg(feature = "svg")] self.records.clear();