1#![warn(missing_docs)]
11
12use std::error::Error;
13use std::fmt;
14
15use crate::color::ColorSpace;
16use crate::paint::rgba_f32_in;
17use crate::tree::Color;
18
19use bytemuck::{Pod, Zeroable};
20use lyon_tessellation::geometry_builder::{BuffersBuilder, VertexBuffers};
21use lyon_tessellation::math::point;
22use lyon_tessellation::path::Path as LyonPath;
23use lyon_tessellation::{
24 FillOptions, FillTessellator, FillVertex, LineCap, LineJoin, StrokeOptions, StrokeTessellator,
25 StrokeVertex,
26};
27use usvg::tiny_skia_path;
28
29#[derive(Clone, Debug, PartialEq)]
34pub struct VectorAsset {
35 pub view_box: [f32; 4],
38 pub paths: Vec<VectorPath>,
41 pub gradients: Vec<VectorGradient>,
44}
45
46#[derive(Clone, Copy, Debug, Default, PartialEq)]
53pub enum VectorRenderMode {
54 #[default]
56 Painted,
57 Mask {
59 color: Color,
61 },
62}
63
64impl VectorRenderMode {
65 pub fn resolved_palette(self, palette: &crate::palette::Palette) -> Self {
68 match self {
69 Self::Painted => Self::Painted,
70 Self::Mask { color } => Self::Mask {
71 color: palette.resolve(color),
72 },
73 }
74 }
75}
76
77impl VectorAsset {
78 pub fn from_paths(view_box: [f32; 4], paths: Vec<VectorPath>) -> Self {
86 Self {
87 view_box,
88 paths,
89 gradients: Vec::new(),
90 }
91 }
92
93 pub fn has_gradient(&self) -> bool {
95 self.paths.iter().any(|p| {
96 p.fill
97 .map(|f| matches!(f.color, VectorColor::Gradient(_)))
98 .unwrap_or(false)
99 || p.stroke
100 .map(|s| matches!(s.color, VectorColor::Gradient(_)))
101 .unwrap_or(false)
102 })
103 }
104
105 pub fn resolved_palette(&self, palette: &crate::palette::Palette) -> Self {
110 let mut out = self.clone();
111 for path in &mut out.paths {
112 if let Some(fill) = &mut path.fill {
113 fill.color = resolve_vector_color(fill.color, palette);
114 }
115 if let Some(stroke) = &mut path.stroke {
116 stroke.color = resolve_vector_color(stroke.color, palette);
117 }
118 }
119 out
120 }
121
122 pub fn content_hash(&self) -> u64 {
133 use std::hash::Hasher;
134 let mut h = StableHasher::new();
135 hash_view_box(&mut h, self.view_box);
136 write_len(&mut h, self.paths.len());
137 for path in &self.paths {
138 hash_path(&mut h, path);
139 }
140 write_len(&mut h, self.gradients.len());
141 for grad in &self.gradients {
142 hash_gradient(&mut h, grad);
143 }
144 h.finish()
145 }
146}
147
148fn resolve_vector_color(color: VectorColor, palette: &crate::palette::Palette) -> VectorColor {
149 match color {
150 VectorColor::Solid(c) => VectorColor::Solid(palette.resolve(c)),
151 VectorColor::CurrentColor | VectorColor::Gradient(_) => color,
152 }
153}
154
155struct StableHasher {
160 state: u64,
161}
162
163impl StableHasher {
164 const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
165 const PRIME: u64 = 0x0000_0100_0000_01b3;
166
167 fn new() -> Self {
168 Self {
169 state: Self::OFFSET,
170 }
171 }
172}
173
174impl std::hash::Hasher for StableHasher {
175 fn write(&mut self, bytes: &[u8]) {
176 for byte in bytes {
177 self.state ^= *byte as u64;
178 self.state = self.state.wrapping_mul(Self::PRIME);
179 }
180 }
181
182 fn finish(&self) -> u64 {
183 self.state
184 }
185}
186
187fn write_len(h: &mut impl std::hash::Hasher, len: usize) {
188 h.write_u64(len as u64);
189}
190
191fn hash_str(h: &mut impl std::hash::Hasher, value: &str) {
192 write_len(h, value.len());
193 h.write(value.as_bytes());
194}
195
196fn hash_view_box(h: &mut impl std::hash::Hasher, vb: [f32; 4]) {
197 for v in vb {
198 h.write_u32(v.to_bits());
199 }
200}
201
202fn hash_path(h: &mut impl std::hash::Hasher, path: &VectorPath) {
203 write_len(h, path.segments.len());
204 for seg in &path.segments {
205 hash_segment(h, seg);
206 }
207 match path.fill {
208 Some(f) => {
209 h.write_u8(1);
210 hash_fill(h, f);
211 }
212 None => h.write_u8(0),
213 }
214 match path.stroke {
215 Some(s) => {
216 h.write_u8(1);
217 hash_stroke(h, s);
218 }
219 None => h.write_u8(0),
220 }
221}
222
223fn hash_segment(h: &mut impl std::hash::Hasher, seg: &VectorSegment) {
224 match *seg {
225 VectorSegment::MoveTo(p) => {
226 h.write_u8(0);
227 hash_pt(h, p);
228 }
229 VectorSegment::LineTo(p) => {
230 h.write_u8(1);
231 hash_pt(h, p);
232 }
233 VectorSegment::QuadTo(c, p) => {
234 h.write_u8(2);
235 hash_pt(h, c);
236 hash_pt(h, p);
237 }
238 VectorSegment::CubicTo(c1, c2, p) => {
239 h.write_u8(3);
240 hash_pt(h, c1);
241 hash_pt(h, c2);
242 hash_pt(h, p);
243 }
244 VectorSegment::Close => h.write_u8(4),
245 }
246}
247
248fn hash_pt(h: &mut impl std::hash::Hasher, p: [f32; 2]) {
249 h.write_u32(p[0].to_bits());
250 h.write_u32(p[1].to_bits());
251}
252
253fn hash_fill(h: &mut impl std::hash::Hasher, f: VectorFill) {
254 hash_color(h, f.color);
255 h.write_u32(f.opacity.to_bits());
256 h.write_u8(match f.rule {
257 VectorFillRule::NonZero => 0,
258 VectorFillRule::EvenOdd => 1,
259 });
260}
261
262fn hash_stroke(h: &mut impl std::hash::Hasher, s: VectorStroke) {
263 hash_color(h, s.color);
264 h.write_u32(s.opacity.to_bits());
265 h.write_u32(s.width.to_bits());
266 h.write_u8(match s.line_cap {
267 VectorLineCap::Butt => 0,
268 VectorLineCap::Round => 1,
269 VectorLineCap::Square => 2,
270 });
271 h.write_u8(match s.line_join {
272 VectorLineJoin::Miter => 0,
273 VectorLineJoin::MiterClip => 1,
274 VectorLineJoin::Round => 2,
275 VectorLineJoin::Bevel => 3,
276 });
277 h.write_u32(s.miter_limit.to_bits());
278}
279
280fn hash_color(h: &mut impl std::hash::Hasher, c: VectorColor) {
281 match c {
282 VectorColor::CurrentColor => h.write_u8(0),
283 VectorColor::Solid(col) => {
284 h.write_u8(1);
285 h.write_u32(col.r.to_bits());
286 h.write_u32(col.g.to_bits());
287 h.write_u32(col.b.to_bits());
288 h.write_u32(col.a.to_bits());
289 std::hash::Hash::hash(&col.space, h);
293 match col.token {
299 Some(name) => {
300 h.write_u8(1);
301 hash_str(h, name);
302 }
303 None => h.write_u8(0),
304 }
305 }
306 VectorColor::Gradient(idx) => {
307 h.write_u8(2);
308 h.write_u32(idx);
309 }
310 }
311}
312
313fn hash_gradient(h: &mut impl std::hash::Hasher, g: &VectorGradient) {
314 match g {
315 VectorGradient::Linear(lin) => {
316 h.write_u8(0);
317 hash_pt(h, lin.p1);
318 hash_pt(h, lin.p2);
319 hash_stops(h, &lin.stops);
320 hash_spread(h, lin.spread);
321 for v in lin.absolute_to_local {
322 h.write_u32(v.to_bits());
323 }
324 }
325 VectorGradient::Radial(rad) => {
326 h.write_u8(1);
327 hash_pt(h, rad.center);
328 h.write_u32(rad.radius.to_bits());
329 hash_pt(h, rad.focal);
330 h.write_u32(rad.focal_radius.to_bits());
331 hash_stops(h, &rad.stops);
332 hash_spread(h, rad.spread);
333 for v in rad.absolute_to_local {
334 h.write_u32(v.to_bits());
335 }
336 }
337 }
338}
339
340fn hash_stops(h: &mut impl std::hash::Hasher, stops: &[VectorGradientStop]) {
341 write_len(h, stops.len());
342 for stop in stops {
343 h.write_u32(stop.offset.to_bits());
344 for c in stop.color {
345 h.write_u32(c.to_bits());
346 }
347 }
348}
349
350fn hash_spread(h: &mut impl std::hash::Hasher, s: VectorSpreadMethod) {
351 h.write_u8(match s {
352 VectorSpreadMethod::Pad => 0,
353 VectorSpreadMethod::Reflect => 1,
354 VectorSpreadMethod::Repeat => 2,
355 });
356}
357
358#[derive(Clone, Debug)]
381pub struct PathBuilder {
382 segments: Vec<VectorSegment>,
383 fill: Option<VectorFill>,
384 stroke: Option<VectorStroke>,
385}
386
387impl Default for PathBuilder {
388 fn default() -> Self {
389 Self::new()
390 }
391}
392
393impl PathBuilder {
394 pub fn new() -> Self {
396 Self {
397 segments: Vec::new(),
398 fill: None,
399 stroke: None,
400 }
401 }
402
403 pub fn move_to(mut self, x: f32, y: f32) -> Self {
405 self.segments.push(VectorSegment::MoveTo([x, y]));
406 self
407 }
408
409 pub fn line_to(mut self, x: f32, y: f32) -> Self {
411 self.segments.push(VectorSegment::LineTo([x, y]));
412 self
413 }
414
415 pub fn quad_to(mut self, cx: f32, cy: f32, x: f32, y: f32) -> Self {
417 self.segments.push(VectorSegment::QuadTo([cx, cy], [x, y]));
418 self
419 }
420
421 pub fn cubic_to(mut self, c1x: f32, c1y: f32, c2x: f32, c2y: f32, x: f32, y: f32) -> Self {
423 self.segments
424 .push(VectorSegment::CubicTo([c1x, c1y], [c2x, c2y], [x, y]));
425 self
426 }
427
428 pub fn close(mut self) -> Self {
430 self.segments.push(VectorSegment::Close);
431 self
432 }
433
434 pub fn fill_solid(mut self, color: crate::tree::Color) -> Self {
437 self.fill = Some(VectorFill {
438 color: VectorColor::Solid(color),
439 opacity: 1.0,
440 rule: VectorFillRule::NonZero,
441 });
442 self
443 }
444
445 pub fn fill(mut self, fill: Option<VectorFill>) -> Self {
447 self.fill = fill;
448 self
449 }
450
451 pub fn stroke_solid(mut self, color: crate::tree::Color, width: f32) -> Self {
456 self.stroke = Some(VectorStroke {
457 color: VectorColor::Solid(color),
458 opacity: 1.0,
459 width,
460 line_cap: VectorLineCap::Butt,
461 line_join: VectorLineJoin::Miter,
462 miter_limit: 4.0,
463 });
464 self
465 }
466
467 pub fn stroke(mut self, stroke: Option<VectorStroke>) -> Self {
469 self.stroke = stroke;
470 self
471 }
472
473 pub fn stroke_line_cap(mut self, cap: VectorLineCap) -> Self {
475 if let Some(s) = self.stroke.as_mut() {
476 s.line_cap = cap;
477 }
478 self
479 }
480
481 pub fn stroke_line_join(mut self, join: VectorLineJoin) -> Self {
483 if let Some(s) = self.stroke.as_mut() {
484 s.line_join = join;
485 }
486 self
487 }
488
489 pub fn stroke_miter_limit(mut self, limit: f32) -> Self {
491 if let Some(s) = self.stroke.as_mut() {
492 s.miter_limit = limit;
493 }
494 self
495 }
496
497 pub fn stroke_opacity(mut self, opacity: f32) -> Self {
500 if let Some(s) = self.stroke.as_mut() {
501 s.opacity = opacity;
502 }
503 self
504 }
505
506 pub fn build(self) -> VectorPath {
508 VectorPath {
509 segments: self.segments,
510 fill: self.fill,
511 stroke: self.stroke,
512 }
513 }
514}
515
516#[derive(Clone, Debug, PartialEq)]
520pub struct VectorPath {
521 pub segments: Vec<VectorSegment>,
523 pub fill: Option<VectorFill>,
526 pub stroke: Option<VectorStroke>,
528}
529
530#[derive(Clone, Copy, Debug, PartialEq)]
533pub enum VectorSegment {
534 MoveTo([f32; 2]),
536 LineTo([f32; 2]),
538 QuadTo([f32; 2], [f32; 2]),
540 CubicTo([f32; 2], [f32; 2], [f32; 2]),
543 Close,
545}
546
547#[derive(Clone, Copy, Debug, PartialEq)]
549pub struct VectorFill {
550 pub color: VectorColor,
552 pub opacity: f32,
555 pub rule: VectorFillRule,
557}
558
559#[derive(Clone, Copy, Debug, PartialEq)]
561pub struct VectorStroke {
562 pub color: VectorColor,
564 pub opacity: f32,
567 pub width: f32,
571 pub line_cap: VectorLineCap,
573 pub line_join: VectorLineJoin,
575 pub miter_limit: f32,
577}
578
579#[derive(Clone, Copy, Debug, PartialEq)]
582pub enum VectorColor {
583 CurrentColor,
586 Solid(Color),
589 Gradient(u32),
591}
592
593#[derive(Clone, Debug, PartialEq)]
598pub enum VectorGradient {
599 Linear(VectorLinearGradient),
601 Radial(VectorRadialGradient),
603}
604
605#[derive(Clone, Debug, PartialEq)]
608pub struct VectorLinearGradient {
609 pub p1: [f32; 2],
611 pub p2: [f32; 2],
613 pub stops: Vec<VectorGradientStop>,
615 pub spread: VectorSpreadMethod,
617 pub absolute_to_local: [f32; 6],
620}
621
622#[derive(Clone, Debug, PartialEq)]
629pub struct VectorRadialGradient {
630 pub center: [f32; 2],
632 pub radius: f32,
634 pub focal: [f32; 2],
636 pub focal_radius: f32,
638 pub stops: Vec<VectorGradientStop>,
640 pub spread: VectorSpreadMethod,
642 pub absolute_to_local: [f32; 6],
645}
646
647#[derive(Clone, Copy, Debug, PartialEq)]
651pub struct VectorGradientStop {
652 pub offset: f32,
655 pub color: [f32; 4],
659}
660
661#[derive(Clone, Copy, Debug, PartialEq, Eq)]
664pub enum VectorSpreadMethod {
665 Pad,
667 Reflect,
669 Repeat,
671}
672
673#[derive(Clone, Copy, Debug, PartialEq, Eq)]
676pub enum VectorFillRule {
677 NonZero,
679 EvenOdd,
681}
682
683#[derive(Clone, Copy, Debug, PartialEq, Eq)]
685pub enum VectorLineCap {
686 Butt,
688 Round,
690 Square,
692}
693
694#[derive(Clone, Copy, Debug, PartialEq, Eq)]
696pub enum VectorLineJoin {
697 Miter,
699 MiterClip,
702 Round,
704 Bevel,
706}
707
708#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
711pub enum IconMaterial {
712 #[default]
715 Flat,
716 Relief,
721 Glass,
724}
725
726#[repr(C)]
729#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
730pub struct VectorMeshVertex {
731 pub pos: [f32; 2],
734 pub local: [f32; 2],
737 pub color: [f32; 4],
741 pub meta: [f32; 4],
744}
745
746#[derive(Clone, Debug, Default, PartialEq)]
748pub struct VectorMesh {
749 pub vertices: Vec<VectorMeshVertex>,
752}
753
754#[derive(Clone, Copy, Debug, PartialEq)]
757pub struct VectorMeshRun {
758 pub first: u32,
760 pub count: u32,
763}
764
765#[derive(Clone, Copy, Debug, PartialEq)]
767pub struct VectorMeshOptions {
768 pub rect: crate::tree::Rect,
771 pub current_color: Color,
773 pub stroke_width: f32,
776 pub tolerance: f32,
780 pub working_color_space: ColorSpace,
785}
786
787impl VectorMeshOptions {
788 pub fn icon(
792 rect: crate::tree::Rect,
793 current_color: Color,
794 stroke_width: f32,
795 working_color_space: ColorSpace,
796 ) -> Self {
797 Self {
798 rect,
799 current_color,
800 stroke_width,
801 tolerance: 0.05,
802 working_color_space,
803 }
804 }
805}
806
807#[derive(Clone, Debug, PartialEq, Eq)]
810pub struct VectorParseError {
811 message: String,
812}
813
814impl VectorParseError {
815 fn new(message: impl Into<String>) -> Self {
816 Self {
817 message: message.into(),
818 }
819 }
820}
821
822impl fmt::Display for VectorParseError {
823 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
824 f.write_str(&self.message)
825 }
826}
827
828impl Error for VectorParseError {}
829
830pub fn parse_svg_asset(svg: &str) -> Result<VectorAsset, VectorParseError> {
839 parse_svg_asset_with_color_mode(svg, false)
840}
841
842pub fn tessellate_vector_asset(asset: &VectorAsset, options: VectorMeshOptions) -> VectorMesh {
846 let mut mesh = VectorMesh::default();
847 append_vector_asset_mesh(asset, options, &mut mesh.vertices);
848 mesh
849}
850
851pub fn append_vector_asset_mesh(
860 asset: &VectorAsset,
861 options: VectorMeshOptions,
862 out: &mut Vec<VectorMeshVertex>,
863) -> VectorMeshRun {
864 let first = out.len() as u32;
865 if options.rect.w <= 0.0 || options.rect.h <= 0.0 {
866 return VectorMeshRun { first, count: 0 };
867 }
868
869 let [vx, vy, vw, vh] = asset.view_box;
870 if vw <= 0.0 || vh <= 0.0 {
876 return VectorMeshRun { first, count: 0 };
877 }
878 let sx = options.rect.w / vw;
879 let sy = options.rect.h / vh;
880 let stroke_scale = (sx + sy) * 0.5;
881
882 for (path_index, vector_path) in asset.paths.iter().enumerate() {
883 let path = build_lyon_path(vector_path, options.rect, [vx, vy], [sx, sy]);
884 if let Some(fill) = vector_path.fill {
885 let sampler = ColorSampler::build(
886 fill.color,
887 fill.opacity,
888 options.current_color,
889 &asset.gradients,
890 options.working_color_space,
891 );
892 let mut geometry: VertexBuffers<VectorMeshVertex, u16> = VertexBuffers::new();
893 let fill_options =
894 FillOptions::tolerance(options.tolerance).with_fill_rule(match fill.rule {
895 VectorFillRule::NonZero => lyon_tessellation::FillRule::NonZero,
896 VectorFillRule::EvenOdd => lyon_tessellation::FillRule::EvenOdd,
897 });
898 let _ = FillTessellator::new().tessellate_path(
899 &path,
900 &fill_options,
901 &mut BuffersBuilder::new(&mut geometry, |v: FillVertex<'_>| {
902 make_mesh_vertex_sampled(
903 v.position(),
904 options.rect,
905 [vx, vy],
906 [sx, sy],
907 &sampler,
908 path_index,
909 VectorPrimitiveKind::Fill,
910 )
911 }),
912 );
913 append_indexed(&geometry, out);
914 }
915
916 if let Some(stroke) = vector_path.stroke {
917 let sampler = ColorSampler::build(
918 stroke.color,
919 stroke.opacity,
920 options.current_color,
921 &asset.gradients,
922 options.working_color_space,
923 );
924 let width = if matches!(stroke.color, VectorColor::CurrentColor) {
925 options.stroke_width * stroke_scale
926 } else {
927 stroke.width * stroke_scale
928 }
929 .max(0.5);
930 let mut geometry: VertexBuffers<VectorMeshVertex, u16> = VertexBuffers::new();
931 let stroke_options = StrokeOptions::tolerance(options.tolerance)
932 .with_line_width(width)
933 .with_line_cap(match stroke.line_cap {
934 VectorLineCap::Butt => LineCap::Butt,
935 VectorLineCap::Round => LineCap::Round,
936 VectorLineCap::Square => LineCap::Square,
937 })
938 .with_line_join(match stroke.line_join {
939 VectorLineJoin::Miter => LineJoin::Miter,
940 VectorLineJoin::MiterClip => LineJoin::MiterClip,
941 VectorLineJoin::Round => LineJoin::Round,
942 VectorLineJoin::Bevel => LineJoin::Bevel,
943 })
944 .with_miter_limit(stroke.miter_limit.max(1.0));
945 let _ = StrokeTessellator::new().tessellate_path(
946 &path,
947 &stroke_options,
948 &mut BuffersBuilder::new(&mut geometry, |v: StrokeVertex<'_, '_>| {
949 make_mesh_vertex_sampled(
950 v.position(),
951 options.rect,
952 [vx, vy],
953 [sx, sy],
954 &sampler,
955 path_index,
956 VectorPrimitiveKind::Stroke,
957 )
958 }),
959 );
960 append_indexed(&geometry, out);
961 }
962 }
963
964 VectorMeshRun {
965 first,
966 count: out.len() as u32 - first,
967 }
968}
969
970pub(crate) fn parse_current_color_svg_asset(svg: &str) -> Result<VectorAsset, VectorParseError> {
971 parse_svg_asset_with_color_mode(svg, true)
972}
973
974fn parse_svg_asset_with_color_mode(
975 svg: &str,
976 force_current_color: bool,
977) -> Result<VectorAsset, VectorParseError> {
978 let tree = usvg::Tree::from_str(svg, &usvg::Options::default())
979 .map_err(|e| VectorParseError::new(format!("invalid SVG: {e}")))?;
980 let size = tree.size();
981 let mut asset = VectorAsset {
982 view_box: [0.0, 0.0, size.width(), size.height()],
983 paths: Vec::new(),
984 gradients: Vec::new(),
985 };
986 collect_group(
987 tree.root(),
988 force_current_color,
989 &mut asset.paths,
990 &mut asset.gradients,
991 );
992 if asset.paths.is_empty() {
993 return Err(VectorParseError::new("SVG produced no renderable paths"));
994 }
995 Ok(asset)
996}
997
998fn collect_group(
999 group: &usvg::Group,
1000 force_current_color: bool,
1001 out: &mut Vec<VectorPath>,
1002 gradients: &mut Vec<VectorGradient>,
1003) {
1004 for node in group.children() {
1005 match node {
1006 usvg::Node::Group(group) => collect_group(group, force_current_color, out, gradients),
1007 usvg::Node::Path(path) if path.is_visible() => {
1008 if let Some(vector_path) = convert_path(path, force_current_color, gradients) {
1009 out.push(vector_path);
1010 }
1011 }
1012 _ => {}
1013 }
1014 }
1015}
1016
1017fn convert_path(
1018 path: &usvg::Path,
1019 force_current_color: bool,
1020 gradients: &mut Vec<VectorGradient>,
1021) -> Option<VectorPath> {
1022 let transform = path.abs_transform();
1023 let mut segments = Vec::new();
1024 for segment in path.data().segments() {
1025 match segment {
1026 tiny_skia_path::PathSegment::MoveTo(p) => {
1027 segments.push(VectorSegment::MoveTo(map_point(transform, p)));
1028 }
1029 tiny_skia_path::PathSegment::LineTo(p) => {
1030 segments.push(VectorSegment::LineTo(map_point(transform, p)));
1031 }
1032 tiny_skia_path::PathSegment::QuadTo(p0, p1) => {
1033 segments.push(VectorSegment::QuadTo(
1034 map_point(transform, p0),
1035 map_point(transform, p1),
1036 ));
1037 }
1038 tiny_skia_path::PathSegment::CubicTo(p0, p1, p2) => {
1039 segments.push(VectorSegment::CubicTo(
1040 map_point(transform, p0),
1041 map_point(transform, p1),
1042 map_point(transform, p2),
1043 ));
1044 }
1045 tiny_skia_path::PathSegment::Close => segments.push(VectorSegment::Close),
1046 }
1047 }
1048 if segments.is_empty() {
1049 return None;
1050 }
1051
1052 Some(VectorPath {
1053 segments,
1054 fill: path
1055 .fill()
1056 .and_then(|fill| convert_fill(fill, transform, force_current_color, gradients)),
1057 stroke: path
1058 .stroke()
1059 .and_then(|stroke| convert_stroke(stroke, transform, force_current_color, gradients)),
1060 })
1061}
1062
1063fn convert_fill(
1064 fill: &usvg::Fill,
1065 abs_transform: tiny_skia_path::Transform,
1066 force_current_color: bool,
1067 gradients: &mut Vec<VectorGradient>,
1068) -> Option<VectorFill> {
1069 Some(VectorFill {
1070 color: convert_paint(fill.paint(), abs_transform, force_current_color, gradients)?,
1071 opacity: fill.opacity().get(),
1072 rule: match fill.rule() {
1073 usvg::FillRule::NonZero => VectorFillRule::NonZero,
1074 usvg::FillRule::EvenOdd => VectorFillRule::EvenOdd,
1075 },
1076 })
1077}
1078
1079fn convert_stroke(
1080 stroke: &usvg::Stroke,
1081 abs_transform: tiny_skia_path::Transform,
1082 force_current_color: bool,
1083 gradients: &mut Vec<VectorGradient>,
1084) -> Option<VectorStroke> {
1085 Some(VectorStroke {
1086 color: convert_paint(
1087 stroke.paint(),
1088 abs_transform,
1089 force_current_color,
1090 gradients,
1091 )?,
1092 opacity: stroke.opacity().get(),
1093 width: stroke.width().get(),
1094 line_cap: match stroke.linecap() {
1095 usvg::LineCap::Butt => VectorLineCap::Butt,
1096 usvg::LineCap::Round => VectorLineCap::Round,
1097 usvg::LineCap::Square => VectorLineCap::Square,
1098 },
1099 line_join: match stroke.linejoin() {
1100 usvg::LineJoin::Miter => VectorLineJoin::Miter,
1101 usvg::LineJoin::MiterClip => VectorLineJoin::MiterClip,
1102 usvg::LineJoin::Round => VectorLineJoin::Round,
1103 usvg::LineJoin::Bevel => VectorLineJoin::Bevel,
1104 },
1105 miter_limit: stroke.miterlimit().get(),
1106 })
1107}
1108
1109fn convert_paint(
1110 paint: &usvg::Paint,
1111 abs_transform: tiny_skia_path::Transform,
1112 force_current_color: bool,
1113 gradients: &mut Vec<VectorGradient>,
1114) -> Option<VectorColor> {
1115 if force_current_color {
1116 return Some(VectorColor::CurrentColor);
1117 }
1118 match paint {
1119 usvg::Paint::Color(c) => Some(VectorColor::Solid(Color::srgb_u8a(
1120 c.red, c.green, c.blue, 255,
1121 ))),
1122 usvg::Paint::LinearGradient(lg) => {
1123 let g = convert_linear_gradient(lg, abs_transform)?;
1124 let idx = gradients.len() as u32;
1125 gradients.push(VectorGradient::Linear(g));
1126 Some(VectorColor::Gradient(idx))
1127 }
1128 usvg::Paint::RadialGradient(rg) => {
1129 let g = convert_radial_gradient(rg, abs_transform)?;
1130 let idx = gradients.len() as u32;
1131 gradients.push(VectorGradient::Radial(g));
1132 Some(VectorColor::Gradient(idx))
1133 }
1134 usvg::Paint::Pattern(_) => None,
1135 }
1136}
1137
1138fn convert_linear_gradient(
1139 lg: &usvg::LinearGradient,
1140 abs_transform: tiny_skia_path::Transform,
1141) -> Option<VectorLinearGradient> {
1142 let stops = convert_stops(lg.stops());
1143 if stops.is_empty() {
1144 return None;
1145 }
1146 let absolute_to_local = build_absolute_to_local(abs_transform, lg.transform())?;
1147 Some(VectorLinearGradient {
1148 p1: [lg.x1(), lg.y1()],
1149 p2: [lg.x2(), lg.y2()],
1150 stops,
1151 spread: convert_spread(lg.spread_method()),
1152 absolute_to_local,
1153 })
1154}
1155
1156fn convert_radial_gradient(
1157 rg: &usvg::RadialGradient,
1158 abs_transform: tiny_skia_path::Transform,
1159) -> Option<VectorRadialGradient> {
1160 let stops = convert_stops(rg.stops());
1161 if stops.is_empty() {
1162 return None;
1163 }
1164 let absolute_to_local = build_absolute_to_local(abs_transform, rg.transform())?;
1165 Some(VectorRadialGradient {
1166 center: [rg.cx(), rg.cy()],
1167 radius: rg.r().get(),
1168 focal: [rg.fx(), rg.fy()],
1169 focal_radius: rg.fr().get(),
1170 stops,
1171 spread: convert_spread(rg.spread_method()),
1172 absolute_to_local,
1173 })
1174}
1175
1176fn convert_stops(stops: &[usvg::Stop]) -> Vec<VectorGradientStop> {
1177 let mut out = Vec::with_capacity(stops.len());
1178 let mut last_offset = 0.0_f32;
1179 for stop in stops {
1180 let offset = stop.offset().get().max(last_offset);
1183 last_offset = offset;
1184 let mut rgba = rgba_f32_in(
1188 Color::srgb_u8a(stop.color().red, stop.color().green, stop.color().blue, 255),
1189 ColorSpace::SRGB_LINEAR,
1190 );
1191 rgba[3] *= stop.opacity().get();
1192 out.push(VectorGradientStop {
1193 offset,
1194 color: rgba,
1195 });
1196 }
1197 out
1198}
1199
1200fn convert_spread(method: usvg::SpreadMethod) -> VectorSpreadMethod {
1201 match method {
1202 usvg::SpreadMethod::Pad => VectorSpreadMethod::Pad,
1203 usvg::SpreadMethod::Reflect => VectorSpreadMethod::Reflect,
1204 usvg::SpreadMethod::Repeat => VectorSpreadMethod::Repeat,
1205 }
1206}
1207
1208fn build_absolute_to_local(
1216 abs_transform: tiny_skia_path::Transform,
1217 gradient_transform: tiny_skia_path::Transform,
1218) -> Option<[f32; 6]> {
1219 let local_to_absolute = abs_transform.pre_concat(gradient_transform);
1220 let inv = local_to_absolute.invert()?;
1221 Some([inv.sx, inv.kx, inv.tx, inv.ky, inv.sy, inv.ty])
1222}
1223
1224fn map_point(transform: tiny_skia_path::Transform, mut point: tiny_skia_path::Point) -> [f32; 2] {
1225 transform.map_point(&mut point);
1226 [point.x, point.y]
1227}
1228
1229#[derive(Clone, Copy)]
1230enum VectorPrimitiveKind {
1231 Fill,
1232 Stroke,
1233}
1234
1235fn build_lyon_path(
1236 path: &VectorPath,
1237 rect: crate::tree::Rect,
1238 view_origin: [f32; 2],
1239 scale: [f32; 2],
1240) -> LyonPath {
1241 let mut builder = LyonPath::builder();
1242 let mut open = false;
1243 for segment in &path.segments {
1244 match *segment {
1245 VectorSegment::MoveTo(p) => {
1246 if open {
1247 builder.end(false);
1248 }
1249 builder.begin(map_mesh_point(rect, view_origin, scale, p));
1250 open = true;
1251 }
1252 VectorSegment::LineTo(p) => {
1253 builder.line_to(map_mesh_point(rect, view_origin, scale, p));
1254 }
1255 VectorSegment::QuadTo(c, p) => {
1256 builder.quadratic_bezier_to(
1257 map_mesh_point(rect, view_origin, scale, c),
1258 map_mesh_point(rect, view_origin, scale, p),
1259 );
1260 }
1261 VectorSegment::CubicTo(c0, c1, p) => {
1262 builder.cubic_bezier_to(
1263 map_mesh_point(rect, view_origin, scale, c0),
1264 map_mesh_point(rect, view_origin, scale, c1),
1265 map_mesh_point(rect, view_origin, scale, p),
1266 );
1267 }
1268 VectorSegment::Close => {
1269 if open {
1270 builder.close();
1271 open = false;
1272 }
1273 }
1274 }
1275 }
1276 if open {
1277 builder.end(false);
1278 }
1279 builder.build()
1280}
1281
1282fn map_mesh_point(
1283 rect: crate::tree::Rect,
1284 view_origin: [f32; 2],
1285 scale: [f32; 2],
1286 p: [f32; 2],
1287) -> lyon_tessellation::math::Point {
1288 point(
1289 rect.x + (p[0] - view_origin[0]) * scale[0],
1290 rect.y + (p[1] - view_origin[1]) * scale[1],
1291 )
1292}
1293
1294fn make_mesh_vertex_sampled(
1295 p: lyon_tessellation::math::Point,
1296 rect: crate::tree::Rect,
1297 view_origin: [f32; 2],
1298 scale: [f32; 2],
1299 sampler: &ColorSampler<'_>,
1300 path_index: usize,
1301 kind: VectorPrimitiveKind,
1302) -> VectorMeshVertex {
1303 let local = [
1304 view_origin[0] + (p.x - rect.x) / scale[0].max(f32::EPSILON),
1305 view_origin[1] + (p.y - rect.y) / scale[1].max(f32::EPSILON),
1306 ];
1307 VectorMeshVertex {
1308 pos: [p.x, p.y],
1309 local,
1310 color: sampler.sample(local),
1311 meta: [
1312 path_index as f32,
1313 match kind {
1314 VectorPrimitiveKind::Fill => 0.0,
1315 VectorPrimitiveKind::Stroke => 1.0,
1316 },
1317 0.0,
1318 0.0,
1319 ],
1320 }
1321}
1322
1323enum ColorSampler<'a> {
1328 Solid([f32; 4]),
1329 Gradient {
1330 gradient: &'a VectorGradient,
1331 opacity: f32,
1332 working: ColorSpace,
1333 },
1334}
1335
1336impl<'a> ColorSampler<'a> {
1337 fn build(
1338 color: VectorColor,
1339 opacity: f32,
1340 current_color: Color,
1341 gradients: &'a [VectorGradient],
1342 working: ColorSpace,
1343 ) -> Self {
1344 let opacity = opacity.clamp(0.0, 1.0);
1345 match color {
1346 VectorColor::CurrentColor => {
1347 let mut c = rgba_f32_in(current_color, working);
1348 c[3] *= opacity;
1349 Self::Solid(c)
1350 }
1351 VectorColor::Solid(c) => {
1352 let mut rgba = rgba_f32_in(c, working);
1353 rgba[3] *= opacity;
1354 Self::Solid(rgba)
1355 }
1356 VectorColor::Gradient(idx) => match gradients.get(idx as usize) {
1357 Some(gradient) => Self::Gradient {
1358 gradient,
1359 opacity,
1360 working,
1361 },
1362 None => Self::Solid([0.0; 4]),
1365 },
1366 }
1367 }
1368
1369 fn sample(&self, abs_local: [f32; 2]) -> [f32; 4] {
1370 match self {
1371 Self::Solid(c) => *c,
1372 Self::Gradient {
1373 gradient,
1374 opacity,
1375 working,
1376 } => {
1377 let [r, g, b, a] = sample_gradient(gradient, abs_local);
1382 let mut c = rgba_f32_in(Color::srgb_linear(r, g, b, a), *working);
1383 c[3] *= *opacity;
1384 c
1385 }
1386 }
1387 }
1388}
1389
1390fn sample_gradient(gradient: &VectorGradient, abs_local: [f32; 2]) -> [f32; 4] {
1391 match gradient {
1392 VectorGradient::Linear(g) => {
1393 let local = apply_affine(&g.absolute_to_local, abs_local);
1394 let dx = g.p2[0] - g.p1[0];
1395 let dy = g.p2[1] - g.p1[1];
1396 let len2 = (dx * dx + dy * dy).max(f32::EPSILON);
1397 let t = ((local[0] - g.p1[0]) * dx + (local[1] - g.p1[1]) * dy) / len2;
1398 sample_stops(&g.stops, apply_spread(t, g.spread))
1399 }
1400 VectorGradient::Radial(g) => {
1401 let local = apply_affine(&g.absolute_to_local, abs_local);
1406 let dx = local[0] - g.center[0];
1407 let dy = local[1] - g.center[1];
1408 let radius = g.radius.max(f32::EPSILON);
1409 let t = (dx * dx + dy * dy).sqrt() / radius;
1410 sample_stops(&g.stops, apply_spread(t, g.spread))
1411 }
1412 }
1413}
1414
1415fn apply_affine(m: &[f32; 6], p: [f32; 2]) -> [f32; 2] {
1416 [
1417 p[0] * m[0] + p[1] * m[1] + m[2],
1418 p[0] * m[3] + p[1] * m[4] + m[5],
1419 ]
1420}
1421
1422fn apply_spread(t: f32, spread: VectorSpreadMethod) -> f32 {
1423 match spread {
1424 VectorSpreadMethod::Pad => t.clamp(0.0, 1.0),
1425 VectorSpreadMethod::Reflect => {
1426 let m = t.rem_euclid(2.0);
1427 if m > 1.0 { 2.0 - m } else { m }
1428 }
1429 VectorSpreadMethod::Repeat => t.rem_euclid(1.0),
1430 }
1431}
1432
1433fn sample_stops(stops: &[VectorGradientStop], t: f32) -> [f32; 4] {
1434 if stops.is_empty() {
1435 return [0.0; 4];
1436 }
1437 if t <= stops[0].offset {
1438 return stops[0].color;
1439 }
1440 let last = stops.len() - 1;
1441 if t >= stops[last].offset {
1442 return stops[last].color;
1443 }
1444 for i in 1..stops.len() {
1445 if t <= stops[i].offset {
1446 let prev = &stops[i - 1];
1447 let next = &stops[i];
1448 let span = (next.offset - prev.offset).max(f32::EPSILON);
1449 let frac = ((t - prev.offset) / span).clamp(0.0, 1.0);
1450 return [
1451 prev.color[0] + (next.color[0] - prev.color[0]) * frac,
1452 prev.color[1] + (next.color[1] - prev.color[1]) * frac,
1453 prev.color[2] + (next.color[2] - prev.color[2]) * frac,
1454 prev.color[3] + (next.color[3] - prev.color[3]) * frac,
1455 ];
1456 }
1457 }
1458 stops[last].color
1459}
1460
1461fn append_indexed(
1462 geometry: &VertexBuffers<VectorMeshVertex, u16>,
1463 out: &mut Vec<VectorMeshVertex>,
1464) {
1465 for index in &geometry.indices {
1466 if let Some(vertex) = geometry.vertices.get(*index as usize) {
1467 out.push(*vertex);
1468 }
1469 }
1470}
1471
1472#[cfg(test)]
1473mod tests {
1474 use super::*;
1475 use crate::icons::{all_icon_names, icon_vector_asset};
1476
1477 #[test]
1478 fn parses_basic_svg_shapes_into_paths() {
1479 let asset = parse_svg_asset(
1480 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>"##,
1481 )
1482 .unwrap();
1483 assert_eq!(asset.view_box, [0.0, 0.0, 24.0, 24.0]);
1484 assert_eq!(asset.paths.len(), 1);
1485 assert!(asset.paths[0].stroke.is_some());
1486 assert!(asset.paths[0].segments.len() > 4);
1487 }
1488
1489 #[test]
1490 fn sub_unit_view_box_scales_to_fill_the_destination_rect() {
1491 let asset = parse_svg_asset(
1494 r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 0.5 0.5"><rect width="0.5" height="0.5" fill="#f00"/></svg>"##,
1495 )
1496 .unwrap();
1497 let mesh = tessellate_vector_asset(
1498 &asset,
1499 VectorMeshOptions::icon(
1500 crate::tree::Rect::new(0.0, 0.0, 100.0, 100.0),
1501 Color::srgb_u8(0, 0, 0),
1502 2.0,
1503 ColorSpace::SRGB_LINEAR,
1504 ),
1505 );
1506 let max_x = mesh
1507 .vertices
1508 .iter()
1509 .map(|v| v.pos[0])
1510 .fold(f32::MIN, f32::max);
1511 let max_y = mesh
1512 .vertices
1513 .iter()
1514 .map(|v| v.pos[1])
1515 .fold(f32::MIN, f32::max);
1516 assert!(
1517 (max_x - 100.0).abs() < 0.5 && (max_y - 100.0).abs() < 0.5,
1518 "0.5-unit square should fill the 100px rect, got extent ({max_x}, {max_y})"
1519 );
1520 }
1521
1522 #[test]
1523 fn zero_dimension_view_box_renders_nothing() {
1524 let asset = VectorAsset::from_paths([0.0, 0.0, 0.0, 24.0], Vec::new());
1527 let mesh = tessellate_vector_asset(
1528 &asset,
1529 VectorMeshOptions::icon(
1530 crate::tree::Rect::new(0.0, 0.0, 16.0, 16.0),
1531 Color::srgb_u8(0, 0, 0),
1532 2.0,
1533 ColorSpace::SRGB_LINEAR,
1534 ),
1535 );
1536 assert!(mesh.vertices.is_empty());
1537 }
1538
1539 #[test]
1540 fn tessellates_every_builtin_icon() {
1541 for name in all_icon_names() {
1542 let mesh = tessellate_vector_asset(
1543 icon_vector_asset(*name),
1544 VectorMeshOptions::icon(
1545 crate::tree::Rect::new(0.0, 0.0, 16.0, 16.0),
1546 Color::srgb_u8(15, 23, 42),
1547 2.0,
1548 ColorSpace::SRGB_LINEAR,
1549 ),
1550 );
1551 assert!(
1552 !mesh.vertices.is_empty(),
1553 "{} produced no tessellated vertices",
1554 name.name()
1555 );
1556 }
1557 }
1558
1559 #[test]
1560 fn parses_linear_gradient_paint() {
1561 let asset = parse_svg_asset(
1562 r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
1563 <defs>
1564 <linearGradient id="g" x1="0" y1="0" x2="100" y2="0" gradientUnits="userSpaceOnUse">
1565 <stop offset="0" stop-color="#ff0000"/>
1566 <stop offset="1" stop-color="#0000ff"/>
1567 </linearGradient>
1568 </defs>
1569 <rect width="100" height="100" fill="url(#g)"/>
1570 </svg>"##,
1571 )
1572 .unwrap();
1573 assert_eq!(asset.gradients.len(), 1);
1574 assert!(matches!(
1575 asset.paths[0].fill.unwrap().color,
1576 VectorColor::Gradient(_)
1577 ));
1578 match &asset.gradients[0] {
1579 VectorGradient::Linear(g) => {
1580 assert_eq!(g.stops.len(), 2);
1581 assert_eq!(g.spread, VectorSpreadMethod::Pad);
1582 assert_eq!(g.p1, [0.0, 0.0]);
1583 assert_eq!(g.p2, [100.0, 0.0]);
1584 }
1585 other => panic!("expected linear gradient, got {other:?}"),
1586 }
1587 }
1588
1589 #[test]
1590 fn bakes_gradient_into_per_vertex_colors() {
1591 let asset = parse_svg_asset(
1592 r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
1593 <defs>
1594 <linearGradient id="g" x1="0" y1="0" x2="100" y2="0" gradientUnits="userSpaceOnUse">
1595 <stop offset="0" stop-color="#ff0000"/>
1596 <stop offset="1" stop-color="#0000ff"/>
1597 </linearGradient>
1598 </defs>
1599 <rect width="100" height="100" fill="url(#g)"/>
1600 </svg>"##,
1601 )
1602 .unwrap();
1603 let mesh = tessellate_vector_asset(
1604 &asset,
1605 VectorMeshOptions::icon(
1606 crate::tree::Rect::new(0.0, 0.0, 200.0, 200.0),
1607 Color::srgb_u8(0, 0, 0),
1608 2.0,
1609 ColorSpace::SRGB_LINEAR,
1610 ),
1611 );
1612 assert!(!mesh.vertices.is_empty());
1613
1614 let mut min_x_vert = mesh.vertices[0];
1618 let mut max_x_vert = mesh.vertices[0];
1619 for v in &mesh.vertices {
1620 if v.local[0] < min_x_vert.local[0] {
1621 min_x_vert = *v;
1622 }
1623 if v.local[0] > max_x_vert.local[0] {
1624 max_x_vert = *v;
1625 }
1626 }
1627 assert!(
1628 min_x_vert.color[0] > min_x_vert.color[2],
1629 "left edge should be redder: {:?}",
1630 min_x_vert.color
1631 );
1632 assert!(
1633 max_x_vert.color[2] > max_x_vert.color[0],
1634 "right edge should be bluer: {:?}",
1635 max_x_vert.color
1636 );
1637 }
1638
1639 #[test]
1640 fn has_gradient_distinguishes_flat_from_gradient_assets() {
1641 let flat = parse_svg_asset(
1642 r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4" fill="#fff"/></svg>"##,
1643 )
1644 .unwrap();
1645 assert!(!flat.has_gradient());
1646
1647 let gradient = parse_svg_asset(
1648 r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
1649 <defs><linearGradient id="g" x1="0" y1="0" x2="100" y2="0" gradientUnits="userSpaceOnUse">
1650 <stop offset="0" stop-color="#ff0000"/><stop offset="1" stop-color="#0000ff"/>
1651 </linearGradient></defs>
1652 <rect width="100" height="100" fill="url(#g)"/>
1653 </svg>"##,
1654 )
1655 .unwrap();
1656 assert!(gradient.has_gradient());
1657 }
1658
1659 #[test]
1660 fn parses_pipewire_volume_icon_with_all_gradients() {
1661 let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024">
1665 <defs>
1666 <linearGradient id="arcGradient" x1="210" y1="720" x2="805" y2="260" gradientUnits="userSpaceOnUse">
1667 <stop offset="0" stop-color="#0667ff"/>
1668 <stop offset="0.52" stop-color="#139cff"/>
1669 <stop offset="1" stop-color="#11e4dc"/>
1670 </linearGradient>
1671 <linearGradient id="dotGradient" x1="585" y1="780" x2="805" y2="455" gradientUnits="userSpaceOnUse">
1672 <stop offset="0" stop-color="#065eff"/>
1673 <stop offset="0.55" stop-color="#0d9fff"/>
1674 <stop offset="1" stop-color="#10e5dc"/>
1675 </linearGradient>
1676 <radialGradient id="knobFace" cx="42%" cy="36%" r="72%">
1677 <stop offset="0" stop-color="#12366c"/>
1678 <stop offset="0.42" stop-color="#0b2554"/>
1679 <stop offset="1" stop-color="#071736"/>
1680 </radialGradient>
1681 <linearGradient id="knobRim" x1="320" y1="310" x2="735" y2="740" gradientUnits="userSpaceOnUse">
1682 <stop offset="0" stop-color="#214f9b"/>
1683 <stop offset="0.48" stop-color="#17386f"/>
1684 <stop offset="1" stop-color="#285aa7"/>
1685 </linearGradient>
1686 <linearGradient id="needleGradient" x1="565" y1="425" x2="670" y2="320" gradientUnits="userSpaceOnUse">
1687 <stop offset="0" stop-color="#0872ff"/>
1688 <stop offset="1" stop-color="#168aff"/>
1689 </linearGradient>
1690 </defs>
1691 <path d="M 296 720 A 300 300 0 1 1 794 409" fill="none" stroke="url(#arcGradient)" stroke-width="36" stroke-linecap="round"/>
1692 <circle cx="512" cy="512" r="210" fill="url(#knobRim)"/>
1693 <circle cx="512" cy="512" r="192" fill="url(#knobFace)"/>
1694 <line x1="569" y1="433" x2="663" y2="339" stroke="url(#needleGradient)" stroke-width="30" stroke-linecap="round"/>
1695 <circle cx="612" cy="787" r="13" fill="url(#dotGradient)"/>
1696 <circle cx="664" cy="764" r="14" fill="url(#dotGradient)"/>
1697</svg>"##;
1698 let asset = parse_svg_asset(svg).unwrap();
1699 assert_eq!(asset.paths.len(), 6);
1701 assert!(asset.gradients.len() >= 5);
1706
1707 let mesh = tessellate_vector_asset(
1708 &asset,
1709 VectorMeshOptions::icon(
1710 crate::tree::Rect::new(0.0, 0.0, 256.0, 256.0),
1711 Color::srgb_u8(0, 0, 0),
1712 2.0,
1713 ColorSpace::SRGB_LINEAR,
1714 ),
1715 );
1716 assert!(!mesh.vertices.is_empty());
1717 let any_lit = mesh
1720 .vertices
1721 .iter()
1722 .any(|v| v.color[0] + v.color[1] + v.color[2] > 0.01);
1723 assert!(any_lit, "no lit vertices — gradients did not render");
1724 }
1725
1726 #[test]
1731 fn vertex_colors_convert_into_the_working_space() {
1732 let p3 = ColorSpace::DISPLAY_P3_LINEAR;
1733 let red = Color::srgb_u8(255, 0, 0);
1734 let expected = rgba_f32_in(red, p3);
1735 assert!(expected[1] > 0.01, "P3 red must have green > 0");
1736
1737 let solid = parse_svg_asset(
1739 r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" fill="#ff0000"/></svg>"##,
1740 )
1741 .unwrap();
1742 let mesh = tessellate_vector_asset(
1743 &solid,
1744 VectorMeshOptions::icon(
1745 crate::tree::Rect::new(0.0, 0.0, 100.0, 100.0),
1746 Color::srgb_u8(0, 0, 0),
1747 2.0,
1748 p3,
1749 ),
1750 );
1751 for v in &mesh.vertices {
1752 for (got, want) in v.color.iter().zip(&expected) {
1753 assert!(
1754 (got - want).abs() < 1e-5,
1755 "solid fill should pack P3-converted red, got {:?} want {expected:?}",
1756 v.color
1757 );
1758 }
1759 }
1760
1761 let grad = parse_svg_asset(
1764 r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
1765 <defs>
1766 <linearGradient id="g" x1="0" y1="0" x2="100" y2="0" gradientUnits="userSpaceOnUse">
1767 <stop offset="0" stop-color="#ff0000"/>
1768 <stop offset="1" stop-color="#ff0000"/>
1769 </linearGradient>
1770 </defs>
1771 <rect width="100" height="100" fill="url(#g)"/>
1772 </svg>"##,
1773 )
1774 .unwrap();
1775 let mesh = tessellate_vector_asset(
1776 &grad,
1777 VectorMeshOptions::icon(
1778 crate::tree::Rect::new(0.0, 0.0, 100.0, 100.0),
1779 Color::srgb_u8(0, 0, 0),
1780 2.0,
1781 p3,
1782 ),
1783 );
1784 assert!(!mesh.vertices.is_empty());
1785 for v in &mesh.vertices {
1786 for (got, want) in v.color.iter().zip(&expected) {
1787 assert!(
1788 (got - want).abs() < 1e-4,
1789 "gradient sample should be P3-converted red, got {:?} want {expected:?}",
1790 v.color
1791 );
1792 }
1793 }
1794 }
1795}