diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..beb8ccd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "rust-analyzer.cargo.features": [ + "svg" + ] +} \ No newline at end of file diff --git a/turtle-lib/Cargo.toml b/turtle-lib/Cargo.toml index 4fe1171..41f7110 100644 --- a/turtle-lib/Cargo.toml +++ b/turtle-lib/Cargo.toml @@ -20,3 +20,10 @@ crossbeam = "0.8" tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } dialog = "*" chrono = "0.4" + +[features] +svg = ["dep:svg"] + +[dependencies.svg] +version = "0.13" +optional = true diff --git a/turtle-lib/examples/export_svg.rs b/turtle-lib/examples/export_svg.rs new file mode 100644 index 0000000..f6a8e94 --- /dev/null +++ b/turtle-lib/examples/export_svg.rs @@ -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"); +} diff --git a/turtle-lib/examples/geometric_art.rs b/turtle-lib/examples/geometric_art.rs index 3022bde..cd14126 100644 --- a/turtle-lib/examples/geometric_art.rs +++ b/turtle-lib/examples/geometric_art.rs @@ -2,8 +2,8 @@ //! //! Ported from the turtle crate example. -use turtle_lib::*; use macroquad::prelude::rand; +use turtle_lib::*; // Parameters to play around with for changing the character of the drawing const WIDTH: f32 = 800.0; diff --git a/turtle-lib/src/drawing.rs b/turtle-lib/src/drawing.rs index fb208a5..085e06d 100644 --- a/turtle-lib/src/drawing.rs +++ b/turtle-lib/src/drawing.rs @@ -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); } diff --git a/turtle-lib/src/execution.rs b/turtle-lib/src/execution.rs index 421621e..83bbf8b 100644 --- a/turtle-lib/src/execution.rs +++ b/turtle-lib/src/execution.rs @@ -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, + }, + }); } } } diff --git a/turtle-lib/src/export.rs b/turtle-lib/src/export.rs new file mode 100644 index 0000000..f4f5dad --- /dev/null +++ b/turtle-lib/src/export.rs @@ -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>; +} diff --git a/turtle-lib/src/export_svg.rs b/turtle-lib/src/export_svg.rs new file mode 100644 index 0000000..265cd47 --- /dev/null +++ b/turtle-lib/src/export_svg.rs @@ -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 + 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 + 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) + } + } +} diff --git a/turtle-lib/src/lib.rs b/turtle-lib/src/lib.rs index 052a80d..afa1b92 100644 --- a/turtle-lib/src/lib.rs +++ b/turtle-lib/src/lib.rs @@ -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 { diff --git a/turtle-lib/src/state.rs b/turtle-lib/src/state.rs index 9e90a84..2f2331e 100644 --- a/turtle-lib/src/state.rs +++ b/turtle-lib/src/state.rs @@ -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, }, }