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