/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 | } |