use lyon::math::Point;
use lyon::path::PathEvent;
use lyon::tessellation;
use lyon::tessellation::geometry_builder::{simple_builder, VertexBuffers};
use abstutil::VecMap;
use geom::{Bounds, Polygon, Pt2D};
use crate::{Color, Fill, GeomBatch, LinearGradient, Prerender};
pub const HIGH_QUALITY: f32 = 0.01;
pub const LOW_QUALITY: f32 = 1.0;
pub fn load_svg(prerender: &Prerender, filename: &str) -> (GeomBatch, Bounds) {
let cache_key = format!("file://{}", filename);
if let Some(pair) = prerender.assets.get_cached_svg(&cache_key) {
return pair;
}
let bytes = (prerender.assets.read_svg)(filename);
load_svg_from_bytes_uncached(&bytes)
.map(|(batch, bounds)| {
prerender
.assets
.cache_svg(cache_key, batch.clone(), bounds.clone());
(batch, bounds)
})
.expect(&format!("error loading svg: {}", filename))
}
pub fn load_svg_bytes(
prerender: &Prerender,
cache_key: &str,
bytes: &[u8],
) -> anyhow::Result<(GeomBatch, Bounds)> {
let cache_key = format!("bytes://{}", cache_key);
if let Some(pair) = prerender.assets.get_cached_svg(&cache_key) {
return Ok(pair);
}
load_svg_from_bytes_uncached(&bytes).map(|(batch, bounds)| {
prerender
.assets
.cache_svg(cache_key, batch.clone(), bounds.clone());
(batch, bounds)
})
}
pub fn load_svg_from_bytes_uncached(bytes: &[u8]) -> anyhow::Result<(GeomBatch, Bounds)> {
let svg_tree = usvg::Tree::from_data(&bytes, &usvg::Options::default())?;
let mut batch = GeomBatch::new();
match add_svg_inner(&mut batch, svg_tree, HIGH_QUALITY) {
Ok(bounds) => Ok((batch, bounds)),
Err(err) => Err(anyhow!(err)),
}
}
pub(crate) fn add_svg_inner(
batch: &mut GeomBatch,
svg_tree: usvg::Tree,
tolerance: f32,
) -> Result<Bounds, String> {
let mut fill_tess = tessellation::FillTessellator::new();
let mut stroke_tess = tessellation::StrokeTessellator::new();
let mut mesh_per_color: VecMap<Fill, VertexBuffers<_, u16>> = VecMap::new();
for node in svg_tree.root().descendants() {
if let usvg::NodeKind::Path(ref p) = *node.borrow() {
if let Some(ref fill) = p.fill {
let color = convert_color(&fill.paint, fill.opacity.value(), &svg_tree);
let geom = mesh_per_color.mut_or_insert(color, VertexBuffers::new);
if fill_tess
.tessellate(
convert_path(p),
&tessellation::FillOptions::tolerance(tolerance),
&mut simple_builder(geom),
)
.is_err()
{
return Err(format!("Couldn't tesellate something"));
}
}
if let Some(ref stroke) = p.stroke {
let (color, stroke_opts) = convert_stroke(stroke, tolerance, &svg_tree);
let geom = mesh_per_color.mut_or_insert(color, VertexBuffers::new);
stroke_tess
.tessellate(convert_path(p), &stroke_opts, &mut simple_builder(geom))
.unwrap();
}
}
}
for (color, mesh) in mesh_per_color.consume() {
batch.push(
color,
Polygon::precomputed(
mesh.vertices
.into_iter()
.map(|v| Pt2D::new(f64::from(v.x), f64::from(v.y)))
.collect(),
mesh.indices.into_iter().map(|idx| idx as usize).collect(),
),
);
}
let size = svg_tree.svg_node().size;
Ok(Bounds::from(&vec![
Pt2D::new(0.0, 0.0),
Pt2D::new(size.width(), size.height()),
]))
}
fn point(x: &f64, y: &f64) -> Point {
Point::new((*x) as f32, (*y) as f32)
}
struct PathConvIter<'a> {
iter: std::slice::Iter<'a, usvg::PathSegment>,
prev: Point,
first: Point,
needs_end: bool,
deferred: Option<PathEvent>,
}
impl<'l> Iterator for PathConvIter<'l> {
type Item = PathEvent;
fn next(&mut self) -> Option<PathEvent> {
if self.deferred.is_some() {
return self.deferred.take();
}
let next = self.iter.next();
match next {
Some(usvg::PathSegment::MoveTo { x, y }) => {
if self.needs_end {
let last = self.prev;
let first = self.first;
self.needs_end = false;
self.prev = point(x, y);
self.deferred = Some(PathEvent::Begin { at: self.prev });
self.first = self.prev;
Some(PathEvent::End {
last,
first,
close: false,
})
} else {
self.first = point(x, y);
Some(PathEvent::Begin { at: self.first })
}
}
Some(usvg::PathSegment::LineTo { x, y }) => {
self.needs_end = true;
let from = self.prev;
self.prev = point(x, y);
Some(PathEvent::Line {
from,
to: self.prev,
})
}
Some(usvg::PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x,
y,
}) => {
self.needs_end = true;
let from = self.prev;
self.prev = point(x, y);
Some(PathEvent::Cubic {
from,
ctrl1: point(x1, y1),
ctrl2: point(x2, y2),
to: self.prev,
})
}
Some(usvg::PathSegment::ClosePath) => {
self.needs_end = false;
self.prev = self.first;
Some(PathEvent::End {
last: self.prev,
first: self.first,
close: true,
})
}
None => {
if self.needs_end {
self.needs_end = false;
let last = self.prev;
let first = self.first;
Some(PathEvent::End {
last,
first,
close: false,
})
} else {
None
}
}
}
}
}
fn convert_path<'a>(p: &'a usvg::Path) -> PathConvIter<'a> {
PathConvIter {
iter: p.data.0.iter(),
first: Point::new(0.0, 0.0),
prev: Point::new(0.0, 0.0),
deferred: None,
needs_end: false,
}
}
fn convert_stroke(
s: &usvg::Stroke,
tolerance: f32,
tree: &usvg::Tree,
) -> (Fill, tessellation::StrokeOptions) {
let color = convert_color(&s.paint, s.opacity.value(), tree);
let linecap = match s.linecap {
usvg::LineCap::Butt => tessellation::LineCap::Butt,
usvg::LineCap::Square => tessellation::LineCap::Square,
usvg::LineCap::Round => tessellation::LineCap::Round,
};
let linejoin = match s.linejoin {
usvg::LineJoin::Miter => tessellation::LineJoin::Miter,
usvg::LineJoin::Bevel => tessellation::LineJoin::Bevel,
usvg::LineJoin::Round => tessellation::LineJoin::Round,
};
let opt = tessellation::StrokeOptions::tolerance(tolerance)
.with_line_width(s.width.value() as f32)
.with_line_cap(linecap)
.with_line_join(linejoin);
(color, opt)
}
fn convert_color(paint: &usvg::Paint, opacity: f64, tree: &usvg::Tree) -> Fill {
match paint {
usvg::Paint::Color(c) => Fill::Color(Color::rgba(
c.red as usize,
c.green as usize,
c.blue as usize,
opacity as f32,
)),
usvg::Paint::Link(name) => match *tree.defs_by_id(name).unwrap().borrow() {
usvg::NodeKind::LinearGradient(ref lg) => LinearGradient::new(lg),
_ => panic!("Unsupported color style {}", name),
},
}
}