1use std::error::Error;
10use std::fmt;
11
12use crate::paint::rgba_f32;
13use crate::tree::Color;
14
15use bytemuck::{Pod, Zeroable};
16use lyon_tessellation::geometry_builder::{BuffersBuilder, VertexBuffers};
17use lyon_tessellation::math::point;
18use lyon_tessellation::path::Path as LyonPath;
19use lyon_tessellation::{
20 FillOptions, FillTessellator, FillVertex, LineCap, LineJoin, StrokeOptions, StrokeTessellator,
21 StrokeVertex,
22};
23use usvg::tiny_skia_path;
24
25#[derive(Clone, Debug, PartialEq)]
26pub struct VectorAsset {
27 pub view_box: [f32; 4],
28 pub paths: Vec<VectorPath>,
29}
30
31#[derive(Clone, Debug, PartialEq)]
32pub struct VectorPath {
33 pub segments: Vec<VectorSegment>,
34 pub fill: Option<VectorFill>,
35 pub stroke: Option<VectorStroke>,
36}
37
38#[derive(Clone, Copy, Debug, PartialEq)]
39pub enum VectorSegment {
40 MoveTo([f32; 2]),
41 LineTo([f32; 2]),
42 QuadTo([f32; 2], [f32; 2]),
43 CubicTo([f32; 2], [f32; 2], [f32; 2]),
44 Close,
45}
46
47#[derive(Clone, Copy, Debug, PartialEq)]
48pub struct VectorFill {
49 pub color: VectorColor,
50 pub opacity: f32,
51 pub rule: VectorFillRule,
52}
53
54#[derive(Clone, Copy, Debug, PartialEq)]
55pub struct VectorStroke {
56 pub color: VectorColor,
57 pub opacity: f32,
58 pub width: f32,
59 pub line_cap: VectorLineCap,
60 pub line_join: VectorLineJoin,
61 pub miter_limit: f32,
62}
63
64#[derive(Clone, Copy, Debug, PartialEq)]
65pub enum VectorColor {
66 CurrentColor,
67 Solid(Color),
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub enum VectorFillRule {
72 NonZero,
73 EvenOdd,
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub enum VectorLineCap {
78 Butt,
79 Round,
80 Square,
81}
82
83#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84pub enum VectorLineJoin {
85 Miter,
86 MiterClip,
87 Round,
88 Bevel,
89}
90
91#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
92pub enum IconMaterial {
93 #[default]
96 Flat,
97 Relief,
102 Glass,
105}
106
107#[repr(C)]
108#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
109pub struct VectorMeshVertex {
110 pub pos: [f32; 2],
113 pub local: [f32; 2],
116 pub color: [f32; 4],
117 pub meta: [f32; 4],
120 pub aa: [f32; 2],
127}
128
129#[derive(Clone, Debug, Default, PartialEq)]
130pub struct VectorMesh {
131 pub vertices: Vec<VectorMeshVertex>,
132}
133
134#[derive(Clone, Copy, Debug, PartialEq)]
135pub struct VectorMeshRun {
136 pub first: u32,
137 pub count: u32,
138}
139
140#[derive(Clone, Copy, Debug, PartialEq)]
141pub struct VectorMeshOptions {
142 pub rect: crate::tree::Rect,
143 pub current_color: Color,
144 pub stroke_width: f32,
145 pub tolerance: f32,
146}
147
148impl VectorMeshOptions {
149 pub fn icon(rect: crate::tree::Rect, current_color: Color, stroke_width: f32) -> Self {
150 Self {
151 rect,
152 current_color,
153 stroke_width,
154 tolerance: 0.05,
155 }
156 }
157}
158
159#[derive(Clone, Debug, PartialEq, Eq)]
160pub struct VectorParseError {
161 message: String,
162}
163
164impl VectorParseError {
165 fn new(message: impl Into<String>) -> Self {
166 Self {
167 message: message.into(),
168 }
169 }
170}
171
172impl fmt::Display for VectorParseError {
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 f.write_str(&self.message)
175 }
176}
177
178impl Error for VectorParseError {}
179
180pub fn parse_svg_asset(svg: &str) -> Result<VectorAsset, VectorParseError> {
181 parse_svg_asset_with_color_mode(svg, false)
182}
183
184pub fn tessellate_vector_asset(asset: &VectorAsset, options: VectorMeshOptions) -> VectorMesh {
185 let mut mesh = VectorMesh::default();
186 append_vector_asset_mesh(asset, options, &mut mesh.vertices);
187 mesh
188}
189
190pub fn append_vector_asset_mesh(
191 asset: &VectorAsset,
192 options: VectorMeshOptions,
193 out: &mut Vec<VectorMeshVertex>,
194) -> VectorMeshRun {
195 let first = out.len() as u32;
196 if options.rect.w <= 0.0 || options.rect.h <= 0.0 {
197 return VectorMeshRun { first, count: 0 };
198 }
199
200 let [vx, vy, vw, vh] = asset.view_box;
201 let sx = options.rect.w / vw.max(1.0);
202 let sy = options.rect.h / vh.max(1.0);
203 let stroke_scale = (sx + sy) * 0.5;
204
205 for (path_index, vector_path) in asset.paths.iter().enumerate() {
206 let path = build_lyon_path(vector_path, options.rect, [vx, vy], [sx, sy]);
207 if let Some(fill) = vector_path.fill {
208 let color = resolve_color(fill.color, options.current_color, fill.opacity);
209 let mut geometry: VertexBuffers<VectorMeshVertex, u16> = VertexBuffers::new();
210 let fill_options =
211 FillOptions::tolerance(options.tolerance).with_fill_rule(match fill.rule {
212 VectorFillRule::NonZero => lyon_tessellation::FillRule::NonZero,
213 VectorFillRule::EvenOdd => lyon_tessellation::FillRule::EvenOdd,
214 });
215 let _ = FillTessellator::new().tessellate_path(
216 &path,
217 &fill_options,
218 &mut BuffersBuilder::new(&mut geometry, |v: FillVertex<'_>| {
219 make_mesh_vertex(
220 v.position(),
221 options.rect,
222 [vx, vy],
223 [sx, sy],
224 color,
225 path_index,
226 VectorPrimitiveKind::Fill,
227 )
228 }),
229 );
230 append_indexed(&geometry, out);
231
232 let mut fringe: VertexBuffers<VectorMeshVertex, u16> = VertexBuffers::new();
242 let fringe_options = StrokeOptions::tolerance(options.tolerance)
247 .with_line_width(1.0)
248 .with_line_cap(LineCap::Butt)
249 .with_line_join(LineJoin::Miter)
250 .with_miter_limit(4.0);
251 let _ = StrokeTessellator::new().tessellate_path(
252 &path,
253 &fringe_options,
254 &mut BuffersBuilder::new(&mut fringe, |v: StrokeVertex<'_, '_>| {
255 let position = v.position();
256 let normal = v.normal();
257 let side_sign = match v.side() {
258 lyon_tessellation::Side::Negative => -1.0_f32,
259 lyon_tessellation::Side::Positive => 1.0_f32,
260 };
261 let path_pos = lyon_tessellation::math::point(
263 position.x - side_sign * normal.x * 0.5,
264 position.y - side_sign * normal.y * 0.5,
265 );
266 let aa = match v.side() {
267 lyon_tessellation::Side::Negative => [0.0, 0.0],
268 lyon_tessellation::Side::Positive => [normal.x, normal.y],
269 };
270 make_mesh_vertex_with_aa(
271 path_pos,
272 options.rect,
273 [vx, vy],
274 [sx, sy],
275 color,
276 path_index,
277 VectorPrimitiveKind::Fill,
278 aa,
279 )
280 }),
281 );
282 append_indexed(&fringe, out);
283 }
284
285 if let Some(stroke) = vector_path.stroke {
286 let color = resolve_color(stroke.color, options.current_color, stroke.opacity);
287 let width = if matches!(stroke.color, VectorColor::CurrentColor) {
288 options.stroke_width * stroke_scale
289 } else {
290 stroke.width * stroke_scale
291 }
292 .max(0.5);
293 let mut geometry: VertexBuffers<VectorMeshVertex, u16> = VertexBuffers::new();
294 let stroke_options = StrokeOptions::tolerance(options.tolerance)
295 .with_line_width(width)
296 .with_line_cap(match stroke.line_cap {
297 VectorLineCap::Butt => LineCap::Butt,
298 VectorLineCap::Round => LineCap::Round,
299 VectorLineCap::Square => LineCap::Square,
300 })
301 .with_line_join(match stroke.line_join {
302 VectorLineJoin::Miter => LineJoin::Miter,
303 VectorLineJoin::MiterClip => LineJoin::MiterClip,
304 VectorLineJoin::Round => LineJoin::Round,
305 VectorLineJoin::Bevel => LineJoin::Bevel,
306 })
307 .with_miter_limit(stroke.miter_limit.max(1.0));
308 let _ = StrokeTessellator::new().tessellate_path(
309 &path,
310 &stroke_options,
311 &mut BuffersBuilder::new(&mut geometry, |v: StrokeVertex<'_, '_>| {
312 make_mesh_vertex(
313 v.position(),
314 options.rect,
315 [vx, vy],
316 [sx, sy],
317 color,
318 path_index,
319 VectorPrimitiveKind::Stroke,
320 )
321 }),
322 );
323 append_indexed(&geometry, out);
324 }
325 }
326
327 VectorMeshRun {
328 first,
329 count: out.len() as u32 - first,
330 }
331}
332
333pub(crate) fn parse_current_color_svg_asset(svg: &str) -> Result<VectorAsset, VectorParseError> {
334 parse_svg_asset_with_color_mode(svg, true)
335}
336
337fn parse_svg_asset_with_color_mode(
338 svg: &str,
339 force_current_color: bool,
340) -> Result<VectorAsset, VectorParseError> {
341 let tree = usvg::Tree::from_str(svg, &usvg::Options::default())
342 .map_err(|e| VectorParseError::new(format!("invalid SVG: {e}")))?;
343 let size = tree.size();
344 let mut asset = VectorAsset {
345 view_box: [0.0, 0.0, size.width(), size.height()],
346 paths: Vec::new(),
347 };
348 collect_group(tree.root(), force_current_color, &mut asset.paths);
349 if asset.paths.is_empty() {
350 return Err(VectorParseError::new("SVG produced no renderable paths"));
351 }
352 Ok(asset)
353}
354
355fn collect_group(group: &usvg::Group, force_current_color: bool, out: &mut Vec<VectorPath>) {
356 for node in group.children() {
357 match node {
358 usvg::Node::Group(group) => collect_group(group, force_current_color, out),
359 usvg::Node::Path(path) if path.is_visible() => {
360 if let Some(vector_path) = convert_path(path, force_current_color) {
361 out.push(vector_path);
362 }
363 }
364 _ => {}
365 }
366 }
367}
368
369fn convert_path(path: &usvg::Path, force_current_color: bool) -> Option<VectorPath> {
370 let transform = path.abs_transform();
371 let mut segments = Vec::new();
372 for segment in path.data().segments() {
373 match segment {
374 tiny_skia_path::PathSegment::MoveTo(p) => {
375 segments.push(VectorSegment::MoveTo(map_point(transform, p)));
376 }
377 tiny_skia_path::PathSegment::LineTo(p) => {
378 segments.push(VectorSegment::LineTo(map_point(transform, p)));
379 }
380 tiny_skia_path::PathSegment::QuadTo(p0, p1) => {
381 segments.push(VectorSegment::QuadTo(
382 map_point(transform, p0),
383 map_point(transform, p1),
384 ));
385 }
386 tiny_skia_path::PathSegment::CubicTo(p0, p1, p2) => {
387 segments.push(VectorSegment::CubicTo(
388 map_point(transform, p0),
389 map_point(transform, p1),
390 map_point(transform, p2),
391 ));
392 }
393 tiny_skia_path::PathSegment::Close => segments.push(VectorSegment::Close),
394 }
395 }
396 if segments.is_empty() {
397 return None;
398 }
399
400 Some(VectorPath {
401 segments,
402 fill: path
403 .fill()
404 .and_then(|fill| convert_fill(fill, force_current_color)),
405 stroke: path
406 .stroke()
407 .and_then(|stroke| convert_stroke(stroke, force_current_color)),
408 })
409}
410
411fn convert_fill(fill: &usvg::Fill, force_current_color: bool) -> Option<VectorFill> {
412 Some(VectorFill {
413 color: convert_paint(fill.paint(), force_current_color)?,
414 opacity: fill.opacity().get(),
415 rule: match fill.rule() {
416 usvg::FillRule::NonZero => VectorFillRule::NonZero,
417 usvg::FillRule::EvenOdd => VectorFillRule::EvenOdd,
418 },
419 })
420}
421
422fn convert_stroke(stroke: &usvg::Stroke, force_current_color: bool) -> Option<VectorStroke> {
423 Some(VectorStroke {
424 color: convert_paint(stroke.paint(), force_current_color)?,
425 opacity: stroke.opacity().get(),
426 width: stroke.width().get(),
427 line_cap: match stroke.linecap() {
428 usvg::LineCap::Butt => VectorLineCap::Butt,
429 usvg::LineCap::Round => VectorLineCap::Round,
430 usvg::LineCap::Square => VectorLineCap::Square,
431 },
432 line_join: match stroke.linejoin() {
433 usvg::LineJoin::Miter => VectorLineJoin::Miter,
434 usvg::LineJoin::MiterClip => VectorLineJoin::MiterClip,
435 usvg::LineJoin::Round => VectorLineJoin::Round,
436 usvg::LineJoin::Bevel => VectorLineJoin::Bevel,
437 },
438 miter_limit: stroke.miterlimit().get(),
439 })
440}
441
442fn convert_paint(paint: &usvg::Paint, force_current_color: bool) -> Option<VectorColor> {
443 if force_current_color {
444 return Some(VectorColor::CurrentColor);
445 }
446 match paint {
447 usvg::Paint::Color(c) => Some(VectorColor::Solid(Color::rgba(c.red, c.green, c.blue, 255))),
448 usvg::Paint::LinearGradient(_)
449 | usvg::Paint::RadialGradient(_)
450 | usvg::Paint::Pattern(_) => None,
451 }
452}
453
454fn map_point(transform: tiny_skia_path::Transform, mut point: tiny_skia_path::Point) -> [f32; 2] {
455 transform.map_point(&mut point);
456 [point.x, point.y]
457}
458
459#[derive(Clone, Copy)]
460enum VectorPrimitiveKind {
461 Fill,
462 Stroke,
463}
464
465fn build_lyon_path(
466 path: &VectorPath,
467 rect: crate::tree::Rect,
468 view_origin: [f32; 2],
469 scale: [f32; 2],
470) -> LyonPath {
471 let mut builder = LyonPath::builder();
472 let mut open = false;
473 for segment in &path.segments {
474 match *segment {
475 VectorSegment::MoveTo(p) => {
476 if open {
477 builder.end(false);
478 }
479 builder.begin(map_mesh_point(rect, view_origin, scale, p));
480 open = true;
481 }
482 VectorSegment::LineTo(p) => {
483 builder.line_to(map_mesh_point(rect, view_origin, scale, p));
484 }
485 VectorSegment::QuadTo(c, p) => {
486 builder.quadratic_bezier_to(
487 map_mesh_point(rect, view_origin, scale, c),
488 map_mesh_point(rect, view_origin, scale, p),
489 );
490 }
491 VectorSegment::CubicTo(c0, c1, p) => {
492 builder.cubic_bezier_to(
493 map_mesh_point(rect, view_origin, scale, c0),
494 map_mesh_point(rect, view_origin, scale, c1),
495 map_mesh_point(rect, view_origin, scale, p),
496 );
497 }
498 VectorSegment::Close => {
499 if open {
500 builder.close();
501 open = false;
502 }
503 }
504 }
505 }
506 if open {
507 builder.end(false);
508 }
509 builder.build()
510}
511
512fn map_mesh_point(
513 rect: crate::tree::Rect,
514 view_origin: [f32; 2],
515 scale: [f32; 2],
516 p: [f32; 2],
517) -> lyon_tessellation::math::Point {
518 point(
519 rect.x + (p[0] - view_origin[0]) * scale[0],
520 rect.y + (p[1] - view_origin[1]) * scale[1],
521 )
522}
523
524fn make_mesh_vertex(
525 p: lyon_tessellation::math::Point,
526 rect: crate::tree::Rect,
527 view_origin: [f32; 2],
528 scale: [f32; 2],
529 color: [f32; 4],
530 path_index: usize,
531 kind: VectorPrimitiveKind,
532) -> VectorMeshVertex {
533 make_mesh_vertex_with_aa(
534 p,
535 rect,
536 view_origin,
537 scale,
538 color,
539 path_index,
540 kind,
541 [0.0, 0.0],
542 )
543}
544
545#[allow(clippy::too_many_arguments)]
546fn make_mesh_vertex_with_aa(
547 p: lyon_tessellation::math::Point,
548 rect: crate::tree::Rect,
549 view_origin: [f32; 2],
550 scale: [f32; 2],
551 color: [f32; 4],
552 path_index: usize,
553 kind: VectorPrimitiveKind,
554 aa: [f32; 2],
555) -> VectorMeshVertex {
556 let local = [
557 view_origin[0] + (p.x - rect.x) / scale[0].max(f32::EPSILON),
558 view_origin[1] + (p.y - rect.y) / scale[1].max(f32::EPSILON),
559 ];
560 VectorMeshVertex {
561 pos: [p.x, p.y],
562 local,
563 color,
564 meta: [
565 path_index as f32,
566 match kind {
567 VectorPrimitiveKind::Fill => 0.0,
568 VectorPrimitiveKind::Stroke => 1.0,
569 },
570 0.0,
571 0.0,
572 ],
573 aa,
574 }
575}
576
577fn resolve_color(color: VectorColor, current_color: Color, opacity: f32) -> [f32; 4] {
578 let mut rgba = match color {
579 VectorColor::CurrentColor => rgba_f32(current_color),
580 VectorColor::Solid(color) => rgba_f32(color),
581 };
582 rgba[3] *= opacity.clamp(0.0, 1.0);
583 rgba
584}
585
586fn append_indexed(
587 geometry: &VertexBuffers<VectorMeshVertex, u16>,
588 out: &mut Vec<VectorMeshVertex>,
589) {
590 for index in &geometry.indices {
591 if let Some(vertex) = geometry.vertices.get(*index as usize) {
592 out.push(*vertex);
593 }
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600 use crate::icons::{all_icon_names, icon_vector_asset};
601
602 #[test]
603 fn parses_basic_svg_shapes_into_paths() {
604 let asset = parse_svg_asset(
605 r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4" fill="none" stroke="#000" stroke-width="2"/></svg>"##,
606 )
607 .unwrap();
608 assert_eq!(asset.view_box, [0.0, 0.0, 24.0, 24.0]);
609 assert_eq!(asset.paths.len(), 1);
610 assert!(asset.paths[0].stroke.is_some());
611 assert!(asset.paths[0].segments.len() > 4);
612 }
613
614 #[test]
615 fn tessellates_every_builtin_icon() {
616 for name in all_icon_names() {
617 let mesh = tessellate_vector_asset(
618 icon_vector_asset(*name),
619 VectorMeshOptions::icon(
620 crate::tree::Rect::new(0.0, 0.0, 16.0, 16.0),
621 Color::rgb(15, 23, 42),
622 2.0,
623 ),
624 );
625 assert!(
626 !mesh.vertices.is_empty(),
627 "{} produced no tessellated vertices",
628 name.name()
629 );
630 }
631 }
632}