Skip to main content

azul_core/
svg_path_parser.rs

1//! SVG `d=""` path data parser.
2//!
3//! Parses the `d` attribute of SVG `<path>` elements into `SvgMultiPolygon`
4//! geometry, supporting all 14 SVG path commands (M/m, L/l, H/h, V/v,
5//! C/c, S/s, Q/q, T/t, A/a, Z/z).
6
7use alloc::{string::String, vec::Vec};
8use azul_css::props::basic::{SvgCubicCurve, SvgPoint, SvgQuadraticCurve};
9
10use crate::svg::{SvgLine, SvgMultiPolygon, SvgPath, SvgPathElement, SvgPathElementVec, SvgPathVec};
11
12/// Bezier approximation constant for quarter-circle arcs.
13const KAPPA: f32 = 0.5522847498;
14
15/// Tolerance for treating two points as coincident (used in closepath and arc degeneracy checks).
16const POINT_EPSILON: f32 = 1e-6;
17
18/// Tolerance for snapping a closepath line (slightly larger to avoid micro-segments).
19const CLOSEPATH_EPSILON: f32 = 0.001;
20
21/// Tolerance for treating a vector length as zero in angle computation.
22const ZERO_LENGTH_EPSILON: f32 = 1e-10;
23
24/// Small offset added to PI/2 when splitting arcs to avoid exact-boundary floating-point issues.
25const ARC_SPLIT_FUDGE: f32 = 0.001;
26
27/// Errors that can occur during SVG path parsing.
28#[derive(Debug, Clone, PartialEq)]
29pub enum SvgPathParseError {
30    /// The path string is empty.
31    EmptyPath,
32    /// Unexpected character encountered at the given byte offset.
33    UnexpectedChar { pos: usize, ch: char },
34    /// Expected a number but found something else.
35    ExpectedNumber { pos: usize },
36    /// Invalid arc flag (must be 0 or 1).
37    InvalidArcFlag { pos: usize },
38}
39
40/// Human-readable error messages for SVG path parse failures.
41impl core::fmt::Display for SvgPathParseError {
42    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
43        match self {
44            Self::EmptyPath => write!(f, "empty path"),
45            Self::UnexpectedChar { pos, ch } => {
46                write!(f, "unexpected char '{}' at byte {}", ch, pos)
47            }
48            Self::ExpectedNumber { pos } => write!(f, "expected number at byte {}", pos),
49            Self::InvalidArcFlag { pos } => write!(f, "invalid arc flag at byte {}", pos),
50        }
51    }
52}
53
54/// Internal parser state.
55struct PathParser<'a> {
56    input: &'a [u8],
57    pos: usize,
58    current: SvgPoint,
59    subpath_start: SvgPoint,
60    last_control: Option<SvgPoint>,
61    last_command: u8,
62}
63
64impl<'a> PathParser<'a> {
65    fn new(input: &'a [u8]) -> Self {
66        Self {
67            input,
68            pos: 0,
69            current: SvgPoint { x: 0.0, y: 0.0 },
70            subpath_start: SvgPoint { x: 0.0, y: 0.0 },
71            last_control: None,
72            last_command: 0,
73        }
74    }
75
76    fn at_end(&self) -> bool {
77        self.pos >= self.input.len()
78    }
79
80    fn peek(&self) -> Option<u8> {
81        self.input.get(self.pos).copied()
82    }
83
84    fn skip_whitespace_and_commas(&mut self) {
85        while let Some(&b) = self.input.get(self.pos) {
86            if b == b' ' || b == b'\t' || b == b'\n' || b == b'\r' || b == b',' {
87                self.pos += 1;
88            } else {
89                break;
90            }
91        }
92    }
93
94    fn skip_whitespace(&mut self) {
95        while let Some(&b) = self.input.get(self.pos) {
96            if b == b' ' || b == b'\t' || b == b'\n' || b == b'\r' {
97                self.pos += 1;
98            } else {
99                break;
100            }
101        }
102    }
103
104    /// Returns true if the current position looks like the start of a number.
105    fn has_number(&self) -> bool {
106        match self.input.get(self.pos) {
107            Some(b'+') | Some(b'-') | Some(b'.') => true,
108            Some(b) if b.is_ascii_digit() => true,
109            _ => false,
110        }
111    }
112
113    fn parse_number(&mut self) -> Result<f32, SvgPathParseError> {
114        self.skip_whitespace_and_commas();
115        let start = self.pos;
116
117        // Optional sign
118        if let Some(&b) = self.input.get(self.pos) {
119            if b == b'+' || b == b'-' {
120                self.pos += 1;
121            }
122        }
123
124        let mut has_digits = false;
125
126        // Integer part
127        while let Some(&b) = self.input.get(self.pos) {
128            if b.is_ascii_digit() {
129                self.pos += 1;
130                has_digits = true;
131            } else {
132                break;
133            }
134        }
135
136        // Decimal part
137        if let Some(&b'.') = self.input.get(self.pos) {
138            self.pos += 1;
139            while let Some(&b) = self.input.get(self.pos) {
140                if b.is_ascii_digit() {
141                    self.pos += 1;
142                    has_digits = true;
143                } else {
144                    break;
145                }
146            }
147        }
148
149        if !has_digits {
150            return Err(SvgPathParseError::ExpectedNumber { pos: start });
151        }
152
153        // Exponent
154        if let Some(&b) = self.input.get(self.pos) {
155            if b == b'e' || b == b'E' {
156                self.pos += 1;
157                if let Some(&b) = self.input.get(self.pos) {
158                    if b == b'+' || b == b'-' {
159                        self.pos += 1;
160                    }
161                }
162                while let Some(&b) = self.input.get(self.pos) {
163                    if b.is_ascii_digit() {
164                        self.pos += 1;
165                    } else {
166                        break;
167                    }
168                }
169            }
170        }
171
172        let s = core::str::from_utf8(&self.input[start..self.pos])
173            .map_err(|_| SvgPathParseError::ExpectedNumber { pos: start })?;
174        s.parse::<f32>()
175            .map_err(|_| SvgPathParseError::ExpectedNumber { pos: start })
176    }
177
178    fn parse_flag(&mut self) -> Result<bool, SvgPathParseError> {
179        self.skip_whitespace_and_commas();
180        match self.input.get(self.pos) {
181            Some(b'0') => {
182                self.pos += 1;
183                Ok(false)
184            }
185            Some(b'1') => {
186                self.pos += 1;
187                Ok(true)
188            }
189            _ => Err(SvgPathParseError::InvalidArcFlag { pos: self.pos }),
190        }
191    }
192
193    fn parse_coordinate_pair(&mut self) -> Result<(f32, f32), SvgPathParseError> {
194        let x = self.parse_number()?;
195        let y = self.parse_number()?;
196        Ok((x, y))
197    }
198
199    fn make_absolute(&self, x: f32, y: f32, relative: bool) -> SvgPoint {
200        if relative {
201            SvgPoint {
202                x: self.current.x + x,
203                y: self.current.y + y,
204            }
205        } else {
206            SvgPoint { x, y }
207        }
208    }
209
210    fn handle_line_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
211        let (x, y) = self.parse_coordinate_pair()?;
212        let end = self.make_absolute(x, y, relative);
213        elements.push(SvgPathElement::Line(SvgLine { start: self.current, end }));
214        self.current = end;
215        self.last_control = None;
216        Ok(())
217    }
218
219    fn handle_horizontal_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
220        let x = self.parse_number()?;
221        let abs_x = if relative { self.current.x + x } else { x };
222        let end = SvgPoint { x: abs_x, y: self.current.y };
223        elements.push(SvgPathElement::Line(SvgLine { start: self.current, end }));
224        self.current = end;
225        self.last_control = None;
226        Ok(())
227    }
228
229    fn handle_vertical_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
230        let y = self.parse_number()?;
231        let abs_y = if relative { self.current.y + y } else { y };
232        let end = SvgPoint { x: self.current.x, y: abs_y };
233        elements.push(SvgPathElement::Line(SvgLine { start: self.current, end }));
234        self.current = end;
235        self.last_control = None;
236        Ok(())
237    }
238
239    fn handle_cubic_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
240        let (c1x, c1y) = self.parse_coordinate_pair()?;
241        let (c2x, c2y) = self.parse_coordinate_pair()?;
242        let (ex, ey) = self.parse_coordinate_pair()?;
243        let ctrl_1 = self.make_absolute(c1x, c1y, relative);
244        let ctrl_2 = self.make_absolute(c2x, c2y, relative);
245        let end = self.make_absolute(ex, ey, relative);
246        elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
247            start: self.current, ctrl_1, ctrl_2, end,
248        }));
249        self.last_control = Some(ctrl_2);
250        self.current = end;
251        Ok(())
252    }
253
254    fn handle_smooth_cubic_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
255        let ctrl_1 = match self.last_control {
256            Some(lc) if matches!(self.last_command.to_ascii_uppercase(), b'C' | b'S') => {
257                SvgPoint {
258                    x: 2.0 * self.current.x - lc.x,
259                    y: 2.0 * self.current.y - lc.y,
260                }
261            }
262            _ => self.current,
263        };
264        let (c2x, c2y) = self.parse_coordinate_pair()?;
265        let (ex, ey) = self.parse_coordinate_pair()?;
266        let ctrl_2 = self.make_absolute(c2x, c2y, relative);
267        let end = self.make_absolute(ex, ey, relative);
268        elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
269            start: self.current, ctrl_1, ctrl_2, end,
270        }));
271        self.last_control = Some(ctrl_2);
272        self.current = end;
273        Ok(())
274    }
275
276    fn handle_quadratic_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
277        let (cx, cy) = self.parse_coordinate_pair()?;
278        let (ex, ey) = self.parse_coordinate_pair()?;
279        let ctrl = self.make_absolute(cx, cy, relative);
280        let end = self.make_absolute(ex, ey, relative);
281        elements.push(SvgPathElement::QuadraticCurve(SvgQuadraticCurve {
282            start: self.current, ctrl, end,
283        }));
284        self.last_control = Some(ctrl);
285        self.current = end;
286        Ok(())
287    }
288
289    fn handle_smooth_quadratic_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
290        let ctrl = match self.last_control {
291            Some(lc) if matches!(self.last_command.to_ascii_uppercase(), b'Q' | b'T') => {
292                SvgPoint {
293                    x: 2.0 * self.current.x - lc.x,
294                    y: 2.0 * self.current.y - lc.y,
295                }
296            }
297            _ => self.current,
298        };
299        let (ex, ey) = self.parse_coordinate_pair()?;
300        let end = self.make_absolute(ex, ey, relative);
301        elements.push(SvgPathElement::QuadraticCurve(SvgQuadraticCurve {
302            start: self.current, ctrl, end,
303        }));
304        self.last_control = Some(ctrl);
305        self.current = end;
306        Ok(())
307    }
308
309    fn handle_arc_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
310        let rx = self.parse_number()?.abs();
311        let ry = self.parse_number()?.abs();
312        let x_rotation = self.parse_number()?;
313        let large_arc = self.parse_flag()?;
314        let sweep = self.parse_flag()?;
315        let (ex, ey) = self.parse_coordinate_pair()?;
316        let end = self.make_absolute(ex, ey, relative);
317        arc_to_cubics(self.current, end, rx, ry, x_rotation, large_arc, sweep, elements);
318        self.current = end;
319        self.last_control = None;
320        Ok(())
321    }
322}
323
324/// Parse an SVG path `d` attribute string into a `SvgMultiPolygon`.
325///
326/// Each M/m command starts a new subpath (ring). All 14 SVG path commands are
327/// supported including arcs (converted to cubic beziers).
328#[must_use]
329pub fn parse_svg_path_d(d: &str) -> Result<SvgMultiPolygon, SvgPathParseError> {
330    let d = d.trim();
331    if d.is_empty() {
332        return Err(SvgPathParseError::EmptyPath);
333    }
334
335    let mut parser = PathParser::new(d.as_bytes());
336    let mut rings: Vec<SvgPath> = Vec::new();
337    let mut current_elements: Vec<SvgPathElement> = Vec::new();
338
339    parser.skip_whitespace();
340
341    while !parser.at_end() {
342        parser.skip_whitespace_and_commas();
343        if parser.at_end() {
344            break;
345        }
346
347        let b = parser.peek().unwrap();
348
349        // Determine if this is a command letter or an implicit repeat
350        let cmd = if b.is_ascii_alphabetic() {
351            parser.pos += 1;
352            b
353        } else if parser.last_command != 0 {
354            // Implicit repeat: after M/m, implicit commands become L/l
355            match parser.last_command {
356                b'M' => b'L',
357                b'm' => b'l',
358                other => other,
359            }
360        } else {
361            return Err(SvgPathParseError::UnexpectedChar {
362                pos: parser.pos,
363                ch: b as char,
364            });
365        };
366
367        let relative = cmd.is_ascii_lowercase();
368        let cmd_upper = cmd.to_ascii_uppercase();
369
370        match cmd_upper {
371            b'M' => {
372                // Flush current subpath
373                if !current_elements.is_empty() {
374                    rings.push(SvgPath {
375                        items: SvgPathElementVec::from_vec(core::mem::take(&mut current_elements)),
376                    });
377                }
378                let (x, y) = parser.parse_coordinate_pair()?;
379                let pt = parser.make_absolute(x, y, relative);
380                parser.current = pt;
381                parser.subpath_start = pt;
382                parser.last_control = None;
383                parser.last_command = cmd;
384            }
385            b'L' => {
386                parser.handle_line_to(relative, &mut current_elements)?;
387                parser.last_command = cmd;
388            }
389            b'H' => {
390                parser.handle_horizontal_to(relative, &mut current_elements)?;
391                parser.last_command = cmd;
392            }
393            b'V' => {
394                parser.handle_vertical_to(relative, &mut current_elements)?;
395                parser.last_command = cmd;
396            }
397            b'C' => {
398                parser.handle_cubic_to(relative, &mut current_elements)?;
399                parser.last_command = cmd;
400            }
401            b'S' => {
402                parser.handle_smooth_cubic_to(relative, &mut current_elements)?;
403                parser.last_command = cmd;
404            }
405            b'Q' => {
406                parser.handle_quadratic_to(relative, &mut current_elements)?;
407                parser.last_command = cmd;
408            }
409            b'T' => {
410                parser.handle_smooth_quadratic_to(relative, &mut current_elements)?;
411                parser.last_command = cmd;
412            }
413            b'A' => {
414                parser.handle_arc_to(relative, &mut current_elements)?;
415                parser.last_command = cmd;
416            }
417            b'Z' => {
418                // Close subpath
419                let dx = parser.current.x - parser.subpath_start.x;
420                let dy = parser.current.y - parser.subpath_start.y;
421                if dx * dx + dy * dy > CLOSEPATH_EPSILON * CLOSEPATH_EPSILON {
422                    current_elements.push(SvgPathElement::Line(SvgLine {
423                        start: parser.current,
424                        end: parser.subpath_start,
425                    }));
426                }
427                parser.current = parser.subpath_start;
428                parser.last_control = None;
429                parser.last_command = cmd;
430
431                // Flush current subpath
432                if !current_elements.is_empty() {
433                    rings.push(SvgPath {
434                        items: SvgPathElementVec::from_vec(core::mem::take(&mut current_elements)),
435                    });
436                }
437            }
438            _ => {
439                return Err(SvgPathParseError::UnexpectedChar {
440                    pos: parser.pos - 1,
441                    ch: cmd as char,
442                });
443            }
444        }
445
446        // After processing one argument group, try to consume more
447        // argument groups for the same command (implicit repeats)
448        if cmd_upper != b'M' && cmd_upper != b'Z' {
449            loop {
450                parser.skip_whitespace_and_commas();
451                if parser.at_end() {
452                    break;
453                }
454                let next = parser.peek().unwrap();
455                if next.is_ascii_alphabetic() {
456                    break; // Next command letter
457                }
458                if !parser.has_number() {
459                    break;
460                }
461
462                // Implicit repeat of the same command
463                match cmd_upper {
464                    b'L' => parser.handle_line_to(relative, &mut current_elements)?,
465                    b'H' => parser.handle_horizontal_to(relative, &mut current_elements)?,
466                    b'V' => parser.handle_vertical_to(relative, &mut current_elements)?,
467                    b'C' => parser.handle_cubic_to(relative, &mut current_elements)?,
468                    b'S' => parser.handle_smooth_cubic_to(relative, &mut current_elements)?,
469                    b'Q' => parser.handle_quadratic_to(relative, &mut current_elements)?,
470                    b'T' => parser.handle_smooth_quadratic_to(relative, &mut current_elements)?,
471                    b'A' => parser.handle_arc_to(relative, &mut current_elements)?,
472                    _ => break,
473                }
474            }
475        }
476    }
477
478    // Flush any remaining elements
479    if !current_elements.is_empty() {
480        rings.push(SvgPath {
481            items: SvgPathElementVec::from_vec(current_elements),
482        });
483    }
484
485    Ok(SvgMultiPolygon {
486        rings: SvgPathVec::from_vec(rings),
487    })
488}
489
490/// Convert an SVG arc to 1–4 cubic bezier curves.
491///
492/// Implements the SVG spec arc endpoint-to-center parameterization (Appendix F.6).
493fn arc_to_cubics(
494    start: SvgPoint,
495    end: SvgPoint,
496    mut rx: f32,
497    mut ry: f32,
498    x_rotation_deg: f32,
499    large_arc: bool,
500    sweep: bool,
501    out: &mut Vec<SvgPathElement>,
502) {
503    // Degenerate cases
504    if (start.x - end.x).abs() < POINT_EPSILON && (start.y - end.y).abs() < POINT_EPSILON {
505        return;
506    }
507    if rx < POINT_EPSILON || ry < POINT_EPSILON {
508        out.push(SvgPathElement::Line(SvgLine { start, end }));
509        return;
510    }
511
512    let phi = x_rotation_deg.to_radians();
513    let cos_phi = phi.cos();
514    let sin_phi = phi.sin();
515
516    // Step 1: Compute (x1', y1')
517    let dx = (start.x - end.x) / 2.0;
518    let dy = (start.y - end.y) / 2.0;
519    let x1p = cos_phi * dx + sin_phi * dy;
520    let y1p = -sin_phi * dx + cos_phi * dy;
521
522    // Step 2: Compute (cx', cy') - correct radii if too small
523    let x1p2 = x1p * x1p;
524    let y1p2 = y1p * y1p;
525    let mut rx2 = rx * rx;
526    let mut ry2 = ry * ry;
527
528    let lambda = x1p2 / rx2 + y1p2 / ry2;
529    if lambda > 1.0 {
530        let sqrt_lambda = lambda.sqrt();
531        rx *= sqrt_lambda;
532        ry *= sqrt_lambda;
533        rx2 = rx * rx;
534        ry2 = ry * ry;
535    }
536
537    let num = (rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2).max(0.0);
538    let den = rx2 * y1p2 + ry2 * x1p2;
539    let sq = if den > 0.0 {
540        (num / den).sqrt()
541    } else {
542        0.0
543    };
544
545    let sign = if large_arc == sweep { -1.0 } else { 1.0 };
546    let cxp = sign * sq * (rx * y1p / ry);
547    let cyp = sign * sq * -(ry * x1p / rx);
548
549    // Step 3: Compute (cx, cy) from (cx', cy')
550    let mx = (start.x + end.x) / 2.0;
551    let my = (start.y + end.y) / 2.0;
552    let cx = cos_phi * cxp - sin_phi * cyp + mx;
553    let cy = sin_phi * cxp + cos_phi * cyp + my;
554
555    // Step 4: Compute theta1 and dtheta
556    let theta1 = angle_between(1.0, 0.0, (x1p - cxp) / rx, (y1p - cyp) / ry);
557    let mut dtheta = angle_between(
558        (x1p - cxp) / rx,
559        (y1p - cyp) / ry,
560        (-x1p - cxp) / rx,
561        (-y1p - cyp) / ry,
562    );
563
564    if !sweep && dtheta > 0.0 {
565        dtheta -= core::f32::consts::TAU;
566    } else if sweep && dtheta < 0.0 {
567        dtheta += core::f32::consts::TAU;
568    }
569
570    // Split into segments of at most PI/2
571    let n_segs = (dtheta.abs() / (core::f32::consts::FRAC_PI_2 + ARC_SPLIT_FUDGE)).ceil() as usize;
572    let n_segs = n_segs.max(1);
573    let seg_angle = dtheta / n_segs as f32;
574
575    let mut prev = start;
576    for i in 0..n_segs {
577        let t1 = theta1 + seg_angle * i as f32;
578        let t2 = theta1 + seg_angle * (i + 1) as f32;
579
580        let (c1, c2, ep) =
581            arc_segment_to_cubic(cx, cy, rx, ry, cos_phi, sin_phi, t1, t2);
582
583        let seg_end = if i + 1 == n_segs { end } else { ep };
584        out.push(SvgPathElement::CubicCurve(SvgCubicCurve {
585            start: prev,
586            ctrl_1: c1,
587            ctrl_2: c2,
588            end: seg_end,
589        }));
590        prev = seg_end;
591    }
592}
593
594/// Compute the angle between two vectors.
595fn angle_between(ux: f32, uy: f32, vx: f32, vy: f32) -> f32 {
596    let dot = ux * vx + uy * vy;
597    let len = ((ux * ux + uy * uy) * (vx * vx + vy * vy)).sqrt();
598    if len < ZERO_LENGTH_EPSILON {
599        return 0.0;
600    }
601    let cos_val = (dot / len).clamp(-1.0, 1.0);
602    let angle = cos_val.acos();
603    if ux * vy - uy * vx < 0.0 {
604        -angle
605    } else {
606        angle
607    }
608}
609
610/// Convert a single arc segment (<=90 degrees) to a cubic bezier.
611fn arc_segment_to_cubic(
612    cx: f32,
613    cy: f32,
614    rx: f32,
615    ry: f32,
616    cos_phi: f32,
617    sin_phi: f32,
618    theta1: f32,
619    theta2: f32,
620) -> (SvgPoint, SvgPoint, SvgPoint) {
621    let alpha = 4.0 / 3.0 * ((theta2 - theta1) / 4.0).tan();
622
623    let cos1 = theta1.cos();
624    let sin1 = theta1.sin();
625    let cos2 = theta2.cos();
626    let sin2 = theta2.sin();
627
628    // Control point 1 (relative to unit circle)
629    let dx1 = rx * (cos1 - alpha * sin1);
630    let dy1 = ry * (sin1 + alpha * cos1);
631    // Control point 2
632    let dx2 = rx * (cos2 + alpha * sin2);
633    let dy2 = ry * (sin2 - alpha * cos2);
634    // End point
635    let dx3 = rx * cos2;
636    let dy3 = ry * sin2;
637
638    let c1 = SvgPoint {
639        x: cos_phi * dx1 - sin_phi * dy1 + cx,
640        y: sin_phi * dx1 + cos_phi * dy1 + cy,
641    };
642    let c2 = SvgPoint {
643        x: cos_phi * dx2 - sin_phi * dy2 + cx,
644        y: sin_phi * dx2 + cos_phi * dy2 + cy,
645    };
646    let ep = SvgPoint {
647        x: cos_phi * dx3 - sin_phi * dy3 + cx,
648        y: sin_phi * dx3 + cos_phi * dy3 + cy,
649    };
650
651    (c1, c2, ep)
652}
653
654/// Approximate a circle with 4 cubic bezier curves.
655///
656/// Uses the standard kappa constant (0.5522847498) for quarter-arc approximation.
657#[must_use]
658pub fn svg_circle_to_paths(cx: f32, cy: f32, r: f32) -> SvgPath {
659    let k = r * KAPPA;
660
661    let elements = vec![
662        // Top to right
663        SvgPathElement::CubicCurve(SvgCubicCurve {
664            start: SvgPoint { x: cx, y: cy - r },
665            ctrl_1: SvgPoint {
666                x: cx + k,
667                y: cy - r,
668            },
669            ctrl_2: SvgPoint {
670                x: cx + r,
671                y: cy - k,
672            },
673            end: SvgPoint { x: cx + r, y: cy },
674        }),
675        // Right to bottom
676        SvgPathElement::CubicCurve(SvgCubicCurve {
677            start: SvgPoint { x: cx + r, y: cy },
678            ctrl_1: SvgPoint {
679                x: cx + r,
680                y: cy + k,
681            },
682            ctrl_2: SvgPoint {
683                x: cx + k,
684                y: cy + r,
685            },
686            end: SvgPoint { x: cx, y: cy + r },
687        }),
688        // Bottom to left
689        SvgPathElement::CubicCurve(SvgCubicCurve {
690            start: SvgPoint { x: cx, y: cy + r },
691            ctrl_1: SvgPoint {
692                x: cx - k,
693                y: cy + r,
694            },
695            ctrl_2: SvgPoint {
696                x: cx - r,
697                y: cy + k,
698            },
699            end: SvgPoint { x: cx - r, y: cy },
700        }),
701        // Left to top
702        SvgPathElement::CubicCurve(SvgCubicCurve {
703            start: SvgPoint { x: cx - r, y: cy },
704            ctrl_1: SvgPoint {
705                x: cx - r,
706                y: cy - k,
707            },
708            ctrl_2: SvgPoint {
709                x: cx - k,
710                y: cy - r,
711            },
712            end: SvgPoint { x: cx, y: cy - r },
713        }),
714    ];
715
716    SvgPath {
717        items: SvgPathElementVec::from_vec(elements),
718    }
719}
720
721/// Convert an SVG `<rect>` to a path with optional rounded corners.
722///
723/// If `rx` and `ry` are both 0, produces 4 line segments.
724/// Otherwise, produces lines for straight edges and cubic curves for corners.
725#[must_use]
726pub fn svg_rect_to_path(x: f32, y: f32, w: f32, h: f32, rx: f32, ry: f32) -> SvgPath {
727    let rx = rx.min(w / 2.0);
728    let ry = ry.min(h / 2.0);
729
730    if rx < CLOSEPATH_EPSILON && ry < CLOSEPATH_EPSILON {
731        // Simple rectangle: 4 lines
732        let tl = SvgPoint { x, y };
733        let tr = SvgPoint { x: x + w, y };
734        let br = SvgPoint { x: x + w, y: y + h };
735        let bl = SvgPoint { x, y: y + h };
736
737        let elements = vec![
738            SvgPathElement::Line(SvgLine { start: tl, end: tr }),
739            SvgPathElement::Line(SvgLine { start: tr, end: br }),
740            SvgPathElement::Line(SvgLine {
741                start: br,
742                end: bl,
743            }),
744            SvgPathElement::Line(SvgLine { start: bl, end: tl }),
745        ];
746
747        return SvgPath {
748            items: SvgPathElementVec::from_vec(elements),
749        };
750    }
751
752    // Rounded rectangle
753    let kx = rx * KAPPA;
754    let ky = ry * KAPPA;
755
756    let mut elements = Vec::with_capacity(8);
757
758    // Top edge (left to right)
759    elements.push(SvgPathElement::Line(SvgLine {
760        start: SvgPoint { x: x + rx, y },
761        end: SvgPoint { x: x + w - rx, y },
762    }));
763    // Top-right corner
764    elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
765        start: SvgPoint { x: x + w - rx, y },
766        ctrl_1: SvgPoint {
767            x: x + w - rx + kx,
768            y,
769        },
770        ctrl_2: SvgPoint {
771            x: x + w,
772            y: y + ry - ky,
773        },
774        end: SvgPoint {
775            x: x + w,
776            y: y + ry,
777        },
778    }));
779    // Right edge
780    elements.push(SvgPathElement::Line(SvgLine {
781        start: SvgPoint {
782            x: x + w,
783            y: y + ry,
784        },
785        end: SvgPoint {
786            x: x + w,
787            y: y + h - ry,
788        },
789    }));
790    // Bottom-right corner
791    elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
792        start: SvgPoint {
793            x: x + w,
794            y: y + h - ry,
795        },
796        ctrl_1: SvgPoint {
797            x: x + w,
798            y: y + h - ry + ky,
799        },
800        ctrl_2: SvgPoint {
801            x: x + w - rx + kx,
802            y: y + h,
803        },
804        end: SvgPoint {
805            x: x + w - rx,
806            y: y + h,
807        },
808    }));
809    // Bottom edge (right to left)
810    elements.push(SvgPathElement::Line(SvgLine {
811        start: SvgPoint {
812            x: x + w - rx,
813            y: y + h,
814        },
815        end: SvgPoint { x: x + rx, y: y + h },
816    }));
817    // Bottom-left corner
818    elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
819        start: SvgPoint { x: x + rx, y: y + h },
820        ctrl_1: SvgPoint {
821            x: x + rx - kx,
822            y: y + h,
823        },
824        ctrl_2: SvgPoint {
825            x,
826            y: y + h - ry + ky,
827        },
828        end: SvgPoint { x, y: y + h - ry },
829    }));
830    // Left edge
831    elements.push(SvgPathElement::Line(SvgLine {
832        start: SvgPoint { x, y: y + h - ry },
833        end: SvgPoint { x, y: y + ry },
834    }));
835    // Top-left corner
836    elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
837        start: SvgPoint { x, y: y + ry },
838        ctrl_1: SvgPoint {
839            x,
840            y: y + ry - ky,
841        },
842        ctrl_2: SvgPoint {
843            x: x + rx - kx,
844            y,
845        },
846        end: SvgPoint { x: x + rx, y },
847    }));
848
849    SvgPath {
850        items: SvgPathElementVec::from_vec(elements),
851    }
852}