1// Copyright 2022 the Kurbo Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! An implementation of cubic Bézier curve fitting based on a quartic
5//! solver making signed area and moment match the source curve.
6
7use core::ops::Range;
8
9use alloc::vec::Vec;
10
11use arrayvec::ArrayVec;
12
13use crate::{
14 common::{
15 factor_quartic_inner, solve_cubic, solve_itp_fallible, solve_quadratic,
16 GAUSS_LEGENDRE_COEFFS_16,
17 },
18 Affine, BezPath, CubicBez, Line, ParamCurve, ParamCurveArclen, ParamCurveNearest, Point, Vec2,
19};
20
21#[cfg(not(feature = "std"))]
22use crate::common::FloatFuncs;
23
24/// The source curve for curve fitting.
25///
26/// This trait is a general representation of curves to be used as input to a curve
27/// fitting process. It can represent source curves with cusps and corners, though
28/// if the corners are known in advance, it may be better to run curve fitting on
29/// subcurves bounded by the corners.
30///
31/// The trait primarily works by sampling the source curve and computing the position
32/// and derivative at each sample. Those derivatives are then used for multiple
33/// sub-tasks, including ensuring G1 continuity at subdivision points, computing the
34/// area and moment of the curve for curve fitting, and casting rays for evaluation
35/// of a distance metric to test accuracy.
36///
37/// A major motivation is computation of offset curves, which often have cusps, but
38/// the presence and location of those cusps is not generally known. It is also
39/// intended for conversion between curve types (for example, piecewise Euler spiral
40/// or NURBS), and distortion effects such as perspective transform.
41///
42/// Note general similarities to [`ParamCurve`] but also important differences.
43/// Instead of separate [`eval`] and evaluation of derivative, have a single
44/// [`sample_pt_deriv`] method which can be more efficient and also handles cusps more
45/// robustly. Also there is no method for subsegment, as that is not needed and
46/// would be annoying to implement.
47///
48/// [`ParamCurve`]: crate::ParamCurve
49/// [`eval`]: crate::ParamCurve::eval
50/// [`sample_pt_deriv`]: ParamCurveFit::sample_pt_deriv
51pub trait ParamCurveFit {
52 /// Evaluate the curve and its tangent at parameter `t`.
53 ///
54 /// For a regular curve (one not containing a cusp or corner), the
55 /// derivative is a good choice for the tangent vector and the `sign`
56 /// parameter can be ignored. Otherwise, the `sign` parameter selects which
57 /// side of the discontinuity the tangent will be sampled from.
58 ///
59 /// Generally `t` is in the range [0..1].
60 fn sample_pt_tangent(&self, t: f64, sign: f64) -> CurveFitSample;
61
62 /// Evaluate the point and derivative at parameter `t`.
63 ///
64 /// In curves with cusps, the derivative can go to zero.
65 fn sample_pt_deriv(&self, t: f64) -> (Point, Vec2);
66
67 /// Compute moment integrals.
68 ///
69 /// This method computes the integrals of y dx, x y dx, and y^2 dx over the
70 /// length of this curve. From these integrals it is fairly straightforward
71 /// to derive the moments needed for curve fitting.
72 ///
73 /// A default implementation is proved which does quadrature integration
74 /// with Green's theorem, in terms of samples evaluated with
75 /// [`sample_pt_deriv`].
76 ///
77 /// [`sample_pt_deriv`]: ParamCurveFit::sample_pt_deriv
78 fn moment_integrals(&self, range: Range<f64>) -> (f64, f64, f64) {
79 let t0 = 0.5 * (range.start + range.end);
80 let dt = 0.5 * (range.end - range.start);
81 let (a, x, y) = GAUSS_LEGENDRE_COEFFS_16
82 .iter()
83 .map(|(wi, xi)| {
84 let t = t0 + xi * dt;
85 let (p, d) = self.sample_pt_deriv(t);
86 let a = wi * d.x * p.y;
87 let x = p.x * a;
88 let y = p.y * a;
89 (a, x, y)
90 })
91 .fold((0.0, 0.0, 0.0), |(a0, x0, y0), (a1, x1, y1)| {
92 (a0 + a1, x0 + x1, y0 + y1)
93 });
94 (a * dt, x * dt, y * dt)
95 }
96
97 /// Find a cusp or corner within the given range.
98 ///
99 /// If the range contains a corner or cusp, return it. If there is more
100 /// than one such discontinuity, any can be reported, as the function will
101 /// be called repeatedly after subdivision of the range.
102 ///
103 /// Do not report cusps at the endpoints of the range, as this may cause
104 /// potentially infinite subdivision. In particular, when a cusp is reported
105 /// and this method is called on a subdivided range bounded by the reported
106 /// cusp, then the subsequent call should not report a cusp there.
107 ///
108 /// The definition of what exactly constitutes a cusp is somewhat loose.
109 /// If a cusp is missed, then the curve fitting algorithm will attempt to
110 /// fit the curve with a smooth curve, which is generally not a disaster
111 /// will usually result in more subdivision. Conversely, it might be useful
112 /// to report near-cusps, specifically points of curvature maxima where the
113 /// curvature is large but still mathematically finite.
114 fn break_cusp(&self, range: Range<f64>) -> Option<f64>;
115}
116
117/// A sample point of a curve for fitting.
118pub struct CurveFitSample {
119 /// A point on the curve at the sample location.
120 pub p: Point,
121 /// A vector tangent to the curve at the sample location.
122 pub tangent: Vec2,
123}
124
125impl CurveFitSample {
126 /// Intersect a ray orthogonal to the tangent with the given cubic.
127 ///
128 /// Returns a vector of `t` values on the cubic.
129 fn intersect(&self, c: CubicBez) -> ArrayVec<f64, 3> {
130 let p1: Vec2 = 3.0 * (c.p1 - c.p0);
131 let p2: Vec2 = 3.0 * c.p2.to_vec2() - 6.0 * c.p1.to_vec2() + 3.0 * c.p0.to_vec2();
132 let p3: Vec2 = (c.p3 - c.p0) - 3.0 * (c.p2 - c.p1);
133 let c0: f64 = (c.p0 - self.p).dot(self.tangent);
134 let c1: f64 = p1.dot(self.tangent);
135 let c2: f64 = p2.dot(self.tangent);
136 let c3: f64 = p3.dot(self.tangent);
137 solve_cubicimpl Iterator(c0, c1, c2, c3)
138 .into_iter()
139 .filter(|t: &f64| (0.0..=1.0).contains(item:t))
140 .collect()
141 }
142}
143
144/// Generate a Bézier path that fits the source curve.
145///
146/// This function is still experimental and the signature might change; it's possible
147/// it might become a method on the [`ParamCurveFit`] trait.
148///
149/// This function recursively subdivides the curve in half by the parameter when the
150/// accuracy is not met. That gives a reasonably optimized result but not necessarily
151/// the minimum number of segments.
152///
153/// In general, the resulting Bézier path should have a Fréchet distance less than
154/// the provided `accuracy` parameter. However, this is not a rigorous guarantee, as
155/// the error metric is computed approximately.
156///
157/// This function is intended for use when the source curve is piecewise continuous,
158/// with the discontinuities reported by the cusp method. In applications (such as
159/// stroke expansion) where this property may not hold, it is up to the client to
160/// detect and handle such cases. Even so, best effort is made to avoid infinite
161/// subdivision.
162///
163/// When a higher degree of optimization is desired (at considerably more runtime cost),
164/// consider [`fit_to_bezpath_opt`] instead.
165pub fn fit_to_bezpath(source: &impl ParamCurveFit, accuracy: f64) -> BezPath {
166 let mut path: BezPath = BezPath::new();
167 fit_to_bezpath_rec(source, range:0.0..1.0, accuracy, &mut path);
168 path
169}
170
171// Discussion question: possibly should take endpoint samples, to avoid
172// duplication of that work.
173fn fit_to_bezpath_rec(
174 source: &impl ParamCurveFit,
175 range: Range<f64>,
176 accuracy: f64,
177 path: &mut BezPath,
178) {
179 let start = range.start;
180 let end = range.end;
181 let start_p = source.sample_pt_tangent(range.start, 1.0).p;
182 let end_p = source.sample_pt_tangent(range.end, -1.0).p;
183 if start_p.distance_squared(end_p) <= accuracy * accuracy {
184 if let Some((c, _)) = try_fit_line(source, accuracy, range, start_p, end_p) {
185 if path.is_empty() {
186 path.move_to(c.p0);
187 }
188 path.curve_to(c.p1, c.p2, c.p3);
189 return;
190 }
191 }
192 let t = if let Some(t) = source.break_cusp(start..end) {
193 t
194 } else if let Some((c, _)) = fit_to_cubic(source, start..end, accuracy) {
195 if path.is_empty() {
196 path.move_to(c.p0);
197 }
198 path.curve_to(c.p1, c.p2, c.p3);
199 return;
200 } else {
201 // A smarter approach is possible than midpoint subdivision, but would be
202 // a significant increase in complexity.
203 0.5 * (start + end)
204 };
205 if t == start || t == end {
206 // infinite recursion, just draw a line
207 let p1 = start_p.lerp(end_p, 1.0 / 3.0);
208 let p2 = end_p.lerp(start_p, 1.0 / 3.0);
209 if path.is_empty() {
210 path.move_to(start_p);
211 }
212 path.curve_to(p1, p2, end_p);
213 return;
214 }
215 fit_to_bezpath_rec(source, start..t, accuracy, path);
216 fit_to_bezpath_rec(source, t..end, accuracy, path);
217}
218
219const N_SAMPLE: usize = 20;
220
221/// An acceleration structure for estimating curve distance
222struct CurveDist {
223 samples: ArrayVec<CurveFitSample, N_SAMPLE>,
224 arcparams: ArrayVec<f64, N_SAMPLE>,
225 range: Range<f64>,
226 /// A "spicy" curve is one with potentially extreme curvature variation,
227 /// so use arc length measurement for better accuracy.
228 spicy: bool,
229}
230
231impl CurveDist {
232 fn from_curve(source: &impl ParamCurveFit, range: Range<f64>) -> Self {
233 let step = (range.end - range.start) * (1.0 / (N_SAMPLE + 1) as f64);
234 let mut last_tan = None;
235 let mut spicy = false;
236 const SPICY_THRESH: f64 = 0.2;
237 let mut samples = ArrayVec::new();
238 for i in 0..N_SAMPLE + 2 {
239 let sample = source.sample_pt_tangent(range.start + i as f64 * step, 1.0);
240 if let Some(last_tan) = last_tan {
241 let cross = sample.tangent.cross(last_tan);
242 let dot = sample.tangent.dot(last_tan);
243 if cross.abs() > SPICY_THRESH * dot.abs() {
244 spicy = true;
245 }
246 }
247 last_tan = Some(sample.tangent);
248 if i > 0 && i < N_SAMPLE + 1 {
249 samples.push(sample);
250 }
251 }
252 CurveDist {
253 samples,
254 arcparams: Default::default(),
255 range,
256 spicy,
257 }
258 }
259
260 fn compute_arc_params(&mut self, source: &impl ParamCurveFit) {
261 const N_SUBSAMPLE: usize = 10;
262 let (start, end) = (self.range.start, self.range.end);
263 let dt = (end - start) * (1.0 / ((N_SAMPLE + 1) * N_SUBSAMPLE) as f64);
264 let mut arclen = 0.0;
265 for i in 0..N_SAMPLE + 1 {
266 for j in 0..N_SUBSAMPLE {
267 let t = start + dt * ((i * N_SUBSAMPLE + j) as f64 + 0.5);
268 let (_, deriv) = source.sample_pt_deriv(t);
269 arclen += deriv.hypot();
270 }
271 if i < N_SAMPLE {
272 self.arcparams.push(arclen);
273 }
274 }
275 let arclen_inv = arclen.recip();
276 for x in &mut self.arcparams {
277 *x *= arclen_inv;
278 }
279 }
280
281 /// Evaluate distance based on arc length parametrization
282 fn eval_arc(&self, c: CubicBez, acc2: f64) -> Option<f64> {
283 // TODO: this could perhaps be tuned.
284 const EPS: f64 = 1e-9;
285 let c_arclen = c.arclen(EPS);
286 let mut max_err2 = 0.0;
287 for (sample, s) in self.samples.iter().zip(&self.arcparams) {
288 let t = c.inv_arclen(c_arclen * s, EPS);
289 let err = sample.p.distance_squared(c.eval(t));
290 max_err2 = err.max(max_err2);
291 if max_err2 > acc2 {
292 return None;
293 }
294 }
295 Some(max_err2)
296 }
297
298 /// Evaluate distance to a cubic approximation.
299 ///
300 /// If distance exceeds stated accuracy, can return `None`. Note that
301 /// `acc2` is the square of the target.
302 ///
303 /// Returns the square of the error, which is intended to be a good
304 /// approximation of the Fréchet distance.
305 fn eval_ray(&self, c: CubicBez, acc2: f64) -> Option<f64> {
306 let mut max_err2 = 0.0;
307 for sample in &self.samples {
308 let mut best = acc2 + 1.0;
309 for t in sample.intersect(c) {
310 let err = sample.p.distance_squared(c.eval(t));
311 best = best.min(err);
312 }
313 max_err2 = best.max(max_err2);
314 if max_err2 > acc2 {
315 return None;
316 }
317 }
318 Some(max_err2)
319 }
320
321 fn eval_dist(&mut self, source: &impl ParamCurveFit, c: CubicBez, acc2: f64) -> Option<f64> {
322 // Always compute cheaper distance, hoping for early-out.
323 let ray_dist = self.eval_ray(c, acc2)?;
324 if !self.spicy {
325 return Some(ray_dist);
326 }
327 if self.arcparams.is_empty() {
328 self.compute_arc_params(source);
329 }
330 self.eval_arc(c, acc2)
331 }
332}
333
334/// As described in [Simplifying Bézier paths], strictly optimizing for
335/// Fréchet distance can create bumps. The problem is curves with long
336/// control arms (distance from the control point to the corresponding
337/// endpoint). We mitigate that by applying a penalty as a multiplier to
338/// the measured error (approximate Fréchet distance). This is ReLU-like,
339/// with a value of 1.0 below the elbow, and a given slope above it. The
340/// values here have been determined empirically to give good results.
341///
342/// [Simplifying Bézier paths]:
343/// https://raphlinus.github.io/curves/2023/04/18/bezpath-simplify.html
344const D_PENALTY_ELBOW: f64 = 0.65;
345const D_PENALTY_SLOPE: f64 = 2.0;
346
347/// Try fitting a line.
348///
349/// This is especially useful for very short chords, in which the standard
350/// cubic fit is not numerically stable. The tangents are not considered, so
351/// it's useful in cusp and near-cusp situations where the tangents are not
352/// reliable, as well.
353///
354/// Returns the line raised to a cubic and the error, if within tolerance.
355fn try_fit_line(
356 source: &impl ParamCurveFit,
357 accuracy: f64,
358 range: Range<f64>,
359 start: Point,
360 end: Point,
361) -> Option<(CubicBez, f64)> {
362 let acc2: f64 = accuracy * accuracy;
363 let chord_l: Line = Line::new(p0:start, p1:end);
364 const SHORT_N: usize = 7;
365 let mut max_err2: f64 = 0.0;
366 let dt: f64 = (range.end - range.start) / (SHORT_N + 1) as f64;
367 for i: usize in 0..SHORT_N {
368 let t: f64 = range.start + (i + 1) as f64 * dt;
369 let p: Point = source.sample_pt_deriv(t).0;
370 let err2: f64 = chord_l.nearest(p, accuracy).distance_sq;
371 if err2 > acc2 {
372 // Not in tolerance; likely subdivision will help.
373 return None;
374 }
375 max_err2 = err2.max(max_err2);
376 }
377 let p1: Point = start.lerp(other:end, t:1.0 / 3.0);
378 let p2: Point = end.lerp(other:start, t:1.0 / 3.0);
379 let c: CubicBez = CubicBez::new(p0:start, p1, p2, p3:end);
380 Some((c, max_err2))
381}
382
383/// Fit a single cubic to a range of the source curve.
384///
385/// Returns the cubic segment and the square of the error.
386/// Discussion question: should this be a method on the trait instead?
387pub fn fit_to_cubic(
388 source: &impl ParamCurveFit,
389 range: Range<f64>,
390 accuracy: f64,
391) -> Option<(CubicBez, f64)> {
392 let start = source.sample_pt_tangent(range.start, 1.0);
393 let end = source.sample_pt_tangent(range.end, -1.0);
394 let d = end.p - start.p;
395 let chord2 = d.hypot2();
396 let acc2 = accuracy * accuracy;
397 if chord2 <= acc2 {
398 // Special case very short chords; try to fit a line.
399 return try_fit_line(source, accuracy, range, start.p, end.p);
400 }
401 let th = d.atan2();
402 fn mod_2pi(th: f64) -> f64 {
403 let th_scaled = th * core::f64::consts::FRAC_1_PI * 0.5;
404 core::f64::consts::PI * 2.0 * (th_scaled - th_scaled.round())
405 }
406 let th0 = mod_2pi(start.tangent.atan2() - th);
407 let th1 = mod_2pi(th - end.tangent.atan2());
408
409 let (mut area, mut x, mut y) = source.moment_integrals(range.clone());
410 let (x0, y0) = (start.p.x, start.p.y);
411 let (dx, dy) = (d.x, d.y);
412 // Subtract off area of chord
413 area -= dx * (y0 + 0.5 * dy);
414 // `area` is signed area of closed curve segment.
415 // This quantity is invariant to translation and rotation.
416
417 // Subtract off moment of chord
418 let dy_3 = dy * (1. / 3.);
419 x -= dx * (x0 * y0 + 0.5 * (x0 * dy + y0 * dx) + dy_3 * dx);
420 y -= dx * (y0 * y0 + y0 * dy + dy_3 * dy);
421 // Translate start point to origin; convert raw integrals to moments.
422 x -= x0 * area;
423 y = 0.5 * y - y0 * area;
424 // Rotate into place (this also scales up by chordlength for efficiency).
425 let moment = d.x * x + d.y * y;
426 // `moment` is the chordlength times the x moment of the curve translated
427 // so its start point is on the origin, and rotated so its end point is on the
428 // x axis.
429
430 let chord2_inv = chord2.recip();
431 let unit_area = area * chord2_inv;
432 let mx = moment * chord2_inv.powi(2);
433 // `unit_area` is signed area scaled to unit chord; `mx` is scaled x moment
434
435 let chord = chord2.sqrt();
436 let aff = Affine::translate(start.p.to_vec2()) * Affine::rotate(th) * Affine::scale(chord);
437 let mut curve_dist = CurveDist::from_curve(source, range);
438 let mut best_c = None;
439 let mut best_err2 = None;
440 for (cand, d0, d1) in cubic_fit(th0, th1, unit_area, mx) {
441 let c = aff * cand;
442 if let Some(err2) = curve_dist.eval_dist(source, c, acc2) {
443 fn scale_f(d: f64) -> f64 {
444 1.0 + (d - D_PENALTY_ELBOW).max(0.0) * D_PENALTY_SLOPE
445 }
446 let scale = scale_f(d0).max(scale_f(d1)).powi(2);
447 let err2 = err2 * scale;
448 if err2 < acc2 && best_err2.map(|best| err2 < best).unwrap_or(true) {
449 best_c = Some(c);
450 best_err2 = Some(err2);
451 }
452 }
453 }
454 match (best_c, best_err2) {
455 (Some(c), Some(err2)) => Some((c, err2)),
456 _ => None,
457 }
458}
459
460/// Returns curves matching area and moment, given unit chord.
461fn cubic_fit(th0: f64, th1: f64, area: f64, mx: f64) -> ArrayVec<(CubicBez, f64, f64), 4> {
462 // Note: maybe we want to take unit vectors instead of angle? Shouldn't
463 // matter much either way though.
464 let (s0, c0) = th0.sin_cos();
465 let (s1, c1) = th1.sin_cos();
466 let a4 = -9.
467 * c0
468 * (((2. * s1 * c1 * c0 + s0 * (2. * c1 * c1 - 1.)) * c0 - 2. * s1 * c1) * c0
469 - c1 * c1 * s0);
470 let a3 = 12.
471 * ((((c1 * (30. * area * c1 - s1) - 15. * area) * c0 + 2. * s0
472 - c1 * s0 * (c1 + 30. * area * s1))
473 * c0
474 + c1 * (s1 - 15. * area * c1))
475 * c0
476 - s0 * c1 * c1);
477 let a2 = 12.
478 * ((((70. * mx + 15. * area) * s1 * s1 + c1 * (9. * s1 - 70. * c1 * mx - 5. * c1 * area))
479 * c0
480 - 5. * s0 * s1 * (3. * s1 - 4. * c1 * (7. * mx + area)))
481 * c0
482 - c1 * (9. * s1 - 70. * c1 * mx - 5. * c1 * area));
483 let a1 = 16.
484 * (((12. * s0 - 5. * c0 * (42. * mx - 17. * area)) * s1
485 - 70. * c1 * (3. * mx - area) * s0
486 - 75. * c0 * c1 * area * area)
487 * s1
488 - 75. * c1 * c1 * area * area * s0);
489 let a0 = 80. * s1 * (42. * s1 * mx - 25. * area * (s1 - c1 * area));
490 // TODO: "roots" is not a good name for this variable, as it also contains
491 // the real part of complex conjugate pairs.
492 let mut roots = ArrayVec::<f64, 4>::new();
493 const EPS: f64 = 1e-12;
494 if a4.abs() > EPS {
495 let a = a3 / a4;
496 let b = a2 / a4;
497 let c = a1 / a4;
498 let d = a0 / a4;
499 if let Some(quads) = factor_quartic_inner(a, b, c, d, false) {
500 for (qc1, qc0) in quads {
501 let qroots = solve_quadratic(qc0, qc1, 1.0);
502 if qroots.is_empty() {
503 // Real part of pair of complex roots
504 roots.push(-0.5 * qc1);
505 } else {
506 roots.extend(qroots);
507 }
508 }
509 }
510 } else if a3.abs() > EPS {
511 roots.extend(solve_cubic(a0, a1, a2, a3));
512 } else {
513 roots.extend(solve_quadratic(a0, a1, a2));
514 }
515
516 let s01 = s0 * c1 + s1 * c0;
517 roots
518 .iter()
519 .filter_map(|&d0| {
520 let (d0, d1) = if d0 > 0.0 {
521 let d1 = (d0 * s0 - area * (10. / 3.)) / (0.5 * d0 * s01 - s1);
522 if d1 > 0.0 {
523 (d0, d1)
524 } else {
525 (s1 / s01, 0.0)
526 }
527 } else {
528 (0.0, s0 / s01)
529 };
530 // We could implement a maximum d value here.
531 if d0 >= 0.0 && d1 >= 0.0 {
532 Some((
533 CubicBez::new(
534 (0.0, 0.0),
535 (d0 * c0, d0 * s0),
536 (1.0 - d1 * c1, d1 * s1),
537 (1.0, 0.0),
538 ),
539 d0,
540 d1,
541 ))
542 } else {
543 None
544 }
545 })
546 .collect()
547}
548
549/// Generate a highly optimized Bézier path that fits the source curve.
550///
551/// This function is still experimental and the signature might change; it's possible
552/// it might become a method on the [`ParamCurveFit`] trait.
553///
554/// This function is considerably slower than [`fit_to_bezpath`], as it computes
555/// optimal subdivision points. Its result is expected to be very close to the optimum
556/// possible Bézier path for the source curve, in that it has a minimal number of curve
557/// segments, and a minimal error over all paths with that number of segments.
558///
559/// See [`fit_to_bezpath`] for an explanation of the `accuracy` parameter.
560pub fn fit_to_bezpath_opt(source: &impl ParamCurveFit, accuracy: f64) -> BezPath {
561 let mut cusps: Vec = Vec::new();
562 let mut path: BezPath = BezPath::new();
563 let mut t0: f64 = 0.0;
564 loop {
565 let t1: f64 = cusps.last().copied().unwrap_or(default:1.0);
566 match fit_to_bezpath_opt_inner(source, accuracy, range:t0..t1, &mut path) {
567 Some(t: f64) => cusps.push(t),
568 None => match cusps.pop() {
569 Some(t: f64) => t0 = t,
570 None => break,
571 },
572 }
573 }
574 path
575}
576
577/// Fit a range without cusps.
578///
579/// On Ok return, range has been added to the path. On Err return, report a cusp (and don't
580/// mutate path).
581fn fit_to_bezpath_opt_inner(
582 source: &impl ParamCurveFit,
583 accuracy: f64,
584 range: Range<f64>,
585 path: &mut BezPath,
586) -> Option<f64> {
587 if let Some(t) = source.break_cusp(range.clone()) {
588 return Some(t);
589 }
590 let err;
591 if let Some((c, err2)) = fit_to_cubic(source, range.clone(), accuracy) {
592 err = err2.sqrt();
593 if err < accuracy {
594 if range.start == 0.0 {
595 path.move_to(c.p0);
596 }
597 path.curve_to(c.p1, c.p2, c.p3);
598 return None;
599 }
600 } else {
601 err = 2.0 * accuracy;
602 }
603 let (mut t0, t1) = (range.start, range.end);
604 let mut n = 0;
605 let last_err;
606 loop {
607 n += 1;
608 match fit_opt_segment(source, accuracy, t0..t1) {
609 FitResult::ParamVal(t) => t0 = t,
610 FitResult::SegmentError(err) => {
611 last_err = err;
612 break;
613 }
614 FitResult::CuspFound(t) => return Some(t),
615 }
616 }
617 t0 = range.start;
618 const EPS: f64 = 1e-9;
619 let f = |x| fit_opt_err_delta(source, x, accuracy, t0..t1, n);
620 let k1 = 0.2 / accuracy;
621 let ya = -err;
622 let yb = accuracy - last_err;
623 let (_, x) = match solve_itp_fallible(f, 0.0, accuracy, EPS, 1, k1, ya, yb) {
624 Ok(x) => x,
625 Err(t) => return Some(t),
626 };
627 //println!("got fit with n={}, err={}", n, x);
628 let path_len = path.elements().len();
629 for i in 0..n {
630 let t1 = if i < n - 1 {
631 match fit_opt_segment(source, x, t0..range.end) {
632 FitResult::ParamVal(t1) => t1,
633 FitResult::SegmentError(_) => range.end,
634 FitResult::CuspFound(t) => {
635 path.truncate(path_len);
636 return Some(t);
637 }
638 }
639 } else {
640 range.end
641 };
642 let (c, _) = fit_to_cubic(source, t0..t1, accuracy).unwrap();
643 if i == 0 && range.start == 0.0 {
644 path.move_to(c.p0);
645 }
646 path.curve_to(c.p1, c.p2, c.p3);
647 t0 = t1;
648 if t0 == range.end {
649 // This is unlikely but could happen when not monotonic.
650 break;
651 }
652 }
653 None
654}
655
656fn measure_one_seg(source: &impl ParamCurveFit, range: Range<f64>, limit: f64) -> Option<f64> {
657 fit_to_cubic(source, range, accuracy:limit).map(|(_, err2: f64)| err2.sqrt())
658}
659
660enum FitResult {
661 /// The parameter (`t`) value that meets the desired accuracy.
662 ParamVal(f64),
663 /// Error of the measured segment.
664 SegmentError(f64),
665 /// The parameter value where a cusp was found.
666 CuspFound(f64),
667}
668
669fn fit_opt_segment(source: &impl ParamCurveFit, accuracy: f64, range: Range<f64>) -> FitResult {
670 if let Some(t: f64) = source.break_cusp(range.clone()) {
671 return FitResult::CuspFound(t);
672 }
673 let missing_err: f64 = accuracy * 2.0;
674 let err: f64 = measure_one_seg(source, range.clone(), accuracy).unwrap_or(default:missing_err);
675 if err <= accuracy {
676 return FitResult::SegmentError(err);
677 }
678 let (t0: f64, t1: f64) = (range.start, range.end);
679 let f: impl Fn(f64) -> Result = |x: f64| {
680 if let Some(t: f64) = source.break_cusp(range.clone()) {
681 return Err(t);
682 }
683 let err: f64 = measure_one_seg(source, t0..x, accuracy).unwrap_or(default:missing_err);
684 Ok(err - accuracy)
685 };
686 const EPS: f64 = 1e-9;
687 let k1: f64 = 2.0 / (t1 - t0);
688 match solve_itp_fallible(f, a:t0, b:t1, EPS, n0:1, k1, -accuracy, yb:err - accuracy) {
689 Ok((t1: f64, _)) => FitResult::ParamVal(t1),
690 Err(t: f64) => FitResult::CuspFound(t),
691 }
692}
693
694// Ok result is delta error (accuracy - error of last seg).
695// Err result is a cusp.
696fn fit_opt_err_delta(
697 source: &impl ParamCurveFit,
698 accuracy: f64,
699 limit: f64,
700 range: Range<f64>,
701 n: usize,
702) -> Result<f64, f64> {
703 let (mut t0: f64, t1: f64) = (range.start, range.end);
704 for _ in 0..n - 1 {
705 t0 = match fit_opt_segment(source, accuracy, range:t0..t1) {
706 FitResult::ParamVal(t0: f64) => t0,
707 // In this case, n - 1 will work, which of course means the error is highly
708 // non-monotonic. We should probably harvest that solution.
709 FitResult::SegmentError(_) => return Ok(accuracy),
710 FitResult::CuspFound(t: f64) => return Err(t),
711 }
712 }
713 let err: f64 = measure_one_seg(source, t0..t1, limit).unwrap_or(default:accuracy * 2.0);
714 Ok(accuracy - err)
715}
716