Coverage Report

Created: 2026-02-14 06:54

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/kurbo-0.13.0/src/fit.rs
Line
Count
Source
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
7
use core::ops::Range;
8
9
use alloc::vec::Vec;
10
11
use arrayvec::ArrayVec;
12
13
use 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"))]
22
use 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
51
pub 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 provided 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
0
    fn moment_integrals(&self, range: Range<f64>) -> (f64, f64, f64) {
79
0
        let t0 = 0.5 * (range.start + range.end);
80
0
        let dt = 0.5 * (range.end - range.start);
81
0
        let (a, x, y) = GAUSS_LEGENDRE_COEFFS_16
82
0
            .iter()
83
0
            .map(|(wi, xi)| {
84
0
                let t = t0 + xi * dt;
85
0
                let (p, d) = self.sample_pt_deriv(t);
86
0
                let a = wi * d.x * p.y;
87
0
                let x = p.x * a;
88
0
                let y = p.y * a;
89
0
                (a, x, y)
90
0
            })
91
0
            .fold((0.0, 0.0, 0.0), |(a0, x0, y0), (a1, x1, y1)| {
92
0
                (a0 + a1, x0 + x1, y0 + y1)
93
0
            });
94
0
        (a * dt, x * dt, y * dt)
95
0
    }
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.
118
pub 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
125
impl 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
0
    fn intersect(&self, c: CubicBez) -> ArrayVec<f64, 3> {
130
0
        let p1 = 3.0 * (c.p1 - c.p0);
131
0
        let p2 = 3.0 * c.p2.to_vec2() - 6.0 * c.p1.to_vec2() + 3.0 * c.p0.to_vec2();
132
0
        let p3 = (c.p3 - c.p0) - 3.0 * (c.p2 - c.p1);
133
0
        let c0 = (c.p0 - self.p).dot(self.tangent);
134
0
        let c1 = p1.dot(self.tangent);
135
0
        let c2 = p2.dot(self.tangent);
136
0
        let c3 = p3.dot(self.tangent);
137
0
        solve_cubic(c0, c1, c2, c3)
138
0
            .into_iter()
139
0
            .filter(|t| (0.0..=1.0).contains(t))
140
0
            .collect()
141
0
    }
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.
165
0
pub fn fit_to_bezpath(source: &impl ParamCurveFit, accuracy: f64) -> BezPath {
166
0
    let mut path = BezPath::new();
167
0
    fit_to_bezpath_rec(source, 0.0..1.0, accuracy, &mut path);
168
0
    path
169
0
}
170
171
// Discussion question: possibly should take endpoint samples, to avoid
172
// duplication of that work.
173
0
fn fit_to_bezpath_rec(
174
0
    source: &impl ParamCurveFit,
175
0
    range: Range<f64>,
176
0
    accuracy: f64,
177
0
    path: &mut BezPath,
178
0
) {
179
0
    let start = range.start;
180
0
    let end = range.end;
181
0
    let start_p = source.sample_pt_tangent(range.start, 1.0).p;
182
0
    let end_p = source.sample_pt_tangent(range.end, -1.0).p;
183
0
    if start_p.distance_squared(end_p) <= accuracy * accuracy {
184
0
        if let Some((c, _)) = try_fit_line(source, accuracy, range, start_p, end_p) {
185
0
            if path.is_empty() {
186
0
                path.move_to(c.p0);
187
0
            }
188
0
            path.curve_to(c.p1, c.p2, c.p3);
189
0
            return;
190
0
        }
191
0
    }
192
0
    let t = if let Some(t) = source.break_cusp(start..end) {
193
0
        t
194
0
    } else if let Some((c, _)) = fit_to_cubic(source, start..end, accuracy) {
195
0
        if path.is_empty() {
196
0
            path.move_to(c.p0);
197
0
        }
198
0
        path.curve_to(c.p1, c.p2, c.p3);
199
0
        return;
200
    } else {
201
        // A smarter approach is possible than midpoint subdivision, but would be
202
        // a significant increase in complexity.
203
0
        0.5 * (start + end)
204
    };
205
0
    if t == start || t == end {
206
        // infinite recursion, just draw a line
207
0
        let p1 = start_p.lerp(end_p, 1.0 / 3.0);
208
0
        let p2 = end_p.lerp(start_p, 1.0 / 3.0);
209
0
        if path.is_empty() {
210
0
            path.move_to(start_p);
211
0
        }
212
0
        path.curve_to(p1, p2, end_p);
213
0
        return;
214
0
    }
215
0
    fit_to_bezpath_rec(source, start..t, accuracy, path);
216
0
    fit_to_bezpath_rec(source, t..end, accuracy, path);
217
0
}
218
219
const N_SAMPLE: usize = 20;
220
221
/// An acceleration structure for estimating curve distance
222
struct 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
231
impl CurveDist {
232
0
    fn from_curve(source: &impl ParamCurveFit, range: Range<f64>) -> Self {
233
0
        let step = (range.end - range.start) * (1.0 / (N_SAMPLE + 1) as f64);
234
0
        let mut last_tan = None;
235
0
        let mut spicy = false;
236
        const SPICY_THRESH: f64 = 0.2;
237
0
        let mut samples = ArrayVec::new();
238
0
        for i in 0..N_SAMPLE + 2 {
239
0
            let sample = source.sample_pt_tangent(range.start + i as f64 * step, 1.0);
240
0
            if let Some(last_tan) = last_tan {
241
0
                let cross = sample.tangent.cross(last_tan);
242
0
                let dot = sample.tangent.dot(last_tan);
243
0
                if cross.abs() > SPICY_THRESH * dot.abs() {
244
0
                    spicy = true;
245
0
                }
246
0
            }
247
0
            last_tan = Some(sample.tangent);
248
0
            if i > 0 && i < N_SAMPLE + 1 {
249
0
                samples.push(sample);
250
0
            }
251
        }
252
0
        CurveDist {
253
0
            samples,
254
0
            arcparams: ArrayVec::default(),
255
0
            range,
256
0
            spicy,
257
0
        }
258
0
    }
259
260
0
    fn compute_arc_params(&mut self, source: &impl ParamCurveFit) {
261
        const N_SUBSAMPLE: usize = 10;
262
0
        let (start, end) = (self.range.start, self.range.end);
263
0
        let dt = (end - start) * (1.0 / ((N_SAMPLE + 1) * N_SUBSAMPLE) as f64);
264
0
        let mut arclen = 0.0;
265
0
        for i in 0..N_SAMPLE + 1 {
266
0
            for j in 0..N_SUBSAMPLE {
267
0
                let t = start + dt * ((i * N_SUBSAMPLE + j) as f64 + 0.5);
268
0
                let (_, deriv) = source.sample_pt_deriv(t);
269
0
                arclen += deriv.hypot();
270
0
            }
271
0
            if i < N_SAMPLE {
272
0
                self.arcparams.push(arclen);
273
0
            }
274
        }
275
0
        let arclen_inv = arclen.recip();
276
0
        for x in &mut self.arcparams {
277
0
            *x *= arclen_inv;
278
0
        }
279
0
    }
280
281
    /// Evaluate distance based on arc length parametrization
282
0
    fn eval_arc(&self, c: CubicBez, acc2: f64) -> Option<f64> {
283
        // TODO: this could perhaps be tuned.
284
        const EPS: f64 = 1e-9;
285
0
        let c_arclen = c.arclen(EPS);
286
0
        let mut max_err2 = 0.0;
287
0
        for (sample, s) in self.samples.iter().zip(&self.arcparams) {
288
0
            let t = c.inv_arclen(c_arclen * s, EPS);
289
0
            let err = sample.p.distance_squared(c.eval(t));
290
0
            max_err2 = err.max(max_err2);
291
0
            if max_err2 > acc2 {
292
0
                return None;
293
0
            }
294
        }
295
0
        Some(max_err2)
296
0
    }
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
0
    fn eval_ray(&self, c: CubicBez, acc2: f64) -> Option<f64> {
306
0
        let mut max_err2 = 0.0;
307
0
        for sample in &self.samples {
308
0
            let mut best = acc2 + 1.0;
309
0
            for t in sample.intersect(c) {
310
0
                let err = sample.p.distance_squared(c.eval(t));
311
0
                best = best.min(err);
312
0
            }
313
0
            max_err2 = best.max(max_err2);
314
0
            if max_err2 > acc2 {
315
0
                return None;
316
0
            }
317
        }
318
0
        Some(max_err2)
319
0
    }
320
321
0
    fn eval_dist(&mut self, source: &impl ParamCurveFit, c: CubicBez, acc2: f64) -> Option<f64> {
322
        // Always compute cheaper distance, hoping for early-out.
323
0
        let ray_dist = self.eval_ray(c, acc2)?;
324
0
        if !self.spicy {
325
0
            return Some(ray_dist);
326
0
        }
327
0
        if self.arcparams.is_empty() {
328
0
            self.compute_arc_params(source);
329
0
        }
330
0
        self.eval_arc(c, acc2)
331
0
    }
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
344
const D_PENALTY_ELBOW: f64 = 0.65;
345
const 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.
355
0
fn try_fit_line(
356
0
    source: &impl ParamCurveFit,
357
0
    accuracy: f64,
358
0
    range: Range<f64>,
359
0
    start: Point,
360
0
    end: Point,
361
0
) -> Option<(CubicBez, f64)> {
362
0
    let acc2 = accuracy * accuracy;
363
0
    let chord_l = Line::new(start, end);
364
    const SHORT_N: usize = 7;
365
0
    let mut max_err2 = 0.0;
366
0
    let dt = (range.end - range.start) / (SHORT_N + 1) as f64;
367
0
    for i in 0..SHORT_N {
368
0
        let t = range.start + (i + 1) as f64 * dt;
369
0
        let p = source.sample_pt_deriv(t).0;
370
0
        let err2 = chord_l.nearest(p, accuracy).distance_sq;
371
0
        if err2 > acc2 {
372
            // Not in tolerance; likely subdivision will help.
373
0
            return None;
374
0
        }
375
0
        max_err2 = err2.max(max_err2);
376
    }
377
0
    let p1 = start.lerp(end, 1.0 / 3.0);
378
0
    let p2 = end.lerp(start, 1.0 / 3.0);
379
0
    let c = CubicBez::new(start, p1, p2, end);
380
0
    Some((c, max_err2))
381
0
}
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?
387
0
pub fn fit_to_cubic(
388
0
    source: &impl ParamCurveFit,
389
0
    range: Range<f64>,
390
0
    accuracy: f64,
391
0
) -> Option<(CubicBez, f64)> {
392
0
    let start = source.sample_pt_tangent(range.start, 1.0);
393
0
    let end = source.sample_pt_tangent(range.end, -1.0);
394
0
    let d = end.p - start.p;
395
0
    let chord2 = d.hypot2();
396
0
    let acc2 = accuracy * accuracy;
397
0
    if chord2 <= acc2 {
398
        // Special case very short chords; try to fit a line.
399
0
        return try_fit_line(source, accuracy, range, start.p, end.p);
400
0
    }
401
0
    let th = d.atan2();
402
0
    fn mod_2pi(th: f64) -> f64 {
403
0
        let th_scaled = th * core::f64::consts::FRAC_1_PI * 0.5;
404
0
        core::f64::consts::PI * 2.0 * (th_scaled - th_scaled.round())
405
0
    }
406
0
    let th0 = mod_2pi(start.tangent.atan2() - th);
407
0
    let th1 = mod_2pi(th - end.tangent.atan2());
408
409
0
    let (mut area, mut x, mut y) = source.moment_integrals(range.clone());
410
0
    let (x0, y0) = (start.p.x, start.p.y);
411
0
    let (dx, dy) = (d.x, d.y);
412
    // Subtract off area of chord
413
0
    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
0
    let dy_3 = dy * (1. / 3.);
419
0
    x -= dx * (x0 * y0 + 0.5 * (x0 * dy + y0 * dx) + dy_3 * dx);
420
0
    y -= dx * (y0 * y0 + y0 * dy + dy_3 * dy);
421
    // Translate start point to origin; convert raw integrals to moments.
422
0
    x -= x0 * area;
423
0
    y = 0.5 * y - y0 * area;
424
    // Rotate into place (this also scales up by chordlength for efficiency).
425
0
    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
0
    let chord2_inv = chord2.recip();
431
0
    let unit_area = area * chord2_inv;
432
0
    let mx = moment * chord2_inv.powi(2);
433
    // `unit_area` is signed area scaled to unit chord; `mx` is scaled x moment
434
435
0
    let chord = chord2.sqrt();
436
0
    let aff = Affine::translate(start.p.to_vec2()) * Affine::rotate(th) * Affine::scale(chord);
437
0
    let mut curve_dist = CurveDist::from_curve(source, range);
438
0
    let mut best_c = None;
439
0
    let mut best_err2 = None;
440
0
    for (cand, d0, d1) in cubic_fit(th0, th1, unit_area, mx) {
441
0
        let c = aff * cand;
442
0
        if let Some(err2) = curve_dist.eval_dist(source, c, acc2) {
443
0
            fn scale_f(d: f64) -> f64 {
444
0
                1.0 + (d - D_PENALTY_ELBOW).max(0.0) * D_PENALTY_SLOPE
445
0
            }
446
0
            let scale = scale_f(d0).max(scale_f(d1)).powi(2);
447
0
            let err2 = err2 * scale;
448
0
            if err2 < acc2 && best_err2.map(|best| err2 < best).unwrap_or(true) {
449
0
                best_c = Some(c);
450
0
                best_err2 = Some(err2);
451
0
            }
452
0
        }
453
    }
454
0
    match (best_c, best_err2) {
455
0
        (Some(c), Some(err2)) => Some((c, err2)),
456
0
        _ => None,
457
    }
458
0
}
459
460
/// Returns curves matching area and moment, given unit chord.
461
0
fn 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
0
    let (s0, c0) = th0.sin_cos();
465
0
    let (s1, c1) = th1.sin_cos();
466
0
    let a4 = -9.
467
0
        * c0
468
0
        * (((2. * s1 * c1 * c0 + s0 * (2. * c1 * c1 - 1.)) * c0 - 2. * s1 * c1) * c0
469
0
            - c1 * c1 * s0);
470
0
    let a3 = 12.
471
0
        * ((((c1 * (30. * area * c1 - s1) - 15. * area) * c0 + 2. * s0
472
0
            - c1 * s0 * (c1 + 30. * area * s1))
473
0
            * c0
474
0
            + c1 * (s1 - 15. * area * c1))
475
0
            * c0
476
0
            - s0 * c1 * c1);
477
0
    let a2 = 12.
478
0
        * ((((70. * mx + 15. * area) * s1 * s1 + c1 * (9. * s1 - 70. * c1 * mx - 5. * c1 * area))
479
0
            * c0
480
0
            - 5. * s0 * s1 * (3. * s1 - 4. * c1 * (7. * mx + area)))
481
0
            * c0
482
0
            - c1 * (9. * s1 - 70. * c1 * mx - 5. * c1 * area));
483
0
    let a1 = 16.
484
0
        * (((12. * s0 - 5. * c0 * (42. * mx - 17. * area)) * s1
485
0
            - 70. * c1 * (3. * mx - area) * s0
486
0
            - 75. * c0 * c1 * area * area)
487
0
            * s1
488
0
            - 75. * c1 * c1 * area * area * s0);
489
0
    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
0
    let mut roots = ArrayVec::<f64, 4>::new();
493
    const EPS: f64 = 1e-12;
494
0
    if a4.abs() > EPS {
495
0
        let a = a3 / a4;
496
0
        let b = a2 / a4;
497
0
        let c = a1 / a4;
498
0
        let d = a0 / a4;
499
0
        if let Some(quads) = factor_quartic_inner(a, b, c, d, false) {
500
0
            for (qc1, qc0) in quads {
501
0
                let qroots = solve_quadratic(qc0, qc1, 1.0);
502
0
                if qroots.is_empty() {
503
0
                    // Real part of pair of complex roots
504
0
                    roots.push(-0.5 * qc1);
505
0
                } else {
506
0
                    roots.extend(qroots);
507
0
                }
508
            }
509
0
        }
510
0
    } else if a3.abs() > EPS {
511
0
        roots.extend(solve_cubic(a0, a1, a2, a3));
512
0
    } else if a2.abs() > EPS || a1.abs() > EPS || a0.abs() > EPS {
513
0
        roots.extend(solve_quadratic(a0, a1, a2));
514
0
    } else {
515
0
        return [(
516
0
            CubicBez::new((0.0, 0.0), (1. / 3., 0.0), (2. / 3., 0.0), (1., 0.0)),
517
0
            1f64 / 3.,
518
0
            1f64 / 3.,
519
0
        )]
520
0
        .into_iter()
521
0
        .collect();
522
    }
523
524
0
    let s01 = s0 * c1 + s1 * c0;
525
0
    roots
526
0
        .iter()
527
0
        .filter_map(|&d0| {
528
0
            let (d0, d1) = if d0 > 0.0 {
529
0
                let d1 = (d0 * s0 - area * (10. / 3.)) / (0.5 * d0 * s01 - s1);
530
0
                if d1 > 0.0 {
531
0
                    (d0, d1)
532
                } else {
533
0
                    (s1 / s01, 0.0)
534
                }
535
            } else {
536
0
                (0.0, s0 / s01)
537
            };
538
            // We could implement a maximum d value here.
539
0
            if d0 >= 0.0 && d1 >= 0.0 {
540
0
                Some((
541
0
                    CubicBez::new(
542
0
                        (0.0, 0.0),
543
0
                        (d0 * c0, d0 * s0),
544
0
                        (1.0 - d1 * c1, d1 * s1),
545
0
                        (1.0, 0.0),
546
0
                    ),
547
0
                    d0,
548
0
                    d1,
549
0
                ))
550
            } else {
551
0
                None
552
            }
553
0
        })
554
0
        .collect()
555
0
}
556
557
/// Generate a highly optimized Bézier path that fits the source curve.
558
///
559
/// This function is still experimental and the signature might change; it's possible
560
/// it might become a method on the [`ParamCurveFit`] trait.
561
///
562
/// This function is considerably slower than [`fit_to_bezpath`], as it computes
563
/// optimal subdivision points. Its result is expected to be very close to the optimum
564
/// possible Bézier path for the source curve, in that it has a minimal number of curve
565
/// segments, and a minimal error over all paths with that number of segments.
566
///
567
/// See [`fit_to_bezpath`] for an explanation of the `accuracy` parameter.
568
0
pub fn fit_to_bezpath_opt(source: &impl ParamCurveFit, accuracy: f64) -> BezPath {
569
0
    let mut cusps = Vec::new();
570
0
    let mut path = BezPath::new();
571
0
    let mut t0 = 0.0;
572
    loop {
573
0
        let t1 = cusps.last().copied().unwrap_or(1.0);
574
0
        match fit_to_bezpath_opt_inner(source, accuracy, t0..t1, &mut path) {
575
0
            Some(t) => cusps.push(t),
576
0
            None => match cusps.pop() {
577
0
                Some(t) => t0 = t,
578
0
                None => break,
579
            },
580
        }
581
    }
582
0
    path
583
0
}
584
585
/// Fit a range without cusps.
586
///
587
/// On Ok return, range has been added to the path. On Err return, report a cusp (and don't
588
/// mutate path).
589
0
fn fit_to_bezpath_opt_inner(
590
0
    source: &impl ParamCurveFit,
591
0
    accuracy: f64,
592
0
    range: Range<f64>,
593
0
    path: &mut BezPath,
594
0
) -> Option<f64> {
595
0
    if let Some(t) = source.break_cusp(range.clone()) {
596
0
        return Some(t);
597
0
    }
598
    let err;
599
0
    if let Some((c, err2)) = fit_to_cubic(source, range.clone(), accuracy) {
600
0
        err = err2.sqrt();
601
0
        if err < accuracy {
602
0
            if range.start == 0.0 {
603
0
                path.move_to(c.p0);
604
0
            }
605
0
            path.curve_to(c.p1, c.p2, c.p3);
606
0
            return None;
607
0
        }
608
0
    } else {
609
0
        err = 2.0 * accuracy;
610
0
    }
611
0
    let (mut t0, t1) = (range.start, range.end);
612
0
    let mut n = 0;
613
    let last_err;
614
    loop {
615
0
        n += 1;
616
0
        match fit_opt_segment(source, accuracy, t0..t1) {
617
0
            FitResult::ParamVal(t) => t0 = t,
618
0
            FitResult::SegmentError(err) => {
619
0
                last_err = err;
620
0
                break;
621
            }
622
0
            FitResult::CuspFound(t) => return Some(t),
623
        }
624
    }
625
0
    t0 = range.start;
626
    const EPS: f64 = 1e-9;
627
0
    let f = |x| fit_opt_err_delta(source, x, accuracy, t0..t1, n);
628
0
    let k1 = 0.2 / accuracy;
629
0
    let ya = -err;
630
0
    let yb = accuracy - last_err;
631
0
    let (_, x) = match solve_itp_fallible(f, 0.0, accuracy, EPS, 1, k1, ya, yb) {
632
0
        Ok(x) => x,
633
0
        Err(t) => return Some(t),
634
    };
635
    //println!("got fit with n={}, err={}", n, x);
636
0
    let path_len = path.elements().len();
637
0
    for i in 0..n {
638
0
        let t1 = if i < n - 1 {
639
0
            match fit_opt_segment(source, x, t0..range.end) {
640
0
                FitResult::ParamVal(t1) => t1,
641
0
                FitResult::SegmentError(_) => range.end,
642
0
                FitResult::CuspFound(t) => {
643
0
                    path.truncate(path_len);
644
0
                    return Some(t);
645
                }
646
            }
647
        } else {
648
0
            range.end
649
        };
650
0
        let (c, _) = fit_to_cubic(source, t0..t1, accuracy).unwrap();
651
0
        if i == 0 && range.start == 0.0 {
652
0
            path.move_to(c.p0);
653
0
        }
654
0
        path.curve_to(c.p1, c.p2, c.p3);
655
0
        t0 = t1;
656
0
        if t0 == range.end {
657
            // This is unlikely but could happen when not monotonic.
658
0
            break;
659
0
        }
660
    }
661
0
    None
662
0
}
663
664
0
fn measure_one_seg(source: &impl ParamCurveFit, range: Range<f64>, limit: f64) -> Option<f64> {
665
0
    fit_to_cubic(source, range, limit).map(|(_, err2)| err2.sqrt())
666
0
}
667
668
enum FitResult {
669
    /// The parameter (`t`) value that meets the desired accuracy.
670
    ParamVal(f64),
671
    /// Error of the measured segment.
672
    SegmentError(f64),
673
    /// The parameter value where a cusp was found.
674
    CuspFound(f64),
675
}
676
677
0
fn fit_opt_segment(source: &impl ParamCurveFit, accuracy: f64, range: Range<f64>) -> FitResult {
678
0
    if let Some(t) = source.break_cusp(range.clone()) {
679
0
        return FitResult::CuspFound(t);
680
0
    }
681
0
    let missing_err = accuracy * 2.0;
682
0
    let err = measure_one_seg(source, range.clone(), accuracy).unwrap_or(missing_err);
683
0
    if err <= accuracy {
684
0
        return FitResult::SegmentError(err);
685
0
    }
686
0
    let (t0, t1) = (range.start, range.end);
687
0
    let f = |x| {
688
0
        if let Some(t) = source.break_cusp(range.clone()) {
689
0
            return Err(t);
690
0
        }
691
0
        let err = measure_one_seg(source, t0..x, accuracy).unwrap_or(missing_err);
692
0
        Ok(err - accuracy)
693
0
    };
694
    const EPS: f64 = 1e-9;
695
0
    let k1 = 2.0 / (t1 - t0);
696
0
    match solve_itp_fallible(f, t0, t1, EPS, 1, k1, -accuracy, err - accuracy) {
697
0
        Ok((t1, _)) => FitResult::ParamVal(t1),
698
0
        Err(t) => FitResult::CuspFound(t),
699
    }
700
0
}
701
702
// Ok result is delta error (accuracy - error of last seg).
703
// Err result is a cusp.
704
0
fn fit_opt_err_delta(
705
0
    source: &impl ParamCurveFit,
706
0
    accuracy: f64,
707
0
    limit: f64,
708
0
    range: Range<f64>,
709
0
    n: usize,
710
0
) -> Result<f64, f64> {
711
0
    let (mut t0, t1) = (range.start, range.end);
712
0
    for _ in 0..n - 1 {
713
0
        t0 = match fit_opt_segment(source, accuracy, t0..t1) {
714
0
            FitResult::ParamVal(t0) => t0,
715
            // In this case, n - 1 will work, which of course means the error is highly
716
            // non-monotonic. We should probably harvest that solution.
717
0
            FitResult::SegmentError(_) => return Ok(accuracy),
718
0
            FitResult::CuspFound(t) => return Err(t),
719
        }
720
    }
721
0
    let err = measure_one_seg(source, t0..t1, limit).unwrap_or(accuracy * 2.0);
722
0
    Ok(accuracy - err)
723
0
}