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