| 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::Line(l) => self.queue.line_to(l.p1), |
| 256 | PathSeg::Quad(q) => self.queue.quad_to(q.p1, q.p2), |
| 257 | PathSeg::Cubic(c) => self.queue.curve_to(c.p1, c.p2, c.p3), |
| 258 | } |
| 259 | } |
| 260 | |
| 261 | fn flush(&mut self, accuracy: f64, options: &SimplifyOptions) { |
| 262 | if self.queue.is_empty() { |
| 263 | return; |
| 264 | } |
| 265 | if self.queue.elements().len() == 2 { |
| 266 | // Queue is just one segment (count is moveto + primitive) |
| 267 | // Just output the segment, no simplification is possible. |
| 268 | self.result |
| 269 | .extend(self.queue.iter().skip(!self.needs_moveto as usize)); |
| 270 | } else { |
| 271 | let s = SimplifyBezPath::new(&self.queue); |
| 272 | let b = match options.opt_level { |
| 273 | SimplifyOptLevel::Subdivide => fit_to_bezpath(&s, accuracy), |
| 274 | SimplifyOptLevel::Optimize => fit_to_bezpath_opt(&s, accuracy), |
| 275 | }; |
| 276 | self.result |
| 277 | .extend(b.iter().skip(!self.needs_moveto as usize)); |
| 278 | } |
| 279 | self.needs_moveto = false; |
| 280 | self.queue.truncate(0); |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | /// Simplify a Bézier path. |
| 285 | /// |
| 286 | /// This function simplifies an arbitrary Bézier path; it is designed to handle |
| 287 | /// multiple subpaths and also corners. |
| 288 | /// |
| 289 | /// The underlying curve-fitting approach works best if the source path is very |
| 290 | /// smooth. If it contains higher frequency noise, then results may be poor, as |
| 291 | /// the resulting curve matches the original with G1 continuity at each subdivision |
| 292 | /// point, and also preserves the area. For such inputs, consider some form of |
| 293 | /// smoothing or low-pass filtering before simplification. In particular, if the |
| 294 | /// input is derived from a sequence of points, consider fitting a smooth spline. |
| 295 | /// |
| 296 | /// We may add such capabilities in the future, possibly as opt-in smoothing |
| 297 | /// specified through the options. |
| 298 | pub fn simplify_bezpath( |
| 299 | path: impl IntoIterator<Item = PathEl>, |
| 300 | accuracy: f64, |
| 301 | options: &SimplifyOptions, |
| 302 | ) -> BezPath { |
| 303 | let mut last_pt = None; |
| 304 | let mut last_seg: Option<PathSeg> = None; |
| 305 | let mut state = SimplifyState::default(); |
| 306 | for el in path { |
| 307 | let mut this_seg = None; |
| 308 | match el { |
| 309 | PathEl::MoveTo(p) => { |
| 310 | state.flush(accuracy, options); |
| 311 | state.needs_moveto = true; |
| 312 | last_pt = Some(p); |
| 313 | } |
| 314 | PathEl::LineTo(p) => { |
| 315 | let last = last_pt.unwrap(); |
| 316 | if last == p { |
| 317 | continue; |
| 318 | } |
| 319 | this_seg = Some(PathSeg::Line(Line::new(last, p))); |
| 320 | } |
| 321 | PathEl::QuadTo(p1, p2) => { |
| 322 | let last = last_pt.unwrap(); |
| 323 | if last == p1 && last == p2 { |
| 324 | continue; |
| 325 | } |
| 326 | this_seg = Some(PathSeg::Quad(QuadBez::new(last, p1, p2))); |
| 327 | } |
| 328 | PathEl::CurveTo(p1, p2, p3) => { |
| 329 | let last = last_pt.unwrap(); |
| 330 | if last == p1 && last == p2 && last == p3 { |
| 331 | continue; |
| 332 | } |
| 333 | this_seg = Some(PathSeg::Cubic(CubicBez::new(last, p1, p2, p3))); |
| 334 | } |
| 335 | PathEl::ClosePath => { |
| 336 | state.flush(accuracy, options); |
| 337 | state.result.close_path(); |
| 338 | state.needs_moveto = true; |
| 339 | last_seg = None; |
| 340 | continue; |
| 341 | } |
| 342 | } |
| 343 | if let Some(seg) = this_seg { |
| 344 | if let Some(last) = last_seg { |
| 345 | let last_tan = last.tangents().1; |
| 346 | let this_tan = seg.tangents().0; |
| 347 | if last_tan.cross(this_tan).abs() |
| 348 | > last_tan.dot(this_tan).abs() * options.angle_thresh |
| 349 | { |
| 350 | state.flush(accuracy, options); |
| 351 | } |
| 352 | } |
| 353 | last_pt = Some(seg.end()); |
| 354 | state.add_seg(seg); |
| 355 | } |
| 356 | last_seg = this_seg; |
| 357 | } |
| 358 | state.flush(accuracy, options); |
| 359 | state.result |
| 360 | } |
| 361 | |
| 362 | impl SimplifyOptions { |
| 363 | /// Set optimization level. |
| 364 | pub fn opt_level(mut self, level: SimplifyOptLevel) -> Self { |
| 365 | self.opt_level = level; |
| 366 | self |
| 367 | } |
| 368 | |
| 369 | /// Set angle threshold. |
| 370 | /// |
| 371 | /// The tangent of the angle below which joins are considered smooth and |
| 372 | /// not corners. The default is approximately 1 milliradian. |
| 373 | pub fn angle_thresh(mut self, thresh: f64) -> Self { |
| 374 | self.angle_thresh = thresh; |
| 375 | self |
| 376 | } |
| 377 | } |
| 378 | |
| 379 | #[cfg (test)] |
| 380 | mod tests { |
| 381 | use crate::BezPath; |
| 382 | |
| 383 | use super::{simplify_bezpath, SimplifyOptions}; |
| 384 | |
| 385 | #[test ] |
| 386 | fn simplify_lines_corner() { |
| 387 | // Make sure lines are passed through unchanged if there is a corner. |
| 388 | let mut path = BezPath::new(); |
| 389 | path.move_to((1., 2.)); |
| 390 | path.line_to((3., 4.)); |
| 391 | path.line_to((10., 5.)); |
| 392 | let options = SimplifyOptions::default(); |
| 393 | let simplified = simplify_bezpath(path.clone(), 1.0, &options); |
| 394 | assert_eq!(path, simplified); |
| 395 | } |
| 396 | } |
| 397 | |