1 | // Copyright 2022 the Kurbo Authors
|
2 | // SPDX-License-Identifier: Apache-2.0 OR MIT
|
3 |
|
4 | //! Simplification of a Bézier path.
|
5 | //!
|
6 | //! This module is currently experimental.
|
7 | //!
|
8 | //! The methods in this module create a `SimplifyBezPath` object, which can then
|
9 | //! be fed to [`fit_to_bezpath`] or [`fit_to_bezpath_opt`] depending on the degree
|
10 | //! of optimization desired.
|
11 | //!
|
12 | //! The implementation uses a number of techniques to achieve high performance and
|
13 | //! accuracy. The parameter (generally written `t`) evenly divides the curve segments
|
14 | //! in the original, so sampling can be done in constant time. The derivatives are
|
15 | //! computed analytically, as that is straightforward with Béziers.
|
16 | //!
|
17 | //! The areas and moments are computed analytically (using Green's theorem), and
|
18 | //! the prefix sum is stored. Thus, it is possible to analytically compute the area
|
19 | //! and moment of any subdivision of the curve, also in constant time, by taking
|
20 | //! the difference of two stored prefix sum values, then fixing up the subsegments.
|
21 | //!
|
22 | //! A current limitation (hoped to be addressed in the future) is that non-regular
|
23 | //! cubic segments may have tangents computed incorrectly. This can easily happen,
|
24 | //! for example when setting a control point equal to an endpoint.
|
25 | //!
|
26 | //! In addition, this method does not report corners (adjoining segments where the
|
27 | //! tangents are not continuous). It is not clear whether it's best to handle such
|
28 | //! cases here, or in client code.
|
29 | //!
|
30 | //! [`fit_to_bezpath`]: crate::fit_to_bezpath
|
31 | //! [`fit_to_bezpath_opt`]: crate::fit_to_bezpath_opt
|
32 |
|
33 | use alloc::vec::Vec;
|
34 |
|
35 | use core::ops::Range;
|
36 |
|
37 | #[cfg (not(feature = "std" ))]
|
38 | use crate::common::FloatFuncs;
|
39 |
|
40 | use crate::{
|
41 | fit_to_bezpath, fit_to_bezpath_opt, BezPath, CubicBez, CurveFitSample, Line, ParamCurve,
|
42 | ParamCurveDeriv, ParamCurveFit, PathEl, PathSeg, Point, QuadBez, Vec2,
|
43 | };
|
44 |
|
45 | /// A Bézier path which has been prepared for simplification.
|
46 | ///
|
47 | /// See the [module-level documentation] for a bit more discussion of the approach,
|
48 | /// and how this struct is to be used.
|
49 | ///
|
50 | /// [module-level documentation]: crate::simplify
|
51 | pub struct SimplifyBezPath(Vec<SimplifyCubic>);
|
52 |
|
53 | struct SimplifyCubic {
|
54 | c: CubicBez,
|
55 | // The inclusive prefix sum of the moment integrals
|
56 | moments: (f64, f64, f64),
|
57 | }
|
58 |
|
59 | /// Options for simplifying paths.
|
60 | pub struct SimplifyOptions {
|
61 | /// The tangent of the minimum angle below which the path is considered smooth.
|
62 | angle_thresh: f64,
|
63 | opt_level: SimplifyOptLevel,
|
64 | }
|
65 |
|
66 | /// Optimization level for simplification.
|
67 | pub enum SimplifyOptLevel {
|
68 | /// Subdivide; faster but not as optimized results.
|
69 | Subdivide,
|
70 | /// Optimize subdivision points.
|
71 | Optimize,
|
72 | }
|
73 |
|
74 | impl Default for SimplifyOptions {
|
75 | fn default() -> Self {
|
76 | let opt_level: SimplifyOptLevel = SimplifyOptLevel::Subdivide;
|
77 | SimplifyOptions {
|
78 | angle_thresh: 1e-3,
|
79 | opt_level,
|
80 | }
|
81 | }
|
82 | }
|
83 |
|
84 | #[doc (hidden)]
|
85 | /// Compute moment integrals.
|
86 | ///
|
87 | /// This is exposed for testing purposes but is an internal detail. We can
|
88 | /// add to the public, documented interface if there is a use case.
|
89 | pub fn moment_integrals(c: CubicBez) -> (f64, f64, f64) {
|
90 | let (x0, y0) = (c.p0.x, c.p0.y);
|
91 | let (x1, y1) = (c.p1.x - x0, c.p1.y - y0);
|
92 | let (x2, y2) = (c.p2.x - x0, c.p2.y - y0);
|
93 | let (x3, y3) = (c.p3.x - x0, c.p3.y - y0);
|
94 |
|
95 | let r0 = 3. * x1;
|
96 | let r1 = 3. * y1;
|
97 | let r2 = x2 * y3;
|
98 | let r3 = x3 * y2;
|
99 | let r4 = x3 * y3;
|
100 | let r5 = 27. * y1;
|
101 | let r6 = x1 * x2;
|
102 | let r7 = 27. * y2;
|
103 | let r8 = 45. * r2;
|
104 | let r9 = 18. * x3;
|
105 | let r10 = x1 * y1;
|
106 | let r11 = 30. * x1;
|
107 | let r12 = 45. * x3;
|
108 | let r13 = x2 * y1;
|
109 | let r14 = 45. * r3;
|
110 | let r15 = x1.powi(2);
|
111 | let r16 = 18. * y3;
|
112 | let r17 = x2.powi(2);
|
113 | let r18 = 45. * y3;
|
114 | let r19 = x3.powi(2);
|
115 | let r20 = 30. * y1;
|
116 | let r21 = y2.powi(2);
|
117 | let r22 = y3.powi(2);
|
118 | let r23 = y1.powi(2);
|
119 | let a = -r0 * y2 - r0 * y3 + r1 * x2 + r1 * x3 - 6. * r2 + 6. * r3 + 10. * r4;
|
120 |
|
121 | // Scale and add chord
|
122 | let lift = x3 * y0;
|
123 | let area = a * 0.05 + lift;
|
124 | let x = r10 * r9 - r11 * r4 + r12 * r13 + r14 * x2 - r15 * r16 - r15 * r7 - r17 * r18
|
125 | + r17 * r5
|
126 | + r19 * r20
|
127 | + 105. * r19 * y2
|
128 | + 280. * r19 * y3
|
129 | - 105. * r2 * x3
|
130 | + r5 * r6
|
131 | - r6 * r7
|
132 | - r8 * x1;
|
133 | let y = -r10 * r16 - r10 * r7 - r11 * r22 + r12 * r21 + r13 * r7 + r14 * y1 - r18 * x1 * y2
|
134 | + r20 * r4
|
135 | - 27. * r21 * x1
|
136 | - 105. * r22 * x2
|
137 | + 140. * r22 * x3
|
138 | + r23 * r9
|
139 | + 27. * r23 * x2
|
140 | + 105. * r3 * y3
|
141 | - r8 * y2;
|
142 |
|
143 | let mx = x * (1. / 840.) + x0 * area + 0.5 * x3 * lift;
|
144 | let my = y * (1. / 420.) + y0 * a * 0.1 + y0 * lift;
|
145 |
|
146 | (area, mx, my)
|
147 | }
|
148 |
|
149 | impl SimplifyBezPath {
|
150 | /// Set up a new Bézier path for simplification.
|
151 | ///
|
152 | /// Currently this is not dealing with discontinuities at all, but it
|
153 | /// could be extended to do so.
|
154 | pub fn new(path: impl IntoIterator<Item = PathEl>) -> Self {
|
155 | let (mut a, mut x, mut y) = (0.0, 0.0, 0.0);
|
156 | let els = crate::segments(path)
|
157 | .map(|seg| {
|
158 | let c = seg.to_cubic();
|
159 | let (ai, xi, yi) = moment_integrals(c);
|
160 | a += ai;
|
161 | x += xi;
|
162 | y += yi;
|
163 | SimplifyCubic {
|
164 | c,
|
165 | moments: (a, x, y),
|
166 | }
|
167 | })
|
168 | .collect();
|
169 | SimplifyBezPath(els)
|
170 | }
|
171 |
|
172 | /// Resolve a `t` value to a cubic.
|
173 | ///
|
174 | /// Also return the resulting `t` value for the selected cubic.
|
175 | fn scale(&self, t: f64) -> (usize, f64) {
|
176 | let t_scale = t * self.0.len() as f64;
|
177 | let t_floor = t_scale.floor();
|
178 | (t_floor as usize, t_scale - t_floor)
|
179 | }
|
180 |
|
181 | fn moment_integrals(&self, i: usize, range: Range<f64>) -> (f64, f64, f64) {
|
182 | if range.end == range.start {
|
183 | (0.0, 0.0, 0.0)
|
184 | } else {
|
185 | moment_integrals(self.0[i].c.subsegment(range))
|
186 | }
|
187 | }
|
188 | }
|
189 |
|
190 | impl ParamCurveFit for SimplifyBezPath {
|
191 | fn sample_pt_deriv(&self, t: f64) -> (Point, Vec2) {
|
192 | let (mut i, mut t0) = self.scale(t);
|
193 | let n = self.0.len();
|
194 | if i == n {
|
195 | i -= 1;
|
196 | t0 = 1.0;
|
197 | }
|
198 | let c = self.0[i].c;
|
199 | (c.eval(t0), c.deriv().eval(t0).to_vec2() * n as f64)
|
200 | }
|
201 |
|
202 | fn sample_pt_tangent(&self, t: f64, _: f64) -> CurveFitSample {
|
203 | let (mut i, mut t0) = self.scale(t);
|
204 | if i == self.0.len() {
|
205 | i -= 1;
|
206 | t0 = 1.0;
|
207 | }
|
208 | let c = self.0[i].c;
|
209 | let p = c.eval(t0);
|
210 | let tangent = c.deriv().eval(t0).to_vec2();
|
211 | CurveFitSample { p, tangent }
|
212 | }
|
213 |
|
214 | // We could use the default implementation, but provide our own, mostly
|
215 | // because it is possible to efficiently provide an analytically accurate
|
216 | // answer.
|
217 | fn moment_integrals(&self, range: Range<f64>) -> (f64, f64, f64) {
|
218 | let (i0, t0) = self.scale(range.start);
|
219 | let (i1, t1) = self.scale(range.end);
|
220 | if i0 == i1 {
|
221 | self.moment_integrals(i0, t0..t1)
|
222 | } else {
|
223 | let (a0, x0, y0) = self.moment_integrals(i0, t0..1.0);
|
224 | let (a1, x1, y1) = self.moment_integrals(i1, 0.0..t1);
|
225 | let (mut a, mut x, mut y) = (a0 + a1, x0 + x1, y0 + y1);
|
226 | if i1 > i0 + 1 {
|
227 | let (a2, x2, y2) = self.0[i0].moments;
|
228 | let (a3, x3, y3) = self.0[i1 - 1].moments;
|
229 | a += a3 - a2;
|
230 | x += x3 - x2;
|
231 | y += y3 - y2;
|
232 | }
|
233 | (a, x, y)
|
234 | }
|
235 | }
|
236 |
|
237 | fn break_cusp(&self, _: Range<f64>) -> Option<f64> {
|
238 | None
|
239 | }
|
240 | }
|
241 |
|
242 | #[derive (Default)]
|
243 | struct SimplifyState {
|
244 | queue: BezPath,
|
245 | result: BezPath,
|
246 | needs_moveto: bool,
|
247 | }
|
248 |
|
249 | impl SimplifyState {
|
250 | fn add_seg(&mut self, seg: PathSeg) {
|
251 | if self.queue.is_empty() {
|
252 | self.queue.move_to(seg.start());
|
253 | }
|
254 | match seg {
|
255 | PathSeg::Quad(q) => self.queue.quad_to(q.p1, q.p2),
|
256 | PathSeg::Cubic(c) => self.queue.curve_to(c.p1, c.p2, c.p3),
|
257 | _ => unreachable!(),
|
258 | }
|
259 | }
|
260 |
|
261 | fn flush(&mut self, accuracy: f64, options: &SimplifyOptions) {
|
262 | if self.queue.is_empty() {
|
263 | return;
|
264 | }
|
265 | // TODO: if queue is one segment, just output that
|
266 | let s = SimplifyBezPath::new(&self.queue);
|
267 | let b = match options.opt_level {
|
268 | SimplifyOptLevel::Subdivide => fit_to_bezpath(&s, accuracy),
|
269 | SimplifyOptLevel::Optimize => fit_to_bezpath_opt(&s, accuracy),
|
270 | };
|
271 | self.result
|
272 | .extend(b.iter().skip(!self.needs_moveto as usize));
|
273 | self.needs_moveto = false;
|
274 | self.queue.truncate(0);
|
275 | }
|
276 | }
|
277 |
|
278 | /// Simplify a Bézier path.
|
279 | ///
|
280 | /// This function simplifies an arbitrary Bézier path; it is designed to handle
|
281 | /// multiple subpaths and also corners.
|
282 | pub fn simplify_bezpath(
|
283 | path: impl IntoIterator<Item = PathEl>,
|
284 | accuracy: f64,
|
285 | options: &SimplifyOptions,
|
286 | ) -> BezPath {
|
287 | let mut last_pt = None;
|
288 | let mut last_seg: Option<PathSeg> = None;
|
289 | let mut state = SimplifyState::default();
|
290 | for el in path {
|
291 | let mut this_seg = None;
|
292 | match el {
|
293 | PathEl::MoveTo(p) => {
|
294 | state.flush(accuracy, options);
|
295 | state.needs_moveto = true;
|
296 | last_pt = Some(p);
|
297 | }
|
298 | PathEl::LineTo(p) => {
|
299 | let last = last_pt.unwrap();
|
300 | if last == p {
|
301 | continue;
|
302 | }
|
303 | state.flush(accuracy, options);
|
304 | this_seg = Some(PathSeg::Line(Line::new(last, p)));
|
305 | }
|
306 | PathEl::QuadTo(p1, p2) => {
|
307 | let last = last_pt.unwrap();
|
308 | if last == p1 && last == p2 {
|
309 | continue;
|
310 | }
|
311 | this_seg = Some(PathSeg::Quad(QuadBez::new(last, p1, p2)));
|
312 | }
|
313 | PathEl::CurveTo(p1, p2, p3) => {
|
314 | let last = last_pt.unwrap();
|
315 | if last == p1 && last == p2 && last == p3 {
|
316 | continue;
|
317 | }
|
318 | this_seg = Some(PathSeg::Cubic(CubicBez::new(last, p1, p2, p3)));
|
319 | }
|
320 | PathEl::ClosePath => {
|
321 | state.flush(accuracy, options);
|
322 | state.result.close_path();
|
323 | state.needs_moveto = true;
|
324 | last_seg = None;
|
325 | continue;
|
326 | }
|
327 | }
|
328 | if let Some(seg) = this_seg {
|
329 | if let Some(last) = last_seg {
|
330 | let last_tan = last.tangents().1;
|
331 | let this_tan = seg.tangents().0;
|
332 | if last_tan.cross(this_tan).abs()
|
333 | > last_tan.dot(this_tan).abs() * options.angle_thresh
|
334 | {
|
335 | state.flush(accuracy, options);
|
336 | }
|
337 | }
|
338 | last_pt = Some(seg.end());
|
339 | state.add_seg(seg);
|
340 | }
|
341 | last_seg = this_seg;
|
342 | }
|
343 | state.flush(accuracy, options);
|
344 | state.result
|
345 | }
|
346 |
|
347 | impl SimplifyOptions {
|
348 | /// Set optimization level.
|
349 | pub fn opt_level(mut self, level: SimplifyOptLevel) -> Self {
|
350 | self.opt_level = level;
|
351 | self
|
352 | }
|
353 |
|
354 | /// Set angle threshold.
|
355 | ///
|
356 | /// The tangent of the angle below which joins are considered smooth and
|
357 | /// not corners. The default is approximately 1 milliradian.
|
358 | pub fn angle_thresh(mut self, thresh: f64) -> Self {
|
359 | self.angle_thresh = thresh;
|
360 | self
|
361 | }
|
362 | }
|
363 | |