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