aboutsummaryrefslogtreecommitdiff
path: root/src/client/svg/mod.rs
blob: af066f1ac69c7c482687c3a55e8e6a8adfaf3809 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
//! 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 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",
        )),
    }
}

/// Trait that indicates a struct is capable of drawing SVG-elements.
pub trait DrawSVG {
    /// Draw the elements given by `svg_data` and all its children to the implementor, with the
    /// specified scale and translated by `position_px`.
    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 => warn!("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) => {
                warn!("Could not parse path style: {}", err);
                warn!("Using default style instead");
                Style::default()
            }
        }
    } else {
        Style::default()
    };

    let move_data = match path_data.attributes.get("d") {
        Some(d) => d,
        None => {
            error!("Unable to draw path, no move data found");
            return;
        }
    };

    let mut path: SVGPath = match move_data.parse() {
        Ok(mv) => mv,
        Err(err) => {
            error!(
                "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 => warn!("Ignoring unsupported {:?}", other),
        }
    }
}