initial svg support
This commit is contained in:
parent
88d188a794
commit
6e6aa8b27e
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"rust-analyzer.cargo.features": [
|
||||||
|
"svg"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
46
turtle-lib/examples/export_svg.rs
Normal file
46
turtle-lib/examples/export_svg.rs
Normal 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");
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
20
turtle-lib/src/export.rs
Normal 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
turtle-lib/src/export_svg.rs
Normal file
107
turtle-lib/src/export_svg.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user