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