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    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    pub(crate) 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 uniformly scaled copy (icon path data drawn at a target
104    /// size: `parse(d)?.scaled(size / view_box)`).
105    pub fn scaled(&self, factor: f32) -> Self {
106        let subpaths = self
107            .subpaths
108            .iter()
109            .map(|subpath| {
110                subpath
111                    .iter()
112                    .map(|point| Point::new(point.x * factor, point.y * factor))
113                    .collect()
114            })
115            .collect();
116        Self::from_subpaths(subpaths, self.fill_rule)
117    }
118
119    /// A copy of this path translated by `(dx, dy)`.
120    pub fn translated(&self, dx: f32, dy: f32) -> Self {
121        let subpaths = self
122            .subpaths
123            .iter()
124            .map(|subpath| {
125                subpath
126                    .iter()
127                    .map(|point| Point::new(point.x + dx, point.y + dy))
128                    .collect()
129            })
130            .collect();
131        Self::from_subpaths(subpaths, self.fill_rule)
132    }
133
134    /// The fill rule used by [`coverage_mask`](Self::coverage_mask).
135    pub fn fill_rule(&self) -> PathFillRule {
136        self.fill_rule
137    }
138
139    /// Tight bounding box of the flattened path, in path units.
140    pub fn bounds(&self) -> Rect {
141        self.bounds
142    }
143
144    /// Whether the path contains no fillable geometry.
145    pub fn is_empty(&self) -> bool {
146        !self.subpaths.iter().any(|subpath| subpath.len() >= 3)
147    }
148
149    /// Flattened subpaths (each is filled as a closed polygon).
150    pub fn subpaths(&self) -> &[Vec<Point>] {
151        &self.subpaths
152    }
153
154    /// Rasterizes the fill into an anti-aliased 8-bit coverage mask of
155    /// `width x height` pixels, each value scaled by `opacity`. A path point
156    /// `p` maps to the pixel-space position `(p - origin) * scale`.
157    pub fn coverage_mask(
158        &self,
159        width: usize,
160        height: usize,
161        origin: Point,
162        scale: f32,
163        opacity: f32,
164    ) -> Vec<u8> {
165        let mut mask = vec![0u8; width * height];
166        if width == 0 || height == 0 || scale <= 0.0 {
167            return mask;
168        }
169        let mut edges = self.scanline_edges(origin, scale);
170        if edges.is_empty() {
171            return mask;
172        }
173        edges.sort_by(|a, b| a.top.y.total_cmp(&b.top.y));
174        let mut scanner = EdgeScanner::new(&edges);
175        let mut row_coverage = vec![0.0f32; width];
176        let subsample_weight = 1.0 / SUBSAMPLES as f32;
177        for (row, mask_row) in mask.chunks_exact_mut(width).enumerate() {
178            // The pixels some span reached; the others keep their zeros.
179            let mut touched = width..0;
180            for sub in 0..SUBSAMPLES {
181                let sample_y = row as f32 + (sub as f32 + 0.5) * subsample_weight;
182                let crossings = scanner.crossings_at(sample_y);
183                self.fill_rule.for_each_span(crossings, |x0, x1| {
184                    if let Some(span) =
185                        accumulate_span(&mut row_coverage, x0, x1, subsample_weight, width)
186                    {
187                        touched.start = touched.start.min(span.start);
188                        touched.end = touched.end.max(span.end);
189                    }
190                });
191            }
192            if let (Some(mask_span), Some(coverage)) = (
193                mask_row.get_mut(touched.clone()),
194                row_coverage.get_mut(touched),
195            ) {
196                write_row_coverage(mask_span, coverage, opacity);
197            }
198        }
199        mask
200    }
201
202    /// The path's non-horizontal edges in pixel space, each from its top
203    /// to its bottom, with the direction it ran in.
204    fn scanline_edges(&self, origin: Point, scale: f32) -> Vec<Edge> {
205        let map = |p: &Point| Point::new((p.x - origin.x) * scale, (p.y - origin.y) * scale);
206        let closed = self.subpaths.iter().filter(|subpath| subpath.len() >= 3);
207        closed
208            .flat_map(|subpath| {
209                let next = subpath.iter().skip(1).chain(subpath.first());
210                subpath.iter().zip(next).filter_map(move |(a, b)| {
211                    let (a, b) = (map(a), map(b));
212                    match a.y.total_cmp(&b.y) {
213                        std::cmp::Ordering::Less => Some(Edge::new(a, b, 1)),
214                        std::cmp::Ordering::Greater => Some(Edge::new(b, a, -1)),
215                        std::cmp::Ordering::Equal => None,
216                    }
217                })
218            })
219            .collect()
220    }
221}
222
223/// One edge of a path in pixel space, from its top to its bottom, and the
224/// winding direction it ran in.
225struct Edge {
226    top: Point,
227    bottom: Point,
228    /// `bottom - top`.
229    delta: Point,
230    winding: i32,
231}
232
233impl Edge {
234    fn new(top: Point, bottom: Point, winding: i32) -> Self {
235        Self {
236            top,
237            bottom,
238            delta: Point::new(bottom.x - top.x, bottom.y - top.y),
239            winding,
240        }
241    }
242}
243
244/// Walks sample lines down a path's edges sorted by their tops, keeping the
245/// edges the current line may cross: lines only descend, so an edge enters
246/// once and leaves once.
247struct EdgeScanner<'a> {
248    edges: &'a [Edge],
249    next: usize,
250    /// The edges the current line crosses, each with where it crosses.
251    active: Vec<(f32, &'a Edge)>,
252}
253
254impl<'a> EdgeScanner<'a> {
255    fn new(edges: &'a [Edge]) -> Self {
256        Self {
257            edges,
258            next: 0,
259            active: Vec::new(),
260        }
261    }
262
263    /// Where the edges cross the line at `sample_y`, left to right, each
264    /// with its edge.
265    fn crossings_at(&mut self, sample_y: f32) -> &[(f32, &'a Edge)] {
266        while let Some(edge) = self
267            .edges
268            .get(self.next)
269            .filter(|edge| edge.top.y <= sample_y)
270        {
271            self.active.push((0.0, edge));
272            self.next += 1;
273        }
274        self.active.retain(|(_, edge)| sample_y < edge.bottom.y);
275        for (x, edge) in &mut self.active {
276            let t = (sample_y - edge.top.y) / edge.delta.y;
277            *x = edge.top.x + t * edge.delta.x;
278        }
279        // Each crossing is computed once, then sorted: the order barely
280        // changes from one sample line to the next, which the sort finds in
281        // one pass. Crossings at the same x bound an empty span either way.
282        self.active
283            .sort_unstable_by(|(a, _), (b, _)| a.total_cmp(b));
284        &self.active
285    }
286}
287
288impl PathFillRule {
289    /// Hands `span` each run of a sample line that lies inside the fill,
290    /// from `crossings` sorted left to right.
291    fn for_each_span(self, crossings: &[(f32, &Edge)], mut span: impl FnMut(f32, f32)) {
292        let mut winding = 0i32;
293        let mut span_start = 0.0f32;
294        for &(x, edge) in crossings {
295            let was_inside = self.contains(winding);
296            winding += edge.winding;
297            match (was_inside, self.contains(winding)) {
298                (false, true) => span_start = x,
299                (true, false) => span(span_start, x),
300                _ => {}
301            }
302        }
303    }
304}
305
306/// Writes a row's summed coverage, scaled by `opacity`, into its row of the
307/// 8-bit mask, and clears the sums for the next row.
308fn write_row_coverage(mask_row: &mut [u8], row_coverage: &mut [f32], opacity: f32) {
309    for (value, coverage) in mask_row.iter_mut().zip(row_coverage) {
310        let full = (coverage.min(1.0) * 255.0 + 0.5) as u8;
311        *value = (opacity * full as f32 + 0.5) as u8;
312        *coverage = 0.0;
313    }
314}
315
316/// Adds one horizontal span `[x0, x1)` of one sub-scanline into the row
317/// coverage accumulator, handling fractional span ends. Returns the pixels
318/// it touched.
319fn accumulate_span(
320    row_coverage: &mut [f32],
321    x0: f32,
322    x1: f32,
323    weight: f32,
324    width: usize,
325) -> Option<std::ops::Range<usize>> {
326    let x0 = x0.max(0.0);
327    let x1 = x1.min(width as f32);
328    if x1 <= x0 {
329        return None;
330    }
331
332    // Pixels the span covers whole take the weight as it is; only the
333    // pixels holding its ends take a fraction.
334    let first = x0.floor() as usize;
335    let last = (x1.ceil() as usize).min(width);
336    let partial = |pixel: usize| {
337        let pixel_start = pixel as f32;
338        (x1.min(pixel_start + 1.0) - x0.max(pixel_start)).max(0.0) * weight
339    };
340    match row_coverage.get_mut(first..last)? {
341        [] => {}
342        [only] => *only += partial(first),
343        [head, interior @ .., tail] => {
344            *head += partial(first);
345            for coverage in interior {
346                *coverage += weight;
347            }
348            *tail += partial(last - 1);
349        }
350    }
351    Some(first..last)
352}
353
354struct PathLexer<'a> {
355    bytes: &'a [u8],
356    pos: usize,
357}
358
359impl<'a> PathLexer<'a> {
360    fn new(d: &'a str) -> Self {
361        Self {
362            bytes: d.as_bytes(),
363            pos: 0,
364        }
365    }
366
367    fn skip_separators(&mut self) {
368        while self.pos < self.bytes.len() {
369            match self.bytes[self.pos] {
370                b' ' | b'\t' | b'\r' | b'\n' | b',' => self.pos += 1,
371                _ => break,
372            }
373        }
374    }
375
376    fn peek(&mut self) -> Option<u8> {
377        self.skip_separators();
378        self.bytes.get(self.pos).copied()
379    }
380
381    fn at_number(&mut self) -> bool {
382        matches!(self.peek(), Some(b'0'..=b'9' | b'.' | b'-' | b'+'))
383    }
384
385    fn next_command(&mut self) -> Option<u8> {
386        let byte = self.peek()?;
387        if byte.is_ascii_alphabetic() {
388            self.pos += 1;
389            Some(byte)
390        } else {
391            None
392        }
393    }
394
395    fn next_number(&mut self) -> Result<f32, SvgPathError> {
396        self.skip_separators();
397        let start = self.pos;
398        let bytes = self.bytes;
399        let mut pos = self.pos;
400
401        if pos < bytes.len() && (bytes[pos] == b'+' || bytes[pos] == b'-') {
402            pos += 1;
403        }
404        let int_digits = Self::eat_digits(bytes, &mut pos);
405        let mut frac_digits = 0;
406        if pos < bytes.len() && bytes[pos] == b'.' {
407            pos += 1;
408            frac_digits = Self::eat_digits(bytes, &mut pos);
409        }
410        if int_digits == 0 && frac_digits == 0 {
411            return Err(SvgPathError::ExpectedNumber { offset: start });
412        }
413        if pos < bytes.len() && (bytes[pos] == b'e' || bytes[pos] == b'E') {
414            let mut exp_pos = pos + 1;
415            if exp_pos < bytes.len() && (bytes[exp_pos] == b'+' || bytes[exp_pos] == b'-') {
416                exp_pos += 1;
417            }
418            if Self::eat_digits(bytes, &mut exp_pos) > 0 {
419                pos = exp_pos;
420            }
421        }
422
423        let text = std::str::from_utf8(&bytes[start..pos])
424            .map_err(|_| SvgPathError::ExpectedNumber { offset: start })?;
425        let value = text
426            .parse::<f32>()
427            .map_err(|_| SvgPathError::ExpectedNumber { offset: start })?;
428        self.pos = pos;
429        Ok(value)
430    }
431
432    fn eat_digits(bytes: &[u8], pos: &mut usize) -> usize {
433        let start = *pos;
434        while *pos < bytes.len() && bytes[*pos].is_ascii_digit() {
435            *pos += 1;
436        }
437        *pos - start
438    }
439
440    fn next_flag(&mut self) -> Result<bool, SvgPathError> {
441        self.skip_separators();
442        match self.bytes.get(self.pos) {
443            Some(b'0') => {
444                self.pos += 1;
445                Ok(false)
446            }
447            Some(b'1') => {
448                self.pos += 1;
449                Ok(true)
450            }
451            _ => Err(SvgPathError::ExpectedFlag { offset: self.pos }),
452        }
453    }
454
455    fn at_end(&mut self) -> bool {
456        self.peek().is_none()
457    }
458}
459
460struct PathBuilder {
461    subpaths: Vec<Vec<Point>>,
462    current: Vec<Point>,
463    position: Point,
464    subpath_start: Point,
465    last_cubic_control: Option<Point>,
466    last_quad_control: Option<Point>,
467}
468
469impl PathBuilder {
470    fn new() -> Self {
471        Self {
472            subpaths: Vec::new(),
473            current: Vec::new(),
474            position: Point::ZERO,
475            subpath_start: Point::ZERO,
476            last_cubic_control: None,
477            last_quad_control: None,
478        }
479    }
480
481    fn flush_subpath(&mut self) {
482        if self.current.len() >= 2 {
483            self.subpaths.push(std::mem::take(&mut self.current));
484        } else {
485            self.current.clear();
486        }
487    }
488
489    fn move_to(&mut self, point: Point) {
490        self.flush_subpath();
491        self.position = point;
492        self.subpath_start = point;
493        self.current.push(point);
494    }
495
496    fn line_to(&mut self, point: Point) {
497        self.begin_segment();
498        self.current.push(point);
499        self.position = point;
500    }
501
502    /// Starts the current polyline at the pen if nothing has, and returns
503    /// where the next segment starts.
504    fn begin_segment(&mut self) -> Point {
505        if self.current.is_empty() {
506            self.current.push(self.position);
507        }
508        self.position
509    }
510
511    fn close(&mut self) {
512        self.position = self.subpath_start;
513        self.flush_subpath();
514        self.current.push(self.subpath_start);
515    }
516
517    fn finish(mut self) -> Vec<Vec<Point>> {
518        self.flush_subpath();
519        self.subpaths
520    }
521}
522
523fn parse_path_data(d: &str) -> Result<Vec<Vec<Point>>, SvgPathError> {
524    let mut lexer = PathLexer::new(d);
525    let mut builder = PathBuilder::new();
526    let mut command: Option<u8> = None;
527    let mut seen_moveto = false;
528
529    loop {
530        if lexer.at_end() {
531            break;
532        }
533
534        if let Some(next) = lexer.next_command() {
535            command = Some(next);
536        } else if command.is_none() || !lexer.at_number() {
537            let offset = lexer.pos;
538            let byte = lexer.bytes.get(offset).copied().unwrap_or(b'?') as char;
539            return Err(SvgPathError::UnexpectedByte { byte, offset });
540        }
541
542        let Some(cmd) = command else {
543            return Err(SvgPathError::MissingMoveTo);
544        };
545        if !seen_moveto && !matches!(cmd, b'M' | b'm') {
546            return Err(SvgPathError::MissingMoveTo);
547        }
548        let relative = cmd.is_ascii_lowercase();
549        let pos = builder.position;
550        let rel = |value: Point| {
551            if relative {
552                Point::new(pos.x + value.x, pos.y + value.y)
553            } else {
554                value
555            }
556        };
557
558        match cmd.to_ascii_uppercase() {
559            b'M' => {
560                let point = rel(read_point(&mut lexer)?);
561                builder.move_to(point);
562                seen_moveto = true;
563                builder.last_cubic_control = None;
564                builder.last_quad_control = None;
565                command = Some(if relative { b'l' } else { b'L' });
566            }
567            b'L' => {
568                let point = rel(read_point(&mut lexer)?);
569                builder.line_to(point);
570                builder.last_cubic_control = None;
571                builder.last_quad_control = None;
572            }
573            b'H' => {
574                let x = lexer.next_number()?;
575                let x = if relative { pos.x + x } else { x };
576                builder.line_to(Point::new(x, pos.y));
577                builder.last_cubic_control = None;
578                builder.last_quad_control = None;
579            }
580            b'V' => {
581                let y = lexer.next_number()?;
582                let y = if relative { pos.y + y } else { y };
583                builder.line_to(Point::new(pos.x, y));
584                builder.last_cubic_control = None;
585                builder.last_quad_control = None;
586            }
587            b'C' => {
588                let c1 = rel(read_point(&mut lexer)?);
589                let c2 = rel(read_point(&mut lexer)?);
590                let end = rel(read_point(&mut lexer)?);
591                emit_cubic(&mut builder, c1, c2, end);
592            }
593            b'S' => {
594                let c1 = match builder.last_cubic_control {
595                    Some(control) => reflect(pos, control),
596                    None => pos,
597                };
598                let c2 = rel(read_point(&mut lexer)?);
599                let end = rel(read_point(&mut lexer)?);
600                emit_cubic(&mut builder, c1, c2, end);
601            }
602            b'Q' => {
603                let control = rel(read_point(&mut lexer)?);
604                let end = rel(read_point(&mut lexer)?);
605                emit_quad(&mut builder, control, end);
606            }
607            b'T' => {
608                let control = match builder.last_quad_control {
609                    Some(control) => reflect(pos, control),
610                    None => pos,
611                };
612                let end = rel(read_point(&mut lexer)?);
613                emit_quad(&mut builder, control, end);
614            }
615            b'A' => {
616                let rx = lexer.next_number()?;
617                let ry = lexer.next_number()?;
618                let x_rotation_deg = lexer.next_number()?;
619                let large_arc = lexer.next_flag()?;
620                let sweep = lexer.next_flag()?;
621                let end = rel(read_point(&mut lexer)?);
622                emit_arc(&mut builder, rx, ry, x_rotation_deg, large_arc, sweep, end);
623                builder.last_cubic_control = None;
624                builder.last_quad_control = None;
625            }
626            b'Z' => {
627                builder.close();
628                builder.last_cubic_control = None;
629                builder.last_quad_control = None;
630                command = None;
631            }
632            other => {
633                return Err(SvgPathError::UnexpectedByte {
634                    byte: other as char,
635                    offset: lexer.pos.saturating_sub(1),
636                });
637            }
638        }
639    }
640
641    if !seen_moveto {
642        return Err(SvgPathError::MissingMoveTo);
643    }
644    Ok(builder.finish())
645}
646
647fn read_point(lexer: &mut PathLexer<'_>) -> Result<Point, SvgPathError> {
648    let x = lexer.next_number()?;
649    let y = lexer.next_number()?;
650    Ok(Point::new(x, y))
651}
652
653fn reflect(origin: Point, point: Point) -> Point {
654    Point::new(2.0 * origin.x - point.x, 2.0 * origin.y - point.y)
655}
656
657fn emit_cubic(builder: &mut PathBuilder, c1: Point, c2: Point, end: Point) {
658    let start = builder.begin_segment();
659    flatten_cubic_into(&mut builder.current, start, c1, c2, end);
660    builder.position = end;
661    builder.last_cubic_control = Some(c2);
662    builder.last_quad_control = None;
663}
664
665fn emit_quad(builder: &mut PathBuilder, control: Point, end: Point) {
666    let start = builder.begin_segment();
667    let (c1, c2) = quad_as_cubic(start, control, end);
668    flatten_cubic_into(&mut builder.current, start, c1, c2, end);
669    builder.position = end;
670    builder.last_quad_control = Some(control);
671    builder.last_cubic_control = None;
672}
673
674/// The cubic controls that trace the quadratic curve from `start` through
675/// `control` to `end`.
676pub(crate) fn quad_as_cubic(start: Point, control: Point, end: Point) -> (Point, Point) {
677    (
678        Point::new(
679            start.x + 2.0 / 3.0 * (control.x - start.x),
680            start.y + 2.0 / 3.0 * (control.y - start.y),
681        ),
682        Point::new(
683            end.x + 2.0 / 3.0 * (control.x - end.x),
684            end.y + 2.0 / 3.0 * (control.y - end.y),
685        ),
686    )
687}
688
689/// Appends the cubic from `p0` to `p3` to `points` as a polyline within
690/// [`FLATTEN_TOLERANCE`] of the curve, `p0` left out: the caller's polyline
691/// already ends there.
692pub(crate) fn flatten_cubic_into(
693    points: &mut Vec<Point>,
694    p0: Point,
695    p1: Point,
696    p2: Point,
697    p3: Point,
698) {
699    flatten_cubic(points, p0, p1, p2, p3, 0);
700}
701
702fn flatten_cubic(points: &mut Vec<Point>, p0: Point, p1: Point, p2: Point, p3: Point, depth: u32) {
703    if depth >= MAX_FLATTEN_DEPTH || cubic_is_flat(p0, p1, p2, p3) {
704        points.push(p3);
705        return;
706    }
707
708    let mid = |a: Point, b: Point| Point::new((a.x + b.x) * 0.5, (a.y + b.y) * 0.5);
709    let p01 = mid(p0, p1);
710    let p12 = mid(p1, p2);
711    let p23 = mid(p2, p3);
712    let p012 = mid(p01, p12);
713    let p123 = mid(p12, p23);
714    let p0123 = mid(p012, p123);
715
716    flatten_cubic(points, p0, p01, p012, p0123, depth + 1);
717    flatten_cubic(points, p0123, p123, p23, p3, depth + 1);
718}
719
720/// Flatness test: both control points close enough to the chord.
721fn cubic_is_flat(p0: Point, p1: Point, p2: Point, p3: Point) -> bool {
722    let d1 = point_to_chord_distance_squared(p1, p0, p3);
723    let d2 = point_to_chord_distance_squared(p2, p0, p3);
724    let tolerance = FLATTEN_TOLERANCE * FLATTEN_TOLERANCE;
725    d1 <= tolerance && d2 <= tolerance
726}
727
728fn point_to_chord_distance_squared(point: Point, a: Point, b: Point) -> f32 {
729    let ab = Point::new(b.x - a.x, b.y - a.y);
730    let ap = Point::new(point.x - a.x, point.y - a.y);
731    let ab_len_sq = ab.x * ab.x + ab.y * ab.y;
732    if ab_len_sq <= f32::EPSILON {
733        return ap.x * ap.x + ap.y * ap.y;
734    }
735    let cross = ab.x * ap.y - ab.y * ap.x;
736    cross * cross / ab_len_sq
737}
738
739/// Converts an SVG endpoint-parameterized arc to line segments
740/// (W3C SVG 2 appendix B.2.4).
741fn emit_arc(
742    builder: &mut PathBuilder,
743    rx: f32,
744    ry: f32,
745    x_rotation_deg: f32,
746    large_arc: bool,
747    sweep: bool,
748    end: Point,
749) {
750    let start = builder.position;
751    if (start.x - end.x).abs() <= f32::EPSILON && (start.y - end.y).abs() <= f32::EPSILON {
752        return;
753    }
754    let mut rx = rx.abs();
755    let mut ry = ry.abs();
756    if rx <= f32::EPSILON || ry <= f32::EPSILON {
757        builder.line_to(end);
758        return;
759    }
760
761    let phi = x_rotation_deg.to_radians();
762    let (sin_phi, cos_phi) = phi.sin_cos();
763
764    let dx2 = (start.x - end.x) * 0.5;
765    let dy2 = (start.y - end.y) * 0.5;
766    let x1p = cos_phi * dx2 + sin_phi * dy2;
767    let y1p = -sin_phi * dx2 + cos_phi * dy2;
768
769    let lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
770    if lambda > 1.0 {
771        let scale = lambda.sqrt();
772        rx *= scale;
773        ry *= scale;
774    }
775
776    let rx_sq = rx * rx;
777    let ry_sq = ry * ry;
778    let numerator = (rx_sq * ry_sq - rx_sq * y1p * y1p - ry_sq * x1p * x1p).max(0.0);
779    let denominator = rx_sq * y1p * y1p + ry_sq * x1p * x1p;
780    let mut coefficient = if denominator <= f32::EPSILON {
781        0.0
782    } else {
783        (numerator / denominator).sqrt()
784    };
785    if large_arc == sweep {
786        coefficient = -coefficient;
787    }
788    let cxp = coefficient * rx * y1p / ry;
789    let cyp = -coefficient * ry * x1p / rx;
790
791    let cx = cos_phi * cxp - sin_phi * cyp + (start.x + end.x) * 0.5;
792    let cy = sin_phi * cxp + cos_phi * cyp + (start.y + end.y) * 0.5;
793
794    let angle_of = |x: f32, y: f32| y.atan2(x);
795    let theta1 = angle_of((x1p - cxp) / rx, (y1p - cyp) / ry);
796    let theta2 = angle_of((-x1p - cxp) / rx, (-y1p - cyp) / ry);
797    let two_pi = std::f32::consts::TAU;
798    let mut delta = theta2 - theta1;
799    if sweep {
800        if delta < 0.0 {
801            delta += two_pi;
802        }
803    } else if delta > 0.0 {
804        delta -= two_pi;
805    }
806
807    let segments = ((delta.abs() / ARC_MAX_ANGLE_STEP).ceil() as usize).max(2);
808    for i in 1..=segments {
809        let theta = theta1 + delta * (i as f32 / segments as f32);
810        let (sin_theta, cos_theta) = theta.sin_cos();
811        let x = cos_phi * rx * cos_theta - sin_phi * ry * sin_theta + cx;
812        let y = sin_phi * rx * cos_theta + cos_phi * ry * sin_theta + cy;
813        builder.line_to(Point::new(x, y));
814    }
815    builder.line_to(end);
816    builder.position = end;
817}
818
819#[cfg(test)]
820#[path = "tests/vector_path_tests.rs"]
821mod tests;