initial svg support

This commit is contained in:
Franz Dietrich 2025-10-23 09:39:41 +02:00
parent 88d188a794
commit 6e6aa8b27e
10 changed files with 309 additions and 10 deletions

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"rust-analyzer.cargo.features": [
"svg"
]
}

View File

@ -20,3 +20,10 @@ crossbeam = "0.8"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
dialog = "*" dialog = "*"
chrono = "0.4" chrono = "0.4"
[features]
svg = ["dep:svg"]
[dependencies.svg]
version = "0.13"
optional = true

View File

@ -0,0 +1,46 @@
//! Beispiel: Exportiere ein SVG aus einer einfachen Zeichnung
use turtle_lib::*;
#[cfg(feature = "svg")]
#[macroquad::main("Export SVG")]
async fn main() {
let mut plan = create_turtle_plan();
plan.forward(100.0)
.right(90.0)
.forward(100.0)
.circle_left(50.0, 90.0, 4)
.begin_fill()
.forward(100.0)
.right(90.0)
.forward(200.0)
.end_fill();
let mut app = TurtleApp::new().with_commands(plan.build());
use macroquad::{
input::{is_key_pressed, KeyCode},
text::draw_text,
window::{clear_background, next_frame},
};
loop {
clear_background(WHITE);
app.update();
app.render();
draw_text("Drücke E für SVG-Export", 20.0, 40.0, 32.0, BLACK);
if is_key_pressed(KeyCode::E) {
match app.export_drawing("test.svg", export::DrawingFormat::Svg) {
Ok(_) => println!("SVG exportiert nach test.svg"),
Err(e) => println!("Fehler beim Export: {:?}", e),
}
}
next_frame().await;
}
}
#[cfg(not(feature = "svg"))]
fn main() {
println!("SVG-Export ist nicht aktiviert. Baue mit --features svg");
}

View File

@ -2,8 +2,8 @@
//! //!
//! Ported from the turtle crate example. //! Ported from the turtle crate example.
use turtle_lib::*;
use macroquad::prelude::rand; use macroquad::prelude::rand;
use turtle_lib::*;
// Parameters to play around with for changing the character of the drawing // Parameters to play around with for changing the character of the drawing
const WIDTH: f32 = 800.0; const WIDTH: f32 = 800.0;

View File

@ -28,8 +28,10 @@ pub fn render_world(world: &TurtleWorld) {
for turtle in &world.turtles { for turtle in &world.turtles {
for cmd in &turtle.commands { for cmd in &turtle.commands {
match cmd { match cmd {
DrawCommand::Mesh { data } => { DrawCommand::Mesh { data, source } => {
// Rendering wie bisher
draw_mesh(&data.to_mesh()); draw_mesh(&data.to_mesh());
// Hier könnte man das source für Debug/Export loggen
} }
DrawCommand::Text { DrawCommand::Text {
text, text,
@ -37,8 +39,10 @@ pub fn render_world(world: &TurtleWorld) {
heading, heading,
font_size, font_size,
color, color,
source,
} => { } => {
draw_text_command(text, *position, *heading, *font_size, *color); 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 turtle in &world.turtles {
for cmd in &turtle.commands { for cmd in &turtle.commands {
match cmd { match cmd {
DrawCommand::Mesh { data } => { DrawCommand::Mesh { data, source } => {
draw_mesh(&data.to_mesh()); draw_mesh(&data.to_mesh());
} }
DrawCommand::Text { DrawCommand::Text {
@ -85,6 +89,7 @@ pub fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) {
heading, heading,
font_size, font_size,
color, color,
source,
} => { } => {
draw_text_command(text, *position, *heading, *font_size, *color); draw_text_command(text, *position, *heading, *font_size, *color);
} }

View File

@ -63,7 +63,17 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle)
contours = fill_state.contours.len(), contours = fill_state.contours.len(),
"Successfully created fill mesh - persisting to commands" "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 { } else {
tracing::error!( tracing::error!(
turtle_id = state.turtle_id, turtle_id = state.turtle_id,
@ -116,6 +126,14 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle)
heading: state.params.heading, heading: state.params.heading,
font_size: *font_size, font_size: *font_size,
color: state.params.color, 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 true
} }
@ -203,7 +221,17 @@ pub fn execute_command(command: &TurtleCommand, state: &mut Turtle) {
state.params.pen_width, state.params.pen_width,
false, // not closed 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, *steps,
*direction, *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, state.params.pen_width,
false, // not closed 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, start_state.pen_width,
false, 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, *steps,
*direction, *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
turtle-lib/src/export.rs Normal file
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>;
}

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)
}
}
}

View File

@ -67,6 +67,10 @@ pub use shapes::{ShapeType, TurtleShape};
pub use state::{DrawCommand, Turtle, TurtleWorld}; pub use state::{DrawCommand, Turtle, TurtleWorld};
pub use tweening::TweenController; pub use tweening::TweenController;
pub mod export;
#[cfg(feature = "svg")]
pub mod export_svg;
// Re-export the turtle_main macro // Re-export the turtle_main macro
pub use turtle_lib_macros::turtle_main; pub use turtle_lib_macros::turtle_main;
@ -91,6 +95,28 @@ pub struct TurtleApp {
} }
impl 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 /// Create a new `TurtleApp` with default settings
#[must_use] #[must_use]
pub fn new() -> Self { pub fn new() -> Self {

View File

@ -277,10 +277,24 @@ impl MeshData {
/// Drawable elements in the world /// Drawable elements in the world
/// All drawing is done via Lyon-tessellated meshes for consistency and quality /// 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)] #[derive(Clone, Debug)]
pub enum DrawCommand { pub enum DrawCommand {
/// Pre-tessellated mesh data (lines, arcs, circles, polygons - all use this) /// Pre-tessellated mesh data (lines, arcs, circles, polygons - all use this)
Mesh { data: MeshData }, Mesh {
data: MeshData,
source: TurtleSource,
},
/// Text rendering command /// Text rendering command
Text { Text {
text: String, text: String,
@ -288,6 +302,7 @@ pub enum DrawCommand {
heading: f32, heading: f32,
font_size: crate::general::FontSize, font_size: crate::general::FontSize,
color: Color, color: Color,
source: TurtleSource,
}, },
} }