From e6bc79ea7bc566618338a1b1637e901dabb3562d Mon Sep 17 00:00:00 2001 From: Franz Dietrich Date: Thu, 23 Oct 2025 16:15:02 +0200 Subject: [PATCH] now svg lines, circles and fills are exported --- turtle-lib/examples/export_svg.rs | 22 ++++- turtle-lib/src/execution.rs | 16 +++- turtle-lib/src/export_svg.rs | 131 +++++++++++++++++++++++------- turtle-lib/src/state.rs | 3 +- 4 files changed, 138 insertions(+), 34 deletions(-) diff --git a/turtle-lib/examples/export_svg.rs b/turtle-lib/examples/export_svg.rs index f6a8e94..826146d 100644 --- a/turtle-lib/examples/export_svg.rs +++ b/turtle-lib/examples/export_svg.rs @@ -9,11 +9,31 @@ async fn main() { plan.forward(100.0) .right(90.0) .forward(100.0) - .circle_left(50.0, 90.0, 4) + .set_pen_color(macroquad::color::GRAY) + .circle_right(50.0, 90.0, 4) .begin_fill() .forward(100.0) .right(90.0) .forward(200.0) + .end_fill() + .circle_left(200., 180., 24) + .circle_left(90.0, 180.0, 36) + .begin_fill() + .circle_left(90.0, 180.0, 36) + .circle_left(45.0, 180.0, 26) + .circle_right(45.0, 180.0, 26) + .pen_up() + .right(90.0) + .forward(37.0) + .left(90.0) + .pen_down() + .circle_right(8.0, 360.0, 12) + .pen_up() + .right(90.0) + .forward(90.0) + .left(90.0) + .pen_down() + .circle_right(8.0, 360.0, 12) .end_fill(); let mut app = TurtleApp::new().with_commands(plan.build()); use macroquad::{ diff --git a/turtle-lib/src/execution.rs b/turtle-lib/src/execution.rs index 83bbf8b..4049a71 100644 --- a/turtle-lib/src/execution.rs +++ b/turtle-lib/src/execution.rs @@ -72,6 +72,8 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle) pen_width: state.params.pen_width, start_position: fill_state.start_position, end_position: fill_state.start_position, + start_heading: state.params.heading, + contours: Some(fill_state.contours.clone()), }, }); } else { @@ -133,6 +135,8 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle) pen_width: state.params.pen_width, start_position: state.params.position, end_position: state.params.position, + start_heading: state.params.heading, + contours: None, }, }); true @@ -230,6 +234,8 @@ pub fn execute_command(command: &TurtleCommand, state: &mut Turtle) { pen_width: state.params.pen_width, start_position: start, end_position: state.params.position, + start_heading: state.params.heading, + contours: None, }, }); } @@ -270,7 +276,9 @@ pub fn execute_command(command: &TurtleCommand, state: &mut Turtle) { fill_color: state.params.fill_color.unwrap_or(BLACK), pen_width: state.params.pen_width, start_position: state.params.position, - end_position: state.params.position, + end_position: geom.position_at_angle(angle.to_radians()), + start_heading: start_heading, + contours: None, }, }); } @@ -306,6 +314,8 @@ pub fn execute_command(command: &TurtleCommand, state: &mut Turtle) { pen_width: state.params.pen_width, start_position: start, end_position: state.params.position, + start_heading: state.params.heading, + contours: None, }, }); } @@ -371,6 +381,8 @@ pub fn add_draw_for_completed_tween( pen_width: start_state.pen_width, start_position: start_state.position, end_position: end_state.position, + start_heading: start_state.heading, + contours: None, }, }); } @@ -408,6 +420,8 @@ pub fn add_draw_for_completed_tween( pen_width: start_state.pen_width, start_position: start_state.position, end_position: end_state.position, + start_heading: start_state.heading, + contours: None, }, }); } diff --git a/turtle-lib/src/export_svg.rs b/turtle-lib/src/export_svg.rs index 265cd47..b8dfd68 100644 --- a/turtle-lib/src/export_svg.rs +++ b/turtle-lib/src/export_svg.rs @@ -4,7 +4,7 @@ pub mod svg_export { use crate::commands::TurtleCommand; use crate::export::{DrawingExporter, ExportError}; - use crate::state::{DrawCommand, TurtleSource, TurtleWorld}; + use crate::state::{DrawCommand, TurtleWorld}; use std::fs::File; use svg::{ node::element::{Circle, Line, Polygon, Text as SvgText}, @@ -35,38 +35,107 @@ pub mod svg_export { .set("stroke-width", source.pen_width); doc = doc.add(line); } - TurtleCommand::Circle { radius, .. } => { - // Kreis als - let center = source.start_position; - let circle = Circle::new() - .set("cx", center.x) - .set("cy", center.y) - .set("r", *radius) - .set("stroke", color_to_svg(source.color)) - .set("stroke-width", source.pen_width) - .set("fill", "none"); - doc = doc.add(circle); + TurtleCommand::Circle { + radius, + angle, + direction, + .. + } => { + use crate::circle_geometry::CircleGeometry; + let geom = CircleGeometry::new( + source.start_position, + source.start_heading, + *radius, + *direction, + ); + let center = geom.center; + if (*angle - 360.0).abs() < 1e-3 { + // Voller Kreis + let circle = Circle::new() + .set("cx", center.x) + .set("cy", center.y) + .set("r", *radius) + .set("stroke", color_to_svg(source.color)) + .set("stroke-width", source.pen_width) + .set("fill", "none"); + doc = doc.add(circle); + } else { + // Kreisbogen als + let start = source.start_position; + let end = source.end_position; + let large_arc = if *angle > 180.0 { 1 } else { 0 }; + let sweep = match direction { + crate::circle_geometry::CircleDirection::Left => 0, + crate::circle_geometry::CircleDirection::Right => 1, + }; + let d = format!( + "M {} {} A {} {} 0 {} {} {} {}", + start.x, + start.y, + radius, + radius, + large_arc, + sweep, + end.x, + end.y + ); + let path = svg::node::element::Path::new() + .set("d", d) + .set("stroke", color_to_svg(source.color)) + .set("stroke-width", source.pen_width) + .set("fill", "none"); + doc = doc.add(path); + } } TurtleCommand::EndFill => { - // Fills werden als Polygon ausgegeben - // (Vereinfachung: Startposition als Dummy, echte Konturen müssten separat gespeichert werden) - // Hier nur ein Dummy-Polygon - let poly = Polygon::new() - .set( - "points", - format!( - "{},{} {},{} {},{}", - source.start_position.x, - source.start_position.y, - source.start_position.x + 10.0, - source.start_position.y + 10.0, - source.start_position.x + 5.0, - source.start_position.y + 15.0 - ), - ) - .set("fill", color_to_svg(source.fill_color)) - .set("stroke", color_to_svg(source.color)); - doc = doc.add(poly); + // Fills werden als mit Konturen ausgegeben + if let Some(contours) = &source.contours { + let mut d = String::new(); + for (i, contour) in contours.iter().enumerate() { + if !contour.is_empty() { + if i > 0 { + d.push(' '); + } + d.push_str(&format!( + "M {} {}", + contour[0].x, contour[0].y + )); + for point in contour.iter().skip(1) { + d.push_str(&format!( + " L {} {}", + point.x, point.y + )); + } + d.push_str(" Z"); + } + } + if !d.is_empty() { + let path = svg::node::element::Path::new() + .set("d", d) + .set("fill", color_to_svg(source.fill_color)) + .set("fill-rule", "evenodd") + .set("stroke", color_to_svg(source.color)); + doc = doc.add(path); + } + } else { + // Fallback: Dummy-Polygon + let poly = Polygon::new() + .set( + "points", + format!( + "{},{} {},{} {},{}", + source.start_position.x, + source.start_position.y, + source.start_position.x + 10.0, + source.start_position.y + 10.0, + source.start_position.x + 5.0, + source.start_position.y + 15.0 + ), + ) + .set("fill", color_to_svg(source.fill_color)) + .set("stroke", color_to_svg(source.color)); + doc = doc.add(poly); + } } _ => {} } diff --git a/turtle-lib/src/state.rs b/turtle-lib/src/state.rs index 2f2331e..fdf036c 100644 --- a/turtle-lib/src/state.rs +++ b/turtle-lib/src/state.rs @@ -285,7 +285,8 @@ pub struct TurtleSource { pub pen_width: f32, pub start_position: Vec2, pub end_position: Vec2, - // ggf. weitere Metadaten + pub start_heading: f32, + pub contours: Option>>, } #[derive(Clone, Debug)]