1use std::sync::{Arc, Mutex, OnceLock};
4
5use kurbo::Shape as _;
6
7use crate::{Pivot, Point, Rect, Transform, ValidationError, Vector2};
8
9mod construction;
10mod editing;
11mod metrics;
12mod morph;
13#[cfg(test)]
14mod tests;
15mod validation;
16
17pub use morph::MorphPath;
18
19pub const MAX_PATH_VERBS: usize = 16_384;
21
22pub const MAX_SVG_PATH_BYTES: usize = 1_048_576;
24
25#[derive(Clone, Copy, Debug, PartialEq)]
27pub enum PathVerb {
28 MoveTo(Point),
30 LineTo(Point),
32 QuadraticTo(Point, Point),
34 CubicTo(Point, Point, Point),
36 Close,
38}
39
40impl PathVerb {
41 pub fn points(self) -> [Option<Point>; 3] {
44 match self {
45 Self::MoveTo(point) | Self::LineTo(point) => [Some(point), None, None],
46 Self::QuadraticTo(control, end) => [Some(control), Some(end), None],
47 Self::CubicTo(first, second, end) => [Some(first), Some(second), Some(end)],
48 Self::Close => [None, None, None],
49 }
50 }
51}
52
53#[derive(Clone, Debug, Default)]
99pub struct Path2D {
100 data: Arc<PathData>,
101}
102
103type StrokeCache = Arc<Mutex<Vec<([u64; 4], PathData)>>>;
104
105#[derive(Clone, Debug, Default)]
106struct PathData {
107 error: Option<ValidationError>,
108 verbs: Arc<Vec<PathVerb>>,
109 hull: Option<Rect>,
110 current: Option<Point>,
111 start: Option<Point>,
112 bounds: OnceLock<Rect>,
113 fingerprint: OnceLock<u64>,
114 metrics: OnceLock<Result<Arc<metrics::Metrics>, ValidationError>>,
115 strokes: OnceLock<StrokeCache>,
116}
117
118impl PartialEq for Path2D {
119 fn eq(&self, other: &Self) -> bool {
120 self.verbs() == other.verbs() && self.data.error == other.data.error
121 }
122}
123
124impl Path2D {
125 pub fn new() -> Self {
127 Self::default()
128 }
129
130 pub fn verbs(&self) -> &[PathVerb] {
132 &self.data.verbs
133 }
134
135 pub fn bounding_box(&self) -> Rect {
137 *self.data.bounds.get_or_init(|| self.compute_bounds())
138 }
139
140 pub fn transformed(&self, transform: &Transform) -> Result<Self, ValidationError> {
142 self.validate()?;
143 if !transform.is_finite() {
144 return Err(invalid("transform", "Path transform must be finite"));
145 }
146 let mut result = Self::new();
147 for verb in self.verbs() {
148 let mapped = match *verb {
149 PathVerb::MoveTo(point) => PathVerb::MoveTo(transform.apply(point)),
150 PathVerb::LineTo(point) => PathVerb::LineTo(transform.apply(point)),
151 PathVerb::QuadraticTo(control, end) => {
152 PathVerb::QuadraticTo(transform.apply(control), transform.apply(end))
153 }
154 PathVerb::CubicTo(first, second, end) => PathVerb::CubicTo(
155 transform.apply(first),
156 transform.apply(second),
157 transform.apply(end),
158 ),
159 PathVerb::Close => PathVerb::Close,
160 };
161 result.push(mapped)?;
162 }
163 Ok(result)
164 }
165
166 pub fn shared_verbs(&self) -> Arc<Vec<PathVerb>> {
169 self.data.verbs.clone()
170 }
171
172 pub fn axis_aligned_rectangle(&self) -> Option<Rect> {
175 if self.data.error.is_some() {
176 return None;
177 }
178 let [
179 PathVerb::MoveTo(first),
180 PathVerb::LineTo(second),
181 PathVerb::LineTo(third),
182 PathVerb::LineTo(fourth),
183 PathVerb::Close,
184 ] = self.verbs()
185 else {
186 return None;
187 };
188 let horizontal = first.y == second.y
189 && second.x == third.x
190 && third.y == fourth.y
191 && fourth.x == first.x;
192 let vertical = first.x == second.x
193 && second.y == third.y
194 && third.x == fourth.x
195 && fourth.y == first.y;
196 if !(horizontal || vertical) || first.x == third.x || first.y == third.y {
197 return None;
198 }
199 Some(Rect::new(
200 first.x.min(third.x),
201 first.y.min(third.y),
202 first.x.max(third.x),
203 first.y.max(third.y),
204 ))
205 }
206
207 pub fn fingerprint(&self) -> u64 {
210 *self.data.fingerprint.get_or_init(|| {
211 let mut value = 14695981039346656037_u64;
212 let mut write = |bytes: &[u8]| {
213 for byte in bytes {
214 value = (value ^ u64::from(*byte)).wrapping_mul(1099511628211);
215 }
216 };
217 write(&(self.verbs().len() as u64).to_le_bytes());
218 for verb in self.verbs() {
219 write(&[match verb {
220 PathVerb::MoveTo(_) => 0,
221 PathVerb::LineTo(_) => 1,
222 PathVerb::QuadraticTo(..) => 2,
223 PathVerb::CubicTo(..) => 3,
224 PathVerb::Close => 4,
225 }]);
226 for point in verb.points().into_iter().flatten() {
227 write(&point.x.to_bits().to_le_bytes());
228 write(&point.y.to_bits().to_le_bytes());
229 }
230 }
231 value
232 })
233 }
234
235 fn compute_bounds(&self) -> Rect {
236 let Some(hull) = self.data.hull else {
237 return Rect::default();
238 };
239 let normalize = |point: Point| {
240 (
241 if hull.width() == 0.0 {
242 0.0
243 } else {
244 (point.x - hull.left) / hull.width()
245 },
246 if hull.height() == 0.0 {
247 0.0
248 } else {
249 (point.y - hull.top) / hull.height()
250 },
251 )
252 };
253 let mut path = kurbo::BezPath::new();
254 for verb in self.verbs() {
255 match *verb {
256 PathVerb::MoveTo(point) => path.move_to(normalize(point)),
257 PathVerb::LineTo(point) => path.line_to(normalize(point)),
258 PathVerb::QuadraticTo(control, end) => {
259 path.quad_to(normalize(control), normalize(end))
260 }
261 PathVerb::CubicTo(first, second, end) => {
262 path.curve_to(normalize(first), normalize(second), normalize(end))
263 }
264 PathVerb::Close => path.close_path(),
265 }
266 }
267 let bounds = path.bounding_box();
268 if ![bounds.x0, bounds.y0, bounds.x1, bounds.y1]
269 .iter()
270 .all(|value| value.is_finite())
271 {
272 return hull;
273 }
274 let first = hull.anchor(Pivot::Relative(
275 bounds.x0.clamp(0.0, 1.0),
276 bounds.y0.clamp(0.0, 1.0),
277 ));
278 let last = hull.anchor(Pivot::Relative(
279 bounds.x1.clamp(0.0, 1.0),
280 bounds.y1.clamp(0.0, 1.0),
281 ));
282 let mut result = Rect::new(first.x, first.y, last.x, last.y);
283 for verb in self.verbs() {
284 if let PathVerb::MoveTo(point) = verb {
285 result = Rect::new(
286 result.left.min(point.x),
287 result.top.min(point.y),
288 result.right.max(point.x),
289 result.bottom.max(point.y),
290 );
291 }
292 }
293 result
294 }
295
296 pub(crate) fn cached_stroke(&self, key: [u64; 4]) -> Option<Path2D> {
297 let mut entries = self.data.strokes.get()?.lock().ok()?;
298 let index = entries.iter().position(|entry| entry.0 == key)?;
299 let entry = entries.remove(index);
300 let result = Self {
301 data: Arc::new(entry.1.clone()),
302 };
303 entries.push(entry);
304 Some(result)
305 }
306
307 pub(crate) fn retain_stroke(&self, key: [u64; 4], outline: &Path2D) {
308 let bytes = |data: &PathData| {
309 data.verbs.len() * std::mem::size_of::<PathVerb>()
310 + std::mem::size_of::<([u64; 4], PathData)>()
311 };
312 let cost = bytes(&outline.data);
313 if cost > 1024 * 1024 {
314 return;
315 }
316 outline.bounding_box();
317 outline.fingerprint();
318 let mut snapshot = (*outline.data).clone();
319 snapshot.metrics.take();
320 snapshot.strokes.take();
321 let Ok(mut entries) = self
322 .data
323 .strokes
324 .get_or_init(|| Arc::new(Mutex::new(Vec::new())))
325 .lock()
326 else {
327 return;
328 };
329 if entries.iter().any(|entry| entry.0 == key) {
330 return;
331 }
332 while !entries.is_empty()
333 && (entries.len() >= 4
334 || entries.iter().map(|entry| bytes(&entry.1)).sum::<usize>() + cost > 1024 * 1024)
335 {
336 entries.remove(0);
337 }
338 if entries.try_reserve(1).is_ok() {
339 entries.push((key, snapshot));
340 }
341 }
342}
343
344fn invalid(field: &str, message: &str) -> ValidationError {
345 ValidationError::invalid("path2d", field, message)
346}