/rust/registry/src/index.crates.io-1949cf8c6b5b557f/kurbo-0.12.0/src/offset.rs
Line | Count | Source |
1 | | // Copyright 2022 the Kurbo Authors |
2 | | // SPDX-License-Identifier: Apache-2.0 OR MIT |
3 | | |
4 | | //! Computation of offset curves of cubic Béziers. |
5 | | //! |
6 | | //! The main algorithm in this module is a new technique designed for robustness |
7 | | //! and speed. The details are involved; hopefully there will be a paper. |
8 | | |
9 | | use arrayvec::ArrayVec; |
10 | | |
11 | | #[cfg(not(feature = "std"))] |
12 | | use crate::common::FloatFuncs; |
13 | | |
14 | | use crate::{ |
15 | | common::{solve_itp, solve_quadratic}, |
16 | | BezPath, CubicBez, ParamCurve, ParamCurveDeriv, PathSeg, Point, QuadBez, Vec2, |
17 | | }; |
18 | | |
19 | | /// State used for computing an offset curve of a single cubic. |
20 | | struct CubicOffset { |
21 | | /// The cubic being offset. This has been regularized. |
22 | | c: CubicBez, |
23 | | /// The derivative of `c`. |
24 | | q: QuadBez, |
25 | | /// The offset distance (same as the argument). |
26 | | d: f64, |
27 | | /// `c0 + c1 t + c2 t^2` is the cross product of second and first |
28 | | /// derivatives of the underlying cubic, multiplied by the offset. |
29 | | /// This is used for computing cusps on the offset curve. |
30 | | /// |
31 | | /// Note that given a curve `c(t)`, its signed curvature is |
32 | | /// `c''(t) x c'(t) / ||c'(t)||^3`. See also [`Self::cusp_sign`]. |
33 | | c0: f64, |
34 | | c1: f64, |
35 | | c2: f64, |
36 | | /// The tolerance (same as the argument). |
37 | | tolerance: f64, |
38 | | } |
39 | | |
40 | | // We never let cusp values have an absolute value smaller than |
41 | | // this. When a cusp is found, determine its sign and use this value. |
42 | | const CUSP_EPSILON: f64 = 1e-12; |
43 | | |
44 | | /// Number of points for least-squares fit and error evaluation. |
45 | | /// |
46 | | /// This value is a tradeoff between accuracy and performance. The main risk in it |
47 | | /// being to small is under-sampling the error and thus letting excessive error |
48 | | /// slip through. That said, the "arc drawing" approach is designed to be robust |
49 | | /// and not generate approximate results with narrow error peaks, even in near-cusp |
50 | | /// "J" shape curves. |
51 | | const N_LSE: usize = 8; |
52 | | |
53 | | /// The proportion of transverse error that is blended in the least-squares logic. |
54 | | const BLEND: f64 = 1e-3; |
55 | | |
56 | | /// Maximum recursion depth. |
57 | | /// |
58 | | /// Recursion is bounded to this depth, so the total number of subdivisions will |
59 | | /// not exceed two to this power. |
60 | | /// |
61 | | /// This is primarily a "belt and suspenders" robustness guard. In normal operation, |
62 | | /// the recursion bound should never be reached, as accuracy improves very quickly |
63 | | /// on subdivision. For unreasonably large coordinate values or small tolerances, it |
64 | | /// is possible, and in those cases the result will be out of tolerance. |
65 | | /// |
66 | | /// Perhaps should be configurable. |
67 | | const MAX_DEPTH: usize = 8; |
68 | | |
69 | | /// State local to a subdivision |
70 | | struct OffsetRec { |
71 | | t0: f64, |
72 | | t1: f64, |
73 | | // unit tangent at t0 |
74 | | utan0: Vec2, |
75 | | // unit tangent at t1 |
76 | | utan1: Vec2, |
77 | | cusp0: f64, |
78 | | cusp1: f64, |
79 | | /// Recursion depth |
80 | | depth: usize, |
81 | | } |
82 | | |
83 | | /// Result of error evaluation |
84 | | struct ErrEval { |
85 | | /// Maximum detected error |
86 | | err_squared: f64, |
87 | | /// Unit normals sampled uniformly across approximation |
88 | | unorms: [Vec2; N_LSE], |
89 | | /// Difference between point on source curve and normal from approximation. |
90 | | err_vecs: [Vec2; N_LSE], |
91 | | } |
92 | | |
93 | | /// Result of subdivision |
94 | | struct SubdivisionPoint { |
95 | | /// Source curve t value at subdivision point |
96 | | t: f64, |
97 | | /// Unit tangent at subdivision point |
98 | | utan: Vec2, |
99 | | } |
100 | | |
101 | | /// Compute an approximate offset curve. |
102 | | /// |
103 | | /// The parallel curve of `c` offset by `d` is written to the `result` path. |
104 | | /// |
105 | | /// There is a fair amount of attention to robustness, but this method is not suitable |
106 | | /// for degenerate cubics with entirely co-linear control points. Those cases should be |
107 | | /// handled before calling this function, by replacing them with linear segments. |
108 | 0 | pub fn offset_cubic(c: CubicBez, d: f64, tolerance: f64, result: &mut BezPath) { |
109 | 0 | result.truncate(0); |
110 | | // A tuning parameter for regularization. A value too large may distort the curve, |
111 | | // while a value too small may fail to generate smooth curves. This is a somewhat |
112 | | // arbitrary value, and should be revisited. |
113 | | const DIM_TUNE: f64 = 0.25; |
114 | | // We use regularization to perturb the curve to avoid *interior* zero-derivative |
115 | | // cusps. There is robustness logic in place to handle zero derivatives at the |
116 | | // endpoints. |
117 | | // |
118 | | // As a performance note, it might be a good idea to move regularization and |
119 | | // tangent determination to the caller, as those computations are the same for both |
120 | | // signs of `d`. |
121 | 0 | let c_regularized = c.regularize_cusp(tolerance * DIM_TUNE); |
122 | 0 | let co = CubicOffset::new(c_regularized, d, tolerance); |
123 | 0 | let (tan0, tan1) = PathSeg::Cubic(c).tangents(); |
124 | 0 | let utan0 = tan0.normalize(); |
125 | 0 | let utan1 = tan1.normalize(); |
126 | 0 | let cusp0 = co.endpoint_cusp(co.q.p0, co.c0); |
127 | 0 | let cusp1 = co.endpoint_cusp(co.q.p2, co.c0 + co.c1 + co.c2); |
128 | 0 | result.move_to(c.p0 + d * utan0.turn_90()); |
129 | 0 | let rec = OffsetRec::new(0., 1., utan0, utan1, cusp0, cusp1, 0); |
130 | 0 | co.offset_rec(&rec, result); |
131 | 0 | } |
132 | | |
133 | | impl CubicOffset { |
134 | | /// Create a new curve from Bézier segment and offset. |
135 | 0 | fn new(c: CubicBez, d: f64, tolerance: f64) -> Self { |
136 | 0 | let q = c.deriv(); |
137 | 0 | let d2 = 2.0 * d; |
138 | 0 | let p1xp0 = q.p1.to_vec2().cross(q.p0.to_vec2()); |
139 | 0 | let p2xp0 = q.p2.to_vec2().cross(q.p0.to_vec2()); |
140 | 0 | let p2xp1 = q.p2.to_vec2().cross(q.p1.to_vec2()); |
141 | 0 | CubicOffset { |
142 | 0 | c, |
143 | 0 | q, |
144 | 0 | d, |
145 | 0 | c0: d2 * p1xp0, |
146 | 0 | c1: d2 * (p2xp0 - 2.0 * p1xp0), |
147 | 0 | c2: d2 * (p2xp1 - p2xp0 + p1xp0), |
148 | 0 | tolerance, |
149 | 0 | } |
150 | 0 | } |
151 | | |
152 | | /// Compute curvature of the source curve times offset plus 1. |
153 | | /// |
154 | | /// This quantity is called "cusp" because cusps appear in the offset curve |
155 | | /// where this value crosses zero. This is based on the geometric property |
156 | | /// that the offset curve has a cusp when the radius of curvature of the |
157 | | /// source curve is equal to the offset curve's distance. |
158 | | /// |
159 | | /// Note: there is a potential division by zero when the derivative vanishes. |
160 | | /// We avoid doing so for interior points by regularizing the cubic beforehand. |
161 | | /// We avoid doing so for endpoints by calling `endpoint_cusp` instead. |
162 | 0 | fn cusp_sign(&self, t: f64) -> f64 { |
163 | 0 | let ds2 = self.q.eval(t).to_vec2().hypot2(); |
164 | 0 | ((self.c2 * t + self.c1) * t + self.c0) / (ds2 * ds2.sqrt()) + 1.0 |
165 | 0 | } |
166 | | |
167 | | /// Compute cusp value of endpoint. |
168 | | /// |
169 | | /// This is a special case of [`Self::cusp_sign`]. For the start point, `tan` should be |
170 | | /// the start point tangent and `y` should be `c0`. For the end point, `tan` should be |
171 | | /// the end point tangent and `y` should be `c0 + c1 + c2`. |
172 | | /// |
173 | | /// This is just evaluating the polynomial at t=0 and t=1. |
174 | | /// |
175 | | /// See [`Self::cusp_sign`] for a description of what "cusp value" means. |
176 | 0 | fn endpoint_cusp(&self, tan: Point, y: f64) -> f64 { |
177 | | // Robustness to avoid divide-by-zero when derivatives vanish |
178 | | const TAN_DIST_EPSILON: f64 = 1e-12; |
179 | 0 | let tan_dist = tan.to_vec2().hypot().max(TAN_DIST_EPSILON); |
180 | 0 | let rsqrt = 1.0 / tan_dist; |
181 | 0 | y * (rsqrt * rsqrt * rsqrt) + 1.0 |
182 | 0 | } |
183 | | |
184 | | /// Primary entry point for recursive subdivision. |
185 | | /// |
186 | | /// At a high level, this method determines whether subdivision is necessary. If |
187 | | /// so, it determines a subdivision point and then recursively calls itself on |
188 | | /// both subdivisions. If not, it computes a single cubic Bézier to approximate |
189 | | /// the offset curve, and appends it to `result`. |
190 | 0 | fn offset_rec(&self, rec: &OffsetRec, result: &mut BezPath) { |
191 | | // First, determine whether the offset curve contains a cusp. If the sign |
192 | | // of the cusp value (curvature times offset plus 1) is different at the |
193 | | // subdivision endpoints, then there is definitely a cusp inside. Find it and |
194 | | // subdivide there. |
195 | | // |
196 | | // Note that there's a possibility the curve has two (or, potentially, any |
197 | | // even number). We don't rigorously check for this case; if the measured |
198 | | // error comes in under the tolerance, we simply accept it. Otherwise, in |
199 | | // the common case we expect to detect a sign crossing from the new |
200 | | // subdivision point. |
201 | 0 | if rec.cusp0 * rec.cusp1 < 0.0 { |
202 | 0 | let a = rec.t0; |
203 | 0 | let b = rec.t1; |
204 | 0 | let s = rec.cusp1.signum(); |
205 | 0 | let f = |t| s * self.cusp_sign(t); |
206 | 0 | let k1 = 0.2 / (b - a); |
207 | | const ITP_EPS: f64 = 1e-12; |
208 | 0 | let t = solve_itp(f, a, b, ITP_EPS, 1, k1, s * rec.cusp0, s * rec.cusp1); |
209 | | // TODO(robustness): If we're unlucky, there will be 3 cusps between t0 |
210 | | // and t1, and the solver will land on the middle one. In that case, the |
211 | | // derivative on cusp will be the opposite sign as expected. |
212 | | // |
213 | | // If we're even more unlucky, there is a second-order cusp, both zero |
214 | | // cusp value and zero derivative. |
215 | 0 | let utan_t = self.q.eval(t).to_vec2().normalize(); |
216 | 0 | let cusp_t_minus = CUSP_EPSILON.copysign(rec.cusp0); |
217 | 0 | let cusp_t_plus = CUSP_EPSILON.copysign(rec.cusp1); |
218 | 0 | self.subdivide(rec, result, t, utan_t, cusp_t_minus, cusp_t_plus); |
219 | 0 | return; |
220 | 0 | } |
221 | | // We determine the first approximation to the offset curve. |
222 | 0 | let (mut a, mut b) = self.draw_arc(rec); |
223 | 0 | let dt = (rec.t1 - rec.t0) * (1.0 / (N_LSE + 1) as f64); |
224 | | // These represent t values on the source curve. |
225 | 0 | let mut ts = core::array::from_fn(|i| rec.t0 + (i + 1) as f64 * dt); |
226 | 0 | let mut c_approx = self.apply(rec, a, b); |
227 | 0 | let err_init = self.eval_err(rec, c_approx, &mut ts); |
228 | 0 | let mut err = err_init; |
229 | | // Number of least-squares refinement steps. More gives a smaller |
230 | | // error, but takes more time. |
231 | | const N_REFINE: usize = 2; |
232 | 0 | for _ in 0..N_REFINE { |
233 | 0 | if err.err_squared <= self.tolerance * self.tolerance { |
234 | 0 | break; |
235 | 0 | } |
236 | 0 | let (a2, b2) = self.refine_least_squares(rec, a, b, &err); |
237 | 0 | let c_approx2 = self.apply(rec, a2, b2); |
238 | 0 | let err2 = self.eval_err(rec, c_approx2, &mut ts); |
239 | 0 | if err2.err_squared >= err.err_squared { |
240 | 0 | break; |
241 | 0 | } |
242 | 0 | err = err2; |
243 | 0 | (a, b) = (a2, b2); |
244 | 0 | c_approx = c_approx2; |
245 | | } |
246 | 0 | if rec.depth < MAX_DEPTH && err.err_squared > self.tolerance * self.tolerance { |
247 | 0 | let SubdivisionPoint { t, utan } = self.find_subdivision_point(rec); |
248 | 0 | // TODO(robustness): if cusp is extremely near zero, then assign epsilon |
249 | 0 | // with alternate signs based on derivative of cusp. |
250 | 0 | let cusp = self.cusp_sign(t); |
251 | 0 | self.subdivide(rec, result, t, utan, cusp, cusp); |
252 | 0 | } else { |
253 | 0 | result.curve_to(c_approx.p1, c_approx.p2, c_approx.p3); |
254 | 0 | } |
255 | 0 | } |
256 | | |
257 | | /// Recursively subdivide. |
258 | | /// |
259 | | /// In the case of subdividing at a cusp, the cusp value at the subdivision point |
260 | | /// is mathematically zero, but in those cases we treat it as a signed infinitesimal |
261 | | /// value representing the values at t minus epsilon and t plus epsilon. |
262 | | /// |
263 | | /// Note that unit tangents are passed down explicitly. In the general case, they |
264 | | /// are equal to the derivative (evaluated at that t value) normalized to unit |
265 | | /// length, but in cases where the derivative is near-zero, they are computed more |
266 | | /// robustly. |
267 | 0 | fn subdivide( |
268 | 0 | &self, |
269 | 0 | rec: &OffsetRec, |
270 | 0 | result: &mut BezPath, |
271 | 0 | t: f64, |
272 | 0 | utan_t: Vec2, |
273 | 0 | cusp_t_minus: f64, |
274 | 0 | cusp_t_plus: f64, |
275 | 0 | ) { |
276 | 0 | let rec0 = OffsetRec::new( |
277 | 0 | rec.t0, |
278 | 0 | t, |
279 | 0 | rec.utan0, |
280 | 0 | utan_t, |
281 | 0 | rec.cusp0, |
282 | 0 | cusp_t_minus, |
283 | 0 | rec.depth + 1, |
284 | | ); |
285 | 0 | self.offset_rec(&rec0, result); |
286 | 0 | let rec1 = OffsetRec::new( |
287 | 0 | t, |
288 | 0 | rec.t1, |
289 | 0 | utan_t, |
290 | 0 | rec.utan1, |
291 | 0 | cusp_t_plus, |
292 | 0 | rec.cusp1, |
293 | 0 | rec.depth + 1, |
294 | | ); |
295 | 0 | self.offset_rec(&rec1, result); |
296 | 0 | } |
297 | | |
298 | | /// Convert from (a, b) parameter space to the approximate cubic Bézier. |
299 | | /// |
300 | | /// The offset approximation can be considered `B(t) + d * D(t)`, where `D(t)` |
301 | | /// is roughly a unit vector in the direction of the unit normal of the source |
302 | | /// curve. (The word "roughly" is appropriate because transverse error may |
303 | | /// cancel out normal error, resulting in a lower error than either alone). |
304 | | /// The endpoints of `D(t)` must be the unit normals of the source curve, and |
305 | | /// the endpoint tangents of `D(t)` must tangent to the endpoint tangents of |
306 | | /// the source curve, to ensure G1 continuity. |
307 | | /// |
308 | | /// The (a, b) parameters refer to the magnitude of the vector from the endpoint |
309 | | /// to the corresponding control point in `D(t)`, the direction being determined |
310 | | /// by the unit tangent. |
311 | | /// |
312 | | /// When the candidate solution would lead to negative distance from the |
313 | | /// endpoint to the control point, that distance is clamped to zero. Otherwise |
314 | | /// such solutions should be considered invalid, and have the unpleasant |
315 | | /// property of sometimes passing error tolerance checks. |
316 | 0 | fn apply(&self, rec: &OffsetRec, a: f64, b: f64) -> CubicBez { |
317 | | // wondering if p0 and p3 should be in rec |
318 | | // Scale factor from derivatives to displacements |
319 | 0 | let s = (1. / 3.) * (rec.t1 - rec.t0); |
320 | 0 | let p0 = self.c.eval(rec.t0) + self.d * rec.utan0.turn_90(); |
321 | 0 | let l0 = s * self.q.eval(rec.t0).to_vec2().length() + a * self.d; |
322 | 0 | let mut p1 = p0; |
323 | 0 | if l0 * rec.cusp0 > 0.0 { |
324 | 0 | p1 += l0 * rec.utan0; |
325 | 0 | } |
326 | 0 | let p3 = self.c.eval(rec.t1) + self.d * rec.utan1.turn_90(); |
327 | 0 | let mut p2 = p3; |
328 | 0 | let l1 = s * self.q.eval(rec.t1).to_vec2().length() - b * self.d; |
329 | 0 | if l1 * rec.cusp1 > 0.0 { |
330 | 0 | p2 -= l1 * rec.utan1; |
331 | 0 | } |
332 | 0 | CubicBez::new(p0, p1, p2, p3) |
333 | 0 | } |
334 | | |
335 | | /// Compute arc approximation. |
336 | | /// |
337 | | /// This is called "arc drawing" because if we just look at the delta |
338 | | /// vector, it describes an arc from the initial unit normal to the final unit |
339 | | /// normal, with "as smooth as possible" parametrization. This approximation |
340 | | /// is not necessarily great, but is very robust, and in particular, accuracy |
341 | | /// does not degrade for J shaped near-cusp source curves or when the offset |
342 | | /// distance is large (with respect to the source curve arc length). |
343 | | /// |
344 | | /// It is a pretty good approximation overall and has very clean O(n^4) scaling. |
345 | | /// Its worst performance is on curves with a large cubic component, where it |
346 | | /// undershoots. The theory is that the least squares refinement improves those |
347 | | /// cases. |
348 | 0 | fn draw_arc(&self, rec: &OffsetRec) -> (f64, f64) { |
349 | | // possible optimization: this can probably be done with vectors |
350 | | // rather than arctangent |
351 | 0 | let th = rec.utan1.cross(rec.utan0).atan2(rec.utan1.dot(rec.utan0)); |
352 | 0 | let a = (2. / 3.) / (1.0 + (0.5 * th).cos()) * 2.0 * (0.5 * th).sin(); |
353 | 0 | let b = -a; |
354 | 0 | (a, b) |
355 | 0 | } |
356 | | |
357 | | /// Evaluate error and also refine t values. |
358 | | /// |
359 | | /// Returns evaluation of error including error vectors and (squared) |
360 | | /// maximum error. |
361 | | /// |
362 | | /// The vector of t values represents points on the source curve; the logic |
363 | | /// here is a Newton step to bring those points closer to the normal ray of |
364 | | /// the approximation. |
365 | 0 | fn eval_err(&self, rec: &OffsetRec, c_approx: CubicBez, ts: &mut [f64; N_LSE]) -> ErrEval { |
366 | 0 | let qa = c_approx.deriv(); |
367 | 0 | let mut err_squared = 0.0; |
368 | 0 | let mut unorms = [Vec2::ZERO; N_LSE]; |
369 | 0 | let mut err_vecs = [Vec2::ZERO; N_LSE]; |
370 | 0 | for i in 0..N_LSE { |
371 | 0 | let ta = (i + 1) as f64 * (1.0 / (N_LSE + 1) as f64); |
372 | 0 | let mut t = ts[i]; |
373 | 0 | let p = self.c.eval(t); |
374 | | // Newton step to refine t value |
375 | 0 | let pa = c_approx.eval(ta); |
376 | 0 | let tana = qa.eval(ta).to_vec2(); |
377 | 0 | t += tana.dot(pa - p) / tana.dot(self.q.eval(t).to_vec2()); |
378 | 0 | t = t.max(rec.t0).min(rec.t1); |
379 | 0 | ts[i] = t; |
380 | 0 | let cusp = rec.cusp0.signum(); |
381 | 0 | let unorm = cusp * tana.normalize().turn_90(); |
382 | 0 | unorms[i] = unorm; |
383 | 0 | let p_new = self.c.eval(t) + self.d * unorm; |
384 | 0 | let err_vec = pa - p_new; |
385 | 0 | err_vecs[i] = err_vec; |
386 | 0 | let mut dist_err_squared = err_vec.length_squared(); |
387 | 0 | if !dist_err_squared.is_finite() { |
388 | 0 | // A hack to make sure we reject bad refinements |
389 | 0 | dist_err_squared = 1e12; |
390 | 0 | } |
391 | | // Note: consider also incorporating angle error |
392 | 0 | err_squared = dist_err_squared.max(err_squared); |
393 | | } |
394 | 0 | ErrEval { |
395 | 0 | err_squared, |
396 | 0 | unorms, |
397 | 0 | err_vecs, |
398 | 0 | } |
399 | 0 | } |
400 | | |
401 | | /// Refine an approximation, minimizing least squares error. |
402 | | /// |
403 | | /// Compute the approximation that minimizes least squares error, based on the given error |
404 | | /// evaluation. |
405 | | /// |
406 | | /// The effectiveness of this refinement varies. Basically, if the curve has a large cubic |
407 | | /// component, then the arc drawing will undershoot systematically and this refinement will |
408 | | /// reduce error considerably. In other cases, it will eventually converge to a local |
409 | | /// minimum, but convergence is slow. |
410 | | /// |
411 | | /// The `BLEND` parameter controls a tradeoff between robustness and speed of convergence. |
412 | | /// In the happy case, convergence is fast and not very sensitive to this parameter. If the |
413 | | /// parameter is too small, then in near-parabola cases the determinant will be small and |
414 | | /// the result not numerically stable. |
415 | | /// |
416 | | /// A value of 1.0 for `BLEND` corresponds to essentially the Hoschek method, minimizing |
417 | | /// Euclidean distance (which tends to over-anchor on the given t values). A value of 0 would |
418 | | /// minimize the dot product of error wrt the normal vector, ignoring the cross product |
419 | | /// component. |
420 | | /// |
421 | | /// A possible future direction would be to tune the parameter adaptively. |
422 | 0 | fn refine_least_squares(&self, rec: &OffsetRec, a: f64, b: f64, err: &ErrEval) -> (f64, f64) { |
423 | 0 | let mut aa = 0.0; |
424 | 0 | let mut ab = 0.0; |
425 | 0 | let mut ac = 0.0; |
426 | 0 | let mut bb = 0.0; |
427 | 0 | let mut bc = 0.0; |
428 | 0 | for i in 0..N_LSE { |
429 | 0 | let n = err.unorms[i]; |
430 | 0 | let err_vec = err.err_vecs[i]; |
431 | 0 | let c_n = err_vec.dot(n); |
432 | 0 | let c_t = err_vec.cross(n); |
433 | 0 | let a_n = A_WEIGHTS[i] * rec.utan0.dot(n); |
434 | 0 | let a_t = A_WEIGHTS[i] * rec.utan0.cross(n); |
435 | 0 | let b_n = B_WEIGHTS[i] * rec.utan1.dot(n); |
436 | 0 | let b_t = B_WEIGHTS[i] * rec.utan1.cross(n); |
437 | 0 | aa += a_n * a_n + BLEND * (a_t * a_t); |
438 | 0 | ab += a_n * b_n + BLEND * a_t * b_t; |
439 | 0 | ac += a_n * c_n + BLEND * a_t * c_t; |
440 | 0 | bb += b_n * b_n + BLEND * (b_t * b_t); |
441 | 0 | bc += b_n * c_n + BLEND * b_t * c_t; |
442 | 0 | } |
443 | 0 | let idet = 1.0 / (self.d * (aa * bb - ab * ab)); |
444 | 0 | let delta_a = idet * (ac * bb - ab * bc); |
445 | 0 | let delta_b = idet * (aa * bc - ac * ab); |
446 | 0 | (a - delta_a, b - delta_b) |
447 | 0 | } |
448 | | |
449 | | /// Decide where to subdivide when error is exceeded. |
450 | | /// |
451 | | /// For curves not containing an inflection point, subdivide at the tangent |
452 | | /// bisecting the endpoint tangents. The logic is that for a near cusp in the |
453 | | /// source curve, you want the subdivided sections to be approximately |
454 | | /// circular arcs with progressively smaller angles. |
455 | | /// |
456 | | /// When there is an inflection point (or, more specifically, when the curve |
457 | | /// crosses its chord), bisecting the angle can lead to very lopsided arc |
458 | | /// lengths, so just subdivide by t in that case. |
459 | 0 | fn find_subdivision_point(&self, rec: &OffsetRec) -> SubdivisionPoint { |
460 | 0 | let t = 0.5 * (rec.t0 + rec.t1); |
461 | 0 | let q_t = self.q.eval(t).to_vec2(); |
462 | 0 | let x0 = rec.utan0.cross(q_t).abs(); |
463 | 0 | let x1 = rec.utan1.cross(q_t).abs(); |
464 | | const SUBDIVIDE_THRESH: f64 = 0.1; |
465 | 0 | if x0 > SUBDIVIDE_THRESH * x1 && x1 > SUBDIVIDE_THRESH * x0 { |
466 | 0 | let utan = q_t.normalize(); |
467 | 0 | return SubdivisionPoint { t, utan }; |
468 | 0 | } |
469 | | |
470 | | // Note: do we want to track p0 & p3 in rec, to avoid repeated eval? |
471 | 0 | let chord = self.c.eval(rec.t1) - self.c.eval(rec.t0); |
472 | 0 | if chord.cross(rec.utan0) * chord.cross(rec.utan1) < 0.0 { |
473 | 0 | let tan = rec.utan0 + rec.utan1; |
474 | 0 | if let Some(subdivision) = |
475 | 0 | self.subdivide_for_tangent(rec.utan0, rec.t0, rec.t1, tan, false) |
476 | | { |
477 | 0 | return subdivision; |
478 | 0 | } |
479 | 0 | } |
480 | | // Curve definitely has an inflection point |
481 | | // Try to subdivide based on integral of absolute curvature. |
482 | | |
483 | | // Tangents at recursion endpoints and inflection points. |
484 | 0 | let mut tangents: ArrayVec<Vec2, 4> = ArrayVec::new(); |
485 | 0 | let mut ts: ArrayVec<f64, 4> = ArrayVec::new(); |
486 | 0 | tangents.push(rec.utan0); |
487 | 0 | ts.push(rec.t0); |
488 | 0 | for t in self.c.inflections() { |
489 | 0 | if t > rec.t0 && t < rec.t1 { |
490 | 0 | tangents.push(self.q.eval(t).to_vec2()); |
491 | 0 | ts.push(t); |
492 | 0 | } |
493 | | } |
494 | 0 | tangents.push(rec.utan1); |
495 | 0 | ts.push(rec.t1); |
496 | 0 | let mut arc_angles: ArrayVec<f64, 3> = ArrayVec::new(); |
497 | 0 | let mut sum = 0.0; |
498 | 0 | for i in 0..tangents.len() - 1 { |
499 | 0 | let tan0 = tangents[i]; |
500 | 0 | let tan1 = tangents[i + 1]; |
501 | 0 | let th = tan0.cross(tan1).atan2(tan0.dot(tan1)); |
502 | 0 | sum += th.abs(); |
503 | 0 | arc_angles.push(th); |
504 | 0 | } |
505 | 0 | let mut target = sum * 0.5; |
506 | 0 | let mut i = 0; |
507 | 0 | while arc_angles[i].abs() < target { |
508 | 0 | target -= arc_angles[i].abs(); |
509 | 0 | i += 1; |
510 | 0 | } |
511 | 0 | let rotation = Vec2::from_angle(target.copysign(arc_angles[i])); |
512 | 0 | let base = tangents[i]; |
513 | 0 | let tan = base.rotate_scale(rotation); |
514 | 0 | let utan0 = if i == 0 { rec.utan0 } else { base.normalize() }; |
515 | 0 | self.subdivide_for_tangent(utan0, ts[i], ts[i + 1], tan, true) |
516 | 0 | .unwrap() |
517 | 0 | } |
518 | | |
519 | | /// Find a subdivision point, given a tangent vector. |
520 | | /// |
521 | | /// When subdividing by bisecting the angle (or, more generally, subdividing by |
522 | | /// the L1 norm of curvature when there are inflection points), we find the |
523 | | /// subdivision point by solving for the tangent matching, specifically the |
524 | | /// cross-product of the tangent and the curve's derivative being zero. For |
525 | | /// internal cusps, subdividing near the cusp is a good thing, but there is |
526 | | /// still a robustness concern for vanishing derivative at the endpoints. |
527 | 0 | fn subdivide_for_tangent( |
528 | 0 | &self, |
529 | 0 | utan0: Vec2, |
530 | 0 | t0: f64, |
531 | 0 | t1: f64, |
532 | 0 | tan: Vec2, |
533 | 0 | force: bool, |
534 | 0 | ) -> Option<SubdivisionPoint> { |
535 | 0 | let mut t = 0.0; |
536 | 0 | let mut n_soln = 0; |
537 | | // set up quadratic equation for matching tangents |
538 | 0 | let z0 = tan.cross(self.q.p0.to_vec2()); |
539 | 0 | let z1 = tan.cross(self.q.p1.to_vec2()); |
540 | 0 | let z2 = tan.cross(self.q.p2.to_vec2()); |
541 | 0 | let c0 = z0; |
542 | 0 | let c1 = 2.0 * (z1 - z0); |
543 | 0 | let c2 = (z2 - z1) - (z1 - z0); |
544 | 0 | for root in solve_quadratic(c0, c1, c2) { |
545 | 0 | if root >= t0 && root <= t1 { |
546 | 0 | t = root; |
547 | 0 | n_soln += 1; |
548 | 0 | } |
549 | | } |
550 | 0 | if n_soln != 1 { |
551 | 0 | if !force { |
552 | 0 | return None; |
553 | 0 | } |
554 | | // Numerical failure, try to subdivide at cusp; we pick the |
555 | | // smaller derivative. |
556 | 0 | if self.q.eval(t0).to_vec2().length_squared() |
557 | 0 | > self.q.eval(t1).to_vec2().length_squared() |
558 | 0 | { |
559 | 0 | t = t1; |
560 | 0 | } else { |
561 | 0 | t = t0; |
562 | 0 | } |
563 | 0 | } |
564 | 0 | let q = self.q.eval(t).to_vec2(); |
565 | | const UTAN_EPSILON: f64 = 1e-12; |
566 | 0 | let utan = if n_soln == 1 && q.length_squared() >= UTAN_EPSILON { |
567 | 0 | q.normalize() |
568 | 0 | } else if tan.length_squared() >= UTAN_EPSILON { |
569 | | // Curve has a zero-derivative cusp but angles well defined |
570 | 0 | tan.normalize() |
571 | | } else { |
572 | | // 180 degree U-turn, arbitrarily pick a direction. |
573 | | // If we get to this point, there will probably be a failure. |
574 | 0 | utan0.turn_90() |
575 | | }; |
576 | 0 | Some(SubdivisionPoint { t, utan }) |
577 | 0 | } |
578 | | } |
579 | | |
580 | | impl OffsetRec { |
581 | | #[allow(clippy::too_many_arguments)] |
582 | 0 | fn new( |
583 | 0 | t0: f64, |
584 | 0 | t1: f64, |
585 | 0 | utan0: Vec2, |
586 | 0 | utan1: Vec2, |
587 | 0 | cusp0: f64, |
588 | 0 | cusp1: f64, |
589 | 0 | depth: usize, |
590 | 0 | ) -> Self { |
591 | 0 | OffsetRec { |
592 | 0 | t0, |
593 | 0 | t1, |
594 | 0 | utan0, |
595 | 0 | utan1, |
596 | 0 | cusp0, |
597 | 0 | cusp1, |
598 | 0 | depth, |
599 | 0 | } |
600 | 0 | } |
601 | | } |
602 | | |
603 | | /// Compute Bézier weights for evenly subdivided t values. |
604 | 0 | const fn mk_a_weights(rev: bool) -> [f64; N_LSE] { |
605 | 0 | let mut result = [0.0; N_LSE]; |
606 | 0 | let mut i = 0; |
607 | 0 | while i < N_LSE { |
608 | 0 | let t = (i + 1) as f64 / (N_LSE + 1) as f64; |
609 | 0 | let mt = 1. - t; |
610 | 0 | let ix = if rev { N_LSE - 1 - i } else { i }; |
611 | 0 | result[ix] = 3.0 * mt * t * mt; |
612 | 0 | i += 1; |
613 | | } |
614 | 0 | result |
615 | 0 | } |
616 | | |
617 | | const A_WEIGHTS: [f64; N_LSE] = mk_a_weights(false); |
618 | | const B_WEIGHTS: [f64; N_LSE] = mk_a_weights(true); |
619 | | |
620 | | #[cfg(test)] |
621 | | mod tests { |
622 | | use super::offset_cubic; |
623 | | use crate::{BezPath, CubicBez, PathEl}; |
624 | | |
625 | | // This test tries combinations of parameters that have caused problems in the past. |
626 | | #[test] |
627 | | fn pathological_curves() { |
628 | | let curve = CubicBez { |
629 | | p0: (-1236.3746269978635, 152.17981429574826).into(), |
630 | | p1: (-1175.18662093517, 108.04721798590596).into(), |
631 | | p2: (-1152.142883879584, 105.76260301083356).into(), |
632 | | p3: (-1151.842639804639, 105.73040758939104).into(), |
633 | | }; |
634 | | let offset = 3603.7267536453924; |
635 | | let accuracy = 0.1; |
636 | | |
637 | | let mut result = BezPath::new(); |
638 | | offset_cubic(curve, offset, accuracy, &mut result); |
639 | | assert!(matches!(result.iter().next(), Some(PathEl::MoveTo(_)))); |
640 | | |
641 | | let mut result = BezPath::new(); |
642 | | offset_cubic(curve, offset, accuracy, &mut result); |
643 | | assert!(matches!(result.iter().next(), Some(PathEl::MoveTo(_)))); |
644 | | } |
645 | | |
646 | | /// Cubic offset that used to trigger infinite recursion. |
647 | | #[test] |
648 | | fn infinite_recursion() { |
649 | | const TOLERANCE: f64 = 0.1; |
650 | | const OFFSET: f64 = -0.5; |
651 | | let c = CubicBez::new( |
652 | | (1096.2962962962963, 593.90243902439033), |
653 | | (1043.6213991769548, 593.90243902439033), |
654 | | (1030.4526748971193, 593.90243902439033), |
655 | | (1056.7901234567901, 593.90243902439033), |
656 | | ); |
657 | | |
658 | | let mut result = BezPath::new(); |
659 | | offset_cubic(c, OFFSET, TOLERANCE, &mut result); |
660 | | } |
661 | | |
662 | | #[test] |
663 | | fn test_cubic_offset_simple_line() { |
664 | | let cubic = CubicBez::new((0., 0.), (10., 0.), (20., 0.), (30., 0.)); |
665 | | let mut result = BezPath::new(); |
666 | | offset_cubic(cubic, 5., 1e-6, &mut result); |
667 | | } |
668 | | } |