Skip to main content

cranpose_ui_graphics/
vector_path.rs

1//! SVG path-data (`d` attribute) parsing and CPU fill rasterization.
2//!
3//! [`VectorPath`] parses the SVG path mini-language
4//! (`M/m L/l H/h V/v C/c S/s Q/q T/t A/a Z/z`) into subpaths flattened to
5//! polylines: curves are subdivided adaptively, arcs are converted via the
6//! W3C endpoint-to-center parameterization and sampled. Fills are rendered
7//! with an anti-aliased scanline rasterizer into a coverage mask, which the
8//! draw pipeline turns into an [`crate::ImageBitmap`] primitive — so every
9//! render backend gets vector shapes without new renderer primitives.
10//!
11//! Parse once (`VectorPath::parse`), draw per frame
12//! (`DrawScope::draw_vector_path`); the one-shot
13//! `DrawScope::draw_svg_path(d, brush)` convenience re-parses each call.
14
15use thiserror::Error;
16
17use crate::geometry::{Point, Rect};
18
19/// Maximum recursion depth for adaptive curve flattening.
20const MAX_FLATTEN_DEPTH: u32 = 12;
21/// Curve flattening tolerance in path units.
22const FLATTEN_TOLERANCE: f32 = 0.05;
23/// Arc sampling: maximum angle step per segment.
24const ARC_MAX_ANGLE_STEP: f32 = std::f32::consts::PI / 16.0;
25/// Anti-aliasing sub-scanlines per pixel row.
26const SUBSAMPLES: usize = 4;
27
28/// Errors produced while parsing SVG path data.
29#[derive(Debug, Clone, PartialEq, Eq, Error)]
30pub enum SvgPathError {
31    #[error("unexpected byte {byte:?} at offset {offset}")]
32    UnexpectedByte { byte: char, offset: usize },
33    #[error("expected a number at offset {offset}")]
34    ExpectedNumber { offset: usize },
35    #[error("expected an arc flag (0 or 1) at offset {offset}")]
36    ExpectedFlag { offset: usize },
37    #[error("path data must start with a moveto (M/m) command")]
38    MissingMoveTo,
39}
40
41/// Fill rule for [`VectorPath`] rasterization.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub enum PathFillRule {
44    /// Fill where the winding number is non-zero (SVG default).
45    #[default]
46    NonZero,
47    /// Fill where a ray crosses an odd number of edges.
48    EvenOdd,
49}
50
51/// A parsed SVG path: subpaths flattened to polylines, ready to fill.
52#[derive(Debug, Clone)]
53pub struct VectorPath {
54    /// Flattened subpaths. Fill treats every subpath as closed.
55    subpaths: Vec<Vec<Point>>,
56    fill_rule: PathFillRule,
57    bounds: Rect,
58}
59
60impl VectorPath {
61    /// Parses SVG path data (the `d` attribute syntax).
62    pub fn parse(d: &str) -> Result<Self, SvgPathError> {
63        let subpaths = parse_path_data(d)?;
64        Ok(Self::from_subpaths(subpaths, PathFillRule::NonZero))
65    }
66
67    /// Parses SVG path data with an explicit fill rule.
68    pub fn parse_with_fill_rule(d: &str, fill_rule: PathFillRule) -> Result<Self, SvgPathError> {
69        let subpaths = parse_path_data(d)?;
70        Ok(Self::from_subpaths(subpaths, fill_rule))
71    }
72
73    fn from_subpaths(subpaths: Vec<Vec<Point>>, fill_rule: PathFillRule) -> Self {
74        let mut min = Point::new(f32::INFINITY, f32::INFINITY);
75        let mut max = Point::new(f32::NEG_INFINITY, f32::NEG_INFINITY);
76        for point in subpaths.iter().flatten() {
77            min.x = min.x.min(point.x);
78            min.y = min.y.min(point.y);
79            max.x = max.x.max(point.x);
80            max.y = max.y.max(point.y);
81        }
82        let bounds = if min.x.is_finite() {
83            Rect {
84                x: min.x,
85                y: min.y,
86                width: (max.x - min.x).max(0.0),
87                height: (max.y - min.y).max(0.0),
88            }
89        } else {
90            Rect {
91                x: 0.0,
92                y: 0.0,
93                width: 0.0,
94                height: 0.0,
95            }
96        };
97        Self {
98            subpaths,
99            fill_rule,
100            bounds,
101        }
102    }
103
104    /// Returns a uniformly scaled copy (icon path data drawn at a target
105    /// size: `parse(d)?.scaled(size / view_box)`).
106    pub fn scaled(&self, factor: f32) -> Self {
107        let subpaths = self
108            .subpaths
109            .iter()
110            .map(|subpath| {
111                subpath
112                    .iter()
113                    .map(|point| Point::new(point.x * factor, point.y * factor))
114                    .collect()
115            })
116            .collect();
117        Self::from_subpaths(subpaths, self.fill_rule)
118    }
119
120    /// A copy of this path translated by `(dx, dy)`.
121    pub fn translated(&self, dx: f32, dy: f32) -> Self {
122        let subpaths = self
123            .subpaths
124            .iter()
125            .map(|subpath| {
126                subpath
127                    .iter()
128                    .map(|point| Point::new(point.x + dx, point.y + dy))
129                    .collect()
130            })
131            .collect();
132        Self::from_subpaths(subpaths, self.fill_rule)
133    }
134
135    /// The fill rule used by [`coverage_mask`](Self::coverage_mask).
136    pub fn fill_rule(&self) -> PathFillRule {
137        self.fill_rule
138    }
139
140    /// Tight bounding box of the flattened path, in path units.
141    pub fn bounds(&self) -> Rect {
142        self.bounds
143    }
144
145    /// Whether the path contains no fillable geometry.
146    pub fn is_empty(&self) -> bool {
147        !self.subpaths.iter().any(|subpath| subpath.len() >= 3)
148    }
149
150    /// Flattened subpaths (each is filled as a closed polygon).
151    pub fn subpaths(&self) -> &[Vec<Point>] {
152        &self.subpaths
153    }
154
155    /// Rasterizes the fill into an anti-aliased 8-bit coverage mask of
156    /// `width x height` pixels. A path point `p` maps to the pixel-space
157    /// position `(p - origin) * scale`.
158    pub fn coverage_mask(&self, width: usize, height: usize, origin: Point, scale: f32) -> Vec<u8> {
159        let mut mask = vec![0u8; width * height];
160        if width == 0 || height == 0 || scale <= 0.0 {
161            return mask;
162        }
163
164        // Collect pixel-space edges from all subpaths (implicitly closed).
165        struct Edge {
166            top: Point,
167            bottom: Point,
168            /// +1 when the original edge points downward (top -> bottom),
169            /// -1 when it points upward.
170            winding: i32,
171        }
172        let mut edges = Vec::new();
173        for subpath in &self.subpaths {
174            if subpath.len() < 3 {
175                continue;
176            }
177            let map = |p: &Point| Point::new((p.x - origin.x) * scale, (p.y - origin.y) * scale);
178            for i in 0..subpath.len() {
179                let a = map(&subpath[i]);
180                let b = map(&subpath[(i + 1) % subpath.len()]);
181                if a.y == b.y {
182                    continue;
183                }
184                if a.y < b.y {
185                    edges.push(Edge {
186                        top: a,
187                        bottom: b,
188                        winding: 1,
189                    });
190                } else {
191                    edges.push(Edge {
192                        top: b,
193                        bottom: a,
194                        winding: -1,
195                    });
196                }
197            }
198        }
199        if edges.is_empty() {
200            return mask;
201        }
202
203        let mut crossings: Vec<(f32, i32)> = Vec::new();
204        let mut row_coverage = vec![0.0f32; width];
205        let subsample_weight = 1.0 / SUBSAMPLES as f32;
206
207        for row in 0..height {
208            row_coverage.fill(0.0);
209            let mut row_touched = false;
210
211            for sub in 0..SUBSAMPLES {
212                let sample_y = row as f32 + (sub as f32 + 0.5) * subsample_weight;
213
214                crossings.clear();
215                for edge in &edges {
216                    if edge.top.y <= sample_y && sample_y < edge.bottom.y {
217                        let t = (sample_y - edge.top.y) / (edge.bottom.y - edge.top.y);
218                        let x = edge.top.x + t * (edge.bottom.x - edge.top.x);
219                        crossings.push((x, edge.winding));
220                    }
221                }
222                if crossings.len() < 2 {
223                    continue;
224                }
225                crossings.sort_by(|a, b| a.0.total_cmp(&b.0));
226
227                // Walk crossings, accumulating spans per fill rule.
228                let mut winding = 0i32;
229                let mut span_start = 0.0f32;
230                for &(x, direction) in crossings.iter() {
231                    let was_inside = match self.fill_rule {
232                        PathFillRule::NonZero => winding != 0,
233                        PathFillRule::EvenOdd => winding % 2 != 0,
234                    };
235                    winding += match self.fill_rule {
236                        PathFillRule::NonZero => direction,
237                        PathFillRule::EvenOdd => 1,
238                    };
239                    let is_inside = match self.fill_rule {
240                        PathFillRule::NonZero => winding != 0,
241                        PathFillRule::EvenOdd => winding % 2 != 0,
242                    };
243                    if !was_inside && is_inside {
244                        span_start = x;
245                    } else if was_inside && !is_inside {
246                        row_touched |= accumulate_span(
247                            &mut row_coverage,
248                            span_start,
249                            x,
250                            subsample_weight,
251                            width,
252                        );
253                    }
254                }
255            }
256
257            if row_touched {
258                let mask_row = &mut mask[row * width..(row + 1) * width];
259                for (dst, coverage) in mask_row.iter_mut().zip(row_coverage.iter()) {
260                    let existing = *dst as f32 / 255.0;
261                    let combined = (existing + coverage).min(1.0);
262                    *dst = (combined * 255.0 + 0.5) as u8;
263                }
264            }
265        }
266
267        mask
268    }
269}
270
271/// Adds one horizontal span `[x0, x1)` of one sub-scanline into the row
272/// coverage accumulator, handling fractional span ends. Returns whether any
273/// pixel was touched.
274fn accumulate_span(row_coverage: &mut [f32], x0: f32, x1: f32, weight: f32, width: usize) -> bool {
275    let x0 = x0.max(0.0);
276    let x1 = x1.min(width as f32);
277    if x1 <= x0 {
278        return false;
279    }
280
281    let first = x0.floor() as usize;
282    let last = (x1.ceil() as usize).min(width);
283    for (pixel, coverage) in row_coverage.iter_mut().enumerate().take(last).skip(first) {
284        let pixel_start = pixel as f32;
285        let pixel_end = pixel_start + 1.0;
286        let covered = (x1.min(pixel_end) - x0.max(pixel_start)).max(0.0);
287        *coverage += covered * weight;
288    }
289    true
290}
291
292// ============================================================================
293// Path data parsing
294// ============================================================================
295
296struct PathLexer<'a> {
297    bytes: &'a [u8],
298    pos: usize,
299}
300
301impl<'a> PathLexer<'a> {
302    fn new(d: &'a str) -> Self {
303        Self {
304            bytes: d.as_bytes(),
305            pos: 0,
306        }
307    }
308
309    fn skip_separators(&mut self) {
310        while self.pos < self.bytes.len() {
311            match self.bytes[self.pos] {
312                b' ' | b'\t' | b'\r' | b'\n' | b',' => self.pos += 1,
313                _ => break,
314            }
315        }
316    }
317
318    fn peek(&mut self) -> Option<u8> {
319        self.skip_separators();
320        self.bytes.get(self.pos).copied()
321    }
322
323    /// Whether the next token can start a number.
324    fn at_number(&mut self) -> bool {
325        matches!(self.peek(), Some(b'0'..=b'9' | b'.' | b'-' | b'+'))
326    }
327
328    fn next_command(&mut self) -> Option<u8> {
329        let byte = self.peek()?;
330        if byte.is_ascii_alphabetic() {
331            self.pos += 1;
332            Some(byte)
333        } else {
334            None
335        }
336    }
337
338    /// Parses one SVG number: `[+-]? (digits [. digits?]? | . digits) exponent?`.
339    /// A second `.` terminates the number, so `1.5.5` lexes as `1.5`, `.5`.
340    fn next_number(&mut self) -> Result<f32, SvgPathError> {
341        self.skip_separators();
342        let start = self.pos;
343        let bytes = self.bytes;
344        let mut pos = self.pos;
345
346        if pos < bytes.len() && (bytes[pos] == b'+' || bytes[pos] == b'-') {
347            pos += 1;
348        }
349        let int_digits = Self::eat_digits(bytes, &mut pos);
350        let mut frac_digits = 0;
351        if pos < bytes.len() && bytes[pos] == b'.' {
352            pos += 1;
353            frac_digits = Self::eat_digits(bytes, &mut pos);
354        }
355        if int_digits == 0 && frac_digits == 0 {
356            return Err(SvgPathError::ExpectedNumber { offset: start });
357        }
358        if pos < bytes.len() && (bytes[pos] == b'e' || bytes[pos] == b'E') {
359            let mut exp_pos = pos + 1;
360            if exp_pos < bytes.len() && (bytes[exp_pos] == b'+' || bytes[exp_pos] == b'-') {
361                exp_pos += 1;
362            }
363            if Self::eat_digits(bytes, &mut exp_pos) > 0 {
364                pos = exp_pos;
365            }
366        }
367
368        let text = std::str::from_utf8(&bytes[start..pos])
369            .map_err(|_| SvgPathError::ExpectedNumber { offset: start })?;
370        let value = text
371            .parse::<f32>()
372            .map_err(|_| SvgPathError::ExpectedNumber { offset: start })?;
373        self.pos = pos;
374        Ok(value)
375    }
376
377    fn eat_digits(bytes: &[u8], pos: &mut usize) -> usize {
378        let start = *pos;
379        while *pos < bytes.len() && bytes[*pos].is_ascii_digit() {
380            *pos += 1;
381        }
382        *pos - start
383    }
384
385    /// Arc flags are single characters and may be packed (`110 10` etc).
386    fn next_flag(&mut self) -> Result<bool, SvgPathError> {
387        self.skip_separators();
388        match self.bytes.get(self.pos) {
389            Some(b'0') => {
390                self.pos += 1;
391                Ok(false)
392            }
393            Some(b'1') => {
394                self.pos += 1;
395                Ok(true)
396            }
397            _ => Err(SvgPathError::ExpectedFlag { offset: self.pos }),
398        }
399    }
400
401    fn at_end(&mut self) -> bool {
402        self.peek().is_none()
403    }
404}
405
406struct PathBuilder {
407    subpaths: Vec<Vec<Point>>,
408    current: Vec<Point>,
409    position: Point,
410    subpath_start: Point,
411    /// Reflection anchors for smooth curves (S/T).
412    last_cubic_control: Option<Point>,
413    last_quad_control: Option<Point>,
414}
415
416impl PathBuilder {
417    fn new() -> Self {
418        Self {
419            subpaths: Vec::new(),
420            current: Vec::new(),
421            position: Point::ZERO,
422            subpath_start: Point::ZERO,
423            last_cubic_control: None,
424            last_quad_control: None,
425        }
426    }
427
428    fn flush_subpath(&mut self) {
429        if self.current.len() >= 2 {
430            self.subpaths.push(std::mem::take(&mut self.current));
431        } else {
432            self.current.clear();
433        }
434    }
435
436    fn move_to(&mut self, point: Point) {
437        self.flush_subpath();
438        self.position = point;
439        self.subpath_start = point;
440        self.current.push(point);
441    }
442
443    fn line_to(&mut self, point: Point) {
444        if self.current.is_empty() {
445            self.current.push(self.position);
446        }
447        self.current.push(point);
448        self.position = point;
449    }
450
451    fn close(&mut self) {
452        self.position = self.subpath_start;
453        self.flush_subpath();
454        // Commands after Z continue from the subpath start.
455        self.current.push(self.subpath_start);
456    }
457
458    fn finish(mut self) -> Vec<Vec<Point>> {
459        self.flush_subpath();
460        self.subpaths
461    }
462}
463
464fn parse_path_data(d: &str) -> Result<Vec<Vec<Point>>, SvgPathError> {
465    let mut lexer = PathLexer::new(d);
466    let mut builder = PathBuilder::new();
467    let mut command: Option<u8> = None;
468    let mut seen_moveto = false;
469
470    loop {
471        if lexer.at_end() {
472            break;
473        }
474
475        if let Some(next) = lexer.next_command() {
476            command = Some(next);
477        } else if command.is_none() || !lexer.at_number() {
478            let offset = lexer.pos;
479            let byte = lexer.bytes.get(offset).copied().unwrap_or(b'?') as char;
480            return Err(SvgPathError::UnexpectedByte { byte, offset });
481        }
482
483        let Some(cmd) = command else {
484            return Err(SvgPathError::MissingMoveTo);
485        };
486        if !seen_moveto && !matches!(cmd, b'M' | b'm') {
487            return Err(SvgPathError::MissingMoveTo);
488        }
489        let relative = cmd.is_ascii_lowercase();
490        let pos = builder.position;
491        let rel = |value: Point| {
492            if relative {
493                Point::new(pos.x + value.x, pos.y + value.y)
494            } else {
495                value
496            }
497        };
498
499        match cmd.to_ascii_uppercase() {
500            b'M' => {
501                let point = rel(read_point(&mut lexer)?);
502                builder.move_to(point);
503                seen_moveto = true;
504                builder.last_cubic_control = None;
505                builder.last_quad_control = None;
506                // Extra coordinate pairs are implicit linetos.
507                command = Some(if relative { b'l' } else { b'L' });
508            }
509            b'L' => {
510                let point = rel(read_point(&mut lexer)?);
511                builder.line_to(point);
512                builder.last_cubic_control = None;
513                builder.last_quad_control = None;
514            }
515            b'H' => {
516                let x = lexer.next_number()?;
517                let x = if relative { pos.x + x } else { x };
518                builder.line_to(Point::new(x, pos.y));
519                builder.last_cubic_control = None;
520                builder.last_quad_control = None;
521            }
522            b'V' => {
523                let y = lexer.next_number()?;
524                let y = if relative { pos.y + y } else { y };
525                builder.line_to(Point::new(pos.x, y));
526                builder.last_cubic_control = None;
527                builder.last_quad_control = None;
528            }
529            b'C' => {
530                let c1 = rel(read_point(&mut lexer)?);
531                let c2 = rel(read_point(&mut lexer)?);
532                let end = rel(read_point(&mut lexer)?);
533                emit_cubic(&mut builder, c1, c2, end);
534            }
535            b'S' => {
536                let c1 = match builder.last_cubic_control {
537                    Some(control) => reflect(pos, control),
538                    None => pos,
539                };
540                let c2 = rel(read_point(&mut lexer)?);
541                let end = rel(read_point(&mut lexer)?);
542                emit_cubic(&mut builder, c1, c2, end);
543            }
544            b'Q' => {
545                let control = rel(read_point(&mut lexer)?);
546                let end = rel(read_point(&mut lexer)?);
547                emit_quad(&mut builder, control, end);
548            }
549            b'T' => {
550                let control = match builder.last_quad_control {
551                    Some(control) => reflect(pos, control),
552                    None => pos,
553                };
554                let end = rel(read_point(&mut lexer)?);
555                emit_quad(&mut builder, control, end);
556            }
557            b'A' => {
558                let rx = lexer.next_number()?;
559                let ry = lexer.next_number()?;
560                let x_rotation_deg = lexer.next_number()?;
561                let large_arc = lexer.next_flag()?;
562                let sweep = lexer.next_flag()?;
563                let end = rel(read_point(&mut lexer)?);
564                emit_arc(&mut builder, rx, ry, x_rotation_deg, large_arc, sweep, end);
565                builder.last_cubic_control = None;
566                builder.last_quad_control = None;
567            }
568            b'Z' => {
569                builder.close();
570                builder.last_cubic_control = None;
571                builder.last_quad_control = None;
572                // Z takes no arguments; require an explicit next command.
573                command = None;
574            }
575            other => {
576                return Err(SvgPathError::UnexpectedByte {
577                    byte: other as char,
578                    offset: lexer.pos.saturating_sub(1),
579                });
580            }
581        }
582    }
583
584    if !seen_moveto {
585        return Err(SvgPathError::MissingMoveTo);
586    }
587    Ok(builder.finish())
588}
589
590fn read_point(lexer: &mut PathLexer<'_>) -> Result<Point, SvgPathError> {
591    let x = lexer.next_number()?;
592    let y = lexer.next_number()?;
593    Ok(Point::new(x, y))
594}
595
596fn reflect(origin: Point, point: Point) -> Point {
597    Point::new(2.0 * origin.x - point.x, 2.0 * origin.y - point.y)
598}
599
600fn emit_cubic(builder: &mut PathBuilder, c1: Point, c2: Point, end: Point) {
601    let start = builder.position;
602    flatten_cubic(builder, start, c1, c2, end, 0);
603    builder.position = end;
604    builder.last_cubic_control = Some(c2);
605    builder.last_quad_control = None;
606}
607
608fn emit_quad(builder: &mut PathBuilder, control: Point, end: Point) {
609    // Elevate the quadratic to a cubic and reuse the cubic flattener.
610    let start = builder.position;
611    let c1 = Point::new(
612        start.x + 2.0 / 3.0 * (control.x - start.x),
613        start.y + 2.0 / 3.0 * (control.y - start.y),
614    );
615    let c2 = Point::new(
616        end.x + 2.0 / 3.0 * (control.x - end.x),
617        end.y + 2.0 / 3.0 * (control.y - end.y),
618    );
619    flatten_cubic(builder, start, c1, c2, end, 0);
620    builder.position = end;
621    builder.last_quad_control = Some(control);
622    builder.last_cubic_control = None;
623}
624
625fn flatten_cubic(
626    builder: &mut PathBuilder,
627    p0: Point,
628    p1: Point,
629    p2: Point,
630    p3: Point,
631    depth: u32,
632) {
633    if depth >= MAX_FLATTEN_DEPTH || cubic_is_flat(p0, p1, p2, p3) {
634        builder.line_to(p3);
635        return;
636    }
637
638    let mid = |a: Point, b: Point| Point::new((a.x + b.x) * 0.5, (a.y + b.y) * 0.5);
639    let p01 = mid(p0, p1);
640    let p12 = mid(p1, p2);
641    let p23 = mid(p2, p3);
642    let p012 = mid(p01, p12);
643    let p123 = mid(p12, p23);
644    let p0123 = mid(p012, p123);
645
646    flatten_cubic(builder, p0, p01, p012, p0123, depth + 1);
647    flatten_cubic(builder, p0123, p123, p23, p3, depth + 1);
648}
649
650/// Flatness test: both control points close enough to the chord.
651fn cubic_is_flat(p0: Point, p1: Point, p2: Point, p3: Point) -> bool {
652    let d1 = point_to_chord_distance_squared(p1, p0, p3);
653    let d2 = point_to_chord_distance_squared(p2, p0, p3);
654    let tolerance = FLATTEN_TOLERANCE * FLATTEN_TOLERANCE;
655    d1 <= tolerance && d2 <= tolerance
656}
657
658fn point_to_chord_distance_squared(point: Point, a: Point, b: Point) -> f32 {
659    let ab = Point::new(b.x - a.x, b.y - a.y);
660    let ap = Point::new(point.x - a.x, point.y - a.y);
661    let ab_len_sq = ab.x * ab.x + ab.y * ab.y;
662    if ab_len_sq <= f32::EPSILON {
663        return ap.x * ap.x + ap.y * ap.y;
664    }
665    let cross = ab.x * ap.y - ab.y * ap.x;
666    cross * cross / ab_len_sq
667}
668
669/// Converts an SVG endpoint-parameterized arc to line segments
670/// (W3C SVG 2 appendix B.2.4).
671fn emit_arc(
672    builder: &mut PathBuilder,
673    rx: f32,
674    ry: f32,
675    x_rotation_deg: f32,
676    large_arc: bool,
677    sweep: bool,
678    end: Point,
679) {
680    let start = builder.position;
681    if (start.x - end.x).abs() <= f32::EPSILON && (start.y - end.y).abs() <= f32::EPSILON {
682        return;
683    }
684    let mut rx = rx.abs();
685    let mut ry = ry.abs();
686    if rx <= f32::EPSILON || ry <= f32::EPSILON {
687        builder.line_to(end);
688        return;
689    }
690
691    let phi = x_rotation_deg.to_radians();
692    let (sin_phi, cos_phi) = phi.sin_cos();
693
694    // Step 1: half the vector between endpoints, in the rotated frame.
695    let dx2 = (start.x - end.x) * 0.5;
696    let dy2 = (start.y - end.y) * 0.5;
697    let x1p = cos_phi * dx2 + sin_phi * dy2;
698    let y1p = -sin_phi * dx2 + cos_phi * dy2;
699
700    // Correct out-of-range radii.
701    let lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
702    if lambda > 1.0 {
703        let scale = lambda.sqrt();
704        rx *= scale;
705        ry *= scale;
706    }
707
708    // Step 2: center in the rotated frame.
709    let rx_sq = rx * rx;
710    let ry_sq = ry * ry;
711    let numerator = (rx_sq * ry_sq - rx_sq * y1p * y1p - ry_sq * x1p * x1p).max(0.0);
712    let denominator = rx_sq * y1p * y1p + ry_sq * x1p * x1p;
713    let mut coefficient = if denominator <= f32::EPSILON {
714        0.0
715    } else {
716        (numerator / denominator).sqrt()
717    };
718    if large_arc == sweep {
719        coefficient = -coefficient;
720    }
721    let cxp = coefficient * rx * y1p / ry;
722    let cyp = -coefficient * ry * x1p / rx;
723
724    // Step 3: center in the original frame.
725    let cx = cos_phi * cxp - sin_phi * cyp + (start.x + end.x) * 0.5;
726    let cy = sin_phi * cxp + cos_phi * cyp + (start.y + end.y) * 0.5;
727
728    // Step 4: start angle and sweep extent.
729    let angle_of = |x: f32, y: f32| y.atan2(x);
730    let theta1 = angle_of((x1p - cxp) / rx, (y1p - cyp) / ry);
731    let theta2 = angle_of((-x1p - cxp) / rx, (-y1p - cyp) / ry);
732    let two_pi = std::f32::consts::TAU;
733    let mut delta = theta2 - theta1;
734    if sweep {
735        if delta < 0.0 {
736            delta += two_pi;
737        }
738    } else if delta > 0.0 {
739        delta -= two_pi;
740    }
741
742    let segments = ((delta.abs() / ARC_MAX_ANGLE_STEP).ceil() as usize).max(2);
743    for i in 1..=segments {
744        let theta = theta1 + delta * (i as f32 / segments as f32);
745        let (sin_theta, cos_theta) = theta.sin_cos();
746        let x = cos_phi * rx * cos_theta - sin_phi * ry * sin_theta + cx;
747        let y = sin_phi * rx * cos_theta + cos_phi * ry * sin_theta + cy;
748        builder.line_to(Point::new(x, y));
749    }
750    // Land exactly on the endpoint despite floating-point sampling error.
751    builder.line_to(end);
752    builder.position = end;
753}
754
755#[cfg(test)]
756mod tests {
757    use super::*;
758
759    fn mask_at(mask: &[u8], width: usize, x: usize, y: usize) -> u8 {
760        mask[y * width + x]
761    }
762
763    // ── parser ──────────────────────────────────────────────────────────
764
765    #[test]
766    fn parses_absolute_triangle() {
767        let path = VectorPath::parse("M 0 0 L 10 0 L 10 10 Z").expect("valid path");
768        assert_eq!(path.subpaths().len(), 1);
769        assert_eq!(
770            path.subpaths()[0],
771            vec![
772                Point::new(0.0, 0.0),
773                Point::new(10.0, 0.0),
774                Point::new(10.0, 10.0)
775            ]
776        );
777        let bounds = path.bounds();
778        assert_eq!((bounds.x, bounds.y), (0.0, 0.0));
779        assert_eq!((bounds.width, bounds.height), (10.0, 10.0));
780    }
781
782    #[test]
783    fn parses_relative_commands_and_h_v() {
784        let path = VectorPath::parse("m 5 5 l 10 0 v 10 h -10 z").expect("valid path");
785        assert_eq!(
786            path.subpaths()[0],
787            vec![
788                Point::new(5.0, 5.0),
789                Point::new(15.0, 5.0),
790                Point::new(15.0, 15.0),
791                Point::new(5.0, 15.0)
792            ]
793        );
794    }
795
796    #[test]
797    fn parses_packed_numbers_and_negative_shorthand() {
798        // "10-5" is two numbers; ".5.5" is (0.5, 0.5).
799        let path = VectorPath::parse("M10-5L.5.5Z").expect("valid path");
800        assert_eq!(
801            path.subpaths()[0],
802            vec![Point::new(10.0, -5.0), Point::new(0.5, 0.5)]
803        );
804    }
805
806    #[test]
807    fn implicit_lineto_after_moveto() {
808        let path = VectorPath::parse("M 0 0 10 0 10 10").expect("valid path");
809        assert_eq!(path.subpaths()[0].len(), 3);
810        assert_eq!(path.subpaths()[0][2], Point::new(10.0, 10.0));
811    }
812
813    #[test]
814    fn cubic_flattening_hits_endpoints() {
815        let path = VectorPath::parse("M 0 0 C 0 10 10 10 10 0").expect("valid path");
816        let points = &path.subpaths()[0];
817        assert_eq!(points[0], Point::new(0.0, 0.0));
818        assert_eq!(*points.last().unwrap(), Point::new(10.0, 0.0));
819        assert!(points.len() > 4, "curve must be subdivided");
820        // The curve midpoint of this symmetric cubic is (5, 7.5).
821        let mid = points
822            .iter()
823            .min_by(|a, b| (a.x - 5.0).abs().total_cmp(&(b.x - 5.0).abs()))
824            .unwrap();
825        assert!(
826            (mid.y - 7.5).abs() < 0.2,
827            "flattened curve must pass near the true midpoint, got {mid:?}"
828        );
829    }
830
831    #[test]
832    fn smooth_cubic_reflects_control_point() {
833        // S after C reflects the previous control point; the joined curves
834        // are C1-continuous, so the polyline has no kink at the join (5,5).
835        let path = VectorPath::parse("M 0 0 C 0 5 2 5 5 5 S 10 5 10 10").expect("valid path");
836        let points = &path.subpaths()[0];
837        assert_eq!(*points.last().unwrap(), Point::new(10.0, 10.0));
838        assert!(
839            points
840                .iter()
841                .any(|p| (p.x - 5.0).abs() < 0.1 && (p.y - 5.0).abs() < 0.1)
842        );
843    }
844
845    #[test]
846    fn quadratic_and_smooth_quadratic() {
847        let path = VectorPath::parse("M 0 0 Q 5 10 10 0 T 20 0").expect("valid path");
848        let points = &path.subpaths()[0];
849        assert_eq!(*points.last().unwrap(), Point::new(20.0, 0.0));
850        // Quadratic apex at t=0.5 is (5, 5).
851        assert!(
852            points
853                .iter()
854                .any(|p| (p.x - 5.0).abs() < 0.3 && (p.y - 5.0).abs() < 0.3)
855        );
856        // T mirrors the control: the second hump dips to (15, -5).
857        assert!(
858            points
859                .iter()
860                .any(|p| (p.x - 15.0).abs() < 0.3 && (p.y + 5.0).abs() < 0.3)
861        );
862    }
863
864    #[test]
865    fn arc_travels_through_expected_quadrant() {
866        // Half circle of radius 5 from (0,0) to (10,0), sweeping below.
867        let path = VectorPath::parse("M 0 0 A 5 5 0 0 1 10 0").expect("valid path");
868        let points = &path.subpaths()[0];
869        assert_eq!(*points.last().unwrap(), Point::new(10.0, 0.0));
870        let lowest = points.iter().fold(0.0f32, |acc, p| acc.min(p.y));
871        assert!(
872            (lowest + 5.0).abs() < 0.1,
873            "sweep=1 arc must pass through (5,-5), lowest y = {lowest}"
874        );
875
876        let path = VectorPath::parse("M 0 0 A 5 5 0 0 0 10 0").expect("valid path");
877        let highest = path.subpaths()[0]
878            .iter()
879            .fold(0.0f32, |acc, p| acc.max(p.y));
880        assert!(
881            (highest - 5.0).abs() < 0.1,
882            "sweep=0 arc must pass through (5,5), highest y = {highest}"
883        );
884    }
885
886    #[test]
887    fn arc_flags_may_be_packed() {
888        let spaced = VectorPath::parse("M 0 0 A 5 5 0 0 1 10 0").expect("valid path");
889        let packed = VectorPath::parse("M0 0A5 5 0 0110 0").expect("valid path");
890        assert_eq!(
891            spaced.subpaths()[0].len(),
892            packed.subpaths()[0].len(),
893            "packed arc flags must parse identically"
894        );
895    }
896
897    #[test]
898    fn multiple_subpaths() {
899        let path =
900            VectorPath::parse("M 0 0 h 4 v 4 h -4 Z M 10 10 h 4 v 4 h -4 Z").expect("valid path");
901        assert_eq!(path.subpaths().len(), 2);
902    }
903
904    #[test]
905    fn rejects_garbage() {
906        assert!(VectorPath::parse("this is not a path").is_err());
907        assert!(
908            VectorPath::parse("L 10 10").is_err(),
909            "must start with moveto"
910        );
911        assert!(VectorPath::parse("M 10").is_err(), "missing y coordinate");
912        assert!(
913            VectorPath::parse("M 0 0 A 5 5 0 2 1 10 0").is_err(),
914            "bad flag"
915        );
916        assert_eq!(
917            VectorPath::parse("").unwrap_err(),
918            SvgPathError::MissingMoveTo
919        );
920    }
921
922    // ── rasterizer ──────────────────────────────────────────────────────
923
924    #[test]
925    fn fills_axis_aligned_rectangle() {
926        let path = VectorPath::parse("M 2 2 H 8 V 8 H 2 Z").expect("valid path");
927        let mask = path.coverage_mask(10, 10, Point::ZERO, 1.0);
928
929        assert_eq!(mask_at(&mask, 10, 5, 5), 255, "interior must be opaque");
930        assert_eq!(mask_at(&mask, 10, 4, 2), 255, "top edge row is inside");
931        assert_eq!(mask_at(&mask, 10, 0, 0), 0, "outside must stay empty");
932        assert_eq!(mask_at(&mask, 10, 9, 9), 0, "outside must stay empty");
933    }
934
935    #[test]
936    fn triangle_edge_is_antialiased() {
937        let path = VectorPath::parse("M 0 0 L 8 0 L 0 8 Z").expect("valid path");
938        let mask = path.coverage_mask(8, 8, Point::ZERO, 1.0);
939
940        assert_eq!(mask_at(&mask, 8, 1, 1), 255, "deep interior is opaque");
941        assert_eq!(mask_at(&mask, 8, 7, 7), 0, "far corner is empty");
942        // Pixels straddling the diagonal must have partial coverage.
943        let diagonal = mask_at(&mask, 8, 4, 3);
944        assert!(
945            diagonal > 30 && diagonal < 225,
946            "diagonal pixel should be partially covered, got {diagonal}"
947        );
948    }
949
950    #[test]
951    fn even_odd_ring_has_a_hole() {
952        // Outer square with an inner square drawn in the SAME winding
953        // direction: even-odd punches the hole, non-zero fills it solid.
954        let d = "M 0 0 H 12 V 12 H 0 Z M 4 4 H 8 V 8 H 4 Z";
955        let even_odd =
956            VectorPath::parse_with_fill_rule(d, PathFillRule::EvenOdd).expect("valid path");
957        let non_zero = VectorPath::parse(d).expect("valid path");
958
959        let even_odd_mask = even_odd.coverage_mask(12, 12, Point::ZERO, 1.0);
960        let non_zero_mask = non_zero.coverage_mask(12, 12, Point::ZERO, 1.0);
961
962        assert_eq!(mask_at(&even_odd_mask, 12, 6, 6), 0, "even-odd hole");
963        assert_eq!(mask_at(&even_odd_mask, 12, 2, 6), 255, "even-odd ring");
964        assert_eq!(mask_at(&non_zero_mask, 12, 6, 6), 255, "non-zero solid");
965    }
966
967    #[test]
968    fn non_zero_ring_with_reversed_inner_winding_has_a_hole() {
969        // Inner square wound the opposite way: non-zero also punches it.
970        let d = "M 0 0 H 12 V 12 H 0 Z M 4 4 V 8 H 8 V 4 Z";
971        let path = VectorPath::parse(d).expect("valid path");
972        let mask = path.coverage_mask(12, 12, Point::ZERO, 1.0);
973        assert_eq!(mask_at(&mask, 12, 6, 6), 0, "reversed winding hole");
974        assert_eq!(mask_at(&mask, 12, 2, 6), 255, "ring stays filled");
975    }
976
977    #[test]
978    fn circle_from_arcs_fills_center_and_respects_radius() {
979        // Full circle of radius 8 centered at (8, 8) from two arcs.
980        let path =
981            VectorPath::parse("M 0 8 A 8 8 0 1 1 16 8 A 8 8 0 1 1 0 8 Z").expect("valid path");
982        let mask = path.coverage_mask(16, 16, Point::ZERO, 1.0);
983
984        assert_eq!(mask_at(&mask, 16, 8, 8), 255, "circle center is opaque");
985        assert_eq!(mask_at(&mask, 16, 0, 0), 0, "circle corner is empty");
986        assert_eq!(mask_at(&mask, 16, 15, 0), 0, "circle corner is empty");
987        // Roughly correct area: sum of coverage ~ pi * r^2.
988        let area: f32 = mask.iter().map(|&value| value as f32 / 255.0).sum();
989        let expected = std::f32::consts::PI * 8.0 * 8.0;
990        assert!(
991            (area - expected).abs() / expected < 0.05,
992            "filled area {area} should be close to {expected}"
993        );
994    }
995
996    #[test]
997    fn scale_and_origin_map_path_units_to_pixels() {
998        let path = VectorPath::parse("M 10 10 H 14 V 14 H 10 Z").expect("valid path");
999        // Rasterize the 4x4 square at 2x with the mask origin at (10, 10).
1000        let mask = path.coverage_mask(8, 8, Point::new(10.0, 10.0), 2.0);
1001        assert_eq!(mask_at(&mask, 8, 4, 4), 255, "scaled interior");
1002        let full: usize = mask.iter().filter(|&&value| value == 255).count();
1003        assert_eq!(full, 64, "the 8x8 pixel mask must be fully covered");
1004    }
1005
1006    #[test]
1007    fn empty_and_degenerate_paths_produce_empty_masks() {
1008        let path = VectorPath::parse("M 5 5 L 6 6").expect("valid path");
1009        assert!(path.is_empty());
1010        let mask = path.coverage_mask(8, 8, Point::ZERO, 1.0);
1011        assert!(mask.iter().all(|&value| value == 0));
1012    }
1013}