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