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>>,
56 fill_rule: PathFillRule,
57 bounds: Rect,
58}
59
60impl VectorPath {
61 pub fn parse(d: &str) -> Result<Self, SvgPathError> {
63 let subpaths = parse_path_data(d)?;
64 Ok(Self::from_subpaths(subpaths, PathFillRule::NonZero))
65 }
66
67 pub fn parse_with_fill_rule(d: &str, fill_rule: PathFillRule) -> Result<Self, SvgPathError> {
69 let subpaths = parse_path_data(d)?;
70 Ok(Self::from_subpaths(subpaths, fill_rule))
71 }
72
73 fn from_subpaths(subpaths: Vec<Vec<Point>>, fill_rule: PathFillRule) -> Self {
74 let mut min = Point::new(f32::INFINITY, f32::INFINITY);
75 let mut max = Point::new(f32::NEG_INFINITY, f32::NEG_INFINITY);
76 for point in subpaths.iter().flatten() {
77 min.x = min.x.min(point.x);
78 min.y = min.y.min(point.y);
79 max.x = max.x.max(point.x);
80 max.y = max.y.max(point.y);
81 }
82 let bounds = if min.x.is_finite() {
83 Rect {
84 x: min.x,
85 y: min.y,
86 width: (max.x - min.x).max(0.0),
87 height: (max.y - min.y).max(0.0),
88 }
89 } else {
90 Rect {
91 x: 0.0,
92 y: 0.0,
93 width: 0.0,
94 height: 0.0,
95 }
96 };
97 Self {
98 subpaths,
99 fill_rule,
100 bounds,
101 }
102 }
103
104 pub fn scaled(&self, factor: f32) -> Self {
107 let subpaths = self
108 .subpaths
109 .iter()
110 .map(|subpath| {
111 subpath
112 .iter()
113 .map(|point| Point::new(point.x * factor, point.y * factor))
114 .collect()
115 })
116 .collect();
117 Self::from_subpaths(subpaths, self.fill_rule)
118 }
119
120 pub fn translated(&self, dx: f32, dy: f32) -> Self {
122 let subpaths = self
123 .subpaths
124 .iter()
125 .map(|subpath| {
126 subpath
127 .iter()
128 .map(|point| Point::new(point.x + dx, point.y + dy))
129 .collect()
130 })
131 .collect();
132 Self::from_subpaths(subpaths, self.fill_rule)
133 }
134
135 pub fn fill_rule(&self) -> PathFillRule {
137 self.fill_rule
138 }
139
140 pub fn bounds(&self) -> Rect {
142 self.bounds
143 }
144
145 pub fn is_empty(&self) -> bool {
147 !self.subpaths.iter().any(|subpath| subpath.len() >= 3)
148 }
149
150 pub fn subpaths(&self) -> &[Vec<Point>] {
152 &self.subpaths
153 }
154
155 pub fn coverage_mask(&self, width: usize, height: usize, origin: Point, scale: f32) -> Vec<u8> {
159 let mut mask = vec![0u8; width * height];
160 if width == 0 || height == 0 || scale <= 0.0 {
161 return mask;
162 }
163
164 struct Edge {
166 top: Point,
167 bottom: Point,
168 winding: i32,
171 }
172 let mut edges = Vec::new();
173 for subpath in &self.subpaths {
174 if subpath.len() < 3 {
175 continue;
176 }
177 let map = |p: &Point| Point::new((p.x - origin.x) * scale, (p.y - origin.y) * scale);
178 for i in 0..subpath.len() {
179 let a = map(&subpath[i]);
180 let b = map(&subpath[(i + 1) % subpath.len()]);
181 if a.y == b.y {
182 continue;
183 }
184 if a.y < b.y {
185 edges.push(Edge {
186 top: a,
187 bottom: b,
188 winding: 1,
189 });
190 } else {
191 edges.push(Edge {
192 top: b,
193 bottom: a,
194 winding: -1,
195 });
196 }
197 }
198 }
199 if edges.is_empty() {
200 return mask;
201 }
202
203 let mut crossings: Vec<(f32, i32)> = Vec::new();
204 let mut row_coverage = vec![0.0f32; width];
205 let subsample_weight = 1.0 / SUBSAMPLES as f32;
206
207 for row in 0..height {
208 row_coverage.fill(0.0);
209 let mut row_touched = false;
210
211 for sub in 0..SUBSAMPLES {
212 let sample_y = row as f32 + (sub as f32 + 0.5) * subsample_weight;
213
214 crossings.clear();
215 for edge in &edges {
216 if edge.top.y <= sample_y && sample_y < edge.bottom.y {
217 let t = (sample_y - edge.top.y) / (edge.bottom.y - edge.top.y);
218 let x = edge.top.x + t * (edge.bottom.x - edge.top.x);
219 crossings.push((x, edge.winding));
220 }
221 }
222 if crossings.len() < 2 {
223 continue;
224 }
225 crossings.sort_by(|a, b| a.0.total_cmp(&b.0));
226
227 let mut winding = 0i32;
229 let mut span_start = 0.0f32;
230 for &(x, direction) in crossings.iter() {
231 let was_inside = match self.fill_rule {
232 PathFillRule::NonZero => winding != 0,
233 PathFillRule::EvenOdd => winding % 2 != 0,
234 };
235 winding += match self.fill_rule {
236 PathFillRule::NonZero => direction,
237 PathFillRule::EvenOdd => 1,
238 };
239 let is_inside = match self.fill_rule {
240 PathFillRule::NonZero => winding != 0,
241 PathFillRule::EvenOdd => winding % 2 != 0,
242 };
243 if !was_inside && is_inside {
244 span_start = x;
245 } else if was_inside && !is_inside {
246 row_touched |= accumulate_span(
247 &mut row_coverage,
248 span_start,
249 x,
250 subsample_weight,
251 width,
252 );
253 }
254 }
255 }
256
257 if row_touched {
258 let mask_row = &mut mask[row * width..(row + 1) * width];
259 for (dst, coverage) in mask_row.iter_mut().zip(row_coverage.iter()) {
260 let existing = *dst as f32 / 255.0;
261 let combined = (existing + coverage).min(1.0);
262 *dst = (combined * 255.0 + 0.5) as u8;
263 }
264 }
265 }
266
267 mask
268 }
269}
270
271fn accumulate_span(row_coverage: &mut [f32], x0: f32, x1: f32, weight: f32, width: usize) -> bool {
275 let x0 = x0.max(0.0);
276 let x1 = x1.min(width as f32);
277 if x1 <= x0 {
278 return false;
279 }
280
281 let first = x0.floor() as usize;
282 let last = (x1.ceil() as usize).min(width);
283 for (pixel, coverage) in row_coverage.iter_mut().enumerate().take(last).skip(first) {
284 let pixel_start = pixel as f32;
285 let pixel_end = pixel_start + 1.0;
286 let covered = (x1.min(pixel_end) - x0.max(pixel_start)).max(0.0);
287 *coverage += covered * weight;
288 }
289 true
290}
291
292struct PathLexer<'a> {
297 bytes: &'a [u8],
298 pos: usize,
299}
300
301impl<'a> PathLexer<'a> {
302 fn new(d: &'a str) -> Self {
303 Self {
304 bytes: d.as_bytes(),
305 pos: 0,
306 }
307 }
308
309 fn skip_separators(&mut self) {
310 while self.pos < self.bytes.len() {
311 match self.bytes[self.pos] {
312 b' ' | b'\t' | b'\r' | b'\n' | b',' => self.pos += 1,
313 _ => break,
314 }
315 }
316 }
317
318 fn peek(&mut self) -> Option<u8> {
319 self.skip_separators();
320 self.bytes.get(self.pos).copied()
321 }
322
323 fn at_number(&mut self) -> bool {
325 matches!(self.peek(), Some(b'0'..=b'9' | b'.' | b'-' | b'+'))
326 }
327
328 fn next_command(&mut self) -> Option<u8> {
329 let byte = self.peek()?;
330 if byte.is_ascii_alphabetic() {
331 self.pos += 1;
332 Some(byte)
333 } else {
334 None
335 }
336 }
337
338 fn next_number(&mut self) -> Result<f32, SvgPathError> {
341 self.skip_separators();
342 let start = self.pos;
343 let bytes = self.bytes;
344 let mut pos = self.pos;
345
346 if pos < bytes.len() && (bytes[pos] == b'+' || bytes[pos] == b'-') {
347 pos += 1;
348 }
349 let int_digits = Self::eat_digits(bytes, &mut pos);
350 let mut frac_digits = 0;
351 if pos < bytes.len() && bytes[pos] == b'.' {
352 pos += 1;
353 frac_digits = Self::eat_digits(bytes, &mut pos);
354 }
355 if int_digits == 0 && frac_digits == 0 {
356 return Err(SvgPathError::ExpectedNumber { offset: start });
357 }
358 if pos < bytes.len() && (bytes[pos] == b'e' || bytes[pos] == b'E') {
359 let mut exp_pos = pos + 1;
360 if exp_pos < bytes.len() && (bytes[exp_pos] == b'+' || bytes[exp_pos] == b'-') {
361 exp_pos += 1;
362 }
363 if Self::eat_digits(bytes, &mut exp_pos) > 0 {
364 pos = exp_pos;
365 }
366 }
367
368 let text = std::str::from_utf8(&bytes[start..pos])
369 .map_err(|_| SvgPathError::ExpectedNumber { offset: start })?;
370 let value = text
371 .parse::<f32>()
372 .map_err(|_| SvgPathError::ExpectedNumber { offset: start })?;
373 self.pos = pos;
374 Ok(value)
375 }
376
377 fn eat_digits(bytes: &[u8], pos: &mut usize) -> usize {
378 let start = *pos;
379 while *pos < bytes.len() && bytes[*pos].is_ascii_digit() {
380 *pos += 1;
381 }
382 *pos - start
383 }
384
385 fn next_flag(&mut self) -> Result<bool, SvgPathError> {
387 self.skip_separators();
388 match self.bytes.get(self.pos) {
389 Some(b'0') => {
390 self.pos += 1;
391 Ok(false)
392 }
393 Some(b'1') => {
394 self.pos += 1;
395 Ok(true)
396 }
397 _ => Err(SvgPathError::ExpectedFlag { offset: self.pos }),
398 }
399 }
400
401 fn at_end(&mut self) -> bool {
402 self.peek().is_none()
403 }
404}
405
406struct PathBuilder {
407 subpaths: Vec<Vec<Point>>,
408 current: Vec<Point>,
409 position: Point,
410 subpath_start: Point,
411 last_cubic_control: Option<Point>,
413 last_quad_control: Option<Point>,
414}
415
416impl PathBuilder {
417 fn new() -> Self {
418 Self {
419 subpaths: Vec::new(),
420 current: Vec::new(),
421 position: Point::ZERO,
422 subpath_start: Point::ZERO,
423 last_cubic_control: None,
424 last_quad_control: None,
425 }
426 }
427
428 fn flush_subpath(&mut self) {
429 if self.current.len() >= 2 {
430 self.subpaths.push(std::mem::take(&mut self.current));
431 } else {
432 self.current.clear();
433 }
434 }
435
436 fn move_to(&mut self, point: Point) {
437 self.flush_subpath();
438 self.position = point;
439 self.subpath_start = point;
440 self.current.push(point);
441 }
442
443 fn line_to(&mut self, point: Point) {
444 if self.current.is_empty() {
445 self.current.push(self.position);
446 }
447 self.current.push(point);
448 self.position = point;
449 }
450
451 fn close(&mut self) {
452 self.position = self.subpath_start;
453 self.flush_subpath();
454 self.current.push(self.subpath_start);
456 }
457
458 fn finish(mut self) -> Vec<Vec<Point>> {
459 self.flush_subpath();
460 self.subpaths
461 }
462}
463
464fn parse_path_data(d: &str) -> Result<Vec<Vec<Point>>, SvgPathError> {
465 let mut lexer = PathLexer::new(d);
466 let mut builder = PathBuilder::new();
467 let mut command: Option<u8> = None;
468 let mut seen_moveto = false;
469
470 loop {
471 if lexer.at_end() {
472 break;
473 }
474
475 if let Some(next) = lexer.next_command() {
476 command = Some(next);
477 } else if command.is_none() || !lexer.at_number() {
478 let offset = lexer.pos;
479 let byte = lexer.bytes.get(offset).copied().unwrap_or(b'?') as char;
480 return Err(SvgPathError::UnexpectedByte { byte, offset });
481 }
482
483 let Some(cmd) = command else {
484 return Err(SvgPathError::MissingMoveTo);
485 };
486 if !seen_moveto && !matches!(cmd, b'M' | b'm') {
487 return Err(SvgPathError::MissingMoveTo);
488 }
489 let relative = cmd.is_ascii_lowercase();
490 let pos = builder.position;
491 let rel = |value: Point| {
492 if relative {
493 Point::new(pos.x + value.x, pos.y + value.y)
494 } else {
495 value
496 }
497 };
498
499 match cmd.to_ascii_uppercase() {
500 b'M' => {
501 let point = rel(read_point(&mut lexer)?);
502 builder.move_to(point);
503 seen_moveto = true;
504 builder.last_cubic_control = None;
505 builder.last_quad_control = None;
506 command = Some(if relative { b'l' } else { b'L' });
508 }
509 b'L' => {
510 let point = rel(read_point(&mut lexer)?);
511 builder.line_to(point);
512 builder.last_cubic_control = None;
513 builder.last_quad_control = None;
514 }
515 b'H' => {
516 let x = lexer.next_number()?;
517 let x = if relative { pos.x + x } else { x };
518 builder.line_to(Point::new(x, pos.y));
519 builder.last_cubic_control = None;
520 builder.last_quad_control = None;
521 }
522 b'V' => {
523 let y = lexer.next_number()?;
524 let y = if relative { pos.y + y } else { y };
525 builder.line_to(Point::new(pos.x, y));
526 builder.last_cubic_control = None;
527 builder.last_quad_control = None;
528 }
529 b'C' => {
530 let c1 = rel(read_point(&mut lexer)?);
531 let c2 = rel(read_point(&mut lexer)?);
532 let end = rel(read_point(&mut lexer)?);
533 emit_cubic(&mut builder, c1, c2, end);
534 }
535 b'S' => {
536 let c1 = match builder.last_cubic_control {
537 Some(control) => reflect(pos, control),
538 None => pos,
539 };
540 let c2 = rel(read_point(&mut lexer)?);
541 let end = rel(read_point(&mut lexer)?);
542 emit_cubic(&mut builder, c1, c2, end);
543 }
544 b'Q' => {
545 let control = rel(read_point(&mut lexer)?);
546 let end = rel(read_point(&mut lexer)?);
547 emit_quad(&mut builder, control, end);
548 }
549 b'T' => {
550 let control = match builder.last_quad_control {
551 Some(control) => reflect(pos, control),
552 None => pos,
553 };
554 let end = rel(read_point(&mut lexer)?);
555 emit_quad(&mut builder, control, end);
556 }
557 b'A' => {
558 let rx = lexer.next_number()?;
559 let ry = lexer.next_number()?;
560 let x_rotation_deg = lexer.next_number()?;
561 let large_arc = lexer.next_flag()?;
562 let sweep = lexer.next_flag()?;
563 let end = rel(read_point(&mut lexer)?);
564 emit_arc(&mut builder, rx, ry, x_rotation_deg, large_arc, sweep, end);
565 builder.last_cubic_control = None;
566 builder.last_quad_control = None;
567 }
568 b'Z' => {
569 builder.close();
570 builder.last_cubic_control = None;
571 builder.last_quad_control = None;
572 command = None;
574 }
575 other => {
576 return Err(SvgPathError::UnexpectedByte {
577 byte: other as char,
578 offset: lexer.pos.saturating_sub(1),
579 });
580 }
581 }
582 }
583
584 if !seen_moveto {
585 return Err(SvgPathError::MissingMoveTo);
586 }
587 Ok(builder.finish())
588}
589
590fn read_point(lexer: &mut PathLexer<'_>) -> Result<Point, SvgPathError> {
591 let x = lexer.next_number()?;
592 let y = lexer.next_number()?;
593 Ok(Point::new(x, y))
594}
595
596fn reflect(origin: Point, point: Point) -> Point {
597 Point::new(2.0 * origin.x - point.x, 2.0 * origin.y - point.y)
598}
599
600fn emit_cubic(builder: &mut PathBuilder, c1: Point, c2: Point, end: Point) {
601 let start = builder.position;
602 flatten_cubic(builder, start, c1, c2, end, 0);
603 builder.position = end;
604 builder.last_cubic_control = Some(c2);
605 builder.last_quad_control = None;
606}
607
608fn emit_quad(builder: &mut PathBuilder, control: Point, end: Point) {
609 let start = builder.position;
611 let c1 = Point::new(
612 start.x + 2.0 / 3.0 * (control.x - start.x),
613 start.y + 2.0 / 3.0 * (control.y - start.y),
614 );
615 let c2 = Point::new(
616 end.x + 2.0 / 3.0 * (control.x - end.x),
617 end.y + 2.0 / 3.0 * (control.y - end.y),
618 );
619 flatten_cubic(builder, start, c1, c2, end, 0);
620 builder.position = end;
621 builder.last_quad_control = Some(control);
622 builder.last_cubic_control = None;
623}
624
625fn flatten_cubic(
626 builder: &mut PathBuilder,
627 p0: Point,
628 p1: Point,
629 p2: Point,
630 p3: Point,
631 depth: u32,
632) {
633 if depth >= MAX_FLATTEN_DEPTH || cubic_is_flat(p0, p1, p2, p3) {
634 builder.line_to(p3);
635 return;
636 }
637
638 let mid = |a: Point, b: Point| Point::new((a.x + b.x) * 0.5, (a.y + b.y) * 0.5);
639 let p01 = mid(p0, p1);
640 let p12 = mid(p1, p2);
641 let p23 = mid(p2, p3);
642 let p012 = mid(p01, p12);
643 let p123 = mid(p12, p23);
644 let p0123 = mid(p012, p123);
645
646 flatten_cubic(builder, p0, p01, p012, p0123, depth + 1);
647 flatten_cubic(builder, p0123, p123, p23, p3, depth + 1);
648}
649
650fn cubic_is_flat(p0: Point, p1: Point, p2: Point, p3: Point) -> bool {
652 let d1 = point_to_chord_distance_squared(p1, p0, p3);
653 let d2 = point_to_chord_distance_squared(p2, p0, p3);
654 let tolerance = FLATTEN_TOLERANCE * FLATTEN_TOLERANCE;
655 d1 <= tolerance && d2 <= tolerance
656}
657
658fn point_to_chord_distance_squared(point: Point, a: Point, b: Point) -> f32 {
659 let ab = Point::new(b.x - a.x, b.y - a.y);
660 let ap = Point::new(point.x - a.x, point.y - a.y);
661 let ab_len_sq = ab.x * ab.x + ab.y * ab.y;
662 if ab_len_sq <= f32::EPSILON {
663 return ap.x * ap.x + ap.y * ap.y;
664 }
665 let cross = ab.x * ap.y - ab.y * ap.x;
666 cross * cross / ab_len_sq
667}
668
669fn emit_arc(
672 builder: &mut PathBuilder,
673 rx: f32,
674 ry: f32,
675 x_rotation_deg: f32,
676 large_arc: bool,
677 sweep: bool,
678 end: Point,
679) {
680 let start = builder.position;
681 if (start.x - end.x).abs() <= f32::EPSILON && (start.y - end.y).abs() <= f32::EPSILON {
682 return;
683 }
684 let mut rx = rx.abs();
685 let mut ry = ry.abs();
686 if rx <= f32::EPSILON || ry <= f32::EPSILON {
687 builder.line_to(end);
688 return;
689 }
690
691 let phi = x_rotation_deg.to_radians();
692 let (sin_phi, cos_phi) = phi.sin_cos();
693
694 let dx2 = (start.x - end.x) * 0.5;
696 let dy2 = (start.y - end.y) * 0.5;
697 let x1p = cos_phi * dx2 + sin_phi * dy2;
698 let y1p = -sin_phi * dx2 + cos_phi * dy2;
699
700 let lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
702 if lambda > 1.0 {
703 let scale = lambda.sqrt();
704 rx *= scale;
705 ry *= scale;
706 }
707
708 let rx_sq = rx * rx;
710 let ry_sq = ry * ry;
711 let numerator = (rx_sq * ry_sq - rx_sq * y1p * y1p - ry_sq * x1p * x1p).max(0.0);
712 let denominator = rx_sq * y1p * y1p + ry_sq * x1p * x1p;
713 let mut coefficient = if denominator <= f32::EPSILON {
714 0.0
715 } else {
716 (numerator / denominator).sqrt()
717 };
718 if large_arc == sweep {
719 coefficient = -coefficient;
720 }
721 let cxp = coefficient * rx * y1p / ry;
722 let cyp = -coefficient * ry * x1p / rx;
723
724 let cx = cos_phi * cxp - sin_phi * cyp + (start.x + end.x) * 0.5;
726 let cy = sin_phi * cxp + cos_phi * cyp + (start.y + end.y) * 0.5;
727
728 let angle_of = |x: f32, y: f32| y.atan2(x);
730 let theta1 = angle_of((x1p - cxp) / rx, (y1p - cyp) / ry);
731 let theta2 = angle_of((-x1p - cxp) / rx, (-y1p - cyp) / ry);
732 let two_pi = std::f32::consts::TAU;
733 let mut delta = theta2 - theta1;
734 if sweep {
735 if delta < 0.0 {
736 delta += two_pi;
737 }
738 } else if delta > 0.0 {
739 delta -= two_pi;
740 }
741
742 let segments = ((delta.abs() / ARC_MAX_ANGLE_STEP).ceil() as usize).max(2);
743 for i in 1..=segments {
744 let theta = theta1 + delta * (i as f32 / segments as f32);
745 let (sin_theta, cos_theta) = theta.sin_cos();
746 let x = cos_phi * rx * cos_theta - sin_phi * ry * sin_theta + cx;
747 let y = sin_phi * rx * cos_theta + cos_phi * ry * sin_theta + cy;
748 builder.line_to(Point::new(x, y));
749 }
750 builder.line_to(end);
752 builder.position = end;
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758
759 fn mask_at(mask: &[u8], width: usize, x: usize, y: usize) -> u8 {
760 mask[y * width + x]
761 }
762
763 #[test]
766 fn parses_absolute_triangle() {
767 let path = VectorPath::parse("M 0 0 L 10 0 L 10 10 Z").expect("valid path");
768 assert_eq!(path.subpaths().len(), 1);
769 assert_eq!(
770 path.subpaths()[0],
771 vec![
772 Point::new(0.0, 0.0),
773 Point::new(10.0, 0.0),
774 Point::new(10.0, 10.0)
775 ]
776 );
777 let bounds = path.bounds();
778 assert_eq!((bounds.x, bounds.y), (0.0, 0.0));
779 assert_eq!((bounds.width, bounds.height), (10.0, 10.0));
780 }
781
782 #[test]
783 fn parses_relative_commands_and_h_v() {
784 let path = VectorPath::parse("m 5 5 l 10 0 v 10 h -10 z").expect("valid path");
785 assert_eq!(
786 path.subpaths()[0],
787 vec![
788 Point::new(5.0, 5.0),
789 Point::new(15.0, 5.0),
790 Point::new(15.0, 15.0),
791 Point::new(5.0, 15.0)
792 ]
793 );
794 }
795
796 #[test]
797 fn parses_packed_numbers_and_negative_shorthand() {
798 let path = VectorPath::parse("M10-5L.5.5Z").expect("valid path");
800 assert_eq!(
801 path.subpaths()[0],
802 vec![Point::new(10.0, -5.0), Point::new(0.5, 0.5)]
803 );
804 }
805
806 #[test]
807 fn implicit_lineto_after_moveto() {
808 let path = VectorPath::parse("M 0 0 10 0 10 10").expect("valid path");
809 assert_eq!(path.subpaths()[0].len(), 3);
810 assert_eq!(path.subpaths()[0][2], Point::new(10.0, 10.0));
811 }
812
813 #[test]
814 fn cubic_flattening_hits_endpoints() {
815 let path = VectorPath::parse("M 0 0 C 0 10 10 10 10 0").expect("valid path");
816 let points = &path.subpaths()[0];
817 assert_eq!(points[0], Point::new(0.0, 0.0));
818 assert_eq!(*points.last().unwrap(), Point::new(10.0, 0.0));
819 assert!(points.len() > 4, "curve must be subdivided");
820 let mid = points
822 .iter()
823 .min_by(|a, b| (a.x - 5.0).abs().total_cmp(&(b.x - 5.0).abs()))
824 .unwrap();
825 assert!(
826 (mid.y - 7.5).abs() < 0.2,
827 "flattened curve must pass near the true midpoint, got {mid:?}"
828 );
829 }
830
831 #[test]
832 fn smooth_cubic_reflects_control_point() {
833 let path = VectorPath::parse("M 0 0 C 0 5 2 5 5 5 S 10 5 10 10").expect("valid path");
836 let points = &path.subpaths()[0];
837 assert_eq!(*points.last().unwrap(), Point::new(10.0, 10.0));
838 assert!(
839 points
840 .iter()
841 .any(|p| (p.x - 5.0).abs() < 0.1 && (p.y - 5.0).abs() < 0.1)
842 );
843 }
844
845 #[test]
846 fn quadratic_and_smooth_quadratic() {
847 let path = VectorPath::parse("M 0 0 Q 5 10 10 0 T 20 0").expect("valid path");
848 let points = &path.subpaths()[0];
849 assert_eq!(*points.last().unwrap(), Point::new(20.0, 0.0));
850 assert!(
852 points
853 .iter()
854 .any(|p| (p.x - 5.0).abs() < 0.3 && (p.y - 5.0).abs() < 0.3)
855 );
856 assert!(
858 points
859 .iter()
860 .any(|p| (p.x - 15.0).abs() < 0.3 && (p.y + 5.0).abs() < 0.3)
861 );
862 }
863
864 #[test]
865 fn arc_travels_through_expected_quadrant() {
866 let path = VectorPath::parse("M 0 0 A 5 5 0 0 1 10 0").expect("valid path");
868 let points = &path.subpaths()[0];
869 assert_eq!(*points.last().unwrap(), Point::new(10.0, 0.0));
870 let lowest = points.iter().fold(0.0f32, |acc, p| acc.min(p.y));
871 assert!(
872 (lowest + 5.0).abs() < 0.1,
873 "sweep=1 arc must pass through (5,-5), lowest y = {lowest}"
874 );
875
876 let path = VectorPath::parse("M 0 0 A 5 5 0 0 0 10 0").expect("valid path");
877 let highest = path.subpaths()[0]
878 .iter()
879 .fold(0.0f32, |acc, p| acc.max(p.y));
880 assert!(
881 (highest - 5.0).abs() < 0.1,
882 "sweep=0 arc must pass through (5,5), highest y = {highest}"
883 );
884 }
885
886 #[test]
887 fn arc_flags_may_be_packed() {
888 let spaced = VectorPath::parse("M 0 0 A 5 5 0 0 1 10 0").expect("valid path");
889 let packed = VectorPath::parse("M0 0A5 5 0 0110 0").expect("valid path");
890 assert_eq!(
891 spaced.subpaths()[0].len(),
892 packed.subpaths()[0].len(),
893 "packed arc flags must parse identically"
894 );
895 }
896
897 #[test]
898 fn multiple_subpaths() {
899 let path =
900 VectorPath::parse("M 0 0 h 4 v 4 h -4 Z M 10 10 h 4 v 4 h -4 Z").expect("valid path");
901 assert_eq!(path.subpaths().len(), 2);
902 }
903
904 #[test]
905 fn rejects_garbage() {
906 assert!(VectorPath::parse("this is not a path").is_err());
907 assert!(
908 VectorPath::parse("L 10 10").is_err(),
909 "must start with moveto"
910 );
911 assert!(VectorPath::parse("M 10").is_err(), "missing y coordinate");
912 assert!(
913 VectorPath::parse("M 0 0 A 5 5 0 2 1 10 0").is_err(),
914 "bad flag"
915 );
916 assert_eq!(
917 VectorPath::parse("").unwrap_err(),
918 SvgPathError::MissingMoveTo
919 );
920 }
921
922 #[test]
925 fn fills_axis_aligned_rectangle() {
926 let path = VectorPath::parse("M 2 2 H 8 V 8 H 2 Z").expect("valid path");
927 let mask = path.coverage_mask(10, 10, Point::ZERO, 1.0);
928
929 assert_eq!(mask_at(&mask, 10, 5, 5), 255, "interior must be opaque");
930 assert_eq!(mask_at(&mask, 10, 4, 2), 255, "top edge row is inside");
931 assert_eq!(mask_at(&mask, 10, 0, 0), 0, "outside must stay empty");
932 assert_eq!(mask_at(&mask, 10, 9, 9), 0, "outside must stay empty");
933 }
934
935 #[test]
936 fn triangle_edge_is_antialiased() {
937 let path = VectorPath::parse("M 0 0 L 8 0 L 0 8 Z").expect("valid path");
938 let mask = path.coverage_mask(8, 8, Point::ZERO, 1.0);
939
940 assert_eq!(mask_at(&mask, 8, 1, 1), 255, "deep interior is opaque");
941 assert_eq!(mask_at(&mask, 8, 7, 7), 0, "far corner is empty");
942 let diagonal = mask_at(&mask, 8, 4, 3);
944 assert!(
945 diagonal > 30 && diagonal < 225,
946 "diagonal pixel should be partially covered, got {diagonal}"
947 );
948 }
949
950 #[test]
951 fn even_odd_ring_has_a_hole() {
952 let d = "M 0 0 H 12 V 12 H 0 Z M 4 4 H 8 V 8 H 4 Z";
955 let even_odd =
956 VectorPath::parse_with_fill_rule(d, PathFillRule::EvenOdd).expect("valid path");
957 let non_zero = VectorPath::parse(d).expect("valid path");
958
959 let even_odd_mask = even_odd.coverage_mask(12, 12, Point::ZERO, 1.0);
960 let non_zero_mask = non_zero.coverage_mask(12, 12, Point::ZERO, 1.0);
961
962 assert_eq!(mask_at(&even_odd_mask, 12, 6, 6), 0, "even-odd hole");
963 assert_eq!(mask_at(&even_odd_mask, 12, 2, 6), 255, "even-odd ring");
964 assert_eq!(mask_at(&non_zero_mask, 12, 6, 6), 255, "non-zero solid");
965 }
966
967 #[test]
968 fn non_zero_ring_with_reversed_inner_winding_has_a_hole() {
969 let d = "M 0 0 H 12 V 12 H 0 Z M 4 4 V 8 H 8 V 4 Z";
971 let path = VectorPath::parse(d).expect("valid path");
972 let mask = path.coverage_mask(12, 12, Point::ZERO, 1.0);
973 assert_eq!(mask_at(&mask, 12, 6, 6), 0, "reversed winding hole");
974 assert_eq!(mask_at(&mask, 12, 2, 6), 255, "ring stays filled");
975 }
976
977 #[test]
978 fn circle_from_arcs_fills_center_and_respects_radius() {
979 let path =
981 VectorPath::parse("M 0 8 A 8 8 0 1 1 16 8 A 8 8 0 1 1 0 8 Z").expect("valid path");
982 let mask = path.coverage_mask(16, 16, Point::ZERO, 1.0);
983
984 assert_eq!(mask_at(&mask, 16, 8, 8), 255, "circle center is opaque");
985 assert_eq!(mask_at(&mask, 16, 0, 0), 0, "circle corner is empty");
986 assert_eq!(mask_at(&mask, 16, 15, 0), 0, "circle corner is empty");
987 let area: f32 = mask.iter().map(|&value| value as f32 / 255.0).sum();
989 let expected = std::f32::consts::PI * 8.0 * 8.0;
990 assert!(
991 (area - expected).abs() / expected < 0.05,
992 "filled area {area} should be close to {expected}"
993 );
994 }
995
996 #[test]
997 fn scale_and_origin_map_path_units_to_pixels() {
998 let path = VectorPath::parse("M 10 10 H 14 V 14 H 10 Z").expect("valid path");
999 let mask = path.coverage_mask(8, 8, Point::new(10.0, 10.0), 2.0);
1001 assert_eq!(mask_at(&mask, 8, 4, 4), 255, "scaled interior");
1002 let full: usize = mask.iter().filter(|&&value| value == 255).count();
1003 assert_eq!(full, 64, "the 8x8 pixel mask must be fully covered");
1004 }
1005
1006 #[test]
1007 fn empty_and_degenerate_paths_produce_empty_masks() {
1008 let path = VectorPath::parse("M 5 5 L 6 6").expect("valid path");
1009 assert!(path.is_empty());
1010 let mask = path.coverage_mask(8, 8, Point::ZERO, 1.0);
1011 assert!(mask.iter().all(|&value| value == 0));
1012 }
1013}