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