Skip to main content

sequoia_assets/
raster.rs

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}