widgetry/
svg.rs

1use lyon::math::Point;
2use lyon::path::Path;
3use lyon::tessellation;
4use lyon::tessellation::geometry_builder::{simple_builder, VertexBuffers};
5use usvg::TreeParsing;
6
7use abstutil::VecMap;
8use geom::{Bounds, Pt2D, Tessellation};
9
10use crate::{Color, Fill, GeomBatch, LinearGradient, Prerender};
11
12pub const HIGH_QUALITY: f32 = 0.01;
13pub const LOW_QUALITY: f32 = 1.0;
14
15// Code here adapted from
16// https://github.com/nical/lyon/blob/0d0ee771180fb317b986d9cf30266722e0773e01/examples/wgpu_svg/src/main.rs
17
18pub fn load_svg(prerender: &Prerender, filename: &str) -> (GeomBatch, Bounds) {
19    let cache_key = format!("file://{}", filename);
20    if let Some(pair) = prerender.assets.get_cached_svg(&cache_key) {
21        return pair;
22    }
23
24    let bytes = (prerender.assets.read_svg)(filename);
25    load_svg_from_bytes_uncached(&bytes)
26        .map(|(batch, bounds)| {
27            prerender.assets.cache_svg(cache_key, batch.clone(), bounds);
28            (batch, bounds)
29        })
30        .unwrap_or_else(|_| panic!("error loading svg: {}", filename))
31}
32
33pub fn load_svg_bytes(
34    prerender: &Prerender,
35    cache_key: &str,
36    bytes: &[u8],
37) -> anyhow::Result<(GeomBatch, Bounds)> {
38    let cache_key = format!("bytes://{}", cache_key);
39    if let Some(pair) = prerender.assets.get_cached_svg(&cache_key) {
40        return Ok(pair);
41    }
42
43    load_svg_from_bytes_uncached(bytes).map(|(batch, bounds)| {
44        prerender.assets.cache_svg(cache_key, batch.clone(), bounds);
45        (batch, bounds)
46    })
47}
48
49pub fn load_svg_from_bytes_uncached(bytes: &[u8]) -> anyhow::Result<(GeomBatch, Bounds)> {
50    let svg_tree = usvg::Tree::from_data(bytes, &usvg::Options::default())?;
51    let mut batch = GeomBatch::new();
52    match add_svg_inner(&mut batch, svg_tree, HIGH_QUALITY) {
53        Ok(bounds) => Ok((batch, bounds)),
54        Err(err) => Err(anyhow!(err)),
55    }
56}
57
58// No offset. I'm not exactly sure how the simplification in usvg works, but this doesn't support
59// transforms or strokes or text, just fills. Luckily, all of the files exported from Figma so far
60// work just fine.
61pub(crate) fn add_svg_inner(
62    batch: &mut GeomBatch,
63    svg_tree: usvg::Tree,
64    tolerance: f32,
65) -> Result<Bounds, String> {
66    let mut fill_tess = tessellation::FillTessellator::new();
67    let mut stroke_tess = tessellation::StrokeTessellator::new();
68    // TODO This breaks on start.svg; the order there matters. color1, color2, then color1 again.
69    let mut mesh_per_color: VecMap<Fill, VertexBuffers<_, u16>> = VecMap::new();
70
71    for node in svg_tree.root.descendants() {
72        if let usvg::NodeKind::Path(ref p) = *node.borrow() {
73            // TODO Handle transforms
74
75            if let Some(ref fill) = p.fill {
76                let color = convert_color(&fill.paint, fill.opacity.get());
77                let geom = mesh_per_color.mut_or_insert(color, VertexBuffers::new);
78                if let Err(err) = fill_tess.tessellate(
79                    &convert_path(p),
80                    &tessellation::FillOptions::tolerance(tolerance),
81                    &mut simple_builder(geom),
82                ) {
83                    return Err(format!("Couldn't tessellate something: {err}"));
84                }
85            }
86
87            if let Some(ref stroke) = p.stroke {
88                let (color, stroke_opts) = convert_stroke(stroke, tolerance);
89                let geom = mesh_per_color.mut_or_insert(color, VertexBuffers::new);
90                stroke_tess
91                    .tessellate(&convert_path(p), &stroke_opts, &mut simple_builder(geom))
92                    .unwrap();
93            }
94        }
95    }
96
97    for (color, mesh) in mesh_per_color.consume() {
98        batch.push(
99            color,
100            Tessellation::new(
101                mesh.vertices
102                    .into_iter()
103                    .map(|v| Pt2D::new(f64::from(v.x), f64::from(v.y)))
104                    .collect(),
105                mesh.indices.into_iter().map(|idx| idx as usize).collect(),
106            ),
107        );
108    }
109    Ok(Bounds::from(&[
110        Pt2D::new(0.0, 0.0),
111        Pt2D::new(svg_tree.size.width(), svg_tree.size.height()),
112    ]))
113}
114
115fn convert_path(p: &usvg::Path) -> Path {
116    let mut builder = Path::builder().with_svg();
117    for segment in p.data.segments() {
118        match segment {
119            usvg::PathSegment::MoveTo { x, y } => {
120                builder.move_to(Point::new(x as f32, y as f32));
121            }
122            usvg::PathSegment::LineTo { x, y } => {
123                builder.line_to(Point::new(x as f32, y as f32));
124            }
125            usvg::PathSegment::CurveTo {
126                x1,
127                y1,
128                x2,
129                y2,
130                x,
131                y,
132            } => {
133                builder.cubic_bezier_to(
134                    Point::new(x1 as f32, y1 as f32),
135                    Point::new(x2 as f32, y2 as f32),
136                    Point::new(x as f32, y as f32),
137                );
138            }
139            usvg::PathSegment::ClosePath => {
140                builder.close();
141            }
142        }
143    }
144    builder.build()
145}
146
147fn convert_stroke(s: &usvg::Stroke, tolerance: f32) -> (Fill, tessellation::StrokeOptions) {
148    let color = convert_color(&s.paint, s.opacity.get());
149    let linecap = match s.linecap {
150        usvg::LineCap::Butt => tessellation::LineCap::Butt,
151        usvg::LineCap::Square => tessellation::LineCap::Square,
152        usvg::LineCap::Round => tessellation::LineCap::Round,
153    };
154    let linejoin = match s.linejoin {
155        usvg::LineJoin::Miter => tessellation::LineJoin::Miter,
156        usvg::LineJoin::Bevel => tessellation::LineJoin::Bevel,
157        usvg::LineJoin::Round => tessellation::LineJoin::Round,
158    };
159
160    let opt = tessellation::StrokeOptions::tolerance(tolerance)
161        .with_line_width(s.width.get() as f32)
162        .with_line_cap(linecap)
163        .with_line_join(linejoin);
164
165    (color, opt)
166}
167
168fn convert_color(paint: &usvg::Paint, opacity: f64) -> Fill {
169    match paint {
170        usvg::Paint::Color(c) => Fill::Color(Color::rgba(
171            c.red as usize,
172            c.green as usize,
173            c.blue as usize,
174            opacity as f32,
175        )),
176        usvg::Paint::LinearGradient(lg) => LinearGradient::new_fill(lg),
177        // No patterns or radial gradiants
178        _ => panic!("Unsupported color style {:?}", paint),
179    }
180}