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 pub gradients: Vec<VectorGradient>,
32}
33
34#[derive(Clone, Copy, Debug, Default, PartialEq)]
41pub enum VectorRenderMode {
42 #[default]
43 Painted,
44 Mask {
45 color: Color,
46 },
47}
48
49impl VectorRenderMode {
50 pub fn resolved_palette(self, palette: &crate::palette::Palette) -> Self {
51 match self {
52 Self::Painted => Self::Painted,
53 Self::Mask { color } => Self::Mask {
54 color: palette.resolve(color),
55 },
56 }
57 }
58}
59
60impl VectorAsset {
61 pub fn from_paths(view_box: [f32; 4], paths: Vec<VectorPath>) -> Self {
69 Self {
70 view_box,
71 paths,
72 gradients: Vec::new(),
73 }
74 }
75
76 pub fn has_gradient(&self) -> bool {
78 self.paths.iter().any(|p| {
79 p.fill
80 .map(|f| matches!(f.color, VectorColor::Gradient(_)))
81 .unwrap_or(false)
82 || p.stroke
83 .map(|s| matches!(s.color, VectorColor::Gradient(_)))
84 .unwrap_or(false)
85 })
86 }
87
88 pub fn resolved_palette(&self, palette: &crate::palette::Palette) -> Self {
93 let mut out = self.clone();
94 for path in &mut out.paths {
95 if let Some(fill) = &mut path.fill {
96 fill.color = resolve_vector_color(fill.color, palette);
97 }
98 if let Some(stroke) = &mut path.stroke {
99 stroke.color = resolve_vector_color(stroke.color, palette);
100 }
101 }
102 out
103 }
104
105 pub fn content_hash(&self) -> u64 {
116 use std::hash::Hasher;
117 let mut h = StableHasher::new();
118 hash_view_box(&mut h, self.view_box);
119 write_len(&mut h, self.paths.len());
120 for path in &self.paths {
121 hash_path(&mut h, path);
122 }
123 write_len(&mut h, self.gradients.len());
124 for grad in &self.gradients {
125 hash_gradient(&mut h, grad);
126 }
127 h.finish()
128 }
129}
130
131fn resolve_vector_color(color: VectorColor, palette: &crate::palette::Palette) -> VectorColor {
132 match color {
133 VectorColor::Solid(c) => VectorColor::Solid(palette.resolve(c)),
134 VectorColor::CurrentColor | VectorColor::Gradient(_) => color,
135 }
136}
137
138struct StableHasher {
143 state: u64,
144}
145
146impl StableHasher {
147 const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
148 const PRIME: u64 = 0x0000_0100_0000_01b3;
149
150 fn new() -> Self {
151 Self {
152 state: Self::OFFSET,
153 }
154 }
155}
156
157impl std::hash::Hasher for StableHasher {
158 fn write(&mut self, bytes: &[u8]) {
159 for byte in bytes {
160 self.state ^= *byte as u64;
161 self.state = self.state.wrapping_mul(Self::PRIME);
162 }
163 }
164
165 fn finish(&self) -> u64 {
166 self.state
167 }
168}
169
170fn write_len(h: &mut impl std::hash::Hasher, len: usize) {
171 h.write_u64(len as u64);
172}
173
174fn hash_str(h: &mut impl std::hash::Hasher, value: &str) {
175 write_len(h, value.len());
176 h.write(value.as_bytes());
177}
178
179fn hash_view_box(h: &mut impl std::hash::Hasher, vb: [f32; 4]) {
180 for v in vb {
181 h.write_u32(v.to_bits());
182 }
183}
184
185fn hash_path(h: &mut impl std::hash::Hasher, path: &VectorPath) {
186 write_len(h, path.segments.len());
187 for seg in &path.segments {
188 hash_segment(h, seg);
189 }
190 match path.fill {
191 Some(f) => {
192 h.write_u8(1);
193 hash_fill(h, f);
194 }
195 None => h.write_u8(0),
196 }
197 match path.stroke {
198 Some(s) => {
199 h.write_u8(1);
200 hash_stroke(h, s);
201 }
202 None => h.write_u8(0),
203 }
204}
205
206fn hash_segment(h: &mut impl std::hash::Hasher, seg: &VectorSegment) {
207 match *seg {
208 VectorSegment::MoveTo(p) => {
209 h.write_u8(0);
210 hash_pt(h, p);
211 }
212 VectorSegment::LineTo(p) => {
213 h.write_u8(1);
214 hash_pt(h, p);
215 }
216 VectorSegment::QuadTo(c, p) => {
217 h.write_u8(2);
218 hash_pt(h, c);
219 hash_pt(h, p);
220 }
221 VectorSegment::CubicTo(c1, c2, p) => {
222 h.write_u8(3);
223 hash_pt(h, c1);
224 hash_pt(h, c2);
225 hash_pt(h, p);
226 }
227 VectorSegment::Close => h.write_u8(4),
228 }
229}
230
231fn hash_pt(h: &mut impl std::hash::Hasher, p: [f32; 2]) {
232 h.write_u32(p[0].to_bits());
233 h.write_u32(p[1].to_bits());
234}
235
236fn hash_fill(h: &mut impl std::hash::Hasher, f: VectorFill) {
237 hash_color(h, f.color);
238 h.write_u32(f.opacity.to_bits());
239 h.write_u8(match f.rule {
240 VectorFillRule::NonZero => 0,
241 VectorFillRule::EvenOdd => 1,
242 });
243}
244
245fn hash_stroke(h: &mut impl std::hash::Hasher, s: VectorStroke) {
246 hash_color(h, s.color);
247 h.write_u32(s.opacity.to_bits());
248 h.write_u32(s.width.to_bits());
249 h.write_u8(match s.line_cap {
250 VectorLineCap::Butt => 0,
251 VectorLineCap::Round => 1,
252 VectorLineCap::Square => 2,
253 });
254 h.write_u8(match s.line_join {
255 VectorLineJoin::Miter => 0,
256 VectorLineJoin::MiterClip => 1,
257 VectorLineJoin::Round => 2,
258 VectorLineJoin::Bevel => 3,
259 });
260 h.write_u32(s.miter_limit.to_bits());
261}
262
263fn hash_color(h: &mut impl std::hash::Hasher, c: VectorColor) {
264 match c {
265 VectorColor::CurrentColor => h.write_u8(0),
266 VectorColor::Solid(col) => {
267 h.write_u8(1);
268 h.write_u8(col.r);
269 h.write_u8(col.g);
270 h.write_u8(col.b);
271 h.write_u8(col.a);
272 match col.token {
278 Some(name) => {
279 h.write_u8(1);
280 hash_str(h, name);
281 }
282 None => h.write_u8(0),
283 }
284 }
285 VectorColor::Gradient(idx) => {
286 h.write_u8(2);
287 h.write_u32(idx);
288 }
289 }
290}
291
292fn hash_gradient(h: &mut impl std::hash::Hasher, g: &VectorGradient) {
293 match g {
294 VectorGradient::Linear(lin) => {
295 h.write_u8(0);
296 hash_pt(h, lin.p1);
297 hash_pt(h, lin.p2);
298 hash_stops(h, &lin.stops);
299 hash_spread(h, lin.spread);
300 for v in lin.absolute_to_local {
301 h.write_u32(v.to_bits());
302 }
303 }
304 VectorGradient::Radial(rad) => {
305 h.write_u8(1);
306 hash_pt(h, rad.center);
307 h.write_u32(rad.radius.to_bits());
308 hash_pt(h, rad.focal);
309 h.write_u32(rad.focal_radius.to_bits());
310 hash_stops(h, &rad.stops);
311 hash_spread(h, rad.spread);
312 for v in rad.absolute_to_local {
313 h.write_u32(v.to_bits());
314 }
315 }
316 }
317}
318
319fn hash_stops(h: &mut impl std::hash::Hasher, stops: &[VectorGradientStop]) {
320 write_len(h, stops.len());
321 for stop in stops {
322 h.write_u32(stop.offset.to_bits());
323 for c in stop.color {
324 h.write_u32(c.to_bits());
325 }
326 }
327}
328
329fn hash_spread(h: &mut impl std::hash::Hasher, s: VectorSpreadMethod) {
330 h.write_u8(match s {
331 VectorSpreadMethod::Pad => 0,
332 VectorSpreadMethod::Reflect => 1,
333 VectorSpreadMethod::Repeat => 2,
334 });
335}
336
337#[derive(Clone, Debug)]
360pub struct PathBuilder {
361 segments: Vec<VectorSegment>,
362 fill: Option<VectorFill>,
363 stroke: Option<VectorStroke>,
364}
365
366impl Default for PathBuilder {
367 fn default() -> Self {
368 Self::new()
369 }
370}
371
372impl PathBuilder {
373 pub fn new() -> Self {
374 Self {
375 segments: Vec::new(),
376 fill: None,
377 stroke: None,
378 }
379 }
380
381 pub fn move_to(mut self, x: f32, y: f32) -> Self {
383 self.segments.push(VectorSegment::MoveTo([x, y]));
384 self
385 }
386
387 pub fn line_to(mut self, x: f32, y: f32) -> Self {
389 self.segments.push(VectorSegment::LineTo([x, y]));
390 self
391 }
392
393 pub fn quad_to(mut self, cx: f32, cy: f32, x: f32, y: f32) -> Self {
395 self.segments.push(VectorSegment::QuadTo([cx, cy], [x, y]));
396 self
397 }
398
399 pub fn cubic_to(mut self, c1x: f32, c1y: f32, c2x: f32, c2y: f32, x: f32, y: f32) -> Self {
401 self.segments
402 .push(VectorSegment::CubicTo([c1x, c1y], [c2x, c2y], [x, y]));
403 self
404 }
405
406 pub fn close(mut self) -> Self {
408 self.segments.push(VectorSegment::Close);
409 self
410 }
411
412 pub fn fill_solid(mut self, color: crate::tree::Color) -> Self {
415 self.fill = Some(VectorFill {
416 color: VectorColor::Solid(color),
417 opacity: 1.0,
418 rule: VectorFillRule::NonZero,
419 });
420 self
421 }
422
423 pub fn fill(mut self, fill: Option<VectorFill>) -> Self {
425 self.fill = fill;
426 self
427 }
428
429 pub fn stroke_solid(mut self, color: crate::tree::Color, width: f32) -> Self {
434 self.stroke = Some(VectorStroke {
435 color: VectorColor::Solid(color),
436 opacity: 1.0,
437 width,
438 line_cap: VectorLineCap::Butt,
439 line_join: VectorLineJoin::Miter,
440 miter_limit: 4.0,
441 });
442 self
443 }
444
445 pub fn stroke(mut self, stroke: Option<VectorStroke>) -> Self {
447 self.stroke = stroke;
448 self
449 }
450
451 pub fn stroke_line_cap(mut self, cap: VectorLineCap) -> Self {
452 if let Some(s) = self.stroke.as_mut() {
453 s.line_cap = cap;
454 }
455 self
456 }
457
458 pub fn stroke_line_join(mut self, join: VectorLineJoin) -> Self {
459 if let Some(s) = self.stroke.as_mut() {
460 s.line_join = join;
461 }
462 self
463 }
464
465 pub fn stroke_miter_limit(mut self, limit: f32) -> Self {
466 if let Some(s) = self.stroke.as_mut() {
467 s.miter_limit = limit;
468 }
469 self
470 }
471
472 pub fn stroke_opacity(mut self, opacity: f32) -> Self {
473 if let Some(s) = self.stroke.as_mut() {
474 s.opacity = opacity;
475 }
476 self
477 }
478
479 pub fn build(self) -> VectorPath {
480 VectorPath {
481 segments: self.segments,
482 fill: self.fill,
483 stroke: self.stroke,
484 }
485 }
486}
487
488#[derive(Clone, Debug, PartialEq)]
489pub struct VectorPath {
490 pub segments: Vec<VectorSegment>,
491 pub fill: Option<VectorFill>,
492 pub stroke: Option<VectorStroke>,
493}
494
495#[derive(Clone, Copy, Debug, PartialEq)]
496pub enum VectorSegment {
497 MoveTo([f32; 2]),
498 LineTo([f32; 2]),
499 QuadTo([f32; 2], [f32; 2]),
500 CubicTo([f32; 2], [f32; 2], [f32; 2]),
501 Close,
502}
503
504#[derive(Clone, Copy, Debug, PartialEq)]
505pub struct VectorFill {
506 pub color: VectorColor,
507 pub opacity: f32,
508 pub rule: VectorFillRule,
509}
510
511#[derive(Clone, Copy, Debug, PartialEq)]
512pub struct VectorStroke {
513 pub color: VectorColor,
514 pub opacity: f32,
515 pub width: f32,
516 pub line_cap: VectorLineCap,
517 pub line_join: VectorLineJoin,
518 pub miter_limit: f32,
519}
520
521#[derive(Clone, Copy, Debug, PartialEq)]
522pub enum VectorColor {
523 CurrentColor,
524 Solid(Color),
525 Gradient(u32),
527}
528
529#[derive(Clone, Debug, PartialEq)]
534pub enum VectorGradient {
535 Linear(VectorLinearGradient),
536 Radial(VectorRadialGradient),
537}
538
539#[derive(Clone, Debug, PartialEq)]
540pub struct VectorLinearGradient {
541 pub p1: [f32; 2],
542 pub p2: [f32; 2],
543 pub stops: Vec<VectorGradientStop>,
544 pub spread: VectorSpreadMethod,
545 pub absolute_to_local: [f32; 6],
548}
549
550#[derive(Clone, Debug, PartialEq)]
551pub struct VectorRadialGradient {
552 pub center: [f32; 2],
553 pub radius: f32,
554 pub focal: [f32; 2],
555 pub focal_radius: f32,
556 pub stops: Vec<VectorGradientStop>,
557 pub spread: VectorSpreadMethod,
558 pub absolute_to_local: [f32; 6],
559}
560
561#[derive(Clone, Copy, Debug, PartialEq)]
565pub struct VectorGradientStop {
566 pub offset: f32,
567 pub color: [f32; 4],
568}
569
570#[derive(Clone, Copy, Debug, PartialEq, Eq)]
571pub enum VectorSpreadMethod {
572 Pad,
573 Reflect,
574 Repeat,
575}
576
577#[derive(Clone, Copy, Debug, PartialEq, Eq)]
578pub enum VectorFillRule {
579 NonZero,
580 EvenOdd,
581}
582
583#[derive(Clone, Copy, Debug, PartialEq, Eq)]
584pub enum VectorLineCap {
585 Butt,
586 Round,
587 Square,
588}
589
590#[derive(Clone, Copy, Debug, PartialEq, Eq)]
591pub enum VectorLineJoin {
592 Miter,
593 MiterClip,
594 Round,
595 Bevel,
596}
597
598#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
599pub enum IconMaterial {
600 #[default]
603 Flat,
604 Relief,
609 Glass,
612}
613
614#[repr(C)]
615#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
616pub struct VectorMeshVertex {
617 pub pos: [f32; 2],
620 pub local: [f32; 2],
623 pub color: [f32; 4],
624 pub meta: [f32; 4],
627}
628
629#[derive(Clone, Debug, Default, PartialEq)]
630pub struct VectorMesh {
631 pub vertices: Vec<VectorMeshVertex>,
632}
633
634#[derive(Clone, Copy, Debug, PartialEq)]
635pub struct VectorMeshRun {
636 pub first: u32,
637 pub count: u32,
638}
639
640#[derive(Clone, Copy, Debug, PartialEq)]
641pub struct VectorMeshOptions {
642 pub rect: crate::tree::Rect,
643 pub current_color: Color,
644 pub stroke_width: f32,
645 pub tolerance: f32,
646}
647
648impl VectorMeshOptions {
649 pub fn icon(rect: crate::tree::Rect, current_color: Color, stroke_width: f32) -> Self {
650 Self {
651 rect,
652 current_color,
653 stroke_width,
654 tolerance: 0.05,
655 }
656 }
657}
658
659#[derive(Clone, Debug, PartialEq, Eq)]
660pub struct VectorParseError {
661 message: String,
662}
663
664impl VectorParseError {
665 fn new(message: impl Into<String>) -> Self {
666 Self {
667 message: message.into(),
668 }
669 }
670}
671
672impl fmt::Display for VectorParseError {
673 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
674 f.write_str(&self.message)
675 }
676}
677
678impl Error for VectorParseError {}
679
680pub fn parse_svg_asset(svg: &str) -> Result<VectorAsset, VectorParseError> {
681 parse_svg_asset_with_color_mode(svg, false)
682}
683
684pub fn tessellate_vector_asset(asset: &VectorAsset, options: VectorMeshOptions) -> VectorMesh {
685 let mut mesh = VectorMesh::default();
686 append_vector_asset_mesh(asset, options, &mut mesh.vertices);
687 mesh
688}
689
690pub fn append_vector_asset_mesh(
691 asset: &VectorAsset,
692 options: VectorMeshOptions,
693 out: &mut Vec<VectorMeshVertex>,
694) -> VectorMeshRun {
695 let first = out.len() as u32;
696 if options.rect.w <= 0.0 || options.rect.h <= 0.0 {
697 return VectorMeshRun { first, count: 0 };
698 }
699
700 let [vx, vy, vw, vh] = asset.view_box;
701 let sx = options.rect.w / vw.max(1.0);
702 let sy = options.rect.h / vh.max(1.0);
703 let stroke_scale = (sx + sy) * 0.5;
704
705 for (path_index, vector_path) in asset.paths.iter().enumerate() {
706 let path = build_lyon_path(vector_path, options.rect, [vx, vy], [sx, sy]);
707 if let Some(fill) = vector_path.fill {
708 let sampler = ColorSampler::build(
709 fill.color,
710 fill.opacity,
711 options.current_color,
712 &asset.gradients,
713 );
714 let mut geometry: VertexBuffers<VectorMeshVertex, u16> = VertexBuffers::new();
715 let fill_options =
716 FillOptions::tolerance(options.tolerance).with_fill_rule(match fill.rule {
717 VectorFillRule::NonZero => lyon_tessellation::FillRule::NonZero,
718 VectorFillRule::EvenOdd => lyon_tessellation::FillRule::EvenOdd,
719 });
720 let _ = FillTessellator::new().tessellate_path(
721 &path,
722 &fill_options,
723 &mut BuffersBuilder::new(&mut geometry, |v: FillVertex<'_>| {
724 make_mesh_vertex_sampled(
725 v.position(),
726 options.rect,
727 [vx, vy],
728 [sx, sy],
729 &sampler,
730 path_index,
731 VectorPrimitiveKind::Fill,
732 )
733 }),
734 );
735 append_indexed(&geometry, out);
736 }
737
738 if let Some(stroke) = vector_path.stroke {
739 let sampler = ColorSampler::build(
740 stroke.color,
741 stroke.opacity,
742 options.current_color,
743 &asset.gradients,
744 );
745 let width = if matches!(stroke.color, VectorColor::CurrentColor) {
746 options.stroke_width * stroke_scale
747 } else {
748 stroke.width * stroke_scale
749 }
750 .max(0.5);
751 let mut geometry: VertexBuffers<VectorMeshVertex, u16> = VertexBuffers::new();
752 let stroke_options = StrokeOptions::tolerance(options.tolerance)
753 .with_line_width(width)
754 .with_line_cap(match stroke.line_cap {
755 VectorLineCap::Butt => LineCap::Butt,
756 VectorLineCap::Round => LineCap::Round,
757 VectorLineCap::Square => LineCap::Square,
758 })
759 .with_line_join(match stroke.line_join {
760 VectorLineJoin::Miter => LineJoin::Miter,
761 VectorLineJoin::MiterClip => LineJoin::MiterClip,
762 VectorLineJoin::Round => LineJoin::Round,
763 VectorLineJoin::Bevel => LineJoin::Bevel,
764 })
765 .with_miter_limit(stroke.miter_limit.max(1.0));
766 let _ = StrokeTessellator::new().tessellate_path(
767 &path,
768 &stroke_options,
769 &mut BuffersBuilder::new(&mut geometry, |v: StrokeVertex<'_, '_>| {
770 make_mesh_vertex_sampled(
771 v.position(),
772 options.rect,
773 [vx, vy],
774 [sx, sy],
775 &sampler,
776 path_index,
777 VectorPrimitiveKind::Stroke,
778 )
779 }),
780 );
781 append_indexed(&geometry, out);
782 }
783 }
784
785 VectorMeshRun {
786 first,
787 count: out.len() as u32 - first,
788 }
789}
790
791pub(crate) fn parse_current_color_svg_asset(svg: &str) -> Result<VectorAsset, VectorParseError> {
792 parse_svg_asset_with_color_mode(svg, true)
793}
794
795fn parse_svg_asset_with_color_mode(
796 svg: &str,
797 force_current_color: bool,
798) -> Result<VectorAsset, VectorParseError> {
799 let tree = usvg::Tree::from_str(svg, &usvg::Options::default())
800 .map_err(|e| VectorParseError::new(format!("invalid SVG: {e}")))?;
801 let size = tree.size();
802 let mut asset = VectorAsset {
803 view_box: [0.0, 0.0, size.width(), size.height()],
804 paths: Vec::new(),
805 gradients: Vec::new(),
806 };
807 collect_group(
808 tree.root(),
809 force_current_color,
810 &mut asset.paths,
811 &mut asset.gradients,
812 );
813 if asset.paths.is_empty() {
814 return Err(VectorParseError::new("SVG produced no renderable paths"));
815 }
816 Ok(asset)
817}
818
819fn collect_group(
820 group: &usvg::Group,
821 force_current_color: bool,
822 out: &mut Vec<VectorPath>,
823 gradients: &mut Vec<VectorGradient>,
824) {
825 for node in group.children() {
826 match node {
827 usvg::Node::Group(group) => collect_group(group, force_current_color, out, gradients),
828 usvg::Node::Path(path) if path.is_visible() => {
829 if let Some(vector_path) = convert_path(path, force_current_color, gradients) {
830 out.push(vector_path);
831 }
832 }
833 _ => {}
834 }
835 }
836}
837
838fn convert_path(
839 path: &usvg::Path,
840 force_current_color: bool,
841 gradients: &mut Vec<VectorGradient>,
842) -> Option<VectorPath> {
843 let transform = path.abs_transform();
844 let mut segments = Vec::new();
845 for segment in path.data().segments() {
846 match segment {
847 tiny_skia_path::PathSegment::MoveTo(p) => {
848 segments.push(VectorSegment::MoveTo(map_point(transform, p)));
849 }
850 tiny_skia_path::PathSegment::LineTo(p) => {
851 segments.push(VectorSegment::LineTo(map_point(transform, p)));
852 }
853 tiny_skia_path::PathSegment::QuadTo(p0, p1) => {
854 segments.push(VectorSegment::QuadTo(
855 map_point(transform, p0),
856 map_point(transform, p1),
857 ));
858 }
859 tiny_skia_path::PathSegment::CubicTo(p0, p1, p2) => {
860 segments.push(VectorSegment::CubicTo(
861 map_point(transform, p0),
862 map_point(transform, p1),
863 map_point(transform, p2),
864 ));
865 }
866 tiny_skia_path::PathSegment::Close => segments.push(VectorSegment::Close),
867 }
868 }
869 if segments.is_empty() {
870 return None;
871 }
872
873 Some(VectorPath {
874 segments,
875 fill: path
876 .fill()
877 .and_then(|fill| convert_fill(fill, transform, force_current_color, gradients)),
878 stroke: path
879 .stroke()
880 .and_then(|stroke| convert_stroke(stroke, transform, force_current_color, gradients)),
881 })
882}
883
884fn convert_fill(
885 fill: &usvg::Fill,
886 abs_transform: tiny_skia_path::Transform,
887 force_current_color: bool,
888 gradients: &mut Vec<VectorGradient>,
889) -> Option<VectorFill> {
890 Some(VectorFill {
891 color: convert_paint(fill.paint(), abs_transform, force_current_color, gradients)?,
892 opacity: fill.opacity().get(),
893 rule: match fill.rule() {
894 usvg::FillRule::NonZero => VectorFillRule::NonZero,
895 usvg::FillRule::EvenOdd => VectorFillRule::EvenOdd,
896 },
897 })
898}
899
900fn convert_stroke(
901 stroke: &usvg::Stroke,
902 abs_transform: tiny_skia_path::Transform,
903 force_current_color: bool,
904 gradients: &mut Vec<VectorGradient>,
905) -> Option<VectorStroke> {
906 Some(VectorStroke {
907 color: convert_paint(
908 stroke.paint(),
909 abs_transform,
910 force_current_color,
911 gradients,
912 )?,
913 opacity: stroke.opacity().get(),
914 width: stroke.width().get(),
915 line_cap: match stroke.linecap() {
916 usvg::LineCap::Butt => VectorLineCap::Butt,
917 usvg::LineCap::Round => VectorLineCap::Round,
918 usvg::LineCap::Square => VectorLineCap::Square,
919 },
920 line_join: match stroke.linejoin() {
921 usvg::LineJoin::Miter => VectorLineJoin::Miter,
922 usvg::LineJoin::MiterClip => VectorLineJoin::MiterClip,
923 usvg::LineJoin::Round => VectorLineJoin::Round,
924 usvg::LineJoin::Bevel => VectorLineJoin::Bevel,
925 },
926 miter_limit: stroke.miterlimit().get(),
927 })
928}
929
930fn convert_paint(
931 paint: &usvg::Paint,
932 abs_transform: tiny_skia_path::Transform,
933 force_current_color: bool,
934 gradients: &mut Vec<VectorGradient>,
935) -> Option<VectorColor> {
936 if force_current_color {
937 return Some(VectorColor::CurrentColor);
938 }
939 match paint {
940 usvg::Paint::Color(c) => Some(VectorColor::Solid(Color::rgba(c.red, c.green, c.blue, 255))),
941 usvg::Paint::LinearGradient(lg) => {
942 let g = convert_linear_gradient(lg, abs_transform)?;
943 let idx = gradients.len() as u32;
944 gradients.push(VectorGradient::Linear(g));
945 Some(VectorColor::Gradient(idx))
946 }
947 usvg::Paint::RadialGradient(rg) => {
948 let g = convert_radial_gradient(rg, abs_transform)?;
949 let idx = gradients.len() as u32;
950 gradients.push(VectorGradient::Radial(g));
951 Some(VectorColor::Gradient(idx))
952 }
953 usvg::Paint::Pattern(_) => None,
954 }
955}
956
957fn convert_linear_gradient(
958 lg: &usvg::LinearGradient,
959 abs_transform: tiny_skia_path::Transform,
960) -> Option<VectorLinearGradient> {
961 let stops = convert_stops(lg.stops());
962 if stops.is_empty() {
963 return None;
964 }
965 let absolute_to_local = build_absolute_to_local(abs_transform, lg.transform())?;
966 Some(VectorLinearGradient {
967 p1: [lg.x1(), lg.y1()],
968 p2: [lg.x2(), lg.y2()],
969 stops,
970 spread: convert_spread(lg.spread_method()),
971 absolute_to_local,
972 })
973}
974
975fn convert_radial_gradient(
976 rg: &usvg::RadialGradient,
977 abs_transform: tiny_skia_path::Transform,
978) -> Option<VectorRadialGradient> {
979 let stops = convert_stops(rg.stops());
980 if stops.is_empty() {
981 return None;
982 }
983 let absolute_to_local = build_absolute_to_local(abs_transform, rg.transform())?;
984 Some(VectorRadialGradient {
985 center: [rg.cx(), rg.cy()],
986 radius: rg.r().get(),
987 focal: [rg.fx(), rg.fy()],
988 focal_radius: rg.fr().get(),
989 stops,
990 spread: convert_spread(rg.spread_method()),
991 absolute_to_local,
992 })
993}
994
995fn convert_stops(stops: &[usvg::Stop]) -> Vec<VectorGradientStop> {
996 let mut out = Vec::with_capacity(stops.len());
997 let mut last_offset = 0.0_f32;
998 for stop in stops {
999 let offset = stop.offset().get().max(last_offset);
1002 last_offset = offset;
1003 let mut rgba = rgba_f32(Color::rgba(
1004 stop.color().red,
1005 stop.color().green,
1006 stop.color().blue,
1007 255,
1008 ));
1009 rgba[3] *= stop.opacity().get();
1010 out.push(VectorGradientStop {
1011 offset,
1012 color: rgba,
1013 });
1014 }
1015 out
1016}
1017
1018fn convert_spread(method: usvg::SpreadMethod) -> VectorSpreadMethod {
1019 match method {
1020 usvg::SpreadMethod::Pad => VectorSpreadMethod::Pad,
1021 usvg::SpreadMethod::Reflect => VectorSpreadMethod::Reflect,
1022 usvg::SpreadMethod::Repeat => VectorSpreadMethod::Repeat,
1023 }
1024}
1025
1026fn build_absolute_to_local(
1034 abs_transform: tiny_skia_path::Transform,
1035 gradient_transform: tiny_skia_path::Transform,
1036) -> Option<[f32; 6]> {
1037 let local_to_absolute = abs_transform.pre_concat(gradient_transform);
1038 let inv = local_to_absolute.invert()?;
1039 Some([inv.sx, inv.kx, inv.tx, inv.ky, inv.sy, inv.ty])
1040}
1041
1042fn map_point(transform: tiny_skia_path::Transform, mut point: tiny_skia_path::Point) -> [f32; 2] {
1043 transform.map_point(&mut point);
1044 [point.x, point.y]
1045}
1046
1047#[derive(Clone, Copy)]
1048enum VectorPrimitiveKind {
1049 Fill,
1050 Stroke,
1051}
1052
1053fn build_lyon_path(
1054 path: &VectorPath,
1055 rect: crate::tree::Rect,
1056 view_origin: [f32; 2],
1057 scale: [f32; 2],
1058) -> LyonPath {
1059 let mut builder = LyonPath::builder();
1060 let mut open = false;
1061 for segment in &path.segments {
1062 match *segment {
1063 VectorSegment::MoveTo(p) => {
1064 if open {
1065 builder.end(false);
1066 }
1067 builder.begin(map_mesh_point(rect, view_origin, scale, p));
1068 open = true;
1069 }
1070 VectorSegment::LineTo(p) => {
1071 builder.line_to(map_mesh_point(rect, view_origin, scale, p));
1072 }
1073 VectorSegment::QuadTo(c, p) => {
1074 builder.quadratic_bezier_to(
1075 map_mesh_point(rect, view_origin, scale, c),
1076 map_mesh_point(rect, view_origin, scale, p),
1077 );
1078 }
1079 VectorSegment::CubicTo(c0, c1, p) => {
1080 builder.cubic_bezier_to(
1081 map_mesh_point(rect, view_origin, scale, c0),
1082 map_mesh_point(rect, view_origin, scale, c1),
1083 map_mesh_point(rect, view_origin, scale, p),
1084 );
1085 }
1086 VectorSegment::Close => {
1087 if open {
1088 builder.close();
1089 open = false;
1090 }
1091 }
1092 }
1093 }
1094 if open {
1095 builder.end(false);
1096 }
1097 builder.build()
1098}
1099
1100fn map_mesh_point(
1101 rect: crate::tree::Rect,
1102 view_origin: [f32; 2],
1103 scale: [f32; 2],
1104 p: [f32; 2],
1105) -> lyon_tessellation::math::Point {
1106 point(
1107 rect.x + (p[0] - view_origin[0]) * scale[0],
1108 rect.y + (p[1] - view_origin[1]) * scale[1],
1109 )
1110}
1111
1112fn make_mesh_vertex_sampled(
1113 p: lyon_tessellation::math::Point,
1114 rect: crate::tree::Rect,
1115 view_origin: [f32; 2],
1116 scale: [f32; 2],
1117 sampler: &ColorSampler<'_>,
1118 path_index: usize,
1119 kind: VectorPrimitiveKind,
1120) -> VectorMeshVertex {
1121 let local = [
1122 view_origin[0] + (p.x - rect.x) / scale[0].max(f32::EPSILON),
1123 view_origin[1] + (p.y - rect.y) / scale[1].max(f32::EPSILON),
1124 ];
1125 VectorMeshVertex {
1126 pos: [p.x, p.y],
1127 local,
1128 color: sampler.sample(local),
1129 meta: [
1130 path_index as f32,
1131 match kind {
1132 VectorPrimitiveKind::Fill => 0.0,
1133 VectorPrimitiveKind::Stroke => 1.0,
1134 },
1135 0.0,
1136 0.0,
1137 ],
1138 }
1139}
1140
1141enum ColorSampler<'a> {
1145 Solid([f32; 4]),
1146 Gradient {
1147 gradient: &'a VectorGradient,
1148 opacity: f32,
1149 },
1150}
1151
1152impl<'a> ColorSampler<'a> {
1153 fn build(
1154 color: VectorColor,
1155 opacity: f32,
1156 current_color: Color,
1157 gradients: &'a [VectorGradient],
1158 ) -> Self {
1159 let opacity = opacity.clamp(0.0, 1.0);
1160 match color {
1161 VectorColor::CurrentColor => {
1162 let mut c = rgba_f32(current_color);
1163 c[3] *= opacity;
1164 Self::Solid(c)
1165 }
1166 VectorColor::Solid(c) => {
1167 let mut rgba = rgba_f32(c);
1168 rgba[3] *= opacity;
1169 Self::Solid(rgba)
1170 }
1171 VectorColor::Gradient(idx) => match gradients.get(idx as usize) {
1172 Some(gradient) => Self::Gradient { gradient, opacity },
1173 None => Self::Solid([0.0; 4]),
1176 },
1177 }
1178 }
1179
1180 fn sample(&self, abs_local: [f32; 2]) -> [f32; 4] {
1181 match self {
1182 Self::Solid(c) => *c,
1183 Self::Gradient { gradient, opacity } => {
1184 let mut c = sample_gradient(gradient, abs_local);
1185 c[3] *= *opacity;
1186 c
1187 }
1188 }
1189 }
1190}
1191
1192fn sample_gradient(gradient: &VectorGradient, abs_local: [f32; 2]) -> [f32; 4] {
1193 match gradient {
1194 VectorGradient::Linear(g) => {
1195 let local = apply_affine(&g.absolute_to_local, abs_local);
1196 let dx = g.p2[0] - g.p1[0];
1197 let dy = g.p2[1] - g.p1[1];
1198 let len2 = (dx * dx + dy * dy).max(f32::EPSILON);
1199 let t = ((local[0] - g.p1[0]) * dx + (local[1] - g.p1[1]) * dy) / len2;
1200 sample_stops(&g.stops, apply_spread(t, g.spread))
1201 }
1202 VectorGradient::Radial(g) => {
1203 let local = apply_affine(&g.absolute_to_local, abs_local);
1208 let dx = local[0] - g.center[0];
1209 let dy = local[1] - g.center[1];
1210 let radius = g.radius.max(f32::EPSILON);
1211 let t = (dx * dx + dy * dy).sqrt() / radius;
1212 sample_stops(&g.stops, apply_spread(t, g.spread))
1213 }
1214 }
1215}
1216
1217fn apply_affine(m: &[f32; 6], p: [f32; 2]) -> [f32; 2] {
1218 [
1219 p[0] * m[0] + p[1] * m[1] + m[2],
1220 p[0] * m[3] + p[1] * m[4] + m[5],
1221 ]
1222}
1223
1224fn apply_spread(t: f32, spread: VectorSpreadMethod) -> f32 {
1225 match spread {
1226 VectorSpreadMethod::Pad => t.clamp(0.0, 1.0),
1227 VectorSpreadMethod::Reflect => {
1228 let m = t.rem_euclid(2.0);
1229 if m > 1.0 { 2.0 - m } else { m }
1230 }
1231 VectorSpreadMethod::Repeat => t.rem_euclid(1.0),
1232 }
1233}
1234
1235fn sample_stops(stops: &[VectorGradientStop], t: f32) -> [f32; 4] {
1236 if stops.is_empty() {
1237 return [0.0; 4];
1238 }
1239 if t <= stops[0].offset {
1240 return stops[0].color;
1241 }
1242 let last = stops.len() - 1;
1243 if t >= stops[last].offset {
1244 return stops[last].color;
1245 }
1246 for i in 1..stops.len() {
1247 if t <= stops[i].offset {
1248 let prev = &stops[i - 1];
1249 let next = &stops[i];
1250 let span = (next.offset - prev.offset).max(f32::EPSILON);
1251 let frac = ((t - prev.offset) / span).clamp(0.0, 1.0);
1252 return [
1253 prev.color[0] + (next.color[0] - prev.color[0]) * frac,
1254 prev.color[1] + (next.color[1] - prev.color[1]) * frac,
1255 prev.color[2] + (next.color[2] - prev.color[2]) * frac,
1256 prev.color[3] + (next.color[3] - prev.color[3]) * frac,
1257 ];
1258 }
1259 }
1260 stops[last].color
1261}
1262
1263fn append_indexed(
1264 geometry: &VertexBuffers<VectorMeshVertex, u16>,
1265 out: &mut Vec<VectorMeshVertex>,
1266) {
1267 for index in &geometry.indices {
1268 if let Some(vertex) = geometry.vertices.get(*index as usize) {
1269 out.push(*vertex);
1270 }
1271 }
1272}
1273
1274#[cfg(test)]
1275mod tests {
1276 use super::*;
1277 use crate::icons::{all_icon_names, icon_vector_asset};
1278
1279 #[test]
1280 fn parses_basic_svg_shapes_into_paths() {
1281 let asset = parse_svg_asset(
1282 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>"##,
1283 )
1284 .unwrap();
1285 assert_eq!(asset.view_box, [0.0, 0.0, 24.0, 24.0]);
1286 assert_eq!(asset.paths.len(), 1);
1287 assert!(asset.paths[0].stroke.is_some());
1288 assert!(asset.paths[0].segments.len() > 4);
1289 }
1290
1291 #[test]
1292 fn tessellates_every_builtin_icon() {
1293 for name in all_icon_names() {
1294 let mesh = tessellate_vector_asset(
1295 icon_vector_asset(*name),
1296 VectorMeshOptions::icon(
1297 crate::tree::Rect::new(0.0, 0.0, 16.0, 16.0),
1298 Color::rgb(15, 23, 42),
1299 2.0,
1300 ),
1301 );
1302 assert!(
1303 !mesh.vertices.is_empty(),
1304 "{} produced no tessellated vertices",
1305 name.name()
1306 );
1307 }
1308 }
1309
1310 #[test]
1311 fn parses_linear_gradient_paint() {
1312 let asset = parse_svg_asset(
1313 r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
1314 <defs>
1315 <linearGradient id="g" x1="0" y1="0" x2="100" y2="0" gradientUnits="userSpaceOnUse">
1316 <stop offset="0" stop-color="#ff0000"/>
1317 <stop offset="1" stop-color="#0000ff"/>
1318 </linearGradient>
1319 </defs>
1320 <rect width="100" height="100" fill="url(#g)"/>
1321 </svg>"##,
1322 )
1323 .unwrap();
1324 assert_eq!(asset.gradients.len(), 1);
1325 assert!(matches!(
1326 asset.paths[0].fill.unwrap().color,
1327 VectorColor::Gradient(_)
1328 ));
1329 match &asset.gradients[0] {
1330 VectorGradient::Linear(g) => {
1331 assert_eq!(g.stops.len(), 2);
1332 assert_eq!(g.spread, VectorSpreadMethod::Pad);
1333 assert_eq!(g.p1, [0.0, 0.0]);
1334 assert_eq!(g.p2, [100.0, 0.0]);
1335 }
1336 other => panic!("expected linear gradient, got {other:?}"),
1337 }
1338 }
1339
1340 #[test]
1341 fn bakes_gradient_into_per_vertex_colors() {
1342 let asset = parse_svg_asset(
1343 r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
1344 <defs>
1345 <linearGradient id="g" x1="0" y1="0" x2="100" y2="0" gradientUnits="userSpaceOnUse">
1346 <stop offset="0" stop-color="#ff0000"/>
1347 <stop offset="1" stop-color="#0000ff"/>
1348 </linearGradient>
1349 </defs>
1350 <rect width="100" height="100" fill="url(#g)"/>
1351 </svg>"##,
1352 )
1353 .unwrap();
1354 let mesh = tessellate_vector_asset(
1355 &asset,
1356 VectorMeshOptions::icon(
1357 crate::tree::Rect::new(0.0, 0.0, 200.0, 200.0),
1358 Color::rgb(0, 0, 0),
1359 2.0,
1360 ),
1361 );
1362 assert!(!mesh.vertices.is_empty());
1363
1364 let mut min_x_vert = mesh.vertices[0];
1368 let mut max_x_vert = mesh.vertices[0];
1369 for v in &mesh.vertices {
1370 if v.local[0] < min_x_vert.local[0] {
1371 min_x_vert = *v;
1372 }
1373 if v.local[0] > max_x_vert.local[0] {
1374 max_x_vert = *v;
1375 }
1376 }
1377 assert!(
1378 min_x_vert.color[0] > min_x_vert.color[2],
1379 "left edge should be redder: {:?}",
1380 min_x_vert.color
1381 );
1382 assert!(
1383 max_x_vert.color[2] > max_x_vert.color[0],
1384 "right edge should be bluer: {:?}",
1385 max_x_vert.color
1386 );
1387 }
1388
1389 #[test]
1390 fn has_gradient_distinguishes_flat_from_gradient_assets() {
1391 let flat = parse_svg_asset(
1392 r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4" fill="#fff"/></svg>"##,
1393 )
1394 .unwrap();
1395 assert!(!flat.has_gradient());
1396
1397 let gradient = parse_svg_asset(
1398 r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
1399 <defs><linearGradient id="g" x1="0" y1="0" x2="100" y2="0" gradientUnits="userSpaceOnUse">
1400 <stop offset="0" stop-color="#ff0000"/><stop offset="1" stop-color="#0000ff"/>
1401 </linearGradient></defs>
1402 <rect width="100" height="100" fill="url(#g)"/>
1403 </svg>"##,
1404 )
1405 .unwrap();
1406 assert!(gradient.has_gradient());
1407 }
1408
1409 #[test]
1410 fn parses_pipewire_volume_icon_with_all_gradients() {
1411 let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024">
1415 <defs>
1416 <linearGradient id="arcGradient" x1="210" y1="720" x2="805" y2="260" gradientUnits="userSpaceOnUse">
1417 <stop offset="0" stop-color="#0667ff"/>
1418 <stop offset="0.52" stop-color="#139cff"/>
1419 <stop offset="1" stop-color="#11e4dc"/>
1420 </linearGradient>
1421 <linearGradient id="dotGradient" x1="585" y1="780" x2="805" y2="455" gradientUnits="userSpaceOnUse">
1422 <stop offset="0" stop-color="#065eff"/>
1423 <stop offset="0.55" stop-color="#0d9fff"/>
1424 <stop offset="1" stop-color="#10e5dc"/>
1425 </linearGradient>
1426 <radialGradient id="knobFace" cx="42%" cy="36%" r="72%">
1427 <stop offset="0" stop-color="#12366c"/>
1428 <stop offset="0.42" stop-color="#0b2554"/>
1429 <stop offset="1" stop-color="#071736"/>
1430 </radialGradient>
1431 <linearGradient id="knobRim" x1="320" y1="310" x2="735" y2="740" gradientUnits="userSpaceOnUse">
1432 <stop offset="0" stop-color="#214f9b"/>
1433 <stop offset="0.48" stop-color="#17386f"/>
1434 <stop offset="1" stop-color="#285aa7"/>
1435 </linearGradient>
1436 <linearGradient id="needleGradient" x1="565" y1="425" x2="670" y2="320" gradientUnits="userSpaceOnUse">
1437 <stop offset="0" stop-color="#0872ff"/>
1438 <stop offset="1" stop-color="#168aff"/>
1439 </linearGradient>
1440 </defs>
1441 <path d="M 296 720 A 300 300 0 1 1 794 409" fill="none" stroke="url(#arcGradient)" stroke-width="36" stroke-linecap="round"/>
1442 <circle cx="512" cy="512" r="210" fill="url(#knobRim)"/>
1443 <circle cx="512" cy="512" r="192" fill="url(#knobFace)"/>
1444 <line x1="569" y1="433" x2="663" y2="339" stroke="url(#needleGradient)" stroke-width="30" stroke-linecap="round"/>
1445 <circle cx="612" cy="787" r="13" fill="url(#dotGradient)"/>
1446 <circle cx="664" cy="764" r="14" fill="url(#dotGradient)"/>
1447</svg>"##;
1448 let asset = parse_svg_asset(svg).unwrap();
1449 assert_eq!(asset.paths.len(), 6);
1451 assert!(asset.gradients.len() >= 5);
1456
1457 let mesh = tessellate_vector_asset(
1458 &asset,
1459 VectorMeshOptions::icon(
1460 crate::tree::Rect::new(0.0, 0.0, 256.0, 256.0),
1461 Color::rgb(0, 0, 0),
1462 2.0,
1463 ),
1464 );
1465 assert!(!mesh.vertices.is_empty());
1466 let any_lit = mesh
1469 .vertices
1470 .iter()
1471 .any(|v| v.color[0] + v.color[1] + v.color[2] > 0.01);
1472 assert!(any_lit, "no lit vertices — gradients did not render");
1473 }
1474}