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