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