use lyon::math::Point;
use lyon::path::Path;
use lyon::tessellation;
use lyon::tessellation::geometry_builder::{simple_builder, VertexBuffers};
use usvg::TreeParsing;
use abstutil::VecMap;
use geom::{Bounds, Pt2D, Tessellation};
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);
(batch, bounds)
})
.unwrap_or_else(|_| panic!("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);
(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.get());
let geom = mesh_per_color.mut_or_insert(color, VertexBuffers::new);
if let Err(err) = fill_tess.tessellate(
&convert_path(p),
&tessellation::FillOptions::tolerance(tolerance),
&mut simple_builder(geom),
) {
return Err(format!("Couldn't tessellate something: {err}"));
}
}
if let Some(ref stroke) = p.stroke {
let (color, stroke_opts) = convert_stroke(stroke, tolerance);
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,
Tessellation::new(
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(),
),
);
}
Ok(Bounds::from(&[
Pt2D::new(0.0, 0.0),
Pt2D::new(svg_tree.size.width(), svg_tree.size.height()),
]))
}
fn convert_path(p: &usvg::Path) -> Path {
let mut builder = Path::builder().with_svg();
for segment in p.data.segments() {
match segment {
usvg::PathSegment::MoveTo { x, y } => {
builder.move_to(Point::new(x as f32, y as f32));
}
usvg::PathSegment::LineTo { x, y } => {
builder.line_to(Point::new(x as f32, y as f32));
}
usvg::PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x,
y,
} => {
builder.cubic_bezier_to(
Point::new(x1 as f32, y1 as f32),
Point::new(x2 as f32, y2 as f32),
Point::new(x as f32, y as f32),
);
}
usvg::PathSegment::ClosePath => {
builder.close();
}
}
}
builder.build()
}
fn convert_stroke(s: &usvg::Stroke, tolerance: f32) -> (Fill, tessellation::StrokeOptions) {
let color = convert_color(&s.paint, s.opacity.get());
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.get() as f32)
.with_line_cap(linecap)
.with_line_join(linejoin);
(color, opt)
}
fn convert_color(paint: &usvg::Paint, opacity: f64) -> 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::LinearGradient(lg) => LinearGradient::new_fill(lg),
_ => panic!("Unsupported color style {:?}", paint),
}
}