1use facet::Facet;
4
5#[derive(Debug, Clone, PartialEq, Facet)]
7#[repr(u8)]
8pub enum PathCommand {
9 MoveTo { x: f64, y: f64 },
11 MoveToRel { dx: f64, dy: f64 },
13 LineTo { x: f64, y: f64 },
15 LineToRel { dx: f64, dy: f64 },
17 HorizontalLineTo { x: f64 },
19 HorizontalLineToRel { dx: f64 },
21 VerticalLineTo { y: f64 },
23 VerticalLineToRel { dy: f64 },
25 CurveTo {
27 x1: f64,
28 y1: f64,
29 x2: f64,
30 y2: f64,
31 x: f64,
32 y: f64,
33 },
34 CurveToRel {
36 dx1: f64,
37 dy1: f64,
38 dx2: f64,
39 dy2: f64,
40 dx: f64,
41 dy: f64,
42 },
43 SmoothCurveTo { x2: f64, y2: f64, x: f64, y: f64 },
45 SmoothCurveToRel {
47 dx2: f64,
48 dy2: f64,
49 dx: f64,
50 dy: f64,
51 },
52 QuadTo { x1: f64, y1: f64, x: f64, y: f64 },
54 QuadToRel {
56 dx1: f64,
57 dy1: f64,
58 dx: f64,
59 dy: f64,
60 },
61 SmoothQuadTo { x: f64, y: f64 },
63 SmoothQuadToRel { dx: f64, dy: f64 },
65 Arc {
67 rx: f64,
68 ry: f64,
69 x_rotation: f64,
70 large_arc: bool,
71 sweep: bool,
72 x: f64,
73 y: f64,
74 },
75 ArcRel {
77 rx: f64,
78 ry: f64,
79 x_rotation: f64,
80 large_arc: bool,
81 sweep: bool,
82 dx: f64,
83 dy: f64,
84 },
85 ClosePath,
87}
88
89#[derive(Debug, Clone, PartialEq, Default, Facet)]
91#[facet(traits(Default, Display))]
92pub struct PathData {
93 pub commands: Vec<PathCommand>,
94}
95
96impl PathData {
97 pub const fn new() -> Self {
98 Self {
99 commands: Vec::new(),
100 }
101 }
102
103 pub fn m(mut self, x: f64, y: f64) -> Self {
105 self.commands.push(PathCommand::MoveTo { x, y });
106 self
107 }
108
109 pub fn m_rel(mut self, dx: f64, dy: f64) -> Self {
111 self.commands.push(PathCommand::MoveToRel { dx, dy });
112 self
113 }
114
115 pub fn l(mut self, x: f64, y: f64) -> Self {
117 self.commands.push(PathCommand::LineTo { x, y });
118 self
119 }
120
121 pub fn l_rel(mut self, dx: f64, dy: f64) -> Self {
123 self.commands.push(PathCommand::LineToRel { dx, dy });
124 self
125 }
126
127 pub fn h(mut self, x: f64) -> Self {
129 self.commands.push(PathCommand::HorizontalLineTo { x });
130 self
131 }
132
133 pub fn h_rel(mut self, dx: f64) -> Self {
135 self.commands.push(PathCommand::HorizontalLineToRel { dx });
136 self
137 }
138
139 pub fn v(mut self, y: f64) -> Self {
141 self.commands.push(PathCommand::VerticalLineTo { y });
142 self
143 }
144
145 pub fn v_rel(mut self, dy: f64) -> Self {
147 self.commands.push(PathCommand::VerticalLineToRel { dy });
148 self
149 }
150
151 pub fn c(mut self, x1: f64, y1: f64, x2: f64, y2: f64, x: f64, y: f64) -> Self {
153 self.commands.push(PathCommand::CurveTo {
154 x1,
155 y1,
156 x2,
157 y2,
158 x,
159 y,
160 });
161 self
162 }
163
164 pub fn c_rel(mut self, dx1: f64, dy1: f64, dx2: f64, dy2: f64, dx: f64, dy: f64) -> Self {
166 self.commands.push(PathCommand::CurveToRel {
167 dx1,
168 dy1,
169 dx2,
170 dy2,
171 dx,
172 dy,
173 });
174 self
175 }
176
177 pub fn s(mut self, x2: f64, y2: f64, x: f64, y: f64) -> Self {
179 self.commands
180 .push(PathCommand::SmoothCurveTo { x2, y2, x, y });
181 self
182 }
183
184 pub fn s_rel(mut self, dx2: f64, dy2: f64, dx: f64, dy: f64) -> Self {
186 self.commands
187 .push(PathCommand::SmoothCurveToRel { dx2, dy2, dx, dy });
188 self
189 }
190
191 pub fn q(mut self, x1: f64, y1: f64, x: f64, y: f64) -> Self {
193 self.commands.push(PathCommand::QuadTo { x1, y1, x, y });
194 self
195 }
196
197 pub fn q_rel(mut self, dx1: f64, dy1: f64, dx: f64, dy: f64) -> Self {
199 self.commands
200 .push(PathCommand::QuadToRel { dx1, dy1, dx, dy });
201 self
202 }
203
204 pub fn t(mut self, x: f64, y: f64) -> Self {
206 self.commands.push(PathCommand::SmoothQuadTo { x, y });
207 self
208 }
209
210 pub fn t_rel(mut self, dx: f64, dy: f64) -> Self {
212 self.commands.push(PathCommand::SmoothQuadToRel { dx, dy });
213 self
214 }
215
216 #[allow(clippy::too_many_arguments)]
218 pub fn a(
219 mut self,
220 rx: f64,
221 ry: f64,
222 x_rotation: f64,
223 large_arc: bool,
224 sweep: bool,
225 x: f64,
226 y: f64,
227 ) -> Self {
228 self.commands.push(PathCommand::Arc {
229 rx,
230 ry,
231 x_rotation,
232 large_arc,
233 sweep,
234 x,
235 y,
236 });
237 self
238 }
239
240 #[allow(clippy::too_many_arguments)]
242 pub fn a_rel(
243 mut self,
244 rx: f64,
245 ry: f64,
246 x_rotation: f64,
247 large_arc: bool,
248 sweep: bool,
249 dx: f64,
250 dy: f64,
251 ) -> Self {
252 self.commands.push(PathCommand::ArcRel {
253 rx,
254 ry,
255 x_rotation,
256 large_arc,
257 sweep,
258 dx,
259 dy,
260 });
261 self
262 }
263
264 pub fn z(mut self) -> Self {
266 self.commands.push(PathCommand::ClosePath);
267 self
268 }
269
270 pub fn parse(s: &str) -> Result<Self, PathParseError> {
272 let mut commands = Vec::new();
273 let mut chars = s.chars().peekable();
274
275 while let Some(&c) = chars.peek() {
276 if c.is_whitespace() || c == ',' {
278 chars.next();
279 continue;
280 }
281
282 let cmd = chars.next().unwrap();
284 match cmd {
285 'M' => {
286 let (x, y) = parse_coord_pair(&mut chars)?;
287 commands.push(PathCommand::MoveTo { x, y });
288 while let Some((x, y)) = try_parse_coord_pair(&mut chars) {
290 commands.push(PathCommand::LineTo { x, y });
291 }
292 }
293 'm' => {
294 let (dx, dy) = parse_coord_pair(&mut chars)?;
295 commands.push(PathCommand::MoveToRel { dx, dy });
296 while let Some((dx, dy)) = try_parse_coord_pair(&mut chars) {
297 commands.push(PathCommand::LineToRel { dx, dy });
298 }
299 }
300 'L' => {
301 let (x, y) = parse_coord_pair(&mut chars)?;
302 commands.push(PathCommand::LineTo { x, y });
303 while let Some((x, y)) = try_parse_coord_pair(&mut chars) {
304 commands.push(PathCommand::LineTo { x, y });
305 }
306 }
307 'l' => {
308 let (dx, dy) = parse_coord_pair(&mut chars)?;
309 commands.push(PathCommand::LineToRel { dx, dy });
310 while let Some((dx, dy)) = try_parse_coord_pair(&mut chars) {
311 commands.push(PathCommand::LineToRel { dx, dy });
312 }
313 }
314 'H' => {
315 let x = parse_number(&mut chars)?;
316 commands.push(PathCommand::HorizontalLineTo { x });
317 while let Some(x) = try_parse_number(&mut chars) {
318 commands.push(PathCommand::HorizontalLineTo { x });
319 }
320 }
321 'h' => {
322 let dx = parse_number(&mut chars)?;
323 commands.push(PathCommand::HorizontalLineToRel { dx });
324 while let Some(dx) = try_parse_number(&mut chars) {
325 commands.push(PathCommand::HorizontalLineToRel { dx });
326 }
327 }
328 'V' => {
329 let y = parse_number(&mut chars)?;
330 commands.push(PathCommand::VerticalLineTo { y });
331 while let Some(y) = try_parse_number(&mut chars) {
332 commands.push(PathCommand::VerticalLineTo { y });
333 }
334 }
335 'v' => {
336 let dy = parse_number(&mut chars)?;
337 commands.push(PathCommand::VerticalLineToRel { dy });
338 while let Some(dy) = try_parse_number(&mut chars) {
339 commands.push(PathCommand::VerticalLineToRel { dy });
340 }
341 }
342 'C' => {
343 let (x1, y1) = parse_coord_pair(&mut chars)?;
344 let (x2, y2) = parse_coord_pair(&mut chars)?;
345 let (x, y) = parse_coord_pair(&mut chars)?;
346 commands.push(PathCommand::CurveTo {
347 x1,
348 y1,
349 x2,
350 y2,
351 x,
352 y,
353 });
354 }
355 'c' => {
356 let (dx1, dy1) = parse_coord_pair(&mut chars)?;
357 let (dx2, dy2) = parse_coord_pair(&mut chars)?;
358 let (dx, dy) = parse_coord_pair(&mut chars)?;
359 commands.push(PathCommand::CurveToRel {
360 dx1,
361 dy1,
362 dx2,
363 dy2,
364 dx,
365 dy,
366 });
367 }
368 'S' => {
369 let (x2, y2) = parse_coord_pair(&mut chars)?;
370 let (x, y) = parse_coord_pair(&mut chars)?;
371 commands.push(PathCommand::SmoothCurveTo { x2, y2, x, y });
372 }
373 's' => {
374 let (dx2, dy2) = parse_coord_pair(&mut chars)?;
375 let (dx, dy) = parse_coord_pair(&mut chars)?;
376 commands.push(PathCommand::SmoothCurveToRel { dx2, dy2, dx, dy });
377 }
378 'Q' => {
379 let (x1, y1) = parse_coord_pair(&mut chars)?;
380 let (x, y) = parse_coord_pair(&mut chars)?;
381 commands.push(PathCommand::QuadTo { x1, y1, x, y });
382 }
383 'q' => {
384 let (dx1, dy1) = parse_coord_pair(&mut chars)?;
385 let (dx, dy) = parse_coord_pair(&mut chars)?;
386 commands.push(PathCommand::QuadToRel { dx1, dy1, dx, dy });
387 }
388 'T' => {
389 let (x, y) = parse_coord_pair(&mut chars)?;
390 commands.push(PathCommand::SmoothQuadTo { x, y });
391 }
392 't' => {
393 let (dx, dy) = parse_coord_pair(&mut chars)?;
394 commands.push(PathCommand::SmoothQuadToRel { dx, dy });
395 }
396 'A' => {
397 let rx = parse_number(&mut chars)?;
398 let ry = parse_number(&mut chars)?;
399 let x_rotation = parse_number(&mut chars)?;
400 let large_arc = parse_flag(&mut chars)?;
401 let sweep = parse_flag(&mut chars)?;
402 let (x, y) = parse_coord_pair(&mut chars)?;
403 commands.push(PathCommand::Arc {
404 rx,
405 ry,
406 x_rotation,
407 large_arc,
408 sweep,
409 x,
410 y,
411 });
412 }
413 'a' => {
414 let rx = parse_number(&mut chars)?;
415 let ry = parse_number(&mut chars)?;
416 let x_rotation = parse_number(&mut chars)?;
417 let large_arc = parse_flag(&mut chars)?;
418 let sweep = parse_flag(&mut chars)?;
419 let (dx, dy) = parse_coord_pair(&mut chars)?;
420 commands.push(PathCommand::ArcRel {
421 rx,
422 ry,
423 x_rotation,
424 large_arc,
425 sweep,
426 dx,
427 dy,
428 });
429 }
430 'Z' | 'z' => {
431 commands.push(PathCommand::ClosePath);
432 }
433 _ => {
434 return Err(PathParseError::UnknownCommand(cmd));
435 }
436 }
437 }
438
439 Ok(PathData { commands })
440 }
441
442 fn serialize(&self) -> String {
444 let mut result = String::new();
445 for cmd in &self.commands {
446 if !result.is_empty() {
447 }
449 match cmd {
450 PathCommand::MoveTo { x, y } => {
451 result.push_str(&format!("M{},{}", fmt_num(*x), fmt_num(*y)));
452 }
453 PathCommand::MoveToRel { dx, dy } => {
454 result.push_str(&format!("m{},{}", fmt_num(*dx), fmt_num(*dy)));
455 }
456 PathCommand::LineTo { x, y } => {
457 result.push_str(&format!("L{},{}", fmt_num(*x), fmt_num(*y)));
458 }
459 PathCommand::LineToRel { dx, dy } => {
460 result.push_str(&format!("l{},{}", fmt_num(*dx), fmt_num(*dy)));
461 }
462 PathCommand::HorizontalLineTo { x } => {
463 result.push_str(&format!("H{}", fmt_num(*x)));
464 }
465 PathCommand::HorizontalLineToRel { dx } => {
466 result.push_str(&format!("h{}", fmt_num(*dx)));
467 }
468 PathCommand::VerticalLineTo { y } => {
469 result.push_str(&format!("V{}", fmt_num(*y)));
470 }
471 PathCommand::VerticalLineToRel { dy } => {
472 result.push_str(&format!("v{}", fmt_num(*dy)));
473 }
474 PathCommand::CurveTo {
475 x1,
476 y1,
477 x2,
478 y2,
479 x,
480 y,
481 } => {
482 result.push_str(&format!(
483 "C{},{} {},{} {},{}",
484 fmt_num(*x1),
485 fmt_num(*y1),
486 fmt_num(*x2),
487 fmt_num(*y2),
488 fmt_num(*x),
489 fmt_num(*y)
490 ));
491 }
492 PathCommand::CurveToRel {
493 dx1,
494 dy1,
495 dx2,
496 dy2,
497 dx,
498 dy,
499 } => {
500 result.push_str(&format!(
501 "c{},{} {},{} {},{}",
502 fmt_num(*dx1),
503 fmt_num(*dy1),
504 fmt_num(*dx2),
505 fmt_num(*dy2),
506 fmt_num(*dx),
507 fmt_num(*dy)
508 ));
509 }
510 PathCommand::SmoothCurveTo { x2, y2, x, y } => {
511 result.push_str(&format!(
512 "S{},{} {},{}",
513 fmt_num(*x2),
514 fmt_num(*y2),
515 fmt_num(*x),
516 fmt_num(*y)
517 ));
518 }
519 PathCommand::SmoothCurveToRel { dx2, dy2, dx, dy } => {
520 result.push_str(&format!(
521 "s{},{} {},{}",
522 fmt_num(*dx2),
523 fmt_num(*dy2),
524 fmt_num(*dx),
525 fmt_num(*dy)
526 ));
527 }
528 PathCommand::QuadTo { x1, y1, x, y } => {
529 result.push_str(&format!(
530 "Q{},{} {},{}",
531 fmt_num(*x1),
532 fmt_num(*y1),
533 fmt_num(*x),
534 fmt_num(*y)
535 ));
536 }
537 PathCommand::QuadToRel { dx1, dy1, dx, dy } => {
538 result.push_str(&format!(
539 "q{},{} {},{}",
540 fmt_num(*dx1),
541 fmt_num(*dy1),
542 fmt_num(*dx),
543 fmt_num(*dy)
544 ));
545 }
546 PathCommand::SmoothQuadTo { x, y } => {
547 result.push_str(&format!("T{},{}", fmt_num(*x), fmt_num(*y)));
548 }
549 PathCommand::SmoothQuadToRel { dx, dy } => {
550 result.push_str(&format!("t{},{}", fmt_num(*dx), fmt_num(*dy)));
551 }
552 PathCommand::Arc {
553 rx,
554 ry,
555 x_rotation,
556 large_arc,
557 sweep,
558 x,
559 y,
560 } => {
561 result.push_str(&format!(
562 "A{},{} {} {} {} {},{}",
563 fmt_num(*rx),
564 fmt_num(*ry),
565 fmt_num(*x_rotation),
566 if *large_arc { 1 } else { 0 },
567 if *sweep { 1 } else { 0 },
568 fmt_num(*x),
569 fmt_num(*y)
570 ));
571 }
572 PathCommand::ArcRel {
573 rx,
574 ry,
575 x_rotation,
576 large_arc,
577 sweep,
578 dx,
579 dy,
580 } => {
581 result.push_str(&format!(
582 "a{},{} {} {} {} {},{}",
583 fmt_num(*rx),
584 fmt_num(*ry),
585 fmt_num(*x_rotation),
586 if *large_arc { 1 } else { 0 },
587 if *sweep { 1 } else { 0 },
588 fmt_num(*dx),
589 fmt_num(*dy)
590 ));
591 }
592 PathCommand::ClosePath => {
593 result.push('Z');
594 }
595 }
596 }
597 result
598 }
599}
600
601fn fmt_num(v: f64) -> String {
604 let s = format!("{:.3}", v);
605 let s = s.trim_end_matches('0');
606 let s = s.trim_end_matches('.');
607 s.to_string()
608}
609
610#[derive(Debug, Clone, PartialEq)]
612pub enum PathParseError {
613 UnknownCommand(char),
614 ExpectedNumber,
615 ExpectedFlag,
616 InvalidNumber(String),
617}
618
619impl std::fmt::Display for PathParseError {
620 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
621 match self {
622 PathParseError::UnknownCommand(c) => write!(f, "unknown path command: {}", c),
623 PathParseError::ExpectedNumber => write!(f, "expected number"),
624 PathParseError::ExpectedFlag => write!(f, "expected flag (0 or 1)"),
625 PathParseError::InvalidNumber(s) => write!(f, "invalid number: {}", s),
626 }
627 }
628}
629
630impl std::error::Error for PathParseError {}
631
632impl std::fmt::Display for PathData {
633 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
634 f.write_str(&self.serialize())
635 }
636}
637
638#[derive(Facet, Clone, Debug)]
640#[facet(transparent)]
641pub struct PathDataProxy(pub String);
642
643impl TryFrom<PathDataProxy> for PathData {
644 type Error = PathParseError;
645 fn try_from(proxy: PathDataProxy) -> Result<Self, Self::Error> {
646 PathData::parse(&proxy.0)
647 }
648}
649
650#[allow(clippy::infallible_try_from)]
651impl TryFrom<&PathData> for PathDataProxy {
652 type Error = std::convert::Infallible;
653 fn try_from(v: &PathData) -> Result<Self, Self::Error> {
654 Ok(PathDataProxy(v.to_string()))
655 }
656}
657
658impl From<PathDataProxy> for Option<PathData> {
660 fn from(proxy: PathDataProxy) -> Self {
661 PathData::parse(&proxy.0).ok()
662 }
663}
664
665#[allow(clippy::infallible_try_from)]
666impl TryFrom<&Option<PathData>> for PathDataProxy {
667 type Error = std::convert::Infallible;
668 fn try_from(v: &Option<PathData>) -> Result<Self, Self::Error> {
669 match v {
670 Some(data) => Ok(PathDataProxy(data.to_string())),
671 None => Ok(PathDataProxy(String::new())),
672 }
673 }
674}
675
676fn skip_wsp_comma(chars: &mut std::iter::Peekable<std::str::Chars>) {
679 while let Some(&c) = chars.peek() {
680 if c.is_whitespace() || c == ',' {
681 chars.next();
682 } else {
683 break;
684 }
685 }
686}
687
688fn parse_number(chars: &mut std::iter::Peekable<std::str::Chars>) -> Result<f64, PathParseError> {
689 skip_wsp_comma(chars);
690 let mut num_str = String::new();
691
692 if let Some(&c) = chars.peek()
694 && (c == '-' || c == '+')
695 {
696 num_str.push(chars.next().unwrap());
697 }
698
699 let mut has_digits = false;
701 while let Some(&c) = chars.peek() {
702 if c.is_ascii_digit() || c == '.' {
703 num_str.push(chars.next().unwrap());
704 has_digits = true;
705 } else if c == 'e' || c == 'E' {
706 num_str.push(chars.next().unwrap());
708 if let Some(&sign) = chars.peek()
709 && (sign == '-' || sign == '+')
710 {
711 num_str.push(chars.next().unwrap());
712 }
713 while let Some(&d) = chars.peek() {
714 if d.is_ascii_digit() {
715 num_str.push(chars.next().unwrap());
716 } else {
717 break;
718 }
719 }
720 break;
721 } else {
722 break;
723 }
724 }
725
726 if !has_digits {
727 return Err(PathParseError::ExpectedNumber);
728 }
729
730 num_str
731 .parse()
732 .map_err(|_| PathParseError::InvalidNumber(num_str))
733}
734
735fn try_parse_number(chars: &mut std::iter::Peekable<std::str::Chars>) -> Option<f64> {
736 skip_wsp_comma(chars);
737 if let Some(&c) = chars.peek()
738 && (c.is_ascii_digit() || c == '-' || c == '+' || c == '.')
739 {
740 return parse_number(chars).ok();
741 }
742 None
743}
744
745fn parse_coord_pair(
746 chars: &mut std::iter::Peekable<std::str::Chars>,
747) -> Result<(f64, f64), PathParseError> {
748 let x = parse_number(chars)?;
749 let y = parse_number(chars)?;
750 Ok((x, y))
751}
752
753fn try_parse_coord_pair(chars: &mut std::iter::Peekable<std::str::Chars>) -> Option<(f64, f64)> {
754 skip_wsp_comma(chars);
755 if let Some(&c) = chars.peek()
756 && (c.is_ascii_digit() || c == '-' || c == '+' || c == '.')
757 && let Ok(pair) = parse_coord_pair(chars)
758 {
759 return Some(pair);
760 }
761 None
762}
763
764fn parse_flag(chars: &mut std::iter::Peekable<std::str::Chars>) -> Result<bool, PathParseError> {
765 skip_wsp_comma(chars);
766 match chars.next() {
767 Some('0') => Ok(false),
768 Some('1') => Ok(true),
769 _ => Err(PathParseError::ExpectedFlag),
770 }
771}
772
773#[cfg(test)]
774mod tests {
775 use super::*;
776
777 #[test]
778 fn test_parse_simple_path() {
779 let path = PathData::parse("M10,20L30,40Z").unwrap();
780 assert_eq!(path.commands.len(), 3);
781 assert_eq!(path.commands[0], PathCommand::MoveTo { x: 10.0, y: 20.0 });
782 assert_eq!(path.commands[1], PathCommand::LineTo { x: 30.0, y: 40.0 });
783 assert_eq!(path.commands[2], PathCommand::ClosePath);
784 }
785
786 #[test]
787 fn test_parse_box_path() {
788 let path =
790 PathData::parse("M118.239,208.239L226.239,208.239L226.239,136.239L118.239,136.239Z")
791 .unwrap();
792 assert_eq!(path.commands.len(), 5);
793 }
794
795 #[test]
796 fn test_roundtrip() {
797 let original = "M10,20L30,40Z";
798 let path = PathData::parse(original).unwrap();
799 let serialized = path.to_string();
800 let reparsed = PathData::parse(&serialized).unwrap();
801 assert_eq!(path.commands, reparsed.commands);
802 }
803
804 #[test]
805 fn test_arc() {
806 let path = PathData::parse("A10,10 0 0,1 20,20").unwrap();
807 assert_eq!(path.commands.len(), 1);
808 match &path.commands[0] {
809 PathCommand::Arc {
810 rx,
811 ry,
812 x_rotation,
813 large_arc,
814 sweep,
815 x,
816 y,
817 } => {
818 assert_eq!(*rx, 10.0);
819 assert_eq!(*ry, 10.0);
820 assert_eq!(*x_rotation, 0.0);
821 assert!(!*large_arc);
822 assert!(*sweep);
823 assert_eq!(*x, 20.0);
824 assert_eq!(*y, 20.0);
825 }
826 _ => panic!("expected Arc command"),
827 }
828 }
829
830 #[test]
831 fn test_float_tolerance_in_diff() {
832 use facet_assert::{SameOptions, SameReport, check_same_with_report};
833
834 let c_path = PathData::parse("M118.239,208.239L226.239,208.239Z").unwrap();
838 let rust_path =
839 PathData::parse("M118.2387401575,208.2387401575L226.2387401575,208.2387401575Z")
840 .unwrap();
841
842 let tolerance = 0.002;
844 let options = SameOptions::new().float_tolerance(tolerance);
845
846 eprintln!("C path: {:?}", c_path);
847 eprintln!("Rust path: {:?}", rust_path);
848
849 let result = check_same_with_report(&c_path, &rust_path, options);
851
852 match &result {
853 SameReport::Same => eprintln!("Result: Same"),
854 SameReport::Different(report) => {
855 eprintln!("Result: Different");
856 eprintln!("XML diff:\n{}", report.render_ansi_xml());
857 }
858 SameReport::Opaque { type_name } => eprintln!("Result: Opaque({})", type_name),
859 }
860
861 assert!(
862 matches!(result, SameReport::Same),
863 "PathData values within float tolerance should be considered Same"
864 );
865 }
866}