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
11046
    fn new(input: &'a [u8]) -> Self {
66
11046
        Self {
67
11046
            input,
68
11046
            pos: 0,
69
11046
            current: SvgPoint { x: 0.0, y: 0.0 },
70
11046
            subpath_start: SvgPoint { x: 0.0, y: 0.0 },
71
11046
            last_control: None,
72
11046
            last_command: 0,
73
11046
        }
74
11046
    }
75

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

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

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

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

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

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

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

            
124
468552
        let mut has_digits = false;
125

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

            
136
        // Decimal part
137
468552
        if let Some(&b'.') = self.input.get(self.pos) {
138
373254
            self.pos += 1;
139
1044372
            while let Some(&b) = self.input.get(self.pos) {
140
1044078
                if b.is_ascii_digit() {
141
671118
                    self.pos += 1;
142
671118
                    has_digits = true;
143
671118
                } else {
144
372960
                    break;
145
                }
146
            }
147
95298
        }
148

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
339
11046
    parser.skip_whitespace();
340

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
849
168
    SvgPath {
850
168
        items: SvgPathElementVec::from_vec(elements),
851
168
    }
852
378
}