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 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. |
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 | 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. |
165 | pub 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. |
173 | fn 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 | |
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 | 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 |
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 | fn 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? |
387 | pub 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. |
461 | 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 | 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. |
560 | pub 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). |
581 | fn 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 | |
656 | fn 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 | |
660 | enum 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 | |
669 | fn 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. |
696 | fn 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 | |