1use kurbo::{ParamCurve, ParamCurveDeriv, PathSeg};
4
5use super::*;
6
7const BINS: usize = 16;
8
9const MAX_WORK: usize = 16_777_216;
10
11#[derive(Clone, Debug)]
12pub(super) struct Metrics {
13 segments: Vec<Segment>,
14 total: f64,
15}
16
17#[derive(Clone, Debug)]
18struct Segment {
19 curve: PathSeg,
20 start: f64,
21 length: f64,
22 table: [f64; BINS + 1],
23 contour_start: f64,
24 contour: usize,
25 close: bool,
26}
27
28fn point(value: kurbo::Point) -> Point {
29 Point::new(value.x, value.y)
30}
31
32fn subcurve(curve: PathSeg, lower: f64, upper: f64) -> PathSeg {
33 if lower == 0.0 && upper == 1.0 {
34 curve
35 } else {
36 let controls = match curve {
37 PathSeg::Line(line) => [line.p0, line.p1, line.p1, line.p1],
38 PathSeg::Quad(quad) => [quad.p0, quad.p1, quad.p2, quad.p2],
39 PathSeg::Cubic(cubic) => [cubic.p0, cubic.p1, cubic.p2, cubic.p3],
40 };
41 let constant_x = controls.iter().all(|point| point.x == controls[0].x);
42 let constant_y = controls.iter().all(|point| point.y == controls[0].y);
43 let mut result = curve.subsegment(lower..upper);
44 let preserve = |point: &mut kurbo::Point| {
45 if constant_x {
46 point.x = controls[0].x;
47 }
48 if constant_y {
49 point.y = controls[0].y;
50 }
51 };
52 match &mut result {
53 PathSeg::Line(line) => {
54 preserve(&mut line.p0);
55 preserve(&mut line.p1);
56 }
57 PathSeg::Quad(quad) => {
58 preserve(&mut quad.p0);
59 preserve(&mut quad.p1);
60 preserve(&mut quad.p2);
61 }
62 PathSeg::Cubic(cubic) => {
63 preserve(&mut cubic.p0);
64 preserve(&mut cubic.p1);
65 preserve(&mut cubic.p2);
66 preserve(&mut cubic.p3);
67 }
68 }
69 result
70 }
71}
72
73fn derivative(curve: PathSeg, parameter: f64) -> Vector2 {
74 let value = match curve {
75 PathSeg::Line(line) => line.deriv().eval(parameter),
76 PathSeg::Quad(quad) => quad.deriv().eval(parameter),
77 PathSeg::Cubic(cubic) => cubic.deriv().eval(parameter),
78 };
79 Vector2::new(value.x, value.y)
80}
81
82fn gauss(curve: PathSeg, start: f64, end: f64, work: &mut usize) -> Result<f64, ValidationError> {
83 *work += 8;
84 if *work > MAX_WORK {
85 return Err(ValidationError::limit(
86 "path2d.metrics",
87 "work",
88 "Arc-length integration exhausted its work allowance",
89 ));
90 }
91 let midpoint = (start + end) * 0.5;
92 let radius = (end - start) * 0.5;
93 let mut sum = 0.0;
94 for (node, weight) in [
95 (0.1834346424956498, 0.362683783378362),
96 (0.525532409916329, 0.3137066458778873),
97 (0.7966664774136267, 0.2223810344533745),
98 (0.9602898564975363, 0.1012285362903763),
99 ] {
100 sum += weight
101 * (derivative(curve, midpoint - radius * node).length()
102 + derivative(curve, midpoint + radius * node).length());
103 }
104 let length = sum * radius;
105 if !length.is_finite() {
106 return Err(invalid("length", "Arc length must remain finite"));
107 }
108 Ok(length)
109}
110
111fn integrate(
112 curve: PathSeg,
113 start: f64,
114 end: f64,
115 depth: usize,
116 work: &mut usize,
117) -> Result<f64, ValidationError> {
118 if start == end {
119 return Ok(0.0);
120 }
121 let midpoint = (start + end) * 0.5;
122 let coarse = gauss(curve, start, end, work)?;
123 let fine = gauss(curve, start, midpoint, work)? + gauss(curve, midpoint, end, work)?;
124 let scale = match curve {
125 PathSeg::Line(line) => point(line.p0).distance(point(line.p1)),
126 PathSeg::Quad(quad) => {
127 point(quad.p0).distance(point(quad.p1)) + point(quad.p1).distance(point(quad.p2))
128 }
129 PathSeg::Cubic(cubic) => {
130 point(cubic.p0).distance(point(cubic.p1))
131 + point(cubic.p1).distance(point(cubic.p2))
132 + point(cubic.p2).distance(point(cubic.p3))
133 }
134 };
135 if !scale.is_finite() {
136 return Err(invalid("length", "Curve metric scale must remain finite"));
137 }
138 if fine == 0.0 && scale > 0.0 {
139 return Err(invalid(
140 "length",
141 "Curve length is below integration precision",
142 ));
143 }
144 if (fine - coarse).abs() <= fine * 1e-11 + scale * 1e-13 || fine == 0.0 {
145 return Ok(fine);
146 }
147 if depth == 32 {
148 return Err(ValidationError::limit(
149 "path2d.metrics",
150 "precision",
151 "Arc-length integration could not meet its relative tolerance",
152 ));
153 }
154 Ok(integrate(curve, start, midpoint, depth + 1, work)?
155 + integrate(curve, midpoint, end, depth + 1, work)?)
156}
157
158impl Segment {
159 fn parameter(&self, distance: f64) -> Result<f64, ValidationError> {
160 if distance <= 0.0 {
161 return Ok(0.0);
162 }
163 if distance >= self.length {
164 return Ok(1.0);
165 }
166 if matches!(self.curve, PathSeg::Line(_)) {
167 return Ok(distance / self.length);
168 }
169 let bin = self
170 .table
171 .partition_point(|value| *value <= distance)
172 .saturating_sub(1)
173 .min(BINS - 1);
174 let origin = bin as f64 / BINS as f64;
175 let mut lower = origin;
176 let mut upper = (bin + 1) as f64 / BINS as f64;
177 let mut parameter = lower
178 + (upper - lower) * (distance - self.table[bin])
179 / (self.table[bin + 1] - self.table[bin]);
180 let mut work = 0;
181 for _ in 0..48 {
182 let error = self.table[bin] + integrate(self.curve, origin, parameter, 0, &mut work)?
183 - distance;
184 if error.abs() <= self.length * 2e-11 {
185 return Ok(parameter);
186 }
187 if error < 0.0 {
188 lower = parameter;
189 } else {
190 upper = parameter;
191 }
192 let candidate = parameter - error / derivative(self.curve, parameter).length();
193 parameter = if candidate.is_finite() && candidate > lower && candidate < upper {
194 candidate
195 } else {
196 (lower + upper) * 0.5
197 };
198 }
199 Ok((lower + upper) * 0.5)
200 }
201}
202
203impl Metrics {
204 fn new(path: &Path2D) -> Result<Self, ValidationError> {
205 let mut result = Self {
206 segments: Vec::new(),
207 total: 0.0,
208 };
209 let mut current = kurbo::Point::ZERO;
210 let mut start = current;
211 let mut contour = 0;
212 let mut contour_start = 0.0;
213 let mut work = 0;
214 for verb in path.verbs() {
215 let to_kurbo = |value: Point| kurbo::Point::new(value.x, value.y);
216 let curve = match *verb {
217 PathVerb::MoveTo(value) => {
218 current = to_kurbo(value);
219 start = current;
220 contour += 1;
221 contour_start = result.total;
222 continue;
223 }
224 PathVerb::LineTo(value) => {
225 PathSeg::Line(kurbo::Line::new(current, to_kurbo(value)))
226 }
227 PathVerb::QuadraticTo(control, end) => PathSeg::Quad(kurbo::QuadBez::new(
228 current,
229 to_kurbo(control),
230 to_kurbo(end),
231 )),
232 PathVerb::CubicTo(first, second, end) => PathSeg::Cubic(kurbo::CubicBez::new(
233 current,
234 to_kurbo(first),
235 to_kurbo(second),
236 to_kurbo(end),
237 )),
238 PathVerb::Close => PathSeg::Line(kurbo::Line::new(current, start)),
239 };
240 let mut table = [0.0; BINS + 1];
241 let line_length = if let PathSeg::Line(line) = curve {
242 Some(point(line.p0).distance(point(line.p1)))
243 } else {
244 None
245 };
246 for bin in 0..BINS {
247 table[bin + 1] = if let Some(length) = line_length {
248 length * ((bin + 1) as f64 / BINS as f64)
249 } else {
250 table[bin]
251 + integrate(
252 curve,
253 bin as f64 / BINS as f64,
254 (bin + 1) as f64 / BINS as f64,
255 0,
256 &mut work,
257 )?
258 };
259 }
260 let length = table[BINS];
261 result.segments.push(Segment {
262 curve,
263 start: result.total,
264 length,
265 table,
266 contour_start,
267 contour,
268 close: matches!(verb, PathVerb::Close),
269 });
270 let total = result.total + length;
271 if length > 0.0 && total == result.total {
272 return Err(invalid(
273 "length",
274 "Segment length is below cumulative metric precision",
275 ));
276 }
277 result.total = total;
278 if !result.total.is_finite() {
279 return Err(invalid("length", "Total path length must remain finite"));
280 }
281 current = curve.end();
282 }
283 Ok(result)
284 }
285
286 fn append(
287 &self,
288 start: f64,
289 end: f64,
290 output: &mut Path2D,
291 connect: bool,
292 ) -> Result<(), ValidationError> {
293 let first = self
294 .segments
295 .partition_point(|segment| segment.start + segment.length <= start);
296 let mut previous = None;
297 for segment in &self.segments[first..] {
298 if segment.start > end || (segment.start == end && segment.length > 0.0) {
299 break;
300 }
301 if segment.length == 0.0 {
302 if segment.close
303 && start <= segment.contour_start
304 && previous == Some(segment.contour)
305 {
306 output.try_close()?;
307 }
308 continue;
309 }
310 let lower = segment.parameter((start - segment.start).max(0.0))?;
311 let upper = segment.parameter((end - segment.start).min(segment.length))?;
312 if lower >= upper {
313 continue;
314 }
315 let curve = subcurve(segment.curve, lower, upper);
316 let first_point = point(curve.start());
317 if (previous != Some(segment.contour)
318 && !(connect && previous.is_none() && output.data.current == Some(first_point)))
319 || output.data.current != Some(first_point)
320 {
321 output.try_move_to(first_point)?;
322 }
323 if segment.close && lower == 0.0 && upper == 1.0 && start <= segment.contour_start {
324 output.try_close()?;
325 } else {
326 match curve {
327 PathSeg::Line(line) => output.try_line_to(point(line.p1))?,
328 PathSeg::Quad(quad) => {
329 output.try_quadratic_to(point(quad.p1), point(quad.p2))?
330 }
331 PathSeg::Cubic(cubic) => {
332 output.try_cubic_to(point(cubic.p1), point(cubic.p2), point(cubic.p3))?
333 }
334 }
335 }
336 previous = Some(segment.contour);
337 }
338 Ok(())
339 }
340}
341
342impl Path2D {
343 pub fn cubic_breakpoints(&self) -> Result<Vec<f64>, ValidationError> {
345 let metrics = self.metrics()?;
346 let mut points = vec![0.0];
347 if metrics.total > 0.0 {
348 for segment in &metrics.segments {
349 if segment.length > 0.0 {
350 let fraction = (segment.start + segment.length) / metrics.total;
351 if fraction > *points.last().unwrap() {
352 points.push(fraction.min(1.0));
353 }
354 }
355 }
356 }
357 if points.last() != Some(&1.0) {
358 points.push(1.0);
359 }
360 Ok(points)
361 }
362
363 pub fn cubic_intervals(&self, fractions: &[f64]) -> Result<Vec<[Point; 4]>, ValidationError> {
365 if fractions.len() < 2
366 || fractions.len() > MAX_PATH_VERBS
367 || fractions
368 .iter()
369 .any(|value| !value.is_finite() || !(0.0..=1.0).contains(value))
370 || fractions.windows(2).any(|pair| pair[0] >= pair[1])
371 {
372 return Err(invalid(
373 "segments",
374 "Cubic interval boundaries must be finite, increasing and bounded",
375 ));
376 }
377 let metrics = self.metrics()?;
378 if metrics.total == 0.0 {
379 return Ok(vec![[self.try_point_at(0.0)?.0; 4]; fractions.len() - 1]);
380 }
381 let mut output = Vec::with_capacity(fractions.len() - 1);
382 for pair in fractions.windows(2) {
383 let middle = ((pair[0] + pair[1]) * 0.5) * metrics.total;
384 let index = metrics
385 .segments
386 .partition_point(|segment| segment.start + segment.length <= middle);
387 let segment = metrics
388 .segments
389 .get(index)
390 .ok_or_else(|| invalid("segments", "Cubic interval has no measurable segment"))?;
391 let start = pair[0] * metrics.total;
392 let end = pair[1] * metrics.total;
393 let slack = metrics.total * 2e-12;
394 if start < segment.start - slack || end > segment.start + segment.length + slack {
395 return Err(invalid(
396 "segments",
397 "Cubic interval crosses an original curve boundary",
398 ));
399 }
400 let curve = subcurve(
401 segment.curve,
402 segment.parameter((start - segment.start).clamp(0.0, segment.length))?,
403 segment.parameter((end - segment.start).clamp(0.0, segment.length))?,
404 );
405 output.push(match curve {
406 PathSeg::Line(line) => {
407 let first = point(line.p0);
408 let last = point(line.p1);
409 [
410 first,
411 first.interpolate(&last, 1.0 / 3.0),
412 first.interpolate(&last, 2.0 / 3.0),
413 last,
414 ]
415 }
416 PathSeg::Quad(quad) => {
417 let cubic = quad.raise();
418 [
419 point(cubic.p0),
420 point(cubic.p1),
421 point(cubic.p2),
422 point(cubic.p3),
423 ]
424 }
425 PathSeg::Cubic(cubic) => [
426 point(cubic.p0),
427 point(cubic.p1),
428 point(cubic.p2),
429 point(cubic.p3),
430 ],
431 });
432 }
433 Ok(output)
434 }
435
436 fn metrics(&self) -> Result<&Metrics, ValidationError> {
437 self.validate()?;
438 self.data
439 .metrics
440 .get_or_init(|| Metrics::new(self).map(Arc::new))
441 .as_ref()
442 .map(Arc::as_ref)
443 .map_err(Clone::clone)
444 }
445
446 pub fn length(&self) -> f64 {
449 self.try_length()
450 .expect("Path arc-length calculation failed")
451 }
452
453 pub fn try_length(&self) -> Result<f64, ValidationError> {
455 Ok(self.metrics()?.total)
456 }
457
458 pub fn point_at(&self, progress: f64) -> (Point, Vector2) {
461 self.try_point_at(progress)
462 .expect("Path arc-length sampling failed")
463 }
464
465 pub fn try_point_at(&self, progress: f64) -> Result<(Point, Vector2), ValidationError> {
468 if !progress.is_finite() {
469 return Err(invalid("progress", "Path progress must be finite"));
470 }
471 let metrics = self.metrics()?;
472 if metrics.total == 0.0 {
473 return Ok((
474 self.verbs()
475 .iter()
476 .find_map(|verb| {
477 if let PathVerb::MoveTo(value) = verb {
478 Some(*value)
479 } else {
480 None
481 }
482 })
483 .unwrap_or(Point::ORIGIN),
484 Vector2::ZERO,
485 ));
486 }
487 let distance = progress.clamp(0.0, 1.0) * metrics.total;
488 let segment = metrics
489 .segments
490 .iter()
491 .find(|segment| segment.length > 0.0 && segment.start + segment.length > distance)
492 .or_else(|| {
493 metrics
494 .segments
495 .iter()
496 .rev()
497 .find(|segment| segment.length > 0.0)
498 })
499 .expect("positive total length");
500 let parameter = segment.parameter((distance - segment.start).clamp(0.0, segment.length))?;
501 let position = point(segment.curve.eval(parameter));
502 let mut direction = derivative(segment.curve, parameter);
503 if direction == Vector2::ZERO {
504 direction = if parameter < 1.0 {
505 point(segment.curve.eval((parameter + 1e-6).min(1.0))) - position
506 } else {
507 position - point(segment.curve.eval((parameter - 1e-6).max(0.0)))
508 };
509 }
510 Ok((position, direction.normalize()))
511 }
512
513 pub fn trim(&self, start: f64, end: f64) -> Self {
517 self.try_trim(start, end).expect("Path trimming failed")
518 }
519
520 pub fn try_trim(&self, start: f64, end: f64) -> Result<Self, ValidationError> {
522 if !start.is_finite() || !end.is_finite() {
523 return Err(invalid("trim", "Trim endpoints must be finite"));
524 }
525 let start = start.clamp(0.0, 1.0);
526 let end = end.clamp(0.0, 1.0);
527 if start >= end {
528 return Ok(Self::new());
529 }
530 if start == 0.0 && end == 1.0 {
531 return Ok(self.clone());
532 }
533 let metrics = self.metrics()?;
534 let mut output = Self::new();
535 metrics.append(
536 start * metrics.total,
537 end * metrics.total,
538 &mut output,
539 false,
540 )?;
541 Ok(output)
542 }
543
544 pub fn dashed(&self, pattern: &[f64], offset: f64) -> Result<Self, ValidationError> {
549 if !offset.is_finite()
550 || pattern
551 .iter()
552 .any(|value| !value.is_finite() || *value <= 0.0)
553 {
554 return Err(invalid(
555 "dash",
556 "Dash lengths must be finite and positive; offset must be finite",
557 ));
558 }
559 if pattern.len() > 256 {
560 return Err(ValidationError::limit(
561 "path2d",
562 "dash",
563 "Dash pattern exceeds 256 entries",
564 ));
565 }
566 if pattern.is_empty() {
567 return Ok(self.clone());
568 }
569 let count = pattern.len()
570 * if pattern.len().is_multiple_of(2) {
571 1
572 } else {
573 2
574 };
575 let cycle = pattern.iter().sum::<f64>() * if count == pattern.len() { 1.0 } else { 2.0 };
576 if !cycle.is_finite() {
577 return Err(invalid("dash", "Dash cycle must remain finite"));
578 }
579 let metrics = self.metrics()?;
580 let mut output = Self::new();
581 let mut first = 0;
582 while first < metrics.segments.len() {
583 let contour = metrics.segments[first].contour;
584 let end_index = first
585 + metrics.segments[first..].partition_point(|segment| segment.contour == contour);
586 let last = &metrics.segments[end_index - 1];
587 let origin = metrics.segments[first].start;
588 let length = last.start + last.length - origin;
589 let mut phase = offset.rem_euclid(cycle);
590 let mut index = 0;
591 while phase >= pattern[index % pattern.len()] {
592 phase -= pattern[index % pattern.len()];
593 index = (index + 1) % count;
594 }
595 let mut remaining = pattern[index % pattern.len()] - phase;
596 let mut position = 0.0;
597 let mut ranges = Vec::new();
598 while position < length {
599 let next = (position + remaining).min(length);
600 if next <= position {
601 return Err(invalid(
602 "dash",
603 "Dash interval is below local floating-point precision",
604 ));
605 }
606 if index % 2 == 0 {
607 if ranges.len() >= MAX_PATH_VERBS / 2 {
608 return Err(ValidationError::limit(
609 "path2d",
610 "dash",
611 "Dash interval allowance exhausted",
612 ));
613 }
614 ranges.push((origin + position, origin + next));
615 }
616 position = next;
617 index = (index + 1) % count;
618 remaining = pattern[index % pattern.len()];
619 }
620 if last.close
621 && ranges.len() > 1
622 && ranges[0].0 == origin
623 && ranges.last().unwrap().1 == origin + length
624 {
625 let tail = ranges.pop().unwrap();
626 metrics.append(tail.0, tail.1, &mut output, false)?;
627 let head = ranges.remove(0);
628 metrics.append(head.0, head.1, &mut output, true)?;
629 }
630 for (start, end) in ranges {
631 metrics.append(start, end, &mut output, false)?;
632 }
633 first = end_index;
634 }
635 Ok(output)
636 }
637}
638
639#[cfg(test)]
640mod tests {
641 use super::*;
642
643 #[test]
644 fn kurbo_subdivide_retains_exact_control_polygons() {
645 let curve = PathSeg::Cubic(kurbo::CubicBez::new(
646 (0.0, 0.0),
647 (2.0, 4.0),
648 (6.0, 4.0),
649 (8.0, 0.0),
650 ));
651 let (left, right) = curve.subdivide();
652 assert_eq!(
653 left,
654 PathSeg::Cubic(kurbo::CubicBez::new(
655 (0.0, 0.0),
656 (1.0, 2.0),
657 (2.5, 3.0),
658 (4.0, 3.0)
659 ))
660 );
661 assert_eq!(
662 right,
663 PathSeg::Cubic(kurbo::CubicBez::new(
664 (4.0, 3.0),
665 (5.5, 3.0),
666 (7.0, 2.0),
667 (8.0, 0.0)
668 ))
669 );
670 }
671
672 #[test]
673 fn kurbo_subsegments_preserve_parameters_and_full_snapshots() {
674 let curves = [
675 PathSeg::Line(kurbo::Line::new((0.0, 0.0), (12.0, 9.0))),
676 PathSeg::Quad(kurbo::QuadBez::new((0.0, 0.0), (4.0, 12.0), (12.0, 9.0))),
677 PathSeg::Cubic(kurbo::CubicBez::new(
678 (0.0, 0.0),
679 (2.0, 14.0),
680 (10.0, -4.0),
681 (12.0, 9.0),
682 )),
683 ];
684 for curve in curves {
685 assert_eq!(subcurve(curve, 0.0, 1.0), curve);
686 for (lower, upper) in [(0.0, 0.5), (0.25, 1.0), (0.13, 0.77)] {
687 let part = subcurve(curve, lower, upper);
688 for parameter in [0.0, 0.125, 0.5, 0.875, 1.0] {
689 let expected = curve.eval(lower + (upper - lower) * parameter);
690 assert!(part.eval(parameter).distance(expected) < 1e-12);
691 }
692 }
693 }
694 let line = Path2D::parse_svg("M0 0L12 9").unwrap();
695 let controls = line.cubic_intervals(&[0.0, 1.0]).unwrap()[0];
696 assert_eq!(
697 controls,
698 [
699 Point::ORIGIN,
700 Point::new(4.0, 3.0),
701 Point::new(8.0, 6.0),
702 Point::new(12.0, 9.0)
703 ]
704 );
705 }
706}