1use crate::svg::{
2 FillRule, Seg, Stroke, StrokeLineCap, StrokeLineJoin, SubPath, SvgIcon, SvgPoint,
3};
4
5#[derive(Clone, Debug, PartialEq, Eq)]
6pub struct Sprite {
7 pub width: u32,
8
9 pub height: u32,
10
11 pub alpha: Vec<u8>,
12}
13
14impl Sprite {
15 pub fn is_blank(&self) -> bool {
16 self.alpha.iter().all(|&a| a == 0)
17 }
18}
19
20const SS: u32 = 8;
21
22pub const MAX_RASTER_PX: u32 = 512;
23
24const FLATTEN_TOL_PX: f32 = 0.15;
25
26pub(crate) fn rasterize(icon: &SvgIcon, size_px: u32) -> Sprite {
27 let dim = size_px.clamp(1, MAX_RASTER_PX);
28 let size = dim as f32;
29 let [vx, vy, vw, vh] = icon.viewbox;
30 let (vw, vh) = (vw.max(f32::EPSILON), vh.max(f32::EPSILON));
31
32 let s = size / vw.max(vh);
33
34 let off_x = ((size - vw * s) * 0.5 - vx * s).round();
35 let off_y = ((size - vh * s) * 0.5 - vy * s).round();
36 let tx = |p: SvgPoint| SvgPoint::new(p.x * s + off_x, p.y * s + off_y);
37
38 let mut coverage = vec![0u8; dim as usize * dim as usize];
39 for shape in &icon.shapes {
40 if shape.fill {
41 let polys: Vec<Vec<SvgPoint>> = shape
42 .subpaths
43 .iter()
44 .map(|sp| {
45 let transformed = transform_subpath(sp, &tx);
46 flatten_subpath(&transformed, FLATTEN_TOL_PX)
47 })
48 .collect();
49 if !polys.is_empty() {
50 let cov = shape_coverage(&polys, dim, shape.fill_rule);
51 for (dst, src) in coverage.iter_mut().zip(cov) {
52 *dst = (*dst).max(src);
53 }
54 }
55 }
56
57 if let Some(stroke) = &shape.stroke {
58 let width_px = stroke.width * s;
59 if width_px.is_finite() && width_px > 1e-6 {
60 let dash_px: Vec<f32> = stroke.dash_array.iter().map(|v| v * s).collect();
61 let offset_px = stroke.dash_offset * s;
62 let mut stroke_polys: Vec<Vec<SvgPoint>> = Vec::new();
63 for sp in &shape.subpaths {
64 let transformed = transform_subpath(sp, &tx);
65 let (points, closed) = flatten_stroke_subpath(&transformed, FLATTEN_TOL_PX);
66 if points.len() < 2 {
67 if points.len() == 1 && !closed && stroke.cap == StrokeLineCap::Round {
68 stroke_polys.push(dot_poly(points[0], width_px * 0.5));
69 }
70 continue;
71 }
72 let pieces = dash_polyline(&points, closed, &dash_px, offset_px);
73 for (piece, piece_closed) in pieces {
74 if piece.len() < 2 {
75 continue;
76 }
77 stroke_polyline(&piece, piece_closed, width_px, stroke, &mut stroke_polys);
78 }
79 }
80 if !stroke_polys.is_empty() {
81 let cov = shape_coverage(&stroke_polys, dim, FillRule::NonZero);
82 for (dst, src) in coverage.iter_mut().zip(cov) {
83 *dst = (*dst).max(src);
84 }
85 }
86 }
87 }
88 }
89
90 Sprite {
91 width: dim,
92 height: dim,
93 alpha: coverage,
94 }
95}
96
97fn transform_subpath(sp: &SubPath, tx: &impl Fn(SvgPoint) -> SvgPoint) -> SubPath {
98 SubPath {
99 start: tx(sp.start),
100 segs: sp
101 .segs
102 .iter()
103 .map(|seg| match *seg {
104 Seg::Line { to } => Seg::Line { to: tx(to) },
105 Seg::Quad { ctrl, to } => Seg::Quad {
106 ctrl: tx(ctrl),
107 to: tx(to),
108 },
109 Seg::Cubic { c1, c2, to } => Seg::Cubic {
110 c1: tx(c1),
111 c2: tx(c2),
112 to: tx(to),
113 },
114 Seg::Close => Seg::Close,
115 })
116 .collect(),
117 }
118}
119
120fn flatten_subpath(sp: &SubPath, tol: f32) -> Vec<SvgPoint> {
121 let mut pts = vec![sp.start];
122 let mut cur = sp.start;
123 for seg in &sp.segs {
124 match *seg {
125 Seg::Line { to } => {
126 pts.push(to);
127 cur = to;
128 }
129 Seg::Quad { ctrl, to } => {
130 let err = dist_to_chord(ctrl, cur, to);
131 let steps = steps_for(err, tol);
132 for i in 1..=steps {
133 let t = i as f32 / steps as f32;
134 pts.push(eval_quad(cur, ctrl, to, t));
135 }
136 cur = to;
137 }
138 Seg::Cubic { c1, c2, to } => {
139 let err = dist_to_chord(c1, cur, to).max(dist_to_chord(c2, cur, to));
140 let steps = steps_for(err, tol);
141 for i in 1..=steps {
142 let t = i as f32 / steps as f32;
143 pts.push(eval_cubic(cur, c1, c2, to, t));
144 }
145 cur = to;
146 }
147 Seg::Close => {
148 if pts.last() != Some(&sp.start) {
149 pts.push(sp.start);
150 }
151 cur = sp.start;
152 }
153 }
154 }
155 pts
156}
157
158fn steps_for(err: f32, tol: f32) -> usize {
159 ((err / tol).sqrt().ceil() as usize).clamp(1, 64)
160}
161
162fn dist_to_chord(ctrl: SvgPoint, from: SvgPoint, to: SvgPoint) -> f32 {
163 let dx = to.x - from.x;
164 let dy = to.y - from.y;
165 let len2 = dx * dx + dy * dy;
166 if len2 < f32::EPSILON {
167 return (ctrl.x - from.x).abs() + (ctrl.y - from.y).abs();
168 }
169 let t = (((ctrl.x - from.x) * dx + (ctrl.y - from.y) * dy) / len2).clamp(0.0, 1.0);
170 let px = from.x + t * dx;
171 let py = from.y + t * dy;
172 ((ctrl.x - px).powi(2) + (ctrl.y - py).powi(2)).sqrt()
173}
174
175fn eval_quad(p0: SvgPoint, c: SvgPoint, p1: SvgPoint, t: f32) -> SvgPoint {
176 let u = 1.0 - t;
177 SvgPoint::new(
178 u * u * p0.x + 2.0 * u * t * c.x + t * t * p1.x,
179 u * u * p0.y + 2.0 * u * t * c.y + t * t * p1.y,
180 )
181}
182
183fn eval_cubic(p0: SvgPoint, c1: SvgPoint, c2: SvgPoint, p1: SvgPoint, t: f32) -> SvgPoint {
184 let u = 1.0 - t;
185 SvgPoint::new(
186 u * u * u * p0.x + 3.0 * u * u * t * c1.x + 3.0 * u * t * t * c2.x + t * t * t * p1.x,
187 u * u * u * p0.y + 3.0 * u * u * t * c1.y + 3.0 * u * t * t * c2.y + t * t * t * p1.y,
188 )
189}
190
191fn flatten_stroke_subpath(sp: &SubPath, tol: f32) -> (Vec<SvgPoint>, bool) {
192 let closed = matches!(sp.segs.last(), Some(Seg::Close));
193 let mut pts = vec![sp.start];
194 let mut cur = sp.start;
195 for seg in &sp.segs {
196 match *seg {
197 Seg::Line { to } => {
198 pts.push(to);
199 cur = to;
200 }
201 Seg::Quad { ctrl, to } => {
202 let err = dist_to_chord(ctrl, cur, to);
203 let steps = steps_for(err, tol);
204 for i in 1..=steps {
205 let t = i as f32 / steps as f32;
206 pts.push(eval_quad(cur, ctrl, to, t));
207 }
208 cur = to;
209 }
210 Seg::Cubic { c1, c2, to } => {
211 let err = dist_to_chord(c1, cur, to).max(dist_to_chord(c2, cur, to));
212 let steps = steps_for(err, tol);
213 for i in 1..=steps {
214 let t = i as f32 / steps as f32;
215 pts.push(eval_cubic(cur, c1, c2, to, t));
216 }
217 cur = to;
218 }
219 Seg::Close => {
220 cur = sp.start;
221 }
222 }
223 }
224
225 if closed && pts.len() >= 2 && pts.last() == Some(&sp.start) {
226 pts.pop();
227 }
228
229 let mut clean: Vec<SvgPoint> = Vec::with_capacity(pts.len());
230 for p in pts {
231 if clean.last() != Some(&p) {
232 clean.push(p);
233 }
234 }
235
236 (clean, closed)
237}
238
239fn dash_polyline(
240 points: &[SvgPoint],
241 closed: bool,
242 dash_array: &[f32],
243 dash_offset: f32,
244) -> Vec<(Vec<SvgPoint>, bool)> {
245 let norm: Vec<f32> = dash_array
246 .iter()
247 .copied()
248 .filter(|v| v.is_finite() && *v > 1e-4)
249 .collect();
250 if norm.is_empty() {
251 return vec![(points.to_vec(), closed)];
252 }
253
254 let mut pattern = norm;
255 if pattern.len() % 2 == 1 {
256 let copy = pattern.clone();
257 pattern.extend(copy);
258 }
259 let total: f32 = pattern.iter().sum();
260 if !(total.is_finite()) || total <= 1e-4 {
261 return vec![(points.to_vec(), closed)];
262 }
263
264 let mut idx = 0usize;
265 let mut remaining: f32;
266 let mut on: bool;
267 let mut pos = if dash_offset.is_finite() {
268 dash_offset.rem_euclid(total)
269 } else {
270 0.0
271 };
272 loop {
273 let len = pattern[idx];
274 if pos < len {
275 remaining = len - pos;
276 on = idx.is_multiple_of(2);
277 break;
278 }
279 pos -= len;
280 idx = (idx + 1) % pattern.len();
281 }
282 let started_on = on;
283
284 let mut segs: Vec<(SvgPoint, SvgPoint)> = Vec::new();
285 for w in points.windows(2) {
286 if direction(w[0], w[1]).is_some() {
287 segs.push((w[0], w[1]));
288 }
289 }
290 if closed && points.len() >= 2 {
291 let (a, b) = (points[points.len() - 1], points[0]);
292 if direction(a, b).is_some() {
293 segs.push((a, b));
294 }
295 }
296 if segs.is_empty() {
297 return Vec::new();
298 }
299
300 let mut out: Vec<(Vec<SvgPoint>, bool)> = Vec::new();
301 let mut current: Vec<SvgPoint> = Vec::new();
302
303 for (a, b) in segs {
304 let seg_len = ((b.x - a.x).powi(2) + (b.y - a.y).powi(2)).sqrt();
305 if !(seg_len.is_finite()) || seg_len <= 1e-6 {
306 continue;
307 }
308 let mut travelled = 0.0f32;
309
310 while travelled < seg_len - 1e-4 {
311 let step = remaining.min(seg_len - travelled);
312 let t0 = travelled / seg_len;
313 let t1 = (travelled + step) / seg_len;
314 let p0 = SvgPoint::new(a.x + (b.x - a.x) * t0, a.y + (b.y - a.y) * t0);
315 let p1 = SvgPoint::new(a.x + (b.x - a.x) * t1, a.y + (b.y - a.y) * t1);
316 if on {
317 if current.is_empty() {
318 current.push(p0);
319 }
320
321 if current.last() != Some(&p1) {
322 current.push(p1);
323 }
324 } else if current.is_empty() {
325 }
326 let before = travelled;
327 travelled += step;
328 remaining -= step;
329 if !(travelled > before) {
330 break;
331 }
332 if remaining <= 1e-4 {
333 if on {
334 if current.len() >= 2 {
335 out.push((std::mem::take(&mut current), false));
336 } else {
337 current.clear();
338 }
339 } else {
340 current = Vec::new();
341 }
342 on = !on;
343 idx = (idx + 1) % pattern.len();
344 remaining = pattern[idx];
345
346 if on && travelled < seg_len - 1e-4 {
347 current.push(p1);
348 }
349 }
350 }
351 }
352 if on && current.len() >= 2 {
353 out.push((current, false));
354 }
355
356 if closed && started_on && on && out.len() >= 2 {
357 let first = out.remove(0);
358 if let Some(last) = out.last_mut() {
359 let mut merged = std::mem::take(&mut last.0);
360 for p in first.0.into_iter().skip(1) {
361 if merged.last() != Some(&p) {
362 merged.push(p);
363 }
364 }
365 last.0 = merged;
366 } else {
367 out.push(first);
368 }
369 }
370 out
371}
372
373fn stroke_polyline(
374 points: &[SvgPoint],
375 closed: bool,
376 width_px: f32,
377 stroke: &Stroke,
378 out: &mut Vec<Vec<SvgPoint>>,
379) {
380 let half = width_px * 0.5;
381 if !(half.is_finite()) || half <= 0.0 {
382 return;
383 }
384
385 let mut segs: Vec<(SvgPoint, SvgPoint)> = Vec::new();
386 for w in points.windows(2) {
387 if direction(w[0], w[1]).is_some() {
388 segs.push((w[0], w[1]));
389 }
390 }
391 if closed && points.len() >= 2 {
392 let (a, b) = (points[points.len() - 1], points[0]);
393 if direction(a, b).is_some() {
394 segs.push((a, b));
395 }
396 }
397 if segs.is_empty() {
398 return;
399 }
400
401 for &(a, b) in &segs {
402 push_segment_quad(a, b, half, out);
403 }
404
405 let joint_count = if closed {
406 segs.len()
407 } else {
408 segs.len().saturating_sub(1)
409 };
410 for i in 0..joint_count {
411 let (_, v) = segs[i];
412 let next = segs[(i + 1) % segs.len()];
413 let (Some(d0), Some(d1)) = (direction(segs[i].0, v), direction(v, next.1)) else {
414 continue;
415 };
416 push_join(v, d0, d1, half, stroke, out);
417 }
418
419 if !closed
420 && stroke.cap != StrokeLineCap::Butt
421 && let (Some(first), Some(last)) = (segs.first(), segs.last())
422 {
423 if let Some(d) = direction(first.0, first.1) {
424 push_cap(first.0, SvgPoint::new(-d.x, -d.y), half, stroke.cap, out);
425 }
426 if let Some(d) = direction(last.0, last.1) {
427 push_cap(last.1, d, half, stroke.cap, out);
428 }
429 }
430}
431
432fn dot_poly(center: SvgPoint, radius: f32) -> Vec<SvgPoint> {
433 let steps = arc_steps(radius, std::f32::consts::TAU, FLATTEN_TOL_PX).max(8);
434 let mut poly = Vec::with_capacity(steps);
435 for i in 0..steps {
436 let a = i as f32 / steps as f32 * std::f32::consts::TAU;
437 poly.push(SvgPoint::new(
438 center.x + a.cos() * radius,
439 center.y + a.sin() * radius,
440 ));
441 }
442 poly
443}
444
445fn push_segment_quad(a: SvgPoint, b: SvgPoint, half: f32, out: &mut Vec<Vec<SvgPoint>>) {
446 let Some(d) = direction(a, b) else {
447 return;
448 };
449 let n = SvgPoint::new(d.y, -d.x);
450 let quad = vec![
451 SvgPoint::new(a.x + n.x * half, a.y + n.y * half),
452 SvgPoint::new(b.x + n.x * half, b.y + n.y * half),
453 SvgPoint::new(b.x - n.x * half, b.y - n.y * half),
454 SvgPoint::new(a.x - n.x * half, a.y - n.y * half),
455 ];
456 push_poly(quad, out);
457}
458
459fn push_join(
460 v: SvgPoint,
461 d0: SvgPoint,
462 d1: SvgPoint,
463 half: f32,
464 stroke: &Stroke,
465 out: &mut Vec<Vec<SvgPoint>>,
466) {
467 let cross = d0.x * d1.y - d0.y * d1.x;
468 if cross.abs() < 1e-6 && d0.x * d1.x + d0.y * d1.y > 0.0 {
469 return;
470 }
471 let side = if cross >= 0.0 { 1.0 } else { -1.0 };
472 let n0 = SvgPoint::new(d0.y * side, -d0.x * side);
473 let n1 = SvgPoint::new(d1.y * side, -d1.x * side);
474 let m0 = SvgPoint::new(v.x + n0.x * half, v.y + n0.y * half);
475 let m1 = SvgPoint::new(v.x + n1.x * half, v.y + n1.y * half);
476 match stroke.join {
477 StrokeLineJoin::Round => {
478 let a0 = n0.y.atan2(n0.x);
479 let a1 = n1.y.atan2(n1.x);
480 let delta = wrap_pi(a1 - a0);
481 let steps = arc_steps(half, delta.abs(), FLATTEN_TOL_PX).max(1);
482 let mut poly = Vec::with_capacity(steps + 2);
483 poly.push(v);
484 for i in 0..=steps {
485 let a = a0 + delta * (i as f32 / steps as f32);
486 poly.push(SvgPoint::new(v.x + a.cos() * half, v.y + a.sin() * half));
487 }
488 push_poly(poly, out);
489 }
490 StrokeLineJoin::Bevel => {
491 push_poly(vec![v, m0, m1], out);
492 }
493 StrokeLineJoin::Miter => match line_intersection(m0, d0, m1, d1) {
494 Some(tip) => {
495 let limit = stroke.miter_limit.max(1.0) * half;
496 let dist = ((tip.x - v.x).powi(2) + (tip.y - v.y).powi(2)).sqrt();
497 if dist.is_finite() && dist <= limit {
498 push_poly(vec![v, m0, tip, m1], out);
499 } else {
500 push_poly(vec![v, m0, m1], out);
501 }
502 }
503 None => push_poly(vec![v, m0, m1], out),
504 },
505 }
506}
507
508fn push_cap(
509 end: SvgPoint,
510 dir: SvgPoint,
511 half: f32,
512 cap: StrokeLineCap,
513 out: &mut Vec<Vec<SvgPoint>>,
514) {
515 let len = (dir.x * dir.x + dir.y * dir.y).sqrt();
516 if !(len.is_finite()) || len <= f32::MIN_POSITIVE {
517 return;
518 }
519 let d = SvgPoint::new(dir.x / len, dir.y / len);
520 let n = SvgPoint::new(d.y, -d.x);
521 match cap {
522 StrokeLineCap::Butt => {}
523 StrokeLineCap::Square => {
524 let e = SvgPoint::new(end.x + d.x * half, end.y + d.y * half);
525 push_poly(
526 vec![
527 SvgPoint::new(end.x + n.x * half, end.y + n.y * half),
528 SvgPoint::new(e.x + n.x * half, e.y + n.y * half),
529 SvgPoint::new(e.x - n.x * half, e.y - n.y * half),
530 SvgPoint::new(end.x - n.x * half, end.y - n.y * half),
531 ],
532 out,
533 );
534 }
535 StrokeLineCap::Round => {
536 let a0 = n.y.atan2(n.x);
537 let steps = arc_steps(half, std::f32::consts::PI, FLATTEN_TOL_PX).max(4);
538 let mut poly = Vec::with_capacity(steps + 1);
539 for i in 0..=steps {
540 let a = a0 + std::f32::consts::PI * (i as f32 / steps as f32);
541 poly.push(SvgPoint::new(
542 end.x + a.cos() * half,
543 end.y + a.sin() * half,
544 ));
545 }
546 push_poly(poly, out);
547 }
548 }
549}
550
551fn push_poly(points: Vec<SvgPoint>, out: &mut Vec<Vec<SvgPoint>>) {
552 if points.len() < 3 {
553 return;
554 }
555 let area = signed_area(&points);
556 if !(area.is_finite()) || area.abs() <= 1e-9 {
557 return;
558 }
559 if area < 0.0 {
560 let mut rev = points;
561 rev.reverse();
562 out.push(rev);
563 } else {
564 out.push(points);
565 }
566}
567
568fn signed_area(points: &[SvgPoint]) -> f32 {
569 let mut sum = 0.0;
570 for i in 0..points.len() {
571 let a = points[i];
572 let b = points[(i + 1) % points.len()];
573 sum += a.x * b.y - b.x * a.y;
574 }
575 sum * 0.5
576}
577
578fn direction(a: SvgPoint, b: SvgPoint) -> Option<SvgPoint> {
579 let dx = b.x - a.x;
580 let dy = b.y - a.y;
581 let len = (dx * dx + dy * dy).sqrt();
582 if !len.is_finite() || len <= 1e-6 {
583 return None;
584 }
585 Some(SvgPoint::new(dx / len, dy / len))
586}
587
588fn wrap_pi(angle: f32) -> f32 {
589 let two_pi = std::f32::consts::TAU;
590 let mut a = angle % two_pi;
591 if a > std::f32::consts::PI {
592 a -= two_pi;
593 } else if a <= -std::f32::consts::PI {
594 a += two_pi;
595 }
596 a
597}
598
599fn arc_steps(radius: f32, span_rad: f32, tol: f32) -> usize {
600 if !(radius.is_finite()) || !(span_rad.is_finite()) || radius <= tol {
601 return 1;
602 }
603 let cos_half = (1.0 - tol / radius).clamp(-1.0, 1.0);
604 let max_step = 2.0 * cos_half.acos();
605 if !(max_step.is_finite()) || max_step <= 0.0 {
606 return 1;
607 }
608 ((span_rad.abs() / max_step).ceil() as usize).clamp(1, 64)
609}
610
611fn line_intersection(p: SvgPoint, d: SvgPoint, q: SvgPoint, e: SvgPoint) -> Option<SvgPoint> {
612 let denom = d.x * e.y - d.y * e.x;
613 if !denom.is_finite() || denom.abs() < 1e-6 {
614 return None;
615 }
616 let t = ((q.x - p.x) * e.y - (q.y - p.y) * e.x) / denom;
617 let pt = SvgPoint::new(p.x + d.x * t, p.y + d.y * t);
618 if pt.x.is_finite() && pt.y.is_finite() {
619 Some(pt)
620 } else {
621 None
622 }
623}
624
625fn shape_coverage(polys: &[Vec<SvgPoint>], size: u32, rule: FillRule) -> Vec<u8> {
626 let mut cov = vec![0u8; (size * size) as usize];
627 let step = 1.0 / SS as f32;
628 let samples = (SS * SS) as f32;
629 for py in 0..size {
630 for px in 0..size {
631 let mut inside = 0u32;
632 for sy in 0..SS {
633 for sx in 0..SS {
634 let p = SvgPoint::new(
635 px as f32 + (sx as f32 + 0.5) * step,
636 py as f32 + (sy as f32 + 0.5) * step,
637 );
638 let winding = winding_at(polys, p);
639 let hit = match rule {
640 FillRule::NonZero => winding != 0,
641 FillRule::EvenOdd => winding % 2 != 0,
642 };
643 if hit {
644 inside += 1;
645 }
646 }
647 }
648 let alpha = (inside as f32 / samples * 255.0).round() as u8;
649 cov[(py * size + px) as usize] = alpha;
650 }
651 }
652 cov
653}
654
655fn winding_at(polys: &[Vec<SvgPoint>], p: SvgPoint) -> i32 {
656 let mut winding = 0i32;
657 for poly in polys {
658 if poly.len() < 2 {
659 continue;
660 }
661 let mut a = poly[poly.len() - 1];
662 for &b in poly {
663 if (a.y > p.y) != (b.y > p.y) {
664 let t = (p.y - a.y) / (b.y - a.y);
665 let x = a.x + t * (b.x - a.x);
666 if x > p.x {
667 winding += if b.y > a.y { 1 } else { -1 };
668 }
669 }
670 a = b;
671 }
672 }
673 winding
674}
675
676#[cfg(test)]
677mod tests {
678 use super::*;
679 use crate::svg::{FillRule, Shape, SubPath, parse_svg};
680
681 fn square_sub(x: f32, y: f32, w: f32, h: f32) -> SubPath {
682 SubPath {
683 start: SvgPoint::new(x, y),
684 segs: vec![
685 Seg::Line {
686 to: SvgPoint::new(x + w, y),
687 },
688 Seg::Line {
689 to: SvgPoint::new(x + w, y + h),
690 },
691 Seg::Line {
692 to: SvgPoint::new(x, y + h),
693 },
694 Seg::Close,
695 ],
696 }
697 }
698
699 fn square_icon(x: f32, y: f32, w: f32, h: f32, rule: FillRule) -> SvgIcon {
700 SvgIcon {
701 viewbox: [0.0, 0.0, 10.0, 10.0],
702 shapes: vec![Shape::filled(vec![square_sub(x, y, w, h)], rule)],
703 }
704 }
705
706 #[test]
707 fn full_square_is_opaque() {
708 let sprite = rasterize(&square_icon(0.0, 0.0, 10.0, 10.0, FillRule::NonZero), 20);
709 assert_eq!(sprite.width, 20);
710 assert!(sprite.alpha.iter().all(|&a| a == 255));
711 assert!(!sprite.is_blank());
712 }
713
714 #[test]
715 fn half_square_is_about_half_coverage() {
716 let sprite = rasterize(&square_icon(0.0, 0.0, 5.0, 10.0, FillRule::NonZero), 20);
717 for py in 0..10u32 {
718 for px in 0..20u32 {
719 let a = sprite.alpha[(py * 20 + px) as usize];
720 if px < 10 {
721 assert_eq!(a, 255, "left half full ({px},{py})");
722 } else {
723 assert_eq!(a, 0, "right half empty ({px},{py})");
724 }
725 }
726 }
727 }
728
729 #[test]
730 fn edge_pixels_are_antialiased() {
731 let sprite = rasterize(&square_icon(0.0, 0.0, 5.0, 5.0, FillRule::NonZero), 15);
732 let full: u32 = sprite.alpha.iter().filter(|&&a| a == 255).count() as u32;
733 let empty: u32 = sprite.alpha.iter().filter(|&&a| a == 0).count() as u32;
734 let mid: u32 = sprite.alpha.iter().filter(|&&a| a > 0 && a < 255).count() as u32;
735 assert!(mid > 0, "edge has intermediate pixels");
736 assert_eq!(full + empty + mid, 225);
737
738 let sum: u32 = sprite.alpha.iter().map(|&a| a as u32).sum();
739 let approx = sum / 255;
740 assert!(
741 (52..=61).contains(&approx),
742 "coverage ~= 56px, got {approx}"
743 );
744 }
745
746 #[test]
747 fn evenodd_makes_hole_nonzero_does_not() {
748 let sub = |x: f32, y: f32, s: f32| SubPath {
749 start: SvgPoint::new(x, y),
750 segs: vec![
751 Seg::Line {
752 to: SvgPoint::new(x + s, y),
753 },
754 Seg::Line {
755 to: SvgPoint::new(x + s, y + s),
756 },
757 Seg::Line {
758 to: SvgPoint::new(x, y + s),
759 },
760 Seg::Close,
761 ],
762 };
763 let icon = SvgIcon {
764 viewbox: [0.0, 0.0, 10.0, 10.0],
765 shapes: vec![Shape::filled(
766 vec![sub(1.0, 1.0, 8.0), sub(3.0, 3.0, 4.0)],
767 FillRule::EvenOdd,
768 )],
769 };
770 let sprite = rasterize(&icon, 20);
771 let center = sprite.alpha[(10 * 20 + 10) as usize];
772 assert_eq!(center, 0, "hole center empty (evenodd)");
773 let edge = sprite.alpha[(2 * 20 + 2) as usize];
774 assert_eq!(edge, 255, "ring full");
775
776 let mut icon2 = icon.clone();
777 icon2.shapes[0].fill_rule = FillRule::NonZero;
778 let sprite2 = rasterize(&icon2, 20);
779 assert_eq!(
780 sprite2.alpha[(10 * 20 + 10) as usize],
781 255,
782 "nonzero fills the hole"
783 );
784 }
785
786 #[test]
787 fn circle_from_svg_rounds_corners() {
788 let icon = parse_svg(r#"<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/></svg>"#)
789 .unwrap();
790 let sprite = rasterize(&icon, 24);
791 assert_eq!(sprite.alpha[0], 0, "corner empty");
792 assert_eq!(sprite.alpha[(12 * 24 + 12) as usize], 255, "center full");
793
794 let sum: u32 = sprite.alpha.iter().map(|&a| a as u32).sum();
795 let approx = sum / 255;
796 assert!(
797 (300..=328).contains(&approx),
798 "circle area ~= 314px, got {approx}"
799 );
800 }
801
802 #[test]
803 fn oversize_request_is_clamped_not_oom() {
804 let icon = square_icon(0.0, 0.0, 10.0, 10.0, FillRule::NonZero);
805 let sprite = rasterize(&icon, u32::MAX);
806 assert_eq!(
807 (sprite.width, sprite.height),
808 (MAX_RASTER_PX, MAX_RASTER_PX)
809 );
810 assert_eq!(
811 sprite.alpha.len(),
812 MAX_RASTER_PX as usize * MAX_RASTER_PX as usize
813 );
814 assert_eq!(rasterize(&icon, 0).width, 1, "zero to minimum 1");
815 }
816
817 #[test]
818 fn overlapping_shapes_are_union() {
819 let icon = SvgIcon {
820 viewbox: [0.0, 0.0, 10.0, 10.0],
821 shapes: vec![
822 Shape::filled(vec![square_sub(0.0, 0.0, 6.0, 10.0)], FillRule::NonZero),
823 Shape::filled(vec![square_sub(4.0, 0.0, 6.0, 10.0)], FillRule::NonZero),
824 ],
825 };
826 let sprite = rasterize(&icon, 10);
827 assert!(sprite.alpha.iter().all(|&a| a == 255), "union = full rect");
828 }
829
830 #[test]
831 fn centering_keeps_aspect() {
832 let icon =
833 parse_svg(r#"<svg viewBox="0 0 20 10"><rect width="20" height="10"/></svg>"#).unwrap();
834 let sprite = rasterize(&icon, 20);
835 assert!(sprite.alpha[..20 * 5].iter().all(|&a| a == 0), "top margin");
836 assert!(
837 sprite.alpha[20 * 5..20 * 15].iter().all(|&a| a == 255),
838 "center full"
839 );
840 assert!(
841 sprite.alpha[20 * 15..].iter().all(|&a| a == 0),
842 "bottom margin"
843 );
844 }
845
846 #[test]
847 fn outline_line_is_not_blank() {
848 let icon = parse_svg(
849 r#"<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12 H19"/></svg>"#,
850 )
851 .unwrap();
852 let sprite = rasterize(&icon, 24);
853 assert!(!sprite.is_blank(), "outline icon must not be blank");
854
855 assert_eq!(sprite.alpha[(12 * 24 + 12) as usize], 255, "line center");
856 assert_eq!(sprite.alpha[0], 0, "corner empty");
857 }
858
859 #[test]
860 fn outline_circle_renders_ring() {
861 let icon = parse_svg(
862 r#"<svg viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2"><circle cx="12" cy="12" r="9"/></svg>"#,
863 )
864 .unwrap();
865 let sprite = rasterize(&icon, 48);
866 assert!(!sprite.is_blank());
867
868 assert_eq!(
869 sprite.alpha[(24 * 48 + 24) as usize],
870 0,
871 "outlined circle center empty"
872 );
873
874 let top = sprite.alpha[(6 * 48 + 24) as usize];
875 assert!(top > 128, "ring top covered, alpha={top}");
876 }
877
878 #[test]
879 fn fill_none_without_stroke_is_blank() {
880 let icon = parse_svg(r#"<svg viewBox="0 0 10 10"><path fill="none" d="M0 0 H10"/></svg>"#)
881 .unwrap();
882 assert!(icon.shapes.is_empty());
883 let empty = SvgIcon {
884 viewbox: [0.0, 0.0, 10.0, 10.0],
885 shapes: Vec::new(),
886 };
887 assert!(rasterize(&empty, 16).is_blank());
888 }
889
890 #[test]
891 fn stroke_line_element_renders() {
892 let icon = parse_svg(
893 r#"<svg viewBox="0 0 10 10"><line x1="1" y1="5" x2="9" y2="5" stroke="black" stroke-width="1"/></svg>"#,
894 )
895 .unwrap();
896 let sprite = rasterize(&icon, 20);
897 assert!(!sprite.is_blank(), "<line> paints with stroke");
898 }
899
900 #[test]
901 fn fill_and_stroke_union() {
902 let icon = parse_svg(
903 r#"<svg viewBox="0 0 10 10"><rect x="2" y="2" width="6" height="6" fill="black" stroke="black" stroke-width="2"/></svg>"#,
904 )
905 .unwrap();
906 let sprite = rasterize(&icon, 20);
907 assert!(!sprite.is_blank());
908 assert_eq!(sprite.alpha[(10 * 20 + 10) as usize], 255, "center full");
909 }
910
911 #[test]
912 fn dashed_stroke_has_gaps() {
913 let solid = parse_svg(
914 r#"<svg viewBox="0 0 10 10"><path fill="none" stroke="black" stroke-width="2" d="M1 5 H9"/></svg>"#,
915 )
916 .unwrap();
917 let dashed = parse_svg(
918 r#"<svg viewBox="0 0 10 10"><path fill="none" stroke="black" stroke-width="2" stroke-dasharray="2 2" d="M1 5 H9"/></svg>"#,
919 )
920 .unwrap();
921 let s_solid = rasterize(&solid, 40);
922 let s_dash = rasterize(&dashed, 40);
923 let sum = |s: &crate::raster::Sprite| -> u32 { s.alpha.iter().map(|&a| a as u32).sum() };
924 let (a, b) = (sum(&s_solid), sum(&s_dash));
925 assert!(b > 0, "dashed not empty");
926 assert!(b < a, "dashed has gaps: solid={a} dash={b}");
927 }
928
929 #[test]
930 fn dash_absorption_terminates() {
931 let pts = vec![SvgPoint::new(0.0, 0.0), SvgPoint::new(5.12e11, 0.0)];
932 let out = dash_polyline(&pts, false, &[1.0e6, 2.0e-4, 2.0e-4, 2.0e-4], 0.0);
933 assert_eq!(
934 out.len(),
935 1,
936 "pierwszy dash 1e6 jest widoczny, reszta przerwana"
937 );
938 }
939
940 #[test]
941 fn round_cap_extends_beyond_butt() {
942 let butt = parse_svg(
943 r#"<svg viewBox="0 0 10 10"><path fill="none" stroke="black" stroke-width="2" stroke-linecap="butt" d="M2 5 H8"/></svg>"#,
944 )
945 .unwrap();
946 let round = parse_svg(
947 r#"<svg viewBox="0 0 10 10"><path fill="none" stroke="black" stroke-width="2" stroke-linecap="round" d="M2 5 H8"/></svg>"#,
948 )
949 .unwrap();
950 let sum =
951 |icon: &SvgIcon| -> u32 { rasterize(icon, 40).alpha.iter().map(|&a| a as u32).sum() };
952 let (b, r) = (sum(&butt), sum(&round));
953 assert!(r > b, "round cap adds semicircles: butt={b} round={r}");
954 }
955}