1use thiserror::Error;
16
17use crate::geometry::{Point, Rect};
18
19const MAX_FLATTEN_DEPTH: u32 = 12;
21const FLATTEN_TOLERANCE: f32 = 0.05;
23const ARC_MAX_ANGLE_STEP: f32 = std::f32::consts::PI / 16.0;
25const SUBSAMPLES: usize = 4;
27
28#[derive(Debug, Clone, PartialEq, Eq, Error)]
30pub enum SvgPathError {
31 #[error("unexpected byte {byte:?} at offset {offset}")]
32 UnexpectedByte { byte: char, offset: usize },
33 #[error("expected a number at offset {offset}")]
34 ExpectedNumber { offset: usize },
35 #[error("expected an arc flag (0 or 1) at offset {offset}")]
36 ExpectedFlag { offset: usize },
37 #[error("path data must start with a moveto (M/m) command")]
38 MissingMoveTo,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub enum PathFillRule {
44 #[default]
46 NonZero,
47 EvenOdd,
49}
50
51#[derive(Debug, Clone)]
53pub struct VectorPath {
54 subpaths: Vec<Vec<Point>>,
55 fill_rule: PathFillRule,
56 bounds: Rect,
57}
58
59impl VectorPath {
60 pub fn parse(d: &str) -> Result<Self, SvgPathError> {
62 let subpaths = parse_path_data(d)?;
63 Ok(Self::from_subpaths(subpaths, PathFillRule::NonZero))
64 }
65
66 pub fn parse_with_fill_rule(d: &str, fill_rule: PathFillRule) -> Result<Self, SvgPathError> {
68 let subpaths = parse_path_data(d)?;
69 Ok(Self::from_subpaths(subpaths, fill_rule))
70 }
71
72 pub(crate) fn from_subpaths(subpaths: Vec<Vec<Point>>, fill_rule: PathFillRule) -> Self {
73 let mut min = Point::new(f32::INFINITY, f32::INFINITY);
74 let mut max = Point::new(f32::NEG_INFINITY, f32::NEG_INFINITY);
75 for point in subpaths.iter().flatten() {
76 min.x = min.x.min(point.x);
77 min.y = min.y.min(point.y);
78 max.x = max.x.max(point.x);
79 max.y = max.y.max(point.y);
80 }
81 let bounds = if min.x.is_finite() {
82 Rect {
83 x: min.x,
84 y: min.y,
85 width: (max.x - min.x).max(0.0),
86 height: (max.y - min.y).max(0.0),
87 }
88 } else {
89 Rect {
90 x: 0.0,
91 y: 0.0,
92 width: 0.0,
93 height: 0.0,
94 }
95 };
96 Self {
97 subpaths,
98 fill_rule,
99 bounds,
100 }
101 }
102
103 pub fn scaled(&self, factor: f32) -> Self {
106 let subpaths = self
107 .subpaths
108 .iter()
109 .map(|subpath| {
110 subpath
111 .iter()
112 .map(|point| Point::new(point.x * factor, point.y * factor))
113 .collect()
114 })
115 .collect();
116 Self::from_subpaths(subpaths, self.fill_rule)
117 }
118
119 pub fn translated(&self, dx: f32, dy: f32) -> Self {
121 let subpaths = self
122 .subpaths
123 .iter()
124 .map(|subpath| {
125 subpath
126 .iter()
127 .map(|point| Point::new(point.x + dx, point.y + dy))
128 .collect()
129 })
130 .collect();
131 Self::from_subpaths(subpaths, self.fill_rule)
132 }
133
134 pub fn fill_rule(&self) -> PathFillRule {
136 self.fill_rule
137 }
138
139 pub fn bounds(&self) -> Rect {
141 self.bounds
142 }
143
144 pub fn is_empty(&self) -> bool {
146 !self.subpaths.iter().any(|subpath| subpath.len() >= 3)
147 }
148
149 pub fn subpaths(&self) -> &[Vec<Point>] {
151 &self.subpaths
152 }
153
154 pub fn coverage_mask(&self, width: usize, height: usize, origin: Point, scale: f32) -> Vec<u8> {
158 let mut mask = vec![0u8; width * height];
159 if width == 0 || height == 0 || scale <= 0.0 {
160 return mask;
161 }
162 let mut edges = self.scanline_edges(origin, scale);
163 if edges.is_empty() {
164 return mask;
165 }
166 edges.sort_by(|a, b| a.top.y.total_cmp(&b.top.y));
167 let mut scanner = EdgeScanner::new(&edges);
168 let mut row_coverage = vec![0.0f32; width];
169 let subsample_weight = 1.0 / SUBSAMPLES as f32;
170 for (row, mask_row) in mask.chunks_exact_mut(width).enumerate() {
171 row_coverage.fill(0.0);
172 let mut row_touched = false;
173 for sub in 0..SUBSAMPLES {
174 let sample_y = row as f32 + (sub as f32 + 0.5) * subsample_weight;
175 let crossings = scanner.crossings_at(sample_y);
176 row_touched |= self.fill_rule.for_each_span(crossings, |x0, x1| {
177 accumulate_span(&mut row_coverage, x0, x1, subsample_weight, width)
178 });
179 }
180 if row_touched {
181 add_row_coverage(mask_row, &row_coverage);
182 }
183 }
184 mask
185 }
186
187 fn scanline_edges(&self, origin: Point, scale: f32) -> Vec<Edge> {
190 let map = |p: &Point| Point::new((p.x - origin.x) * scale, (p.y - origin.y) * scale);
191 let closed = self.subpaths.iter().filter(|subpath| subpath.len() >= 3);
192 closed
193 .flat_map(|subpath| {
194 let next = subpath.iter().skip(1).chain(subpath.first());
195 subpath.iter().zip(next).filter_map(move |(a, b)| {
196 let (a, b) = (map(a), map(b));
197 match a.y.total_cmp(&b.y) {
198 std::cmp::Ordering::Less => Some(Edge {
199 top: a,
200 bottom: b,
201 winding: 1,
202 }),
203 std::cmp::Ordering::Greater => Some(Edge {
204 top: b,
205 bottom: a,
206 winding: -1,
207 }),
208 std::cmp::Ordering::Equal => None,
209 }
210 })
211 })
212 .collect()
213 }
214}
215
216struct Edge {
219 top: Point,
220 bottom: Point,
221 winding: i32,
222}
223
224struct EdgeScanner<'a> {
228 edges: &'a [Edge],
229 next: usize,
230 active: Vec<&'a Edge>,
231 crossings: Vec<(f32, i32)>,
232}
233
234impl<'a> EdgeScanner<'a> {
235 fn new(edges: &'a [Edge]) -> Self {
236 Self {
237 edges,
238 next: 0,
239 active: Vec::new(),
240 crossings: Vec::new(),
241 }
242 }
243
244 fn crossings_at(&mut self, sample_y: f32) -> &[(f32, i32)] {
247 while let Some(edge) = self
248 .edges
249 .get(self.next)
250 .filter(|edge| edge.top.y <= sample_y)
251 {
252 self.active.push(edge);
253 self.next += 1;
254 }
255 self.active.retain(|edge| sample_y < edge.bottom.y);
256 let x_at = |edge: &Edge| {
257 let t = (sample_y - edge.top.y) / (edge.bottom.y - edge.top.y);
258 edge.top.x + t * (edge.bottom.x - edge.top.x)
259 };
260 self.active.sort_by(|a, b| x_at(a).total_cmp(&x_at(b)));
263 self.crossings.clear();
264 self.crossings
265 .extend(self.active.iter().map(|edge| (x_at(edge), edge.winding)));
266 &self.crossings
267 }
268}
269
270impl PathFillRule {
271 fn for_each_span(
275 self,
276 crossings: &[(f32, i32)],
277 mut span: impl FnMut(f32, f32) -> bool,
278 ) -> bool {
279 if crossings.len() < 2 {
280 return false;
281 }
282 let inside = |winding: i32| match self {
283 Self::NonZero => winding != 0,
284 Self::EvenOdd => winding % 2 != 0,
285 };
286 let mut touched = false;
287 let mut winding = 0i32;
288 let mut span_start = 0.0f32;
289 for &(x, direction) in crossings {
290 let was_inside = inside(winding);
291 winding += match self {
292 Self::NonZero => direction,
293 Self::EvenOdd => 1,
294 };
295 match (was_inside, inside(winding)) {
296 (false, true) => span_start = x,
297 (true, false) => touched |= span(span_start, x),
298 _ => {}
299 }
300 }
301 touched
302 }
303}
304
305fn add_row_coverage(mask_row: &mut [u8], row_coverage: &[f32]) {
307 for (dst, coverage) in mask_row.iter_mut().zip(row_coverage) {
308 let existing = *dst as f32 / 255.0;
309 let combined = (existing + coverage).min(1.0);
310 *dst = (combined * 255.0 + 0.5) as u8;
311 }
312}
313
314fn accumulate_span(row_coverage: &mut [f32], x0: f32, x1: f32, weight: f32, width: usize) -> bool {
318 let x0 = x0.max(0.0);
319 let x1 = x1.min(width as f32);
320 if x1 <= x0 {
321 return false;
322 }
323
324 let first = x0.floor() as usize;
327 let last = (x1.ceil() as usize).min(width);
328 let partial = |pixel: usize| {
329 let pixel_start = pixel as f32;
330 (x1.min(pixel_start + 1.0) - x0.max(pixel_start)).max(0.0) * weight
331 };
332 let Some(span) = row_coverage.get_mut(first..last) else {
333 return false;
334 };
335 match span {
336 [] => {}
337 [only] => *only += partial(first),
338 [head, interior @ .., tail] => {
339 *head += partial(first);
340 for coverage in interior {
341 *coverage += weight;
342 }
343 *tail += partial(last - 1);
344 }
345 }
346 true
347}
348
349struct PathLexer<'a> {
350 bytes: &'a [u8],
351 pos: usize,
352}
353
354impl<'a> PathLexer<'a> {
355 fn new(d: &'a str) -> Self {
356 Self {
357 bytes: d.as_bytes(),
358 pos: 0,
359 }
360 }
361
362 fn skip_separators(&mut self) {
363 while self.pos < self.bytes.len() {
364 match self.bytes[self.pos] {
365 b' ' | b'\t' | b'\r' | b'\n' | b',' => self.pos += 1,
366 _ => break,
367 }
368 }
369 }
370
371 fn peek(&mut self) -> Option<u8> {
372 self.skip_separators();
373 self.bytes.get(self.pos).copied()
374 }
375
376 fn at_number(&mut self) -> bool {
377 matches!(self.peek(), Some(b'0'..=b'9' | b'.' | b'-' | b'+'))
378 }
379
380 fn next_command(&mut self) -> Option<u8> {
381 let byte = self.peek()?;
382 if byte.is_ascii_alphabetic() {
383 self.pos += 1;
384 Some(byte)
385 } else {
386 None
387 }
388 }
389
390 fn next_number(&mut self) -> Result<f32, SvgPathError> {
391 self.skip_separators();
392 let start = self.pos;
393 let bytes = self.bytes;
394 let mut pos = self.pos;
395
396 if pos < bytes.len() && (bytes[pos] == b'+' || bytes[pos] == b'-') {
397 pos += 1;
398 }
399 let int_digits = Self::eat_digits(bytes, &mut pos);
400 let mut frac_digits = 0;
401 if pos < bytes.len() && bytes[pos] == b'.' {
402 pos += 1;
403 frac_digits = Self::eat_digits(bytes, &mut pos);
404 }
405 if int_digits == 0 && frac_digits == 0 {
406 return Err(SvgPathError::ExpectedNumber { offset: start });
407 }
408 if pos < bytes.len() && (bytes[pos] == b'e' || bytes[pos] == b'E') {
409 let mut exp_pos = pos + 1;
410 if exp_pos < bytes.len() && (bytes[exp_pos] == b'+' || bytes[exp_pos] == b'-') {
411 exp_pos += 1;
412 }
413 if Self::eat_digits(bytes, &mut exp_pos) > 0 {
414 pos = exp_pos;
415 }
416 }
417
418 let text = std::str::from_utf8(&bytes[start..pos])
419 .map_err(|_| SvgPathError::ExpectedNumber { offset: start })?;
420 let value = text
421 .parse::<f32>()
422 .map_err(|_| SvgPathError::ExpectedNumber { offset: start })?;
423 self.pos = pos;
424 Ok(value)
425 }
426
427 fn eat_digits(bytes: &[u8], pos: &mut usize) -> usize {
428 let start = *pos;
429 while *pos < bytes.len() && bytes[*pos].is_ascii_digit() {
430 *pos += 1;
431 }
432 *pos - start
433 }
434
435 fn next_flag(&mut self) -> Result<bool, SvgPathError> {
436 self.skip_separators();
437 match self.bytes.get(self.pos) {
438 Some(b'0') => {
439 self.pos += 1;
440 Ok(false)
441 }
442 Some(b'1') => {
443 self.pos += 1;
444 Ok(true)
445 }
446 _ => Err(SvgPathError::ExpectedFlag { offset: self.pos }),
447 }
448 }
449
450 fn at_end(&mut self) -> bool {
451 self.peek().is_none()
452 }
453}
454
455struct PathBuilder {
456 subpaths: Vec<Vec<Point>>,
457 current: Vec<Point>,
458 position: Point,
459 subpath_start: Point,
460 last_cubic_control: Option<Point>,
461 last_quad_control: Option<Point>,
462}
463
464impl PathBuilder {
465 fn new() -> Self {
466 Self {
467 subpaths: Vec::new(),
468 current: Vec::new(),
469 position: Point::ZERO,
470 subpath_start: Point::ZERO,
471 last_cubic_control: None,
472 last_quad_control: None,
473 }
474 }
475
476 fn flush_subpath(&mut self) {
477 if self.current.len() >= 2 {
478 self.subpaths.push(std::mem::take(&mut self.current));
479 } else {
480 self.current.clear();
481 }
482 }
483
484 fn move_to(&mut self, point: Point) {
485 self.flush_subpath();
486 self.position = point;
487 self.subpath_start = point;
488 self.current.push(point);
489 }
490
491 fn line_to(&mut self, point: Point) {
492 self.begin_segment();
493 self.current.push(point);
494 self.position = point;
495 }
496
497 fn begin_segment(&mut self) -> Point {
500 if self.current.is_empty() {
501 self.current.push(self.position);
502 }
503 self.position
504 }
505
506 fn close(&mut self) {
507 self.position = self.subpath_start;
508 self.flush_subpath();
509 self.current.push(self.subpath_start);
510 }
511
512 fn finish(mut self) -> Vec<Vec<Point>> {
513 self.flush_subpath();
514 self.subpaths
515 }
516}
517
518fn parse_path_data(d: &str) -> Result<Vec<Vec<Point>>, SvgPathError> {
519 let mut lexer = PathLexer::new(d);
520 let mut builder = PathBuilder::new();
521 let mut command: Option<u8> = None;
522 let mut seen_moveto = false;
523
524 loop {
525 if lexer.at_end() {
526 break;
527 }
528
529 if let Some(next) = lexer.next_command() {
530 command = Some(next);
531 } else if command.is_none() || !lexer.at_number() {
532 let offset = lexer.pos;
533 let byte = lexer.bytes.get(offset).copied().unwrap_or(b'?') as char;
534 return Err(SvgPathError::UnexpectedByte { byte, offset });
535 }
536
537 let Some(cmd) = command else {
538 return Err(SvgPathError::MissingMoveTo);
539 };
540 if !seen_moveto && !matches!(cmd, b'M' | b'm') {
541 return Err(SvgPathError::MissingMoveTo);
542 }
543 let relative = cmd.is_ascii_lowercase();
544 let pos = builder.position;
545 let rel = |value: Point| {
546 if relative {
547 Point::new(pos.x + value.x, pos.y + value.y)
548 } else {
549 value
550 }
551 };
552
553 match cmd.to_ascii_uppercase() {
554 b'M' => {
555 let point = rel(read_point(&mut lexer)?);
556 builder.move_to(point);
557 seen_moveto = true;
558 builder.last_cubic_control = None;
559 builder.last_quad_control = None;
560 command = Some(if relative { b'l' } else { b'L' });
561 }
562 b'L' => {
563 let point = rel(read_point(&mut lexer)?);
564 builder.line_to(point);
565 builder.last_cubic_control = None;
566 builder.last_quad_control = None;
567 }
568 b'H' => {
569 let x = lexer.next_number()?;
570 let x = if relative { pos.x + x } else { x };
571 builder.line_to(Point::new(x, pos.y));
572 builder.last_cubic_control = None;
573 builder.last_quad_control = None;
574 }
575 b'V' => {
576 let y = lexer.next_number()?;
577 let y = if relative { pos.y + y } else { y };
578 builder.line_to(Point::new(pos.x, y));
579 builder.last_cubic_control = None;
580 builder.last_quad_control = None;
581 }
582 b'C' => {
583 let c1 = rel(read_point(&mut lexer)?);
584 let c2 = rel(read_point(&mut lexer)?);
585 let end = rel(read_point(&mut lexer)?);
586 emit_cubic(&mut builder, c1, c2, end);
587 }
588 b'S' => {
589 let c1 = match builder.last_cubic_control {
590 Some(control) => reflect(pos, control),
591 None => pos,
592 };
593 let c2 = rel(read_point(&mut lexer)?);
594 let end = rel(read_point(&mut lexer)?);
595 emit_cubic(&mut builder, c1, c2, end);
596 }
597 b'Q' => {
598 let control = rel(read_point(&mut lexer)?);
599 let end = rel(read_point(&mut lexer)?);
600 emit_quad(&mut builder, control, end);
601 }
602 b'T' => {
603 let control = match builder.last_quad_control {
604 Some(control) => reflect(pos, control),
605 None => pos,
606 };
607 let end = rel(read_point(&mut lexer)?);
608 emit_quad(&mut builder, control, end);
609 }
610 b'A' => {
611 let rx = lexer.next_number()?;
612 let ry = lexer.next_number()?;
613 let x_rotation_deg = lexer.next_number()?;
614 let large_arc = lexer.next_flag()?;
615 let sweep = lexer.next_flag()?;
616 let end = rel(read_point(&mut lexer)?);
617 emit_arc(&mut builder, rx, ry, x_rotation_deg, large_arc, sweep, end);
618 builder.last_cubic_control = None;
619 builder.last_quad_control = None;
620 }
621 b'Z' => {
622 builder.close();
623 builder.last_cubic_control = None;
624 builder.last_quad_control = None;
625 command = None;
626 }
627 other => {
628 return Err(SvgPathError::UnexpectedByte {
629 byte: other as char,
630 offset: lexer.pos.saturating_sub(1),
631 });
632 }
633 }
634 }
635
636 if !seen_moveto {
637 return Err(SvgPathError::MissingMoveTo);
638 }
639 Ok(builder.finish())
640}
641
642fn read_point(lexer: &mut PathLexer<'_>) -> Result<Point, SvgPathError> {
643 let x = lexer.next_number()?;
644 let y = lexer.next_number()?;
645 Ok(Point::new(x, y))
646}
647
648fn reflect(origin: Point, point: Point) -> Point {
649 Point::new(2.0 * origin.x - point.x, 2.0 * origin.y - point.y)
650}
651
652fn emit_cubic(builder: &mut PathBuilder, c1: Point, c2: Point, end: Point) {
653 let start = builder.begin_segment();
654 flatten_cubic_into(&mut builder.current, start, c1, c2, end);
655 builder.position = end;
656 builder.last_cubic_control = Some(c2);
657 builder.last_quad_control = None;
658}
659
660fn emit_quad(builder: &mut PathBuilder, control: Point, end: Point) {
661 let start = builder.begin_segment();
662 let (c1, c2) = quad_as_cubic(start, control, end);
663 flatten_cubic_into(&mut builder.current, start, c1, c2, end);
664 builder.position = end;
665 builder.last_quad_control = Some(control);
666 builder.last_cubic_control = None;
667}
668
669pub(crate) fn quad_as_cubic(start: Point, control: Point, end: Point) -> (Point, Point) {
672 (
673 Point::new(
674 start.x + 2.0 / 3.0 * (control.x - start.x),
675 start.y + 2.0 / 3.0 * (control.y - start.y),
676 ),
677 Point::new(
678 end.x + 2.0 / 3.0 * (control.x - end.x),
679 end.y + 2.0 / 3.0 * (control.y - end.y),
680 ),
681 )
682}
683
684pub(crate) fn flatten_cubic_into(
688 points: &mut Vec<Point>,
689 p0: Point,
690 p1: Point,
691 p2: Point,
692 p3: Point,
693) {
694 flatten_cubic(points, p0, p1, p2, p3, 0);
695}
696
697fn flatten_cubic(points: &mut Vec<Point>, p0: Point, p1: Point, p2: Point, p3: Point, depth: u32) {
698 if depth >= MAX_FLATTEN_DEPTH || cubic_is_flat(p0, p1, p2, p3) {
699 points.push(p3);
700 return;
701 }
702
703 let mid = |a: Point, b: Point| Point::new((a.x + b.x) * 0.5, (a.y + b.y) * 0.5);
704 let p01 = mid(p0, p1);
705 let p12 = mid(p1, p2);
706 let p23 = mid(p2, p3);
707 let p012 = mid(p01, p12);
708 let p123 = mid(p12, p23);
709 let p0123 = mid(p012, p123);
710
711 flatten_cubic(points, p0, p01, p012, p0123, depth + 1);
712 flatten_cubic(points, p0123, p123, p23, p3, depth + 1);
713}
714
715fn cubic_is_flat(p0: Point, p1: Point, p2: Point, p3: Point) -> bool {
717 let d1 = point_to_chord_distance_squared(p1, p0, p3);
718 let d2 = point_to_chord_distance_squared(p2, p0, p3);
719 let tolerance = FLATTEN_TOLERANCE * FLATTEN_TOLERANCE;
720 d1 <= tolerance && d2 <= tolerance
721}
722
723fn point_to_chord_distance_squared(point: Point, a: Point, b: Point) -> f32 {
724 let ab = Point::new(b.x - a.x, b.y - a.y);
725 let ap = Point::new(point.x - a.x, point.y - a.y);
726 let ab_len_sq = ab.x * ab.x + ab.y * ab.y;
727 if ab_len_sq <= f32::EPSILON {
728 return ap.x * ap.x + ap.y * ap.y;
729 }
730 let cross = ab.x * ap.y - ab.y * ap.x;
731 cross * cross / ab_len_sq
732}
733
734fn emit_arc(
737 builder: &mut PathBuilder,
738 rx: f32,
739 ry: f32,
740 x_rotation_deg: f32,
741 large_arc: bool,
742 sweep: bool,
743 end: Point,
744) {
745 let start = builder.position;
746 if (start.x - end.x).abs() <= f32::EPSILON && (start.y - end.y).abs() <= f32::EPSILON {
747 return;
748 }
749 let mut rx = rx.abs();
750 let mut ry = ry.abs();
751 if rx <= f32::EPSILON || ry <= f32::EPSILON {
752 builder.line_to(end);
753 return;
754 }
755
756 let phi = x_rotation_deg.to_radians();
757 let (sin_phi, cos_phi) = phi.sin_cos();
758
759 let dx2 = (start.x - end.x) * 0.5;
760 let dy2 = (start.y - end.y) * 0.5;
761 let x1p = cos_phi * dx2 + sin_phi * dy2;
762 let y1p = -sin_phi * dx2 + cos_phi * dy2;
763
764 let lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
765 if lambda > 1.0 {
766 let scale = lambda.sqrt();
767 rx *= scale;
768 ry *= scale;
769 }
770
771 let rx_sq = rx * rx;
772 let ry_sq = ry * ry;
773 let numerator = (rx_sq * ry_sq - rx_sq * y1p * y1p - ry_sq * x1p * x1p).max(0.0);
774 let denominator = rx_sq * y1p * y1p + ry_sq * x1p * x1p;
775 let mut coefficient = if denominator <= f32::EPSILON {
776 0.0
777 } else {
778 (numerator / denominator).sqrt()
779 };
780 if large_arc == sweep {
781 coefficient = -coefficient;
782 }
783 let cxp = coefficient * rx * y1p / ry;
784 let cyp = -coefficient * ry * x1p / rx;
785
786 let cx = cos_phi * cxp - sin_phi * cyp + (start.x + end.x) * 0.5;
787 let cy = sin_phi * cxp + cos_phi * cyp + (start.y + end.y) * 0.5;
788
789 let angle_of = |x: f32, y: f32| y.atan2(x);
790 let theta1 = angle_of((x1p - cxp) / rx, (y1p - cyp) / ry);
791 let theta2 = angle_of((-x1p - cxp) / rx, (-y1p - cyp) / ry);
792 let two_pi = std::f32::consts::TAU;
793 let mut delta = theta2 - theta1;
794 if sweep {
795 if delta < 0.0 {
796 delta += two_pi;
797 }
798 } else if delta > 0.0 {
799 delta -= two_pi;
800 }
801
802 let segments = ((delta.abs() / ARC_MAX_ANGLE_STEP).ceil() as usize).max(2);
803 for i in 1..=segments {
804 let theta = theta1 + delta * (i as f32 / segments as f32);
805 let (sin_theta, cos_theta) = theta.sin_cos();
806 let x = cos_phi * rx * cos_theta - sin_phi * ry * sin_theta + cx;
807 let y = sin_phi * rx * cos_theta + cos_phi * ry * sin_theta + cy;
808 builder.line_to(Point::new(x, y));
809 }
810 builder.line_to(end);
811 builder.position = end;
812}
813
814#[cfg(test)]
815#[path = "tests/vector_path_tests.rs"]
816mod tests;