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 <line> elements.
Added .set("stroke-linecap", "round") to partial arc <path> 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.
This commit is contained in:
2026-09-20 10:33:38 +02:00
parent c4831967be
commit 368497f97a
3 changed files with 70 additions and 7 deletions
+4 -4
View File
@@ -9,11 +9,11 @@ fn draw(turtle: &mut TurtlePlan) {
turtle.set_pen_width(0.5); turtle.set_pen_width(0.5);
// Draw a 5-pointed star pattern repeatedly // Draw a 5-pointed star pattern repeatedly
for _i in 0..50000 { for _i in 0..500 {
turtle.forward(200.0); turtle.forward(200.0);
turtle.circle_left(10.0, 72.0, 1000); turtle.circle_left(10.0, 72.0, 100);
turtle.circle_right(5.0, 360.0, 1000); turtle.circle_right(5.0, 360.0, 100);
turtle.circle_left(10.0, 72.0, 1000); turtle.circle_left(10.0, 72.0, 100);
} }
// Set animation speed // Set animation speed
+65 -3
View File
@@ -27,9 +27,11 @@ pub mod svg_export {
pub struct SvgExporter; pub struct SvgExporter;
impl DrawingExporter for SvgExporter { impl SvgExporter {
/// Generate an SVG [`Document`] from the given [`TurtleWorld`].
#[must_use]
#[allow(clippy::too_many_lines)] #[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 doc = Document::new();
let mut min_x = f32::INFINITY; let mut min_x = f32::INFINITY;
@@ -58,7 +60,8 @@ pub mod svg_export {
.set("x2", end.x) .set("x2", end.x)
.set("y2", end.y) .set("y2", end.y)
.set("stroke", color_to_svg(*color)) .set("stroke", color_to_svg(*color))
.set("stroke-width", *pen_width); .set("stroke-width", *pen_width)
.set("stroke-linecap", "round");
doc = doc.add(line); doc = doc.add(line);
} }
@@ -132,6 +135,7 @@ pub mod svg_export {
.set("d", d) .set("d", d)
.set("stroke", color_to_svg(*color)) .set("stroke", color_to_svg(*color))
.set("stroke-width", *pen_width) .set("stroke-width", *pen_width)
.set("stroke-linecap", "round")
.set("fill", "none"); .set("fill", "none");
doc = doc.add(path); doc = doc.add(path);
} }
@@ -218,6 +222,13 @@ pub mod svg_export {
doc = doc.set("viewBox", "0 0 400 400"); 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)?; let mut file = File::create(filename).map_err(ExportError::Io)?;
svg::write(&mut file, &doc).map_err(ExportError::Io)?; svg::write(&mut file, &doc).map_err(ExportError::Io)?;
Ok(()) Ok(())
@@ -234,4 +245,55 @@ pub mod svg_export {
format!("rgb({r},{g},{b})") 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}"
);
}
}
} }
+1
View File
@@ -126,6 +126,7 @@ pub(crate) struct SvgLog {
} }
impl SvgLog { impl SvgLog {
#[allow(clippy::unused_self)]
pub(crate) fn clear(&mut self) { pub(crate) fn clear(&mut self) {
#[cfg(feature = "svg")] #[cfg(feature = "svg")]
self.records.clear(); self.records.clear();