/rust/registry/src/index.crates.io-6f17d22bba15001f/kurbo-0.11.3/src/offset.rs
Line | Count | Source (jump to first uncovered line) |
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, based on a curve fitting |
5 | | //! approach. |
6 | | //! |
7 | | //! See the [Parallel curves of cubic Béziers] blog post for a discussion of how |
8 | | //! this algorithm works and what kind of results can be expected. In general, it |
9 | | //! is expected to perform much better than most published algorithms. The number |
10 | | //! of curve segments needed to attain a given accuracy scales as O(n^6) with |
11 | | //! accuracy. |
12 | | //! |
13 | | //! In general, to compute the offset curve (also known as parallel curve) of |
14 | | //! a cubic Bézier segment, create a [`CubicOffset`] struct with the curve |
15 | | //! segment and offset, then use [`fit_to_bezpath`] or [`fit_to_bezpath_opt`] |
16 | | //! depending on how much time to spend optimizing the resulting path. |
17 | | //! |
18 | | //! [`fit_to_bezpath`]: crate::fit_to_bezpath |
19 | | //! [`fit_to_bezpath_opt`]: crate::fit_to_bezpath_opt |
20 | | //! [Parallel curves of cubic Béziers]: https://raphlinus.github.io/curves/2022/09/09/parallel-beziers.html |
21 | | use core::ops::Range; |
22 | | |
23 | | #[cfg(not(feature = "std"))] |
24 | | use crate::common::FloatFuncs; |
25 | | |
26 | | use crate::{ |
27 | | common::solve_itp, CubicBez, CurveFitSample, ParamCurve, ParamCurveDeriv, ParamCurveFit, Point, |
28 | | QuadBez, Vec2, |
29 | | }; |
30 | | |
31 | | /// The offset curve of a cubic Bézier. |
32 | | /// |
33 | | /// This is a representation of the offset curve of a cubic Bézier segment, for |
34 | | /// purposes of curve fitting. |
35 | | /// |
36 | | /// See the [module-level documentation] for a bit more discussion of the approach, |
37 | | /// and how this struct is to be used. |
38 | | /// |
39 | | /// [module-level documentation]: crate::offset |
40 | | pub struct CubicOffset { |
41 | | /// Source curve. |
42 | | c: CubicBez, |
43 | | /// Derivative of source curve. |
44 | | q: QuadBez, |
45 | | /// Offset. |
46 | | d: f64, |
47 | | // c0 + c1 t + c2 t^2 is the cross product of second and first |
48 | | // derivatives of the underlying cubic, multiplied by offset (for |
49 | | // computing cusp). |
50 | | c0: f64, |
51 | | c1: f64, |
52 | | c2: f64, |
53 | | } |
54 | | |
55 | | impl CubicOffset { |
56 | | /// Create a new curve from Bézier segment and offset. |
57 | | /// |
58 | | /// This method should only be used if the Bézier is smooth. Use |
59 | | /// [`new_regularized`] instead to deal with a wider range of inputs. |
60 | | /// |
61 | | /// [`new_regularized`]: Self::new_regularized |
62 | 0 | pub fn new(c: CubicBez, d: f64) -> Self { |
63 | 0 | let q = c.deriv(); |
64 | 0 | let d0 = q.p0.to_vec2(); |
65 | 0 | let d1 = 2.0 * (q.p1 - q.p0); |
66 | 0 | let d2 = q.p0.to_vec2() - 2.0 * q.p1.to_vec2() + q.p2.to_vec2(); |
67 | 0 | CubicOffset { |
68 | 0 | c, |
69 | 0 | q, |
70 | 0 | d, |
71 | 0 | c0: d * d1.cross(d0), |
72 | 0 | c1: d * 2.0 * d2.cross(d0), |
73 | 0 | c2: d * d2.cross(d1), |
74 | 0 | } |
75 | 0 | } |
76 | | |
77 | | /// Create a new curve from Bézier segment and offset, with numerical robustness tweaks. |
78 | | /// |
79 | | /// The dimension represents a minimum feature size; the regularization is allowed to |
80 | | /// perturb the curve by this amount in order to improve the robustness. |
81 | 0 | pub fn new_regularized(c: CubicBez, d: f64, dimension: f64) -> Self { |
82 | 0 | Self::new(c.regularize(dimension), d) |
83 | 0 | } |
84 | | |
85 | 0 | fn eval_offset(&self, t: f64) -> Vec2 { |
86 | 0 | let dp = self.q.eval(t).to_vec2(); |
87 | 0 | let norm = Vec2::new(-dp.y, dp.x); |
88 | 0 | // TODO: deal with hypot = 0 |
89 | 0 | norm * self.d / dp.hypot() |
90 | 0 | } |
91 | | |
92 | 0 | fn eval(&self, t: f64) -> Point { |
93 | 0 | // Point on source curve. |
94 | 0 | self.c.eval(t) + self.eval_offset(t) |
95 | 0 | } |
96 | | |
97 | | /// Evaluate derivative of curve. |
98 | 0 | fn eval_deriv(&self, t: f64) -> Vec2 { |
99 | 0 | self.cusp_sign(t) * self.q.eval(t).to_vec2() |
100 | 0 | } |
101 | | |
102 | | // Compute a function which has a zero-crossing at cusps, and is |
103 | | // positive at low curvatures on the source curve. |
104 | 0 | fn cusp_sign(&self, t: f64) -> f64 { |
105 | 0 | let ds2 = self.q.eval(t).to_vec2().hypot2(); |
106 | 0 | ((self.c2 * t + self.c1) * t + self.c0) / (ds2 * ds2.sqrt()) + 1.0 |
107 | 0 | } |
108 | | } |
109 | | |
110 | | impl ParamCurveFit for CubicOffset { |
111 | 0 | fn sample_pt_tangent(&self, t: f64, sign: f64) -> CurveFitSample { |
112 | 0 | let p = self.eval(t); |
113 | | const CUSP_EPS: f64 = 1e-8; |
114 | 0 | let mut cusp = self.cusp_sign(t); |
115 | 0 | if cusp.abs() < CUSP_EPS { |
116 | 0 | // This is a numerical derivative, which is probably good enough |
117 | 0 | // for all practical purposes, but an analytical derivative would |
118 | 0 | // be more elegant. |
119 | 0 | // |
120 | 0 | // Also, we're not dealing with second or higher order cusps. |
121 | 0 | cusp = sign * (self.cusp_sign(t + CUSP_EPS) - self.cusp_sign(t - CUSP_EPS)); |
122 | 0 | } |
123 | 0 | let tangent = self.q.eval(t).to_vec2() * cusp.signum(); |
124 | 0 | CurveFitSample { p, tangent } |
125 | 0 | } |
126 | | |
127 | 0 | fn sample_pt_deriv(&self, t: f64) -> (Point, Vec2) { |
128 | 0 | (self.eval(t), self.eval_deriv(t)) |
129 | 0 | } |
130 | | |
131 | 0 | fn break_cusp(&self, range: Range<f64>) -> Option<f64> { |
132 | | const CUSP_EPS: f64 = 1e-8; |
133 | | // When an endpoint is on (or very near) a cusp, move just far enough |
134 | | // away from the cusp that we're confident we have the right sign. |
135 | 0 | let break_cusp_help = |mut x, mut d| { |
136 | 0 | let mut cusp = self.cusp_sign(x); |
137 | 0 | while cusp.abs() < CUSP_EPS && d < 1.0 { |
138 | 0 | x += d; |
139 | 0 | let old_cusp = cusp; |
140 | 0 | cusp = self.cusp_sign(x); |
141 | 0 | if cusp.abs() > old_cusp.abs() { |
142 | 0 | break; |
143 | 0 | } |
144 | 0 | d *= 2.0; |
145 | | } |
146 | 0 | (x, cusp) |
147 | 0 | }; |
148 | 0 | let (a, cusp0) = break_cusp_help(range.start, 1e-12); |
149 | 0 | let (b, cusp1) = break_cusp_help(range.end, -1e-12); |
150 | 0 | if a >= b || cusp0 * cusp1 >= 0.0 { |
151 | | // Discussion point: maybe we should search for double cusps in the interior |
152 | | // of the range. |
153 | 0 | return None; |
154 | 0 | } |
155 | 0 | let s = cusp1.signum(); |
156 | 0 | let f = |t| s * self.cusp_sign(t); |
157 | 0 | let k1 = 0.2 / (b - a); |
158 | | const ITP_EPS: f64 = 1e-12; |
159 | 0 | let x = solve_itp(f, a, b, ITP_EPS, 1, k1, s * cusp0, s * cusp1); |
160 | 0 | Some(x) |
161 | 0 | } |
162 | | } |
163 | | |
164 | | #[cfg(test)] |
165 | | mod tests { |
166 | | use super::CubicOffset; |
167 | | use crate::{fit_to_bezpath, fit_to_bezpath_opt, CubicBez, PathEl}; |
168 | | |
169 | | // This test tries combinations of parameters that have caused problems in the past. |
170 | | #[test] |
171 | | fn pathological_curves() { |
172 | | let curve = CubicBez { |
173 | | p0: (-1236.3746269978635, 152.17981429574826).into(), |
174 | | p1: (-1175.18662093517, 108.04721798590596).into(), |
175 | | p2: (-1152.142883879584, 105.76260301083356).into(), |
176 | | p3: (-1151.842639804639, 105.73040758939104).into(), |
177 | | }; |
178 | | let offset = 3603.7267536453924; |
179 | | let accuracy = 0.1; |
180 | | let offset_path = CubicOffset::new(curve, offset); |
181 | | let path = fit_to_bezpath_opt(&offset_path, accuracy); |
182 | | assert!(matches!(path.iter().next(), Some(PathEl::MoveTo(_)))); |
183 | | let path_opt = fit_to_bezpath(&offset_path, accuracy); |
184 | | assert!(matches!(path_opt.iter().next(), Some(PathEl::MoveTo(_)))); |
185 | | } |
186 | | |
187 | | /// Cubic offset that used to trigger infinite recursion. |
188 | | #[test] |
189 | | fn infinite_recursion() { |
190 | | const DIM_TUNE: f64 = 0.25; |
191 | | const TOLERANCE: f64 = 0.1; |
192 | | let c = CubicBez::new( |
193 | | (1096.2962962962963, 593.90243902439033), |
194 | | (1043.6213991769548, 593.90243902439033), |
195 | | (1030.4526748971193, 593.90243902439033), |
196 | | (1056.7901234567901, 593.90243902439033), |
197 | | ); |
198 | | let co = CubicOffset::new_regularized(c, -0.5, DIM_TUNE * TOLERANCE); |
199 | | fit_to_bezpath(&co, TOLERANCE); |
200 | | } |
201 | | |
202 | | #[test] |
203 | | fn test_cubic_offset_simple_line() { |
204 | | let cubic = CubicBez::new((0., 0.), (10., 0.), (20., 0.), (30., 0.)); |
205 | | let offset = CubicOffset::new(cubic, 5.); |
206 | | let _optimized = fit_to_bezpath(&offset, 1e-6); |
207 | | } |
208 | | } |