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 let mut last = 0.0;
348 if metrics.total > 0.0 {
349 for segment in &metrics.segments {
350 if segment.length > 0.0 {
351 let fraction = (segment.start + segment.length) / metrics.total;
352 if fraction > last {
353 last = fraction.min(1.0);
354 points.push(last);
355 }
356 }
357 }
358 }
359 if points.last() != Some(&1.0) {
360 points.push(1.0);
361 }
362 Ok(points)
363 }
364
365 pub fn cubic_intervals(&self, fractions: &[f64]) -> Result<Vec<[Point; 4]>, ValidationError> {
367 if fractions.len() < 2
368 || fractions.len() > MAX_PATH_VERBS
369 || fractions
370 .iter()
371 .any(|value| !value.is_finite() || !(0.0..=1.0).contains(value))
372 || fractions.windows(2).any(|pair| pair[0] >= pair[1])
373 {
374 return Err(invalid(
375 "segments",
376 "Cubic interval boundaries must be finite, increasing and bounded",
377 ));
378 }
379 let metrics = self.metrics()?;
380 if metrics.total == 0.0 {
381 return Ok(vec![[self.try_point_at(0.0)?.0; 4]; fractions.len() - 1]);
382 }
383 let mut output = Vec::with_capacity(fractions.len() - 1);
384 for pair in fractions.windows(2) {
385 let middle = ((pair[0] + pair[1]) * 0.5) * metrics.total;
386 let index = metrics
387 .segments
388 .partition_point(|segment| segment.start + segment.length <= middle);
389 let segment = metrics
390 .segments
391 .get(index)
392 .ok_or_else(|| invalid("segments", "Cubic interval has no measurable segment"))?;
393 let start = pair[0] * metrics.total;
394 let end = pair[1] * metrics.total;
395 let slack = metrics.total * 2e-12;
396 if start < segment.start - slack || end > segment.start + segment.length + slack {
397 return Err(invalid(
398 "segments",
399 "Cubic interval crosses an original curve boundary",
400 ));
401 }
402 let curve = subcurve(
403 segment.curve,
404 segment.parameter((start - segment.start).clamp(0.0, segment.length))?,
405 segment.parameter((end - segment.start).clamp(0.0, segment.length))?,
406 );
407 output.push(match curve {
408 PathSeg::Line(line) => {
409 let first = point(line.p0);
410 let last = point(line.p1);
411 [
412 first,
413 first.interpolate(&last, 1.0 / 3.0),
414 first.interpolate(&last, 2.0 / 3.0),
415 last,
416 ]
417 }
418 PathSeg::Quad(quad) => {
419 let cubic = quad.raise();
420 [
421 point(cubic.p0),
422 point(cubic.p1),
423 point(cubic.p2),
424 point(cubic.p3),
425 ]
426 }
427 PathSeg::Cubic(cubic) => [
428 point(cubic.p0),
429 point(cubic.p1),
430 point(cubic.p2),
431 point(cubic.p3),
432 ],
433 });
434 }
435 Ok(output)
436 }
437
438 fn metrics(&self) -> Result<&Metrics, ValidationError> {
439 self.data
440 .metrics
441 .get_or_init(|| Metrics::new(self).map(Arc::new))
442 .as_ref()
443 .map(Arc::as_ref)
444 .map_err(Clone::clone)
445 }
446
447 pub fn length(&self) -> f64 {
450 self.try_length()
451 .expect("Path arc-length calculation failed")
452 }
453
454 pub fn try_length(&self) -> Result<f64, ValidationError> {
456 Ok(self.metrics()?.total)
457 }
458
459 pub fn point_at(&self, progress: f64) -> (Point, Vector2) {
462 self.try_point_at(progress)
463 .expect("Path arc-length sampling failed")
464 }
465
466 pub fn try_point_at(&self, progress: f64) -> Result<(Point, Vector2), ValidationError> {
469 if !progress.is_finite() {
470 return Err(invalid("progress", "Path progress must be finite"));
471 }
472 let metrics = self.metrics()?;
473 if metrics.total == 0.0 {
474 return Ok((
475 self.verbs()
476 .iter()
477 .find_map(|verb| {
478 if let PathVerb::MoveTo(value) = verb {
479 Some(*value)
480 } else {
481 None
482 }
483 })
484 .unwrap_or(Point::ORIGIN),
485 Vector2::ZERO,
486 ));
487 }
488 let distance = progress.clamp(0.0, 1.0) * metrics.total;
489 let segment = metrics
490 .segments
491 .iter()
492 .find(|segment| segment.length > 0.0 && segment.start + segment.length > distance)
493 .or_else(|| {
494 metrics
495 .segments
496 .iter()
497 .rev()
498 .find(|segment| segment.length > 0.0)
499 })
500 .expect("positive total length");
501 let parameter = segment.parameter((distance - segment.start).clamp(0.0, segment.length))?;
502 let position = point(segment.curve.eval(parameter));
503 let mut direction = derivative(segment.curve, parameter);
504 if direction == Vector2::ZERO {
505 direction = if parameter < 1.0 {
506 point(segment.curve.eval((parameter + 1e-6).min(1.0))) - position
507 } else {
508 position - point(segment.curve.eval((parameter - 1e-6).max(0.0)))
509 };
510 }
511 Ok((position, direction.normalize()))
512 }
513
514 pub fn trim(&self, start: f64, end: f64) -> Self {
518 self.try_trim(start, end).expect("Path trimming failed")
519 }
520
521 pub fn try_trim(&self, start: f64, end: f64) -> Result<Self, ValidationError> {
523 if !start.is_finite() || !end.is_finite() {
524 return Err(invalid("trim", "Trim endpoints must be finite"));
525 }
526 let start = start.clamp(0.0, 1.0);
527 let end = end.clamp(0.0, 1.0);
528 if start >= end {
529 return Ok(Self::new());
530 }
531 if start == 0.0 && end == 1.0 {
532 return Ok(self.clone());
533 }
534 let metrics = self.metrics()?;
535 let mut output = Self::new();
536 metrics.append(
537 start * metrics.total,
538 end * metrics.total,
539 &mut output,
540 false,
541 )?;
542 Ok(output)
543 }
544
545 pub fn dashed(&self, pattern: &[f64], offset: f64) -> Result<Self, ValidationError> {
550 if !offset.is_finite()
551 || pattern
552 .iter()
553 .any(|value| !value.is_finite() || *value <= 0.0)
554 {
555 return Err(invalid(
556 "dash",
557 "Dash lengths must be finite and positive; offset must be finite",
558 ));
559 }
560 if pattern.len() > 256 {
561 return Err(ValidationError::limit(
562 "path2d",
563 "dash",
564 "Dash pattern exceeds 256 entries",
565 ));
566 }
567 if pattern.is_empty() {
568 return Ok(self.clone());
569 }
570 let count = pattern.len()
571 * if pattern.len().is_multiple_of(2) {
572 1
573 } else {
574 2
575 };
576 let cycle = pattern.iter().sum::<f64>() * if count == pattern.len() { 1.0 } else { 2.0 };
577 if !cycle.is_finite() {
578 return Err(invalid("dash", "Dash cycle must remain finite"));
579 }
580 let metrics = self.metrics()?;
581 let mut output = Self::new();
582 let mut first = 0;
583 while first < metrics.segments.len() {
584 let contour = metrics.segments[first].contour;
585 let end_index = first
586 + metrics.segments[first..].partition_point(|segment| segment.contour == contour);
587 let last = &metrics.segments[end_index - 1];
588 let origin = metrics.segments[first].start;
589 let length = last.start + last.length - origin;
590 let mut phase = offset.rem_euclid(cycle);
591 let mut index = 0;
592 while phase >= pattern[index % pattern.len()] {
593 phase -= pattern[index % pattern.len()];
594 index = (index + 1) % count;
595 }
596 let mut remaining = pattern[index % pattern.len()] - phase;
597 let mut position = 0.0;
598 let mut ranges = Vec::new();
599 while position < length {
600 let next = (position + remaining).min(length);
601 if next <= position {
602 return Err(invalid(
603 "dash",
604 "Dash interval is below local floating-point precision",
605 ));
606 }
607 if index % 2 == 0 {
608 if ranges.len() >= MAX_PATH_VERBS / 2 {
609 return Err(ValidationError::limit(
610 "path2d",
611 "dash",
612 "Dash interval allowance exhausted",
613 ));
614 }
615 ranges.push((origin + position, origin + next));
616 }
617 position = next;
618 index = (index + 1) % count;
619 remaining = pattern[index % pattern.len()];
620 }
621 if last.close
622 && ranges.len() > 1
623 && ranges[0].0 == origin
624 && let Some(tail) = ranges.pop_if(|tail| tail.1 == origin + length)
625 {
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}