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

            
7
use alloc::{string::String, vec::Vec};
8
use azul_css::props::basic::{SvgCubicCurve, SvgPoint, SvgQuadraticCurve};
9

            
10
use crate::svg::{SvgLine, SvgMultiPolygon, SvgPath, SvgPathElement, SvgPathElementVec, SvgPathVec};
11

            
12
/// Bezier approximation constant for quarter-circle arcs.
13
const KAPPA: f32 = 0.5522847498;
14

            
15
/// Tolerance for treating two points as coincident (used in closepath and arc degeneracy checks).
16
const POINT_EPSILON: f32 = 1e-6;
17

            
18
/// Tolerance for snapping a closepath line (slightly larger to avoid micro-segments).
19
const CLOSEPATH_EPSILON: f32 = 0.001;
20

            
21
/// Tolerance for treating a vector length as zero in angle computation.
22
const ZERO_LENGTH_EPSILON: f32 = 1e-10;
23

            
24
/// Small offset added to PI/2 when splitting arcs to avoid exact-boundary floating-point issues.
25
const ARC_SPLIT_FUDGE: f32 = 0.001;
26

            
27
/// Errors that can occur during SVG path parsing.
28
#[derive(Debug, Clone, PartialEq)]
29
pub 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.
41
impl 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.
55
struct 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

            
64
impl<'a> PathParser<'a> {
65
13413
    fn new(input: &'a [u8]) -> Self {
66
13413
        Self {
67
13413
            input,
68
13413
            pos: 0,
69
13413
            current: SvgPoint { x: 0.0, y: 0.0 },
70
13413
            subpath_start: SvgPoint { x: 0.0, y: 0.0 },
71
13413
            last_control: None,
72
13413
            last_command: 0,
73
13413
        }
74
13413
    }
75

            
76
270045
    fn at_end(&self) -> bool {
77
270045
        self.pos >= self.input.len()
78
270045
    }
79

            
80
181407
    fn peek(&self) -> Option<u8> {
81
181407
        self.input.get(self.pos).copied()
82
181407
    }
83

            
84
751689
    fn skip_whitespace_and_commas(&mut self) {
85
1039278
        while let Some(&b) = self.input.get(self.pos) {
86
1038054
            if b == b' ' || b == b'\t' || b == b'\n' || b == b'\r' || b == b',' {
87
287589
                self.pos += 1;
88
287589
            } else {
89
750465
                break;
90
            }
91
        }
92
751689
    }
93

            
94
13413
    fn skip_whitespace(&mut self) {
95
13413
        while let Some(&b) = self.input.get(self.pos) {
96
13413
            if b == b' ' || b == b'\t' || b == b'\n' || b == b'\r' {
97
                self.pos += 1;
98
            } else {
99
13413
                break;
100
            }
101
        }
102
13413
    }
103

            
104
    /// Returns true if the current position looks like the start of a number.
105
60282
    fn has_number(&self) -> bool {
106
60282
        match self.input.get(self.pos) {
107
19380
            Some(b'+') | Some(b'-') | Some(b'.') => true,
108
40902
            Some(b) if b.is_ascii_digit() => true,
109
            _ => false,
110
        }
111
60282
    }
112

            
113
568956
    fn parse_number(&mut self) -> Result<f32, SvgPathParseError> {
114
568956
        self.skip_whitespace_and_commas();
115
568956
        let start = self.pos;
116

            
117
        // Optional sign
118
568956
        if let Some(&b) = self.input.get(self.pos) {
119
568956
            if b == b'+' || b == b'-' {
120
252501
                self.pos += 1;
121
316455
            }
122
        }
123

            
124
568956
        let mut has_digits = false;
125

            
126
        // Integer part
127
1337271
        while let Some(&b) = self.input.get(self.pos) {
128
1336404
            if b.is_ascii_digit() {
129
768315
                self.pos += 1;
130
768315
                has_digits = true;
131
768315
            } else {
132
568089
                break;
133
            }
134
        }
135

            
136
        // Decimal part
137
568956
        if let Some(&b'.') = self.input.get(self.pos) {
138
453237
            self.pos += 1;
139
1268166
            while let Some(&b) = self.input.get(self.pos) {
140
1267809
                if b.is_ascii_digit() {
141
814929
                    self.pos += 1;
142
814929
                    has_digits = true;
143
814929
                } else {
144
452880
                    break;
145
                }
146
            }
147
115719
        }
148

            
149
568956
        if !has_digits {
150
            return Err(SvgPathParseError::ExpectedNumber { pos: start });
151
568956
        }
152

            
153
        // Exponent
154
568956
        if let Some(&b) = self.input.get(self.pos) {
155
567732
            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
567732
            }
170
1224
        }
171

            
172
568956
        let s = core::str::from_utf8(&self.input[start..self.pos])
173
568956
            .map_err(|_| SvgPathParseError::ExpectedNumber { pos: start })?;
174
568956
        s.parse::<f32>()
175
568956
            .map_err(|_| SvgPathParseError::ExpectedNumber { pos: start })
176
568956
    }
177

            
178
102
    fn parse_flag(&mut self) -> Result<bool, SvgPathParseError> {
179
102
        self.skip_whitespace_and_commas();
180
102
        match self.input.get(self.pos) {
181
            Some(b'0') => {
182
51
                self.pos += 1;
183
51
                Ok(false)
184
            }
185
            Some(b'1') => {
186
51
                self.pos += 1;
187
51
                Ok(true)
188
            }
189
            _ => Err(SvgPathParseError::InvalidArcFlag { pos: self.pos }),
190
        }
191
102
    }
192

            
193
284121
    fn parse_coordinate_pair(&mut self) -> Result<(f32, f32), SvgPathParseError> {
194
284121
        let x = self.parse_number()?;
195
284121
        let y = self.parse_number()?;
196
284121
        Ok((x, y))
197
284121
    }
198

            
199
284121
    fn make_absolute(&self, x: f32, y: f32, relative: bool) -> SvgPoint {
200
284121
        if relative {
201
278103
            SvgPoint {
202
278103
                x: self.current.x + x,
203
278103
                y: self.current.y + y,
204
278103
            }
205
        } else {
206
6018
            SvgPoint { x, y }
207
        }
208
284121
    }
209

            
210
11679
    fn handle_line_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
211
11679
        let (x, y) = self.parse_coordinate_pair()?;
212
11679
        let end = self.make_absolute(x, y, relative);
213
11679
        elements.push(SvgPathElement::Line(SvgLine { start: self.current, end }));
214
11679
        self.current = end;
215
11679
        self.last_control = None;
216
11679
        Ok(())
217
11679
    }
218

            
219
102
    fn handle_horizontal_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
220
102
        let x = self.parse_number()?;
221
102
        let abs_x = if relative { self.current.x + x } else { x };
222
102
        let end = SvgPoint { x: abs_x, y: self.current.y };
223
102
        elements.push(SvgPathElement::Line(SvgLine { start: self.current, end }));
224
102
        self.current = end;
225
102
        self.last_control = None;
226
102
        Ok(())
227
102
    }
228

            
229
459
    fn handle_vertical_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
230
459
        let y = self.parse_number()?;
231
459
        let abs_y = if relative { self.current.y + y } else { y };
232
459
        let end = SvgPoint { x: self.current.x, y: abs_y };
233
459
        elements.push(SvgPathElement::Line(SvgLine { start: self.current, end }));
234
459
        self.current = end;
235
459
        self.last_control = None;
236
459
        Ok(())
237
459
    }
238

            
239
66300
    fn handle_cubic_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
240
66300
        let (c1x, c1y) = self.parse_coordinate_pair()?;
241
66300
        let (c2x, c2y) = self.parse_coordinate_pair()?;
242
66300
        let (ex, ey) = self.parse_coordinate_pair()?;
243
66300
        let ctrl_1 = self.make_absolute(c1x, c1y, relative);
244
66300
        let ctrl_2 = self.make_absolute(c2x, c2y, relative);
245
66300
        let end = self.make_absolute(ex, ey, relative);
246
66300
        elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
247
66300
            start: self.current, ctrl_1, ctrl_2, end,
248
66300
        }));
249
66300
        self.last_control = Some(ctrl_2);
250
66300
        self.current = end;
251
66300
        Ok(())
252
66300
    }
253

            
254
29886
    fn handle_smooth_cubic_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
255
29886
        let ctrl_1 = match self.last_control {
256
17850
            Some(lc) if matches!(self.last_command.to_ascii_uppercase(), b'C' | b'S') => {
257
17850
                SvgPoint {
258
17850
                    x: 2.0 * self.current.x - lc.x,
259
17850
                    y: 2.0 * self.current.y - lc.y,
260
17850
                }
261
            }
262
12036
            _ => self.current,
263
        };
264
29886
        let (c2x, c2y) = self.parse_coordinate_pair()?;
265
29886
        let (ex, ey) = self.parse_coordinate_pair()?;
266
29886
        let ctrl_2 = self.make_absolute(c2x, c2y, relative);
267
29886
        let end = self.make_absolute(ex, ey, relative);
268
29886
        elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
269
29886
            start: self.current, ctrl_1, ctrl_2, end,
270
29886
        }));
271
29886
        self.last_control = Some(ctrl_2);
272
29886
        self.current = end;
273
29886
        Ok(())
274
29886
    }
275

            
276
102
    fn handle_quadratic_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
277
102
        let (cx, cy) = self.parse_coordinate_pair()?;
278
102
        let (ex, ey) = self.parse_coordinate_pair()?;
279
102
        let ctrl = self.make_absolute(cx, cy, relative);
280
102
        let end = self.make_absolute(ex, ey, relative);
281
102
        elements.push(SvgPathElement::QuadraticCurve(SvgQuadraticCurve {
282
102
            start: self.current, ctrl, end,
283
102
        }));
284
102
        self.last_control = Some(ctrl);
285
102
        self.current = end;
286
102
        Ok(())
287
102
    }
288

            
289
51
    fn handle_smooth_quadratic_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
290
51
        let ctrl = match self.last_control {
291
51
            Some(lc) if matches!(self.last_command.to_ascii_uppercase(), b'Q' | b'T') => {
292
51
                SvgPoint {
293
51
                    x: 2.0 * self.current.x - lc.x,
294
51
                    y: 2.0 * self.current.y - lc.y,
295
51
                }
296
            }
297
            _ => self.current,
298
        };
299
51
        let (ex, ey) = self.parse_coordinate_pair()?;
300
51
        let end = self.make_absolute(ex, ey, relative);
301
51
        elements.push(SvgPathElement::QuadraticCurve(SvgQuadraticCurve {
302
51
            start: self.current, ctrl, end,
303
51
        }));
304
51
        self.last_control = Some(ctrl);
305
51
        self.current = end;
306
51
        Ok(())
307
51
    }
308

            
309
51
    fn handle_arc_to(&mut self, relative: bool, elements: &mut Vec<SvgPathElement>) -> Result<(), SvgPathParseError> {
310
51
        let rx = self.parse_number()?.abs();
311
51
        let ry = self.parse_number()?.abs();
312
51
        let x_rotation = self.parse_number()?;
313
51
        let large_arc = self.parse_flag()?;
314
51
        let sweep = self.parse_flag()?;
315
51
        let (ex, ey) = self.parse_coordinate_pair()?;
316
51
        let end = self.make_absolute(ex, ey, relative);
317
51
        arc_to_cubics(self.current, end, rx, ry, x_rotation, large_arc, sweep, elements);
318
51
        self.current = end;
319
51
        self.last_control = None;
320
51
        Ok(())
321
51
    }
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]
329
13515
pub fn parse_svg_path_d(d: &str) -> Result<SvgMultiPolygon, SvgPathParseError> {
330
13515
    let d = d.trim();
331
13515
    if d.is_empty() {
332
102
        return Err(SvgPathParseError::EmptyPath);
333
13413
    }
334

            
335
13413
    let mut parser = PathParser::new(d.as_bytes());
336
13413
    let mut rings: Vec<SvgPath> = Vec::new();
337
13413
    let mut current_elements: Vec<SvgPathElement> = Vec::new();
338

            
339
13413
    parser.skip_whitespace();
340

            
341
87414
    while !parser.at_end() {
342
74001
        parser.skip_whitespace_and_commas();
343
74001
        if parser.at_end() {
344
            break;
345
74001
        }
346

            
347
74001
        let b = parser.peek().unwrap();
348

            
349
        // Determine if this is a command letter or an implicit repeat
350
74001
        let cmd = if b.is_ascii_alphabetic() {
351
73389
            parser.pos += 1;
352
73389
            b
353
612
        } else if parser.last_command != 0 {
354
            // Implicit repeat: after M/m, implicit commands become L/l
355
612
            match parser.last_command {
356
153
                b'M' => b'L',
357
459
                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
74001
        let relative = cmd.is_ascii_lowercase();
368
74001
        let cmd_upper = cmd.to_ascii_uppercase();
369

            
370
74001
        match cmd_upper {
371
            b'M' => {
372
                // Flush current subpath
373
13464
                if !current_elements.is_empty() {
374
51
                    rings.push(SvgPath {
375
51
                        items: SvgPathElementVec::from_vec(core::mem::take(&mut current_elements)),
376
51
                    });
377
13413
                }
378
13464
                let (x, y) = parser.parse_coordinate_pair()?;
379
13464
                let pt = parser.make_absolute(x, y, relative);
380
13464
                parser.current = pt;
381
13464
                parser.subpath_start = pt;
382
13464
                parser.last_control = None;
383
13464
                parser.last_command = cmd;
384
            }
385
            b'L' => {
386
8721
                parser.handle_line_to(relative, &mut current_elements)?;
387
8721
                parser.last_command = cmd;
388
            }
389
            b'H' => {
390
102
                parser.handle_horizontal_to(relative, &mut current_elements)?;
391
102
                parser.last_command = cmd;
392
            }
393
            b'V' => {
394
459
                parser.handle_vertical_to(relative, &mut current_elements)?;
395
459
                parser.last_command = cmd;
396
            }
397
            b'C' => {
398
18972
                parser.handle_cubic_to(relative, &mut current_elements)?;
399
18972
                parser.last_command = cmd;
400
            }
401
            b'S' => {
402
19890
                parser.handle_smooth_cubic_to(relative, &mut current_elements)?;
403
19890
                parser.last_command = cmd;
404
            }
405
            b'Q' => {
406
102
                parser.handle_quadratic_to(relative, &mut current_elements)?;
407
102
                parser.last_command = cmd;
408
            }
409
            b'T' => {
410
51
                parser.handle_smooth_quadratic_to(relative, &mut current_elements)?;
411
51
                parser.last_command = cmd;
412
            }
413
            b'A' => {
414
51
                parser.handle_arc_to(relative, &mut current_elements)?;
415
51
                parser.last_command = cmd;
416
            }
417
            b'Z' => {
418
                // Close subpath
419
12189
                let dx = parser.current.x - parser.subpath_start.x;
420
12189
                let dy = parser.current.y - parser.subpath_start.y;
421
12189
                if dx * dx + dy * dy > CLOSEPATH_EPSILON * CLOSEPATH_EPSILON {
422
816
                    current_elements.push(SvgPathElement::Line(SvgLine {
423
816
                        start: parser.current,
424
816
                        end: parser.subpath_start,
425
816
                    }));
426
11373
                }
427
12189
                parser.current = parser.subpath_start;
428
12189
                parser.last_control = None;
429
12189
                parser.last_command = cmd;
430

            
431
                // Flush current subpath
432
12189
                if !current_elements.is_empty() {
433
12138
                    rings.push(SvgPath {
434
12138
                        items: SvgPathElementVec::from_vec(core::mem::take(&mut current_elements)),
435
12138
                    });
436
12138
                }
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
74001
        if cmd_upper != b'M' && cmd_upper != b'Z' {
449
            loop {
450
108630
                parser.skip_whitespace_and_commas();
451
108630
                if parser.at_end() {
452
1224
                    break;
453
107406
                }
454
107406
                let next = parser.peek().unwrap();
455
107406
                if next.is_ascii_alphabetic() {
456
47124
                    break; // Next command letter
457
60282
                }
458
60282
                if !parser.has_number() {
459
                    break;
460
60282
                }
461

            
462
                // Implicit repeat of the same command
463
60282
                match cmd_upper {
464
2958
                    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
47328
                    b'C' => parser.handle_cubic_to(relative, &mut current_elements)?,
468
9996
                    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
25653
        }
476
    }
477

            
478
    // Flush any remaining elements
479
13413
    if !current_elements.is_empty() {
480
1224
        rings.push(SvgPath {
481
1224
            items: SvgPathElementVec::from_vec(current_elements),
482
1224
        });
483
12189
    }
484

            
485
13413
    Ok(SvgMultiPolygon {
486
13413
        rings: SvgPathVec::from_vec(rings),
487
13413
    })
488
13515
}
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).
493
51
fn arc_to_cubics(
494
51
    start: SvgPoint,
495
51
    end: SvgPoint,
496
51
    mut rx: f32,
497
51
    mut ry: f32,
498
51
    x_rotation_deg: f32,
499
51
    large_arc: bool,
500
51
    sweep: bool,
501
51
    out: &mut Vec<SvgPathElement>,
502
51
) {
503
    // Degenerate cases
504
51
    if (start.x - end.x).abs() < POINT_EPSILON && (start.y - end.y).abs() < POINT_EPSILON {
505
        return;
506
51
    }
507
51
    if rx < POINT_EPSILON || ry < POINT_EPSILON {
508
        out.push(SvgPathElement::Line(SvgLine { start, end }));
509
        return;
510
51
    }
511

            
512
51
    let phi = x_rotation_deg.to_radians();
513
51
    let cos_phi = phi.cos();
514
51
    let sin_phi = phi.sin();
515

            
516
    // Step 1: Compute (x1', y1')
517
51
    let dx = (start.x - end.x) / 2.0;
518
51
    let dy = (start.y - end.y) / 2.0;
519
51
    let x1p = cos_phi * dx + sin_phi * dy;
520
51
    let y1p = -sin_phi * dx + cos_phi * dy;
521

            
522
    // Step 2: Compute (cx', cy') - correct radii if too small
523
51
    let x1p2 = x1p * x1p;
524
51
    let y1p2 = y1p * y1p;
525
51
    let mut rx2 = rx * rx;
526
51
    let mut ry2 = ry * ry;
527

            
528
51
    let lambda = x1p2 / rx2 + y1p2 / ry2;
529
51
    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
51
    }
536

            
537
51
    let num = (rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2).max(0.0);
538
51
    let den = rx2 * y1p2 + ry2 * x1p2;
539
51
    let sq = if den > 0.0 {
540
51
        (num / den).sqrt()
541
    } else {
542
        0.0
543
    };
544

            
545
51
    let sign = if large_arc == sweep { -1.0 } else { 1.0 };
546
51
    let cxp = sign * sq * (rx * y1p / ry);
547
51
    let cyp = sign * sq * -(ry * x1p / rx);
548

            
549
    // Step 3: Compute (cx, cy) from (cx', cy')
550
51
    let mx = (start.x + end.x) / 2.0;
551
51
    let my = (start.y + end.y) / 2.0;
552
51
    let cx = cos_phi * cxp - sin_phi * cyp + mx;
553
51
    let cy = sin_phi * cxp + cos_phi * cyp + my;
554

            
555
    // Step 4: Compute theta1 and dtheta
556
51
    let theta1 = angle_between(1.0, 0.0, (x1p - cxp) / rx, (y1p - cyp) / ry);
557
51
    let mut dtheta = angle_between(
558
51
        (x1p - cxp) / rx,
559
51
        (y1p - cyp) / ry,
560
51
        (-x1p - cxp) / rx,
561
51
        (-y1p - cyp) / ry,
562
    );
563

            
564
51
    if !sweep && dtheta > 0.0 {
565
        dtheta -= core::f32::consts::TAU;
566
51
    } else if sweep && dtheta < 0.0 {
567
        dtheta += core::f32::consts::TAU;
568
51
    }
569

            
570
    // Split into segments of at most PI/2
571
51
    let n_segs = (dtheta.abs() / (core::f32::consts::FRAC_PI_2 + ARC_SPLIT_FUDGE)).ceil() as usize;
572
51
    let n_segs = n_segs.max(1);
573
51
    let seg_angle = dtheta / n_segs as f32;
574

            
575
51
    let mut prev = start;
576
102
    for i in 0..n_segs {
577
102
        let t1 = theta1 + seg_angle * i as f32;
578
102
        let t2 = theta1 + seg_angle * (i + 1) as f32;
579

            
580
102
        let (c1, c2, ep) =
581
102
            arc_segment_to_cubic(cx, cy, rx, ry, cos_phi, sin_phi, t1, t2);
582

            
583
102
        let seg_end = if i + 1 == n_segs { end } else { ep };
584
102
        out.push(SvgPathElement::CubicCurve(SvgCubicCurve {
585
102
            start: prev,
586
102
            ctrl_1: c1,
587
102
            ctrl_2: c2,
588
102
            end: seg_end,
589
102
        }));
590
102
        prev = seg_end;
591
    }
592
51
}
593

            
594
/// Compute the angle between two vectors.
595
102
fn angle_between(ux: f32, uy: f32, vx: f32, vy: f32) -> f32 {
596
102
    let dot = ux * vx + uy * vy;
597
102
    let len = ((ux * ux + uy * uy) * (vx * vx + vy * vy)).sqrt();
598
102
    if len < ZERO_LENGTH_EPSILON {
599
        return 0.0;
600
102
    }
601
102
    let cos_val = (dot / len).clamp(-1.0, 1.0);
602
102
    let angle = cos_val.acos();
603
102
    if ux * vy - uy * vx < 0.0 {
604
        -angle
605
    } else {
606
102
        angle
607
    }
608
102
}
609

            
610
/// Convert a single arc segment (<=90 degrees) to a cubic bezier.
611
102
fn arc_segment_to_cubic(
612
102
    cx: f32,
613
102
    cy: f32,
614
102
    rx: f32,
615
102
    ry: f32,
616
102
    cos_phi: f32,
617
102
    sin_phi: f32,
618
102
    theta1: f32,
619
102
    theta2: f32,
620
102
) -> (SvgPoint, SvgPoint, SvgPoint) {
621
102
    let alpha = 4.0 / 3.0 * ((theta2 - theta1) / 4.0).tan();
622

            
623
102
    let cos1 = theta1.cos();
624
102
    let sin1 = theta1.sin();
625
102
    let cos2 = theta2.cos();
626
102
    let sin2 = theta2.sin();
627

            
628
    // Control point 1 (relative to unit circle)
629
102
    let dx1 = rx * (cos1 - alpha * sin1);
630
102
    let dy1 = ry * (sin1 + alpha * cos1);
631
    // Control point 2
632
102
    let dx2 = rx * (cos2 + alpha * sin2);
633
102
    let dy2 = ry * (sin2 - alpha * cos2);
634
    // End point
635
102
    let dx3 = rx * cos2;
636
102
    let dy3 = ry * sin2;
637

            
638
102
    let c1 = SvgPoint {
639
102
        x: cos_phi * dx1 - sin_phi * dy1 + cx,
640
102
        y: sin_phi * dx1 + cos_phi * dy1 + cy,
641
102
    };
642
102
    let c2 = SvgPoint {
643
102
        x: cos_phi * dx2 - sin_phi * dy2 + cx,
644
102
        y: sin_phi * dx2 + cos_phi * dy2 + cy,
645
102
    };
646
102
    let ep = SvgPoint {
647
102
        x: cos_phi * dx3 - sin_phi * dy3 + cx,
648
102
        y: sin_phi * dx3 + cos_phi * dy3 + cy,
649
102
    };
650

            
651
102
    (c1, c2, ep)
652
102
}
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]
658
357
pub fn svg_circle_to_paths(cx: f32, cy: f32, r: f32) -> SvgPath {
659
357
    let k = r * KAPPA;
660

            
661
357
    let elements = vec![
662
        // Top to right
663
357
        SvgPathElement::CubicCurve(SvgCubicCurve {
664
357
            start: SvgPoint { x: cx, y: cy - r },
665
357
            ctrl_1: SvgPoint {
666
357
                x: cx + k,
667
357
                y: cy - r,
668
357
            },
669
357
            ctrl_2: SvgPoint {
670
357
                x: cx + r,
671
357
                y: cy - k,
672
357
            },
673
357
            end: SvgPoint { x: cx + r, y: cy },
674
357
        }),
675
        // Right to bottom
676
357
        SvgPathElement::CubicCurve(SvgCubicCurve {
677
357
            start: SvgPoint { x: cx + r, y: cy },
678
357
            ctrl_1: SvgPoint {
679
357
                x: cx + r,
680
357
                y: cy + k,
681
357
            },
682
357
            ctrl_2: SvgPoint {
683
357
                x: cx + k,
684
357
                y: cy + r,
685
357
            },
686
357
            end: SvgPoint { x: cx, y: cy + r },
687
357
        }),
688
        // Bottom to left
689
357
        SvgPathElement::CubicCurve(SvgCubicCurve {
690
357
            start: SvgPoint { x: cx, y: cy + r },
691
357
            ctrl_1: SvgPoint {
692
357
                x: cx - k,
693
357
                y: cy + r,
694
357
            },
695
357
            ctrl_2: SvgPoint {
696
357
                x: cx - r,
697
357
                y: cy + k,
698
357
            },
699
357
            end: SvgPoint { x: cx - r, y: cy },
700
357
        }),
701
        // Left to top
702
357
        SvgPathElement::CubicCurve(SvgCubicCurve {
703
357
            start: SvgPoint { x: cx - r, y: cy },
704
357
            ctrl_1: SvgPoint {
705
357
                x: cx - r,
706
357
                y: cy - k,
707
357
            },
708
357
            ctrl_2: SvgPoint {
709
357
                x: cx - k,
710
357
                y: cy - r,
711
357
            },
712
357
            end: SvgPoint { x: cx, y: cy - r },
713
357
        }),
714
    ];
715

            
716
357
    SvgPath {
717
357
        items: SvgPathElementVec::from_vec(elements),
718
357
    }
719
357
}
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]
726
459
pub fn svg_rect_to_path(x: f32, y: f32, w: f32, h: f32, rx: f32, ry: f32) -> SvgPath {
727
459
    let rx = rx.min(w / 2.0);
728
459
    let ry = ry.min(h / 2.0);
729

            
730
459
    if rx < CLOSEPATH_EPSILON && ry < CLOSEPATH_EPSILON {
731
        // Simple rectangle: 4 lines
732
255
        let tl = SvgPoint { x, y };
733
255
        let tr = SvgPoint { x: x + w, y };
734
255
        let br = SvgPoint { x: x + w, y: y + h };
735
255
        let bl = SvgPoint { x, y: y + h };
736

            
737
255
        let elements = vec![
738
255
            SvgPathElement::Line(SvgLine { start: tl, end: tr }),
739
255
            SvgPathElement::Line(SvgLine { start: tr, end: br }),
740
255
            SvgPathElement::Line(SvgLine {
741
255
                start: br,
742
255
                end: bl,
743
255
            }),
744
255
            SvgPathElement::Line(SvgLine { start: bl, end: tl }),
745
        ];
746

            
747
255
        return SvgPath {
748
255
            items: SvgPathElementVec::from_vec(elements),
749
255
        };
750
204
    }
751

            
752
    // Rounded rectangle
753
204
    let kx = rx * KAPPA;
754
204
    let ky = ry * KAPPA;
755

            
756
204
    let mut elements = Vec::with_capacity(8);
757

            
758
    // Top edge (left to right)
759
204
    elements.push(SvgPathElement::Line(SvgLine {
760
204
        start: SvgPoint { x: x + rx, y },
761
204
        end: SvgPoint { x: x + w - rx, y },
762
204
    }));
763
    // Top-right corner
764
204
    elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
765
204
        start: SvgPoint { x: x + w - rx, y },
766
204
        ctrl_1: SvgPoint {
767
204
            x: x + w - rx + kx,
768
204
            y,
769
204
        },
770
204
        ctrl_2: SvgPoint {
771
204
            x: x + w,
772
204
            y: y + ry - ky,
773
204
        },
774
204
        end: SvgPoint {
775
204
            x: x + w,
776
204
            y: y + ry,
777
204
        },
778
204
    }));
779
    // Right edge
780
204
    elements.push(SvgPathElement::Line(SvgLine {
781
204
        start: SvgPoint {
782
204
            x: x + w,
783
204
            y: y + ry,
784
204
        },
785
204
        end: SvgPoint {
786
204
            x: x + w,
787
204
            y: y + h - ry,
788
204
        },
789
204
    }));
790
    // Bottom-right corner
791
204
    elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
792
204
        start: SvgPoint {
793
204
            x: x + w,
794
204
            y: y + h - ry,
795
204
        },
796
204
        ctrl_1: SvgPoint {
797
204
            x: x + w,
798
204
            y: y + h - ry + ky,
799
204
        },
800
204
        ctrl_2: SvgPoint {
801
204
            x: x + w - rx + kx,
802
204
            y: y + h,
803
204
        },
804
204
        end: SvgPoint {
805
204
            x: x + w - rx,
806
204
            y: y + h,
807
204
        },
808
204
    }));
809
    // Bottom edge (right to left)
810
204
    elements.push(SvgPathElement::Line(SvgLine {
811
204
        start: SvgPoint {
812
204
            x: x + w - rx,
813
204
            y: y + h,
814
204
        },
815
204
        end: SvgPoint { x: x + rx, y: y + h },
816
204
    }));
817
    // Bottom-left corner
818
204
    elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
819
204
        start: SvgPoint { x: x + rx, y: y + h },
820
204
        ctrl_1: SvgPoint {
821
204
            x: x + rx - kx,
822
204
            y: y + h,
823
204
        },
824
204
        ctrl_2: SvgPoint {
825
204
            x,
826
204
            y: y + h - ry + ky,
827
204
        },
828
204
        end: SvgPoint { x, y: y + h - ry },
829
204
    }));
830
    // Left edge
831
204
    elements.push(SvgPathElement::Line(SvgLine {
832
204
        start: SvgPoint { x, y: y + h - ry },
833
204
        end: SvgPoint { x, y: y + ry },
834
204
    }));
835
    // Top-left corner
836
204
    elements.push(SvgPathElement::CubicCurve(SvgCubicCurve {
837
204
        start: SvgPoint { x, y: y + ry },
838
204
        ctrl_1: SvgPoint {
839
204
            x,
840
204
            y: y + ry - ky,
841
204
        },
842
204
        ctrl_2: SvgPoint {
843
204
            x: x + rx - kx,
844
204
            y,
845
204
        },
846
204
        end: SvgPoint { x: x + rx, y },
847
204
    }));
848

            
849
204
    SvgPath {
850
204
        items: SvgPathElementVec::from_vec(elements),
851
204
    }
852
459
}