1use std::{fmt::Display, str::FromStr};
2
3use lyon::path::Event;
4
5use crate::error::PathError;
6
7pub mod conversions;
8pub mod point;
9pub mod subpath;
10
11#[derive(Debug, Clone)]
12pub struct Path {
13 inner: lyon::path::Path,
14}
15
16impl Path {
17 pub fn iter(&self) -> impl Iterator<Item = Path> + '_ {
18 self.into_iter()
19 }
20
21 pub fn is_closed(&self) -> bool {
22 self.inner
23 .iter()
24 .any(|e| matches!(e, Event::End { close: true, .. }))
25 }
26
27 pub fn find_outer_shell(&self) -> Option<Path> {
32 let subpaths: Vec<Path> = self.iter().collect();
33
34 match subpaths.len() {
35 0 => None,
37
38 1 => subpaths.into_iter().next(),
42
43 _ => {
45 find_shell_by_area(&subpaths)
47 .or_else(|| find_shell_by_containment(&subpaths))
49 }
50 }
51 }
52
53 fn intersect_with(&self, other: &Path) -> bool {
54 let bbox_a = lyon::algorithms::aabb::bounding_box(self.inner.iter());
55 let bbox_b = lyon::algorithms::aabb::bounding_box(other.inner.iter());
56 bbox_a.intersects(&bbox_b)
57 }
58
59 fn contained_by(&self, other_path: &Path) -> bool {
60 !std::ptr::eq(self, other_path)
61 && self.is_closed()
62 && other_path.is_closed()
63 && self.inner.first_endpoint().map_or(false, |(pt, _)| {
64 lyon::algorithms::hit_test::hit_test_path(
65 &pt,
66 &other_path.inner,
67 lyon::path::FillRule::EvenOdd,
68 0.1,
69 )
70 })
71 }
72}
73
74impl FromStr for Path {
75 type Err = PathError;
76
77 fn from_str(s: &str) -> Result<Self, Self::Err> {
78 let mut parser = lyon::extra::parser::PathParser::new();
79 let mut builder = lyon::path::Path::builder();
80 let mut src = lyon::extra::parser::Source::new(s.chars());
81
82 parser.parse(
83 &lyon::extra::parser::ParserOptions::DEFAULT,
84 &mut src,
85 &mut builder,
86 )?;
87
88 let path = builder.build();
89 Ok(Path::from(path))
90 }
91}
92
93impl Display for Path {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 let path_slice = self.inner.as_slice();
96
97 for event in path_slice.iter_with_attributes() {
98 match event {
99 Event::Begin { at: (at, _) } => {
100 write!(f, "M{},{}", at.x, at.y)?;
101 }
102 Event::Line { to: (to, _), .. } => {
103 write!(f, "L{},{}", to.x, to.y)?;
104 }
105 Event::Quadratic {
106 ctrl, to: (to, _), ..
107 } => {
108 write!(f, "Q{},{} {},{}", ctrl.x, ctrl.y, to.x, to.y)?;
109 }
110 Event::Cubic {
111 ctrl1,
112 ctrl2,
113 to: (to, _),
114 ..
115 } => {
116 write!(
117 f,
118 "C{},{} {},{} {},{}",
119 ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, to.x, to.y
120 )?;
121 }
122 Event::End { close, .. } => {
123 if close {
124 write!(f, "Z")?;
125 }
126 }
127 }
128 }
129
130 Ok(())
131 }
132}
133
134fn find_shell_by_area(paths: &[Path]) -> Option<Path> {
137 paths
138 .iter()
139 .filter(|p| p.is_closed())
141 .max_by(|a, b| {
142 let area_a = lyon::algorithms::area::approximate_signed_area(0.01, a.inner.iter());
143 let area_b = lyon::algorithms::area::approximate_signed_area(0.01, b.inner.iter());
144 area_a.total_cmp(&area_b)
146 })
147 .cloned() }
149
150fn find_shell_by_containment(paths: &[Path]) -> Option<Path> {
153 paths
154 .iter()
155 .find(|this_path| {
156 !paths.iter().any(|other_path| {
158 this_path.intersect_with(other_path) && this_path.contained_by(other_path)
160 })
161 })
162 .cloned()
163}