initial svg support

This commit is contained in:
2025-10-23 09:40:08 +02:00
parent 88d188a794
commit 6e6aa8b27e
10 changed files with 309 additions and 10 deletions
+7 -2
View File
@@ -28,8 +28,10 @@ pub fn render_world(world: &TurtleWorld) {
for turtle in &world.turtles {
for cmd in &turtle.commands {
match cmd {
DrawCommand::Mesh { data } => {
DrawCommand::Mesh { data, source } => {
// Rendering wie bisher
draw_mesh(&data.to_mesh());
// Hier könnte man das source für Debug/Export loggen
}
DrawCommand::Text {
text,
@@ -37,8 +39,10 @@ pub fn render_world(world: &TurtleWorld) {
heading,
font_size,
color,
source,
} => {
draw_text_command(text, *position, *heading, *font_size, *color);
// Hier könnte man das source für Debug/Export loggen
}
}
}
@@ -76,7 +80,7 @@ pub fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) {
for turtle in &world.turtles {
for cmd in &turtle.commands {
match cmd {
DrawCommand::Mesh { data } => {
DrawCommand::Mesh { data, source } => {
draw_mesh(&data.to_mesh());
}
DrawCommand::Text {
@@ -85,6 +89,7 @@ pub fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) {
heading,
font_size,
color,
source,
} => {
draw_text_command(text, *position, *heading, *font_size, *color);
}
+74 -6
View File
@@ -63,7 +63,17 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle)
contours = fill_state.contours.len(),
"Successfully created fill mesh - persisting to commands"
);
state.commands.push(DrawCommand::Mesh { data: mesh_data });
state.commands.push(DrawCommand::Mesh {
data: mesh_data,
source: crate::state::TurtleSource {
command: crate::commands::TurtleCommand::EndFill,
color: state.params.color,
fill_color: fill_state.fill_color,
pen_width: state.params.pen_width,
start_position: fill_state.start_position,
end_position: fill_state.start_position,
},
});
} else {
tracing::error!(
turtle_id = state.turtle_id,
@@ -116,6 +126,14 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle)
heading: state.params.heading,
font_size: *font_size,
color: state.params.color,
source: crate::state::TurtleSource {
command: command.clone(),
color: state.params.color,
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,
},
});
true
}
@@ -203,7 +221,17 @@ pub fn execute_command(command: &TurtleCommand, state: &mut Turtle) {
state.params.pen_width,
false, // not closed
) {
state.commands.push(DrawCommand::Mesh { data: mesh_data });
state.commands.push(DrawCommand::Mesh {
data: mesh_data,
source: crate::state::TurtleSource {
command: command.clone(),
color: state.params.color,
fill_color: state.params.fill_color.unwrap_or(BLACK),
pen_width: state.params.pen_width,
start_position: start,
end_position: state.params.position,
},
});
}
}
}
@@ -234,7 +262,17 @@ pub fn execute_command(command: &TurtleCommand, state: &mut Turtle) {
*steps,
*direction,
) {
state.commands.push(DrawCommand::Mesh { data: mesh_data });
state.commands.push(DrawCommand::Mesh {
data: mesh_data,
source: crate::state::TurtleSource {
command: command.clone(),
color: state.params.color,
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,
},
});
}
}
@@ -259,7 +297,17 @@ pub fn execute_command(command: &TurtleCommand, state: &mut Turtle) {
state.params.pen_width,
false, // not closed
) {
state.commands.push(DrawCommand::Mesh { data: mesh_data });
state.commands.push(DrawCommand::Mesh {
data: mesh_data,
source: crate::state::TurtleSource {
command: command.clone(),
color: state.params.color,
fill_color: state.params.fill_color.unwrap_or(BLACK),
pen_width: state.params.pen_width,
start_position: start,
end_position: state.params.position,
},
});
}
}
}
@@ -314,7 +362,17 @@ pub fn add_draw_for_completed_tween(
start_state.pen_width,
false,
) {
return Some(DrawCommand::Mesh { data: mesh_data });
return Some(DrawCommand::Mesh {
data: mesh_data,
source: crate::state::TurtleSource {
command: command.clone(),
color: start_state.color,
fill_color: start_state.fill_color.unwrap_or(BLACK),
pen_width: start_state.pen_width,
start_position: start_state.position,
end_position: end_state.position,
},
});
}
}
}
@@ -341,7 +399,17 @@ pub fn add_draw_for_completed_tween(
*steps,
*direction,
) {
return Some(DrawCommand::Mesh { data: mesh_data });
return Some(DrawCommand::Mesh {
data: mesh_data,
source: crate::state::TurtleSource {
command: command.clone(),
color: start_state.color,
fill_color: start_state.fill_color.unwrap_or(BLACK),
pen_width: start_state.pen_width,
start_position: start_state.position,
end_position: end_state.position,
},
});
}
}
}
+20
View File
@@ -0,0 +1,20 @@
//! Export-Backend-Trait und zentrale Export-Typen
use crate::state::TurtleWorld;
#[derive(Debug)]
pub enum ExportError {
Io(std::io::Error),
Format(String),
// Weitere Formate können ergänzt werden
}
pub enum DrawingFormat {
#[cfg(feature = "svg")]
Svg,
// Weitere Formate wie Png, Pdf, ...
}
pub trait DrawingExporter {
fn export(&self, world: &TurtleWorld, filename: &str) -> Result<(), ExportError>;
}
+107
View File
@@ -0,0 +1,107 @@
//! SVG-Export-Backend für TurtleWorld
#[cfg(feature = "svg")]
pub mod svg_export {
use crate::commands::TurtleCommand;
use crate::export::{DrawingExporter, ExportError};
use crate::state::{DrawCommand, TurtleSource, TurtleWorld};
use std::fs::File;
use svg::{
node::element::{Circle, Line, Polygon, Text as SvgText},
Document,
};
pub struct SvgExporter;
impl DrawingExporter for SvgExporter {
fn export(&self, world: &TurtleWorld, filename: &str) -> Result<(), ExportError> {
let mut doc = Document::new();
for turtle in &world.turtles {
for cmd in &turtle.commands {
match cmd {
DrawCommand::Mesh { source, .. } => {
match &source.command {
TurtleCommand::Move(_) | TurtleCommand::Goto(_) => {
// Linie als <line>
let start = source.start_position;
let end = source.end_position;
let line = Line::new()
.set("x1", start.x)
.set("y1", start.y)
.set("x2", end.x)
.set("y2", end.y)
.set("stroke", color_to_svg(source.color))
.set("stroke-width", source.pen_width);
doc = doc.add(line);
}
TurtleCommand::Circle { radius, .. } => {
// Kreis als <circle>
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::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);
}
_ => {}
}
}
DrawCommand::Text {
text,
position,
source,
..
} => {
let txt = SvgText::new()
.set("x", position.x)
.set("y", position.y)
.set("fill", color_to_svg(source.color))
.add(svg::node::Text::new(text.clone()));
doc = doc.add(txt);
}
}
}
}
let mut file = File::create(filename).map_err(ExportError::Io)?;
svg::write(&mut file, &doc).map_err(ExportError::Io)?;
Ok(())
}
}
fn color_to_svg(color: crate::general::Color) -> String {
let r = (color.r * 255.0) as u8;
let g = (color.g * 255.0) as u8;
let b = (color.b * 255.0) as u8;
if color.a < 1.0 {
format!("rgba({},{},{},{})", r, g, b, color.a)
} else {
format!("rgb({},{},{})", r, g, b)
}
}
}
+26
View File
@@ -67,6 +67,10 @@ pub use shapes::{ShapeType, TurtleShape};
pub use state::{DrawCommand, Turtle, TurtleWorld};
pub use tweening::TweenController;
pub mod export;
#[cfg(feature = "svg")]
pub mod export_svg;
// Re-export the turtle_main macro
pub use turtle_lib_macros::turtle_main;
@@ -91,6 +95,28 @@ pub struct TurtleApp {
}
impl TurtleApp {
/// Exportiere das aktuelle Drawing in das gewünschte Format
#[allow(unused_variables)]
pub fn export_drawing(
&self,
filename: &str,
format: export::DrawingFormat,
) -> Result<(), export::ExportError> {
match format {
#[cfg(feature = "svg")]
export::DrawingFormat::Svg => {
use crate::export::DrawingExporter;
use export_svg::svg_export::SvgExporter;
let exporter = SvgExporter;
exporter.export(&self.world, filename)
}
// Weitere Formate können hier ergänzt werden
#[allow(unreachable_patterns)]
_ => Err(export::ExportError::Format(
"Export-Format nicht unterstützt".to_string(),
)),
}
}
/// Create a new `TurtleApp` with default settings
#[must_use]
pub fn new() -> Self {
+16 -1
View File
@@ -277,10 +277,24 @@ impl MeshData {
/// Drawable elements in the world
/// All drawing is done via Lyon-tessellated meshes for consistency and quality
#[derive(Clone, Debug)]
pub struct TurtleSource {
pub command: crate::commands::TurtleCommand,
pub color: Color,
pub fill_color: Color,
pub pen_width: f32,
pub start_position: Vec2,
pub end_position: Vec2,
// ggf. weitere Metadaten
}
#[derive(Clone, Debug)]
pub enum DrawCommand {
/// Pre-tessellated mesh data (lines, arcs, circles, polygons - all use this)
Mesh { data: MeshData },
Mesh {
data: MeshData,
source: TurtleSource,
},
/// Text rendering command
Text {
text: String,
@@ -288,6 +302,7 @@ pub enum DrawCommand {
heading: f32,
font_size: crate::general::FontSize,
color: Color,
source: TurtleSource,
},
}