kurbo/offset.rs
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
9use arrayvec::ArrayVec;
10
11#[cfg(not(feature = "std"))]
12use crate::common::FloatFuncs;
13
14use 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.
20struct 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.
42const 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.
51const N_LSE: usize = 8;
52
53/// The proportion of transverse error that is blended in the least-squares logic.
54const 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.
67const MAX_DEPTH: usize = 8;
68
69/// State local to a subdivision
70struct 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
84struct 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
94struct 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.
108pub fn offset_cubic(c: CubicBez, d: f64, tolerance: f64, result: &mut BezPath) {
109 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 let c_regularized = c.regularize_cusp(tolerance * DIM_TUNE);
122 let co = CubicOffset::new(c_regularized, d, tolerance);
123 let (tan0, tan1) = PathSeg::Cubic(c).tangents();
124 let utan0 = tan0.normalize();
125 let utan1 = tan1.normalize();
126 let cusp0 = co.endpoint_cusp(co.q.p0, co.c0);
127 let cusp1 = co.endpoint_cusp(co.q.p2, co.c0 + co.c1 + co.c2);
128 result.move_to(c.p0 + d * utan0.turn_90());
129 let rec = OffsetRec::new(0., 1., utan0, utan1, cusp0, cusp1, 0);
130 co.offset_rec(&rec, result);
131}
132
133impl CubicOffset {
134 /// Create a new curve from Bézier segment and offset.
135 fn new(c: CubicBez, d: f64, tolerance: f64) -> Self {
136 let q = c.deriv();
137 let d2 = 2.0 * d;
138 let p1xp0 = q.p1.to_vec2().cross(q.p0.to_vec2());
139 let p2xp0 = q.p2.to_vec2().cross(q.p0.to_vec2());
140 let p2xp1 = q.p2.to_vec2().cross(q.p1.to_vec2());
141 CubicOffset {
142 c,
143 q,
144 d,
145 c0: d2 * p1xp0,
146 c1: d2 * (p2xp0 - 2.0 * p1xp0),
147 c2: d2 * (p2xp1 - p2xp0 + p1xp0),
148 tolerance,
149 }
150 }
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 fn cusp_sign(&self, t: f64) -> f64 {
163 let ds2 = self.q.eval(t).to_vec2().hypot2();
164 ((self.c2 * t + self.c1) * t + self.c0) / (ds2 * ds2.sqrt()) + 1.0
165 }
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 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 let tan_dist = tan.to_vec2().hypot().max(TAN_DIST_EPSILON);
180 let rsqrt = 1.0 / tan_dist;
181 y * (rsqrt * rsqrt * rsqrt) + 1.0
182 }
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 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 if rec.cusp0 * rec.cusp1 < 0.0 {
202 let a = rec.t0;
203 let b = rec.t1;
204 let s = rec.cusp1.signum();
205 let f = |t| s * self.cusp_sign(t);
206 let k1 = 0.2 / (b - a);
207 const ITP_EPS: f64 = 1e-12;
208 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 let utan_t = self.q.eval(t).to_vec2().normalize();
216 let cusp_t_minus = CUSP_EPSILON.copysign(rec.cusp0);
217 let cusp_t_plus = CUSP_EPSILON.copysign(rec.cusp1);
218 self.subdivide(rec, result, t, utan_t, cusp_t_minus, cusp_t_plus);
219 return;
220 }
221 // We determine the first approximation to the offset curve.
222 let (mut a, mut b) = self.draw_arc(rec);
223 let dt = (rec.t1 - rec.t0) * (1.0 / (N_LSE + 1) as f64);
224 // These represent t values on the source curve.
225 let mut ts = core::array::from_fn(|i| rec.t0 + (i + 1) as f64 * dt);
226 let mut c_approx = self.apply(rec, a, b);
227 let err_init = self.eval_err(rec, c_approx, &mut ts);
228 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 for _ in 0..N_REFINE {
233 if err.err_squared <= self.tolerance * self.tolerance {
234 break;
235 }
236 let (a2, b2) = self.refine_least_squares(rec, a, b, &err);
237 let c_approx2 = self.apply(rec, a2, b2);
238 let err2 = self.eval_err(rec, c_approx2, &mut ts);
239 if err2.err_squared >= err.err_squared {
240 break;
241 }
242 err = err2;
243 (a, b) = (a2, b2);
244 c_approx = c_approx2;
245 }
246 if rec.depth < MAX_DEPTH && err.err_squared > self.tolerance * self.tolerance {
247 let SubdivisionPoint { t, utan } = self.find_subdivision_point(rec);
248 // TODO(robustness): if cusp is extremely near zero, then assign epsilon
249 // with alternate signs based on derivative of cusp.
250 let cusp = self.cusp_sign(t);
251 self.subdivide(rec, result, t, utan, cusp, cusp);
252 } else {
253 result.curve_to(c_approx.p1, c_approx.p2, c_approx.p3);
254 }
255 }
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 fn subdivide(
268 &self,
269 rec: &OffsetRec,
270 result: &mut BezPath,
271 t: f64,
272 utan_t: Vec2,
273 cusp_t_minus: f64,
274 cusp_t_plus: f64,
275 ) {
276 let rec0 = OffsetRec::new(
277 rec.t0,
278 t,
279 rec.utan0,
280 utan_t,
281 rec.cusp0,
282 cusp_t_minus,
283 rec.depth + 1,
284 );
285 self.offset_rec(&rec0, result);
286 let rec1 = OffsetRec::new(
287 t,
288 rec.t1,
289 utan_t,
290 rec.utan1,
291 cusp_t_plus,
292 rec.cusp1,
293 rec.depth + 1,
294 );
295 self.offset_rec(&rec1, result);
296 }
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 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 let s = (1. / 3.) * (rec.t1 - rec.t0);
320 let p0 = self.c.eval(rec.t0) + self.d * rec.utan0.turn_90();
321 let l0 = s * self.q.eval(rec.t0).to_vec2().length() + a * self.d;
322 let mut p1 = p0;
323 if l0 * rec.cusp0 > 0.0 {
324 p1 += l0 * rec.utan0;
325 }
326 let p3 = self.c.eval(rec.t1) + self.d * rec.utan1.turn_90();
327 let mut p2 = p3;
328 let l1 = s * self.q.eval(rec.t1).to_vec2().length() - b * self.d;
329 if l1 * rec.cusp1 > 0.0 {
330 p2 -= l1 * rec.utan1;
331 }
332 CubicBez::new(p0, p1, p2, p3)
333 }
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 fn draw_arc(&self, rec: &OffsetRec) -> (f64, f64) {
349 // possible optimization: this can probably be done with vectors
350 // rather than arctangent
351 let th = rec.utan1.cross(rec.utan0).atan2(rec.utan1.dot(rec.utan0));
352 let a = (2. / 3.) / (1.0 + (0.5 * th).cos()) * 2.0 * (0.5 * th).sin();
353 let b = -a;
354 (a, b)
355 }
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 fn eval_err(&self, rec: &OffsetRec, c_approx: CubicBez, ts: &mut [f64; N_LSE]) -> ErrEval {
366 let qa = c_approx.deriv();
367 let mut err_squared = 0.0;
368 let mut unorms = [Vec2::ZERO; N_LSE];
369 let mut err_vecs = [Vec2::ZERO; N_LSE];
370 for i in 0..N_LSE {
371 let ta = (i + 1) as f64 * (1.0 / (N_LSE + 1) as f64);
372 let mut t = ts[i];
373 let p = self.c.eval(t);
374 // Newton step to refine t value
375 let pa = c_approx.eval(ta);
376 let tana = qa.eval(ta).to_vec2();
377 t += tana.dot(pa - p) / tana.dot(self.q.eval(t).to_vec2());
378 t = t.max(rec.t0).min(rec.t1);
379 ts[i] = t;
380 let cusp = rec.cusp0.signum();
381 let unorm = cusp * tana.normalize().turn_90();
382 unorms[i] = unorm;
383 let p_new = self.c.eval(t) + self.d * unorm;
384 let err_vec = pa - p_new;
385 err_vecs[i] = err_vec;
386 let mut dist_err_squared = err_vec.length_squared();
387 if !dist_err_squared.is_finite() {
388 // A hack to make sure we reject bad refinements
389 dist_err_squared = 1e12;
390 }
391 // Note: consider also incorporating angle error
392 err_squared = dist_err_squared.max(err_squared);
393 }
394 ErrEval {
395 err_squared,
396 unorms,
397 err_vecs,
398 }
399 }
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 fn refine_least_squares(&self, rec: &OffsetRec, a: f64, b: f64, err: &ErrEval) -> (f64, f64) {
423 let mut aa = 0.0;
424 let mut ab = 0.0;
425 let mut ac = 0.0;
426 let mut bb = 0.0;
427 let mut bc = 0.0;
428 for i in 0..N_LSE {
429 let n = err.unorms[i];
430 let err_vec = err.err_vecs[i];
431 let c_n = err_vec.dot(n);
432 let c_t = err_vec.cross(n);
433 let a_n = A_WEIGHTS[i] * rec.utan0.dot(n);
434 let a_t = A_WEIGHTS[i] * rec.utan0.cross(n);
435 let b_n = B_WEIGHTS[i] * rec.utan1.dot(n);
436 let b_t = B_WEIGHTS[i] * rec.utan1.cross(n);
437 aa += a_n * a_n + BLEND * (a_t * a_t);
438 ab += a_n * b_n + BLEND * a_t * b_t;
439 ac += a_n * c_n + BLEND * a_t * c_t;
440 bb += b_n * b_n + BLEND * (b_t * b_t);
441 bc += b_n * c_n + BLEND * b_t * c_t;
442 }
443 let idet = 1.0 / (self.d * (aa * bb - ab * ab));
444 let delta_a = idet * (ac * bb - ab * bc);
445 let delta_b = idet * (aa * bc - ac * ab);
446 (a - delta_a, b - delta_b)
447 }
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 fn find_subdivision_point(&self, rec: &OffsetRec) -> SubdivisionPoint {
460 let t = 0.5 * (rec.t0 + rec.t1);
461 let q_t = self.q.eval(t).to_vec2();
462 let x0 = rec.utan0.cross(q_t).abs();
463 let x1 = rec.utan1.cross(q_t).abs();
464 const SUBDIVIDE_THRESH: f64 = 0.1;
465 if x0 > SUBDIVIDE_THRESH * x1 && x1 > SUBDIVIDE_THRESH * x0 {
466 let utan = q_t.normalize();
467 return SubdivisionPoint { t, utan };
468 }
469
470 // Note: do we want to track p0 & p3 in rec, to avoid repeated eval?
471 let chord = self.c.eval(rec.t1) - self.c.eval(rec.t0);
472 if chord.cross(rec.utan0) * chord.cross(rec.utan1) < 0.0 {
473 let tan = rec.utan0 + rec.utan1;
474 if let Some(subdivision) =
475 self.subdivide_for_tangent(rec.utan0, rec.t0, rec.t1, tan, false)
476 {
477 return subdivision;
478 }
479 }
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 let mut tangents: ArrayVec<Vec2, 4> = ArrayVec::new();
485 let mut ts: ArrayVec<f64, 4> = ArrayVec::new();
486 tangents.push(rec.utan0);
487 ts.push(rec.t0);
488 for t in self.c.inflections() {
489 if t > rec.t0 && t < rec.t1 {
490 tangents.push(self.q.eval(t).to_vec2());
491 ts.push(t);
492 }
493 }
494 tangents.push(rec.utan1);
495 ts.push(rec.t1);
496 let mut arc_angles: ArrayVec<f64, 3> = ArrayVec::new();
497 let mut sum = 0.0;
498 for i in 0..tangents.len() - 1 {
499 let tan0 = tangents[i];
500 let tan1 = tangents[i + 1];
501 let th = tan0.cross(tan1).atan2(tan0.dot(tan1));
502 sum += th.abs();
503 arc_angles.push(th);
504 }
505 let mut target = sum * 0.5;
506 let mut i = 0;
507 while arc_angles[i].abs() < target {
508 target -= arc_angles[i].abs();
509 i += 1;
510 }
511 let rotation = Vec2::from_angle(target.copysign(arc_angles[i]));
512 let base = tangents[i];
513 let tan = base.rotate_scale(rotation);
514 let utan0 = if i == 0 { rec.utan0 } else { base.normalize() };
515 self.subdivide_for_tangent(utan0, ts[i], ts[i + 1], tan, true)
516 .unwrap()
517 }
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 fn subdivide_for_tangent(
528 &self,
529 utan0: Vec2,
530 t0: f64,
531 t1: f64,
532 tan: Vec2,
533 force: bool,
534 ) -> Option<SubdivisionPoint> {
535 let mut t = 0.0;
536 let mut n_soln = 0;
537 // set up quadratic equation for matching tangents
538 let z0 = tan.cross(self.q.p0.to_vec2());
539 let z1 = tan.cross(self.q.p1.to_vec2());
540 let z2 = tan.cross(self.q.p2.to_vec2());
541 let c0 = z0;
542 let c1 = 2.0 * (z1 - z0);
543 let c2 = (z2 - z1) - (z1 - z0);
544 for root in solve_quadratic(c0, c1, c2) {
545 if root >= t0 && root <= t1 {
546 t = root;
547 n_soln += 1;
548 }
549 }
550 if n_soln != 1 {
551 if !force {
552 return None;
553 }
554 // Numerical failure, try to subdivide at cusp; we pick the
555 // smaller derivative.
556 if self.q.eval(t0).to_vec2().length_squared()
557 > self.q.eval(t1).to_vec2().length_squared()
558 {
559 t = t1;
560 } else {
561 t = t0;
562 }
563 }
564 let q = self.q.eval(t).to_vec2();
565 const UTAN_EPSILON: f64 = 1e-12;
566 let utan = if n_soln == 1 && q.length_squared() >= UTAN_EPSILON {
567 q.normalize()
568 } else if tan.length_squared() >= UTAN_EPSILON {
569 // Curve has a zero-derivative cusp but angles well defined
570 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 utan0.turn_90()
575 };
576 Some(SubdivisionPoint { t, utan })
577 }
578}
579
580impl OffsetRec {
581 #[allow(clippy::too_many_arguments)]
582 fn new(
583 t0: f64,
584 t1: f64,
585 utan0: Vec2,
586 utan1: Vec2,
587 cusp0: f64,
588 cusp1: f64,
589 depth: usize,
590 ) -> Self {
591 OffsetRec {
592 t0,
593 t1,
594 utan0,
595 utan1,
596 cusp0,
597 cusp1,
598 depth,
599 }
600 }
601}
602
603/// Compute Bézier weights for evenly subdivided t values.
604const fn mk_a_weights(rev: bool) -> [f64; N_LSE] {
605 let mut result = [0.0; N_LSE];
606 let mut i = 0;
607 while i < N_LSE {
608 let t = (i + 1) as f64 / (N_LSE + 1) as f64;
609 let mt = 1. - t;
610 let ix = if rev { N_LSE - 1 - i } else { i };
611 result[ix] = 3.0 * mt * t * mt;
612 i += 1;
613 }
614 result
615}
616
617const A_WEIGHTS: [f64; N_LSE] = mk_a_weights(false);
618const B_WEIGHTS: [f64; N_LSE] = mk_a_weights(true);
619
620#[cfg(test)]
621mod 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}