Coverage Report

Created: 2025-11-16 06:56

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}