diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.rs | 14 | ||||
| -rw-r--r-- | src/svg/mod.rs | 176 | ||||
| -rw-r--r-- | src/svg/style.rs | 172 |
3 files changed, 360 insertions, 2 deletions
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., + } + } +} |
