now svg lines, circles and fills are exported

This commit is contained in:
Franz Dietrich 2025-10-23 16:15:02 +02:00
parent 6e6aa8b27e
commit e6bc79ea7b
4 changed files with 138 additions and 34 deletions

View File

@ -9,11 +9,31 @@ async fn main() {
plan.forward(100.0) plan.forward(100.0)
.right(90.0) .right(90.0)
.forward(100.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() .begin_fill()
.forward(100.0) .forward(100.0)
.right(90.0) .right(90.0)
.forward(200.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(); .end_fill();
let mut app = TurtleApp::new().with_commands(plan.build()); let mut app = TurtleApp::new().with_commands(plan.build());
use macroquad::{ use macroquad::{

View File

@ -72,6 +72,8 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle)
pen_width: state.params.pen_width, pen_width: state.params.pen_width,
start_position: fill_state.start_position, start_position: fill_state.start_position,
end_position: fill_state.start_position, end_position: fill_state.start_position,
start_heading: state.params.heading,
contours: Some(fill_state.contours.clone()),
}, },
}); });
} else { } else {
@ -133,6 +135,8 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle)
pen_width: state.params.pen_width, pen_width: state.params.pen_width,
start_position: state.params.position, start_position: state.params.position,
end_position: state.params.position, end_position: state.params.position,
start_heading: state.params.heading,
contours: None,
}, },
}); });
true true
@ -230,6 +234,8 @@ pub fn execute_command(command: &TurtleCommand, state: &mut Turtle) {
pen_width: state.params.pen_width, pen_width: state.params.pen_width,
start_position: start, start_position: start,
end_position: state.params.position, 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), fill_color: state.params.fill_color.unwrap_or(BLACK),
pen_width: state.params.pen_width, pen_width: state.params.pen_width,
start_position: state.params.position, 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, pen_width: state.params.pen_width,
start_position: start, start_position: start,
end_position: state.params.position, 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, pen_width: start_state.pen_width,
start_position: start_state.position, start_position: start_state.position,
end_position: end_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, pen_width: start_state.pen_width,
start_position: start_state.position, start_position: start_state.position,
end_position: end_state.position, end_position: end_state.position,
start_heading: start_state.heading,
contours: None,
}, },
}); });
} }

View File

@ -4,7 +4,7 @@
pub mod svg_export { pub mod svg_export {
use crate::commands::TurtleCommand; use crate::commands::TurtleCommand;
use crate::export::{DrawingExporter, ExportError}; use crate::export::{DrawingExporter, ExportError};
use crate::state::{DrawCommand, TurtleSource, TurtleWorld}; use crate::state::{DrawCommand, TurtleWorld};
use std::fs::File; use std::fs::File;
use svg::{ use svg::{
node::element::{Circle, Line, Polygon, Text as SvgText}, node::element::{Circle, Line, Polygon, Text as SvgText},
@ -35,9 +35,22 @@ pub mod svg_export {
.set("stroke-width", source.pen_width); .set("stroke-width", source.pen_width);
doc = doc.add(line); doc = doc.add(line);
} }
TurtleCommand::Circle { radius, .. } => { TurtleCommand::Circle {
// Kreis als <circle> radius,
let center = source.start_position; 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() let circle = Circle::new()
.set("cx", center.x) .set("cx", center.x)
.set("cy", center.y) .set("cy", center.y)
@ -46,11 +59,66 @@ pub mod svg_export {
.set("stroke-width", source.pen_width) .set("stroke-width", source.pen_width)
.set("fill", "none"); .set("fill", "none");
doc = doc.add(circle); doc = doc.add(circle);
} else {
// Kreisbogen als <path>
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 => { TurtleCommand::EndFill => {
// Fills werden als Polygon ausgegeben // Fills werden als <path> mit Konturen ausgegeben
// (Vereinfachung: Startposition als Dummy, echte Konturen müssten separat gespeichert werden) if let Some(contours) = &source.contours {
// Hier nur ein Dummy-Polygon 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() let poly = Polygon::new()
.set( .set(
"points", "points",
@ -68,6 +136,7 @@ pub mod svg_export {
.set("stroke", color_to_svg(source.color)); .set("stroke", color_to_svg(source.color));
doc = doc.add(poly); doc = doc.add(poly);
} }
}
_ => {} _ => {}
} }
} }

View File

@ -285,7 +285,8 @@ pub struct TurtleSource {
pub pen_width: f32, pub pen_width: f32,
pub start_position: Vec2, pub start_position: Vec2,
pub end_position: Vec2, pub end_position: Vec2,
// ggf. weitere Metadaten pub start_heading: f32,
pub contours: Option<Vec<Vec<crate::general::Coordinate>>>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]