aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock39
-rw-r--r--Cargo.toml2
-rw-r--r--drawing.svg34
-rw-r--r--src/main.rs14
-rw-r--r--src/svg/mod.rs176
-rw-r--r--src/svg/style.rs172
6 files changed, 435 insertions, 2 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 565736c..360d236 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -69,6 +69,12 @@ dependencies = [
]
[[package]]
+name = "float-cmp"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75224bec9bfe1a65e2d34132933f2de7fe79900c96a0174307554244ece8150e"
+
+[[package]]
name = "fs_extra"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -105,6 +111,8 @@ dependencies = [
"raylib",
"ron",
"serde",
+ "svgtypes",
+ "xmltree",
]
[[package]]
@@ -355,6 +363,22 @@ dependencies = [
]
[[package]]
+name = "siphasher"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
+
+[[package]]
+name = "svgtypes"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff"
+dependencies = [
+ "float-cmp",
+ "siphasher",
+]
+
+[[package]]
name = "syn"
version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -388,3 +412,18 @@ name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "xml-rs"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a"
+
+[[package]]
+name = "xmltree"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f76badaccb0313f1f0cb6582c2973f2dd0620f9652eb7a5ff6ced0cc3ac922b3"
+dependencies = [
+ "xml-rs",
+]
diff --git a/Cargo.toml b/Cargo.toml
index b68ba24..ccd6c11 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,3 +11,5 @@ serde = "*"
nalgebra = "*"
alga = "*"
num-traits = "*"
+svgtypes = "*"
+xmltree = "*"
diff --git a/drawing.svg b/drawing.svg
new file mode 100644
index 0000000..aa03d65
--- /dev/null
+++ b/drawing.svg
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ width="210mm"
+ height="297mm"
+ viewBox="0 0 210 297"
+ version="1.1"
+ id="svg8">
+ <defs
+ id="defs2" />
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1">
+ <path
+ style="fill:none;stroke:#ff0000;stroke-width:26.4583;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 4.8525633,3.3577651 192.15152,41.144727 176.72349,112.68443 291.72297,82.659063"
+ id="path833" />
+ </g>
+</svg>
diff --git a/src/main.rs b/src/main.rs
index f3838a5..c7cf4f0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,11 +2,13 @@ pub mod editor;
pub mod grid;
pub mod map_data;
pub mod math;
+pub mod svg;
pub mod tool;
pub mod transform;
use editor::Editor;
use raylib::prelude::*;
+use svg::DrawSVG;
use transform::Transform;
fn main() {
@@ -15,6 +17,8 @@ fn main() {
let mut editor = Editor::new();
+ let test_svg = svg::read_svg_file("drawing.svg").expect("Could not load svg file");
+
let mut transform = Transform::new();
let mut last_mouse_pos = rl.get_mouse_position();
while !rl.window_should_close() {
@@ -29,10 +33,10 @@ fn main() {
let mouse_wheel_move = rl.get_mouse_wheel_move();
if mouse_wheel_move != 0 {
// Zoom in for positive and zoom out for negative mouse wheel rotations.
- let factor = if mouse_wheel_move > 0 { 1.2 } else { 1. / 1.2 };
+ let scale_factor = if mouse_wheel_move > 0 { 1.2 } else { 1. / 1.2 };
transform.try_zoom(
rl.get_mouse_position().into(),
- mouse_wheel_move.abs() as f32 * factor,
+ mouse_wheel_move.abs() as f32 * scale_factor,
);
}
@@ -47,6 +51,12 @@ fn main() {
d.clear_background(Color::BLACK);
grid::draw_grid(&mut d, screen_width, screen_height, &transform);
+ d.draw_svg(
+ &test_svg,
+ transform.pixels_per_m(),
+ transform.translation_px(),
+ );
+
editor.draw_tools(&mut d, &transform);
}
}
diff --git a/src/svg/mod.rs b/src/svg/mod.rs
new file mode 100644
index 0000000..5527ee0
--- /dev/null
+++ b/src/svg/mod.rs
@@ -0,0 +1,176 @@
+//! Module for drawing SVG files to the screen or maybe a texture etc.
+
+pub mod style;
+
+use crate::math::Vec2;
+use raylib::drawing::RaylibDraw;
+use raylib::ffi::Vector2;
+use std::fs::File;
+use std::io::{self, Read};
+use std::ops::Deref;
+use std::path::Path;
+use std::str::FromStr;
+use style::Style;
+use svgtypes::{Path as SVGPath, PathSegment};
+use xmltree::{Element, XMLNode};
+
+// Find the first XML-Node with the given name. With depth, the maximum depth the
+// algorithm will search to can be set. If it is set to `None`, the algorithm will search the
+// entire sub-tree. Returns `None` if no such child can be found. Returns itself, in case the root
+// node is already of the name given.
+pub fn find_first_node(root: Element, name: &str, depth: Option<usize>) -> Option<Element> {
+ // The abort condition of this recursive function. If the element itself is of the required
+ // name, return it.
+ if root.name == name {
+ return Some(root);
+ }
+ // Also abort, if the depth is reached.
+ if depth == Some(0) {
+ return None;
+ }
+
+ // Decrease the depth by one for all calls on the children, if it is set.
+ let depth = match depth {
+ Some(depth) => Some(depth - 1),
+ None => None,
+ };
+
+ // Recursively look for the element in this node's children.
+ for child in root.children {
+ // We only care for the elements, not for comments, cdata etc.
+ if let XMLNode::Element(element) = child {
+ if let Some(element) = find_first_node(element, name, depth) {
+ return Some(element);
+ }
+ }
+ }
+
+ None
+}
+
+/// Read an svg file from the given path. On success, return the first graphics data that is read
+/// from this file. This can be used to draw an SVG.
+pub fn read_svg_file<P: AsRef<Path>>(file: P) -> io::Result<Element> {
+ let mut file = File::open(file)?;
+ let mut data = String::new();
+ file.read_to_string(&mut data)?;
+
+ let root: Element = match Element::parse(data.as_bytes()) {
+ Ok(root) => root,
+ Err(err) => return Err(io::Error::new(io::ErrorKind::InvalidData, err)),
+ };
+
+ match find_first_node(root, "g", None) {
+ Some(graphics) => Ok(graphics),
+ None => Err(io::Error::new(
+ io::ErrorKind::InvalidData,
+ "No graphics element in the file",
+ )),
+ }
+}
+
+pub trait DrawSVG {
+ fn draw_svg(&mut self, svg_data: &Element, pixels_per_m: f32, position_px: Vec2<f32>);
+}
+
+impl<D> DrawSVG for D
+where
+ D: RaylibDraw,
+{
+ fn draw_svg(&mut self, svg_data: &Element, pixels_per_m: f32, position_px: Vec2<f32>) {
+ assert_eq!(&svg_data.name, "g");
+
+ // Go through all the graphics children and draw them one by one
+ for child in &svg_data.children {
+ if let XMLNode::Element(child) = child {
+ match child.name.as_str() {
+ "path" => draw_path(self, child, pixels_per_m, position_px),
+ other => println!("Unsupported SVG-Element {}", other),
+ }
+ }
+ }
+ }
+}
+
+// Helper functions to draw specific parts of the SVG file --------------------
+
+fn draw_path(
+ d: &mut impl RaylibDraw,
+ path_data: &Element,
+ pixels_per_m: f32,
+ position_px: Vec2<f32>,
+) {
+ let style = if let Some(style_data) = path_data.attributes.get("style") {
+ match Style::from_str(style_data) {
+ Ok(style) => style,
+ Err(err) => {
+ println!("Could not parse path style: {}", err);
+ println!("Using default style instead");
+ Style::default()
+ }
+ }
+ } else {
+ Style::default()
+ };
+
+ let move_data = match path_data.attributes.get("d") {
+ Some(d) => d,
+ None => {
+ println!("Unable to draw path, no move data found");
+ return;
+ }
+ };
+
+ let mut path: SVGPath = match move_data.parse() {
+ Ok(mv) => mv,
+ Err(err) => {
+ println!(
+ "Unable to draw path, move data not correctly formatted: {}",
+ err
+ );
+ return;
+ }
+ };
+
+ path.conv_to_absolute();
+
+ let mut current_pos: Vec2<f32> = Vec2::new(0., 0.);
+ for segment in path.deref() {
+ match segment {
+ PathSegment::MoveTo { x, y, .. } => {
+ current_pos.x = *x as f32;
+ current_pos.y = *y as f32;
+ }
+ PathSegment::LineTo { x, y, .. } => {
+ d.draw_line_ex(
+ current_pos * pixels_per_m / 1000. + position_px,
+ Vec2::new(*x as f32, *y as f32) * pixels_per_m / 1000. + position_px,
+ pixels_per_m * style.stroke_width / 1000.,
+ style.stroke,
+ );
+ current_pos.x = *x as f32;
+ current_pos.y = *y as f32;
+ }
+ PathSegment::HorizontalLineTo { x, .. } => {
+ d.draw_line_ex(
+ current_pos * pixels_per_m / 1000. + position_px,
+ Vec2::new(*x as f32, current_pos.y) * pixels_per_m / 1000. + position_px,
+ pixels_per_m * style.stroke_width / 1000.,
+ style.stroke,
+ );
+ current_pos.x = *x as f32;
+ }
+ PathSegment::VerticalLineTo { y, .. } => {
+ d.draw_line_ex(
+ current_pos * pixels_per_m / 1000. + position_px,
+ Vec2::new(current_pos.x, *y as f32) * pixels_per_m / 1000. + position_px,
+ pixels_per_m * style.stroke_width / 1000.,
+ style.stroke,
+ );
+ current_pos.y = *y as f32;
+ }
+ PathSegment::ClosePath { .. } => return,
+ other => println!("Ignoring unsupported {:?}", other),
+ }
+ }
+}
diff --git a/src/svg/style.rs b/src/svg/style.rs
new file mode 100644
index 0000000..78b800d
--- /dev/null
+++ b/src/svg/style.rs
@@ -0,0 +1,172 @@
+use raylib::ffi::Color;
+use std::str::FromStr;
+
+/// Convert an html-style colour into a raylib Color-struct if possible. If there is an error in
+/// the formatting, it returns `None`.
+pub fn colour_from_html(html: &str) -> Option<Color> {
+ /* The html-code must be exactly seven characters long, one for the hash and two per primary
+ * colour.
+ */
+ if html.len() != 7 {
+ return None;
+ }
+
+ let extract_hex = |string: &str, pos: usize| {
+ u8::from_str_radix(
+ string.get(pos..pos + 2).expect("Could not split string"),
+ 16,
+ )
+ .ok()
+ };
+ let red: Option<u8> = extract_hex(html, 1);
+ let green: Option<u8> = extract_hex(html, 3);
+ let blue: Option<u8> = extract_hex(html, 5);
+
+ if let (Some(r), Some(g), Some(b)) = (red, green, blue) {
+ Some(Color { r, g, b, a: 255 })
+ } else {
+ None
+ }
+}
+
+/// The style of the end of the stroke.
+/// See [stroke-line-cap property](https://www.w3.org/TR/SVG11/painting.html#StrokeLinecapProperty)
+/// in the SVG Documentation.
+pub enum StrokeLineCap {
+ Butt,
+ Round,
+ Square,
+}
+
+/// The style of the joining corners of the stroke.
+/// See [stroke-line-join property](https://www.w3.org/TR/SVG11/painting.html#StrokeLinejoinProperty)
+/// in the SVG Documentation
+pub enum StrokeLineJoin {
+ Miter,
+ Round,
+ Bevel,
+}
+
+/// The style of a path drawing instruction.
+pub struct Style {
+ pub fill: Option<Color>,
+ pub stroke: Color,
+ pub stroke_width: f32,
+ pub stroke_linecap: StrokeLineCap,
+ pub stroke_linejoin: StrokeLineJoin,
+ pub stroke_opacity: f32,
+}
+
+impl FromStr for StrokeLineCap {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "butt" => Ok(Self::Butt),
+ "round" => Ok(Self::Round),
+ "square" => Ok(Self::Square),
+ _ => Err("No such line-cap style".to_owned()),
+ }
+ }
+}
+
+impl Default for StrokeLineCap {
+ fn default() -> Self {
+ StrokeLineCap::Butt
+ }
+}
+
+impl FromStr for StrokeLineJoin {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "miter" => Ok(Self::Miter),
+ "round" => Ok(Self::Round),
+ "bevel" => Ok(Self::Bevel),
+ _ => Err("No such line-join style".to_owned()),
+ }
+ }
+}
+
+impl Default for StrokeLineJoin {
+ fn default() -> Self {
+ StrokeLineJoin::Miter
+ }
+}
+
+impl FromStr for Style {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ // Split into CSS-attributes
+ let attributes: Vec<&str> = s.split(';').collect();
+ // Split the attributes into name and value pairs and parse them into a style struct
+ let mut style = Style::default();
+ for attribute in attributes {
+ let attribute_parts: Vec<&str> = attribute.split(':').collect();
+ if attribute_parts.len() != 2 {
+ continue;
+ }
+
+ match attribute_parts[0].trim() {
+ "fill" => {
+ style.fill = match attribute_parts[1].trim() {
+ "none" => None,
+ colour => colour_from_html(colour),
+ }
+ }
+ "stroke" => {
+ style.stroke = match colour_from_html(attribute_parts[1].trim()) {
+ Some(c) => c,
+ None => {
+ return Err(format!(
+ "Could not parse colour from {}",
+ attribute_parts[1].trim()
+ ))
+ }
+ }
+ }
+ "stroke-width" => {
+ style.stroke_width = match attribute_parts[1].trim().parse::<f32>() {
+ Ok(width) => width,
+ Err(err) => return Err(format!("Could not parse stroke-width: {}", err)),
+ }
+ }
+ "stroke-linecap" => {
+ style.stroke_linecap = StrokeLineCap::from_str(attribute_parts[1].trim())?
+ }
+ "stroke-linejoin" => {
+ style.stroke_linejoin = StrokeLineJoin::from_str(attribute_parts[1].trim())?
+ }
+ "stroke-opacity" => {
+ style.stroke_width = match attribute_parts[1].trim().parse::<f32>() {
+ Ok(opacity) => opacity,
+ Err(err) => return Err(format!("Could not parse stroke-opacity: {}", err)),
+ }
+ }
+ attr => return Err(format!("Unknown attribute {}", attr)),
+ }
+ }
+
+ Ok(style)
+ }
+}
+
+impl Default for Style {
+ fn default() -> Self {
+ Self {
+ fill: None,
+ stroke: Color {
+ r: 0,
+ g: 0,
+ b: 0,
+ a: 255,
+ },
+ stroke_width: 1.,
+ stroke_linecap: StrokeLineCap::default(),
+ stroke_linejoin: StrokeLineJoin::default(),
+ stroke_opacity: 1.,
+ }
+ }
+}