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
33use alloc::vec::Vec;
34
35use core::ops::Range;
36
37#[cfg(not(feature = "std"))]
38use crate::common::FloatFuncs;
39
40use 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
51pub struct SimplifyBezPath(Vec<SimplifyCubic>);
52
53struct 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.
60pub 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.
67pub enum SimplifyOptLevel {
68 /// Subdivide; faster but not as optimized results.
69 Subdivide,
70 /// Optimize subdivision points.
71 Optimize,
72}
73
74impl 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.
89pub 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
149impl 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
190impl 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)]
243struct SimplifyState {
244 queue: BezPath,
245 result: BezPath,
246 needs_moveto: bool,
247}
248
249impl 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.
282pub 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
347impl 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