simplify_rs/
lib.rs

1//! Algorithm for simplifying points to a bezier curve
2//!
3//! ## Example
4//!
5//! ```rust
6//! // example: https://bit.ly/2UYdxLw
7//! let points = vec![
8//!     Point { x:256.0,                    y:318.0},
9//!     Point { x:258.6666666666667,        y:315.3333333333333},
10//!     Point { x:266.6666666666667,        y:308.6666666666667},
11//!     Point { x:314.0,                    y:274.6666666666667},
12//!     Point { x:389.3333333333333,        y:218.0},
13//!     Point { x:448.6666666666667,        y:176.0},
14//!     Point { x:472.0,                    y:160.66666666666666},
15//!     Point { x:503.3333333333333,        y:145.33333333333334},
16//!     Point { x:516.0,                    y:144.66666666666666},
17//!     Point { x:520.0,                    y:156.66666666666666},
18//!     Point { x:479.3333333333333,        y:220.66666666666666},
19//!     Point { x:392.6666666666667,        y:304.0},
20//!     Point { x:314.0,                    y:376.6666666666667},
21//!     Point { x:253.33333333333334,       y:436.6666666666667},
22//!     Point { x:238.0,                    y:454.6666666666667},
23//!     Point { x:228.66666666666666,       y:468.0},
24//!     Point { x:236.0,                    y:467.3333333333333},
25//!     Point { x:293.3333333333333,        y:428.0},
26//!     Point { x:428.0,                    y:337.3333333333333},
27//!     Point { x:516.6666666666666,        y:283.3333333333333},
28//!     Point { x:551.3333333333334,        y:262.0},
29//!     Point { x:566.6666666666666,        y:253.33333333333334},
30//!     Point { x:579.3333333333334,        y:246.0},
31//!     Point { x:590.0,                    y:241.33333333333334},
32//!     Point { x:566.6666666666666,        y:260.0},
33//!     Point { x:532.0,                    y:290.6666666666667},
34//!     Point { x:516.6666666666666,        y:306.0},
35//!     Point { x:510.6666666666667,        y:313.3333333333333},
36//!     Point { x:503.3333333333333,        y:324.6666666666667},
37//!     Point { x:527.3333333333334,        y:324.6666666666667},
38//!     Point { x:570.6666666666666,        y:313.3333333333333},
39//!     Point { x:614.0,                    y:302.6666666666667},
40//!     Point { x:631.3333333333334,        y:301.3333333333333},
41//!     Point { x:650.0,                    y:300.0},
42//!     Point { x:658.6666666666666,        y:304.0},
43//!     Point { x:617.3333333333334,        y:333.3333333333333},
44//!     Point { x:546.0,                    y:381.3333333333333},
45//!     Point { x:518.6666666666666,        y:400.6666666666667},
46//!     Point { x:505.3333333333333,        y:412.6666666666667},
47//!     Point { x:488.0,                    y:430.6666666666667},
48//!     Point { x:489.3333333333333,        y:435.3333333333333},
49//!     Point { x:570.6666666666666,        y:402.0},
50//!     Point { x:700.0,                    y:328.6666666666667},
51//!     Point { x:799.3333333333334,        y:266.0},
52//!     Point { x:838.0,                    y:240.0},
53//!     Point { x:854.0,                    y:228.66666666666666},
54//!     Point { x:868.0,                    y:218.66666666666666},
55//!     Point { x:879.3333333333334,        y:210.66666666666666},
56//!     Point { x:872.6666666666666,        y:216.0},
57//!     Point { x:860.0,                    y:223.33333333333334},
58//! ];
59//!
60//! let result = simplify_rs::simplify(&points, 800.0);
61//!
62//! if result.is_empty() {
63//!     println!("no solution!");
64//! } else if result.len() == 1 {
65//!     println!("solution:\r\n\tpoint {:?}", result[0]);
66//! } else if result.len() == 2 {
67//!     println!("solution:\r\n\tline {:?} - {:?}", result[0], result[1]);
68//! } else {
69//!     println!("solution:");
70//!     for cubic_curve in result.chunks_exact(4) {
71//!         println!("\tcubic curve: [");
72//!         println!("\t\t{:?}", cubic_curve[0]);
73//!         println!("\t\t{:?}", cubic_curve[1]);
74//!         println!("\t\t{:?}", cubic_curve[2]);
75//!         println!("\t\t{:?}", cubic_curve[3]);
76//!         println!("\t]");
77//!     }
78//! }
79//! ```
80//!
81//! ## Performance
82//!
83//! The JavaScript (paper.js) version takes about 6 - 7ms to generalize the 50 points,
84//! the Rust version takes ~40 - 50µs.
85
86extern crate core;
87
88use core::ops::Range;
89use core::hint::unreachable_unchecked;
90use core::fmt;
91
92use wasm_bindgen::prelude::*;
93use serde::{Serialize, Deserialize};
94use serde_wasm_bindgen::{to_value, from_value};
95
96#[cfg(feature = "double_precision")]
97type Float = f32;
98#[cfg(not(feature = "double_precision"))]
99type Float = f64;
100
101/// A 2D point
102#[wasm_bindgen]
103#[derive(Copy, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
104pub struct Point { pub x: Float, pub y: Float }
105
106impl fmt::Debug for Point {
107    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
108        write!(f, "{{\"x\":{},\"y\":{}}}", self.x, self.y)
109    }
110}
111
112#[wasm_bindgen]
113impl Point {
114    #[wasm_bindgen(constructor)]
115    pub fn new(x: f64, y: f64) -> Point {
116        Point { x, y }
117    }
118    #[inline]
119    fn add(&self, p: Point) -> Point {
120        Point {
121            x: self.x + p.x,
122            y: self.y + p.y
123        }
124    }
125
126    #[inline]
127    fn subtract(&self, p: Point) -> Point {
128        Point {
129            x: self.x - p.x,
130            y: self.y - p.y
131        }
132    }
133
134    #[inline]
135    fn multiply(&self, f: Float) -> Point {
136        Point {
137            x: self.x * f,
138            y: self.y * f
139        }
140    }
141
142    #[inline]
143    fn negate(&self) -> Point {
144        Point {
145            x: -self.x,
146            y: -self.y
147        }
148    }
149
150    #[inline]
151    fn distance(&self, p: Point) -> Float {
152        let dx = p.x - self.x;
153        let dy = p.y - self.y;
154        dx.hypot(dy)
155    }
156
157    #[inline]
158    fn dot(&self, p: Point) -> Float {
159        self.x * p.x + self.y * p.y
160    }
161
162    #[inline]
163    fn normalize(&self, length: Float) -> Point {
164        let current =  self.x.hypot(self.y);
165        let scale = if current.abs() > Float::EPSILON { length / current } else { 0.0 };
166        let res = Point { x: self.x * scale, y: self.y * scale };
167        res
168    }
169}
170
171/// Default value for the `simplify(tolerance)` parameter
172pub const DEFAULT_TOLERANCE: f32 = 2.5;
173pub const TARGET_AUTO_SCALE_AREA: Float = 1000000.0;
174
175/// WASM function to simplify a path represented by a sequence of points
176/// to a bezier curve with a maximum error tolerance.
177/// The input points are expected to be in the format:
178/// [{x: 0, y: 0}, {x: 1, y: 1}, ...]
179///
180/// - `tolerance`: the allowed maximum error when fitting the curves through the segment points
181///
182/// - `auto_scale_for_precision``: if true, the polygon will be scaled to a target area
183/// of TARGET_AUTO_SCALE_AREA before simplification and scaled back to the original scale
184/// after simplification. This increases the precision of the simplification.
185///
186/// # Returns
187///
188/// The output is a sequence of cubic bezier curves, each represented by 4 points:
189/// [{x: 0, y: 0}, {x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}]
190/// [0] = start point
191/// [1] = control point 1
192/// [2] = control point 2
193/// [3] = end point
194#[wasm_bindgen]
195pub fn simplify_js(points_js: &JsValue, tolerance: f64, auto_scale_for_precision: bool) -> JsValue {
196    // Deserialize the points from JsValue using serde_wasm_bindgen
197    let mut points: Vec<Point> = from_value(points_js.clone()).unwrap();
198
199    let mut scale = 1.0;
200    // calculate area of the polygon
201    if auto_scale_for_precision {
202        let mut area = 0.0;
203        let mut j = points.len() - 1;
204        for i in 0..points.len() {
205            area += (points[j].x + points[i].x) * (points[j].y - points[i].y);
206            j = i;
207        }
208        area *= 0.5;
209        area = area.abs();
210
211        // scale the polygon to a target area
212        scale = TARGET_AUTO_SCALE_AREA / area;
213
214        for p in points.iter_mut() {
215            p.x *= scale;
216            p.y *= scale;
217        }
218    }
219
220    let scaled_tolerance = tolerance as Float * scale;
221
222    // Call the existing Rust `simplify` function with deserialized points
223    let mut simplified = simplify(&points, scaled_tolerance);
224
225    // scale the simplified polygon back to the original scale
226    if auto_scale_for_precision {
227        for p in simplified.iter_mut() {
228            p.x /= scale;
229            p.y /= scale;
230        }
231    }
232
233    // Serialize the result back into JsValue using serde_wasm_bindgen
234    to_value(&simplified).unwrap()
235}
236
237/// Fits a sequence of as few curves as possible through the path's anchor
238/// points, ignoring the path items's curve-handles, with an allowed maximum
239/// error. This method can be used to process and simplify the point data received
240/// from a mouse or touch device.
241///
242/// - `tolerance`: the allowed maximum error when fitting the curves through the segment points
243///
244/// # Returns
245///
246/// - returns the input if the input is less than 3 points
247/// - x * 4 points (representing cubic bezier curves with duplicated start / end points)
248///   if the input is more than 3 points
249///
250/// **NOTE**: Depending on the tolerance, the endpoint of the input points and
251/// the endpoint of the simplified curve do not have to match up
252pub fn simplify(points: &[Point], tolerance: Float) -> Vec<Point> {
253
254    // filter points for duplicates
255    let mut cur_points = points.windows(2).filter_map(|w| {
256        let first = w.get(0)?;
257        let next = w.get(1)?;
258        if first == next { None } else { Some(*first) }
259    }).collect::<Vec<Point>>();
260
261    // windows() is fast, but excludes the last point
262    if let (Some(last_minus_one), Some(last)) = (points.get(points.len() - 2), points.last()) {
263        if last_minus_one != last { cur_points.push(*last); }
264    }
265
266    // sanity check
267    if cur_points.len() < 2 {
268        return cur_points;
269    } else if cur_points.len() == 2 {
270        return vec![cur_points[0], cur_points[1]];
271    }
272
273    // cur_points.len() is assured to be greater than 2
274    let closed = cur_points.first() == cur_points.last();
275
276    // We need to duplicate the first and last point when
277    // simplifying a closed path
278    if closed {
279        let last = match points.last().copied() {
280            Some(s) => s,
281            None => unsafe { unreachable_unchecked() },
282        };
283        let first = match points.first().copied() {
284            Some(s) => s,
285            None => unsafe { unreachable_unchecked() },
286        };
287        let mut new_cur_points = vec![last];
288        new_cur_points.extend(cur_points.drain(..));
289        new_cur_points.push(first);
290        cur_points = new_cur_points;
291    }
292
293    fit(&cur_points[..], tolerance)
294}
295
296#[derive(Clone)]
297struct Split {
298    global_range: Range<usize>,
299    tan1: Point,
300    tan2: Point,
301}
302
303#[inline]
304fn fit(points: &[Point], tolerance: Float) -> Vec<Point> {
305
306    // To support reducing paths with multiple points in the same place
307    // to one segment:
308    let mut segments = Vec::new();
309    let distances = chord_length_parametrize(points);
310
311    if distances.len() != points.len() {
312        return segments; // never happens, necessary for compiler
313    }
314
315    // elide bounds checks
316    if points.len() == 0 {
317        return Vec::new();
318    } else if points.len() == 1 {
319        return vec![points[0]];
320    } else if points.len() == 2 {
321        return vec![points[0], points[1]];
322    } else {
323        let mut splits_to_eval = vec![Split {
324            global_range: 0..points.len(),
325            tan1: points[1].subtract(points[0]),
326            tan2: points[points.len() - 2].subtract(points[points.len() - 1]),
327        }];
328
329        while let Some(split) = splits_to_eval.pop() {
330
331            // elide slice checks
332            if split.global_range.end > points.len() || split.global_range.end > distances.len() {
333                continue;
334            }
335
336            let result = fit_cubic(FitCubicParams {
337                points: &points[split.global_range.clone()],
338                chord_lengths: &distances[split.global_range.clone()],
339                segments: &mut segments,
340                error: tolerance,
341                tan1: split.tan1,
342                tan2: split.tan2,
343            });
344
345            if let Some(r) = result {
346                // elide slice checks
347                if split.global_range.start > split.global_range.start + r + 1 ||
348                   split.global_range.start + r > split.global_range.end {
349                    continue;
350                }
351                if split.global_range.start + r + 1 >= points.len() || split.global_range.start + r - 1 >= points.len() {
352                    continue;
353                }
354                // Fitting failed -- split at max error point and fit recursively
355                let tan_center = points[split.global_range.start + r - 1].subtract(points[split.global_range.start + r + 1]);
356
357                splits_to_eval.extend_from_slice(&[
358                    Split {
359                        global_range: (split.global_range.start + r)..split.global_range.end,
360                        tan1: tan_center.negate(),
361                        tan2: split.tan2,
362                    },
363                    Split {
364                        global_range: split.global_range.start..(split.global_range.start + r + 1),
365                        tan1: split.tan1,
366                        tan2: tan_center,
367                    },
368                ]);
369            }
370        }
371
372        segments
373    }
374
375}
376
377struct FitCubicParams<'a> {
378    segments: &'a mut Vec<Point>,
379    points: &'a [Point],
380    chord_lengths: &'a [Float],
381    error: Float,
382    tan1: Point,
383    tan2: Point,
384}
385
386#[inline]
387fn fit_cubic(params: FitCubicParams) -> Option<usize> {
388
389    let FitCubicParams { segments, points, chord_lengths, error, tan1, tan2 } = params;
390
391    // Use heuristic if region only has two points in it
392    if points.len() < 2 {
393        return None;
394    } else if points.len() == 2 {
395        let pt1 = points[0];
396        let pt2 = points[1];
397        let dist = pt1.distance(pt2) / 3.0;
398        add_curve(segments, &[
399            pt1,
400            pt1.add(tan1.normalize(dist)),
401            pt2.add(tan2.normalize(dist)),
402            pt2
403        ]);
404        return None;
405    }
406
407    // points.len() at least 4
408
409    // Parameterize points, and attempt to fit curve
410    // (Slightly) faster version of chord lengths, re-uses the results from original count
411    let mut u_prime = chord_lengths.to_owned();
412    let u_prime_first = match u_prime.first().copied() {
413        Some(s) => s,
414        None => unsafe { unreachable_unchecked() },
415    };
416    let u_prime_last = match u_prime.last().copied() {
417        Some(s) => s,
418        None => unsafe { unreachable_unchecked() },
419    };
420    let u_prime_last = u_prime_last - u_prime_first;
421    u_prime.iter_mut().for_each(|p| { *p = (*p - u_prime_first) / u_prime_last; });
422
423    let mut max_error = error.max(error.powi(2));
424    let mut parameters_in_order = true;
425    let mut split = 2;
426
427    // Try 4 iterations
428    for _ in 0..4 {
429
430        let curve = generate_bezier(points, &u_prime, tan1, tan2);
431
432        //  Find max deviation of points to fitted curve
433        let max = find_max_error(points, &curve, &u_prime);
434
435        if max.error < error && parameters_in_order {
436            // solution found
437            add_curve(segments, &curve);
438            return None;
439        }
440
441        split = max.index;
442
443        // If error not too large, try reparameterization and iteration
444        if max.error >= max_error {
445            break;
446        }
447        parameters_in_order = reparameterize(points, &mut u_prime, &curve);
448        max_error = max.error;
449    }
450
451    Some(split)
452}
453
454#[inline]
455fn add_curve(segments: &mut Vec<Point>, curve: &[Point;4]) {
456    segments.extend_from_slice(curve);
457}
458
459#[inline]
460#[allow(non_snake_case)]
461fn generate_bezier(points: &[Point], u_prime: &[Float], tan1: Point, tan2: Point) -> [Point;4] {
462
463    const BEZIER_EPSILON: Float = 1e-12;
464
465    debug_assert!(u_prime.len() > 2);
466    debug_assert!(points.len() > 2);
467    debug_assert!(u_prime.len() == points.len());
468
469    let pt1 = &points[0];
470    let pt2 = &points[points.len() - 1];
471
472    // Create the C and X matrices
473    let mut C = [
474        [0.0, 0.0],
475        [0.0, 0.0]
476    ];
477    let mut X = [0.0, 0.0];
478
479    for (p, u) in points.iter().zip(u_prime.iter()) {
480
481        let t = 1.0 - u;
482        let b = 3.0 * u * t;
483        let b0 = t * t * t;
484        let b1 = b * t;
485        let b2 = b * u;
486        let b3 = u * u * u;
487        let a1 = tan1.normalize(b1);
488        let a2 = tan2.normalize(b2);
489        let pt1_multiplied = pt1.multiply(b0 + b1);
490        let pt2_multiplied = pt2.multiply(b2 + b3);
491        let tmp = p.subtract(pt1_multiplied).subtract(pt2_multiplied);
492
493        C[0][0] += a1.dot(a1);
494        C[0][1] += a1.dot(a2);
495        C[1][0] = C[0][1];
496        C[1][1] += a2.dot(a2);
497
498        X[0] += a1.dot(tmp);
499        X[1] += a2.dot(tmp);
500    }
501
502    // Compute the determinants of C and X
503    let det_c0_c1 = C[0][0] * C[1][1] - C[1][0] * C[0][1];
504
505    let mut alpha1;
506    let mut alpha2;
507
508    if det_c0_c1.abs() > BEZIER_EPSILON {
509        // Kramer's rule
510        let det_c0_x = C[0][0] * X[1]    - C[1][0] * X[0];
511        let det_x_c1 = X[0]    * C[1][1] - X[1]    * C[0][1];
512        // Derive alpha values
513        alpha1 = det_x_c1 / det_c0_c1;
514        alpha2 = det_c0_x / det_c0_c1;
515    } else {
516        // Matrix is under-determined, try assuming alpha1 == alpha2
517        let c0 = C[0][0] + C[0][1];
518        let c1 = C[1][0] + C[1][1];
519        alpha1 = if c0.abs() > BEZIER_EPSILON {
520            X[0] / c0
521        } else if c1.abs() > BEZIER_EPSILON {
522            X[1] / c1
523        } else {
524            0.0
525        };
526        alpha2 = alpha1;
527    }
528
529    // If alpha negative, use the Wu/Barsky heuristic (see text)
530    // (if alpha is 0, you get coincident control points that lead to
531    // divide by zero in any subsequent NewtonRaphsonRootFind() call.
532    let seg_length = pt2.distance(*pt1);
533    let eps = BEZIER_EPSILON * seg_length;
534    let mut handle1_2 = None;
535
536    if alpha1 < eps || alpha2 < eps {
537        // fall back on standard (probably inaccurate) formula,
538        // and subdivide further if needed.
539        alpha1 = seg_length / 3.0;
540        alpha2 = alpha1;
541    } else {
542        // Check if the found control points are in the right order when
543        // projected onto the line through pt1 and pt2.
544        let line = pt2.subtract(*pt1);
545
546        // Control points 1 and 2 are positioned an alpha distance out
547        // on the tangent vectors, left and right, respectively
548        let tmp_handle_1 = tan1.normalize(alpha1);
549        let tmp_handle_2 = tan2.normalize(alpha2);
550
551        let seg_length_squared = seg_length * seg_length;
552
553        if tmp_handle_1.dot(line) - tmp_handle_2.dot(line) > seg_length_squared {
554            // Fall back to the Wu/Barsky heuristic above.
555            alpha1 = seg_length / 3.0;
556            alpha2 = alpha1;
557            // Force recalculation
558            handle1_2 = None;
559        } else {
560            handle1_2 = Some((tmp_handle_1, tmp_handle_2));
561        }
562    }
563
564    // First and last control points of the Bezier curve are
565    // positioned exactly at the first and last data points
566    if let Some((h1, h2)) = handle1_2 {
567        [*pt1, pt1.add(h1), pt2.add(h2), *pt2]
568    } else {
569        [*pt1, pt1.add(tan1.normalize(alpha1)), pt2.add(tan2.normalize(alpha2)), *pt2]
570    }
571}
572
573/// Given set of points and their parameterization, try to find
574/// a better parameterization.
575#[inline]
576fn reparameterize(points: &[Point], u: &mut [Float], curve: &[Point;4]) -> bool {
577
578    points.iter().zip(u.iter_mut()).for_each(|(p, u)| { *u = find_root(curve, p, *u); });
579
580    // Detect if the new parameterization has reordered the points.
581    // In that case, we would fit the points of the path in the wrong order.
582    !u.windows(2).any(|w| w[1] <= w[0])
583}
584
585#[inline]
586fn find_root(curve: &[Point;4], point: &Point, u: Float) -> Float {
587
588    let mut curve1 = [Point { x: 0.0, y: 0.0 };3];
589    let mut curve2 = [Point { x: 0.0, y: 0.0 };2];
590
591    // Generate control vertices for Q'
592    for i in 0..curve1.len() {
593        curve1[i] = curve[i + 1].subtract(curve[i]).multiply(3.0);
594    }
595
596    // Generate control vertices for Q''
597    for i in 0..curve2.len() {
598        curve2[i] = curve1[i + 1].subtract(curve1[i]).multiply(2.0);
599    }
600
601    // Compute Q(u), Q'(u) and Q''(u)
602    let pt = evaluate_4(&curve, u);
603    let pt1 = evaluate_3(&curve1, u);
604    let pt2 = evaluate_2(&curve2, u);
605    let diff = pt.subtract(*point);
606    let df = pt1.dot(pt1) + diff.dot(pt2);
607
608    // Newton: u = u - f(u) / f'(u)
609    if df.abs() < Float::EPSILON {
610        u
611    } else {
612        u - diff.dot(pt1) / df
613    }
614}
615
616macro_rules! evaluate {
617    ($curve:expr, $t:expr) => {{
618        // Copy curve
619        let mut tmp = *$curve;
620
621        // Triangle computation
622        for i in 1..$curve.len() {
623            for j in 0..($curve.len() - i) {
624                tmp[j] = tmp[j].multiply(1.0 - $t).add(tmp[j + 1].multiply($t));
625            }
626        }
627
628        tmp[0]
629    }};
630}
631
632// evaluate the bezier curve at point t
633#[inline]
634fn evaluate_4(curve: &[Point;4], t: Float) -> Point { let ret = evaluate!(curve, t); ret }
635#[inline]
636fn evaluate_3(curve: &[Point;3], t: Float) -> Point { let ret = evaluate!(curve, t); ret }
637#[inline]
638fn evaluate_2(curve: &[Point;2], t: Float) -> Point { let ret = evaluate!(curve, t); ret }
639
640// chord length parametrize the curve points[first..last]
641#[inline]
642fn chord_length_parametrize(points: &[Point]) -> Vec<Float> {
643
644    let mut u = vec![0.0;points.len()];
645    let mut last_dist = 0.0;
646
647    for (prev, (next_id, next)) in points.iter().zip(points.iter().enumerate().skip(1)) {
648        let new_dist = last_dist + prev.distance(*next);
649        unsafe { *u.get_unchecked_mut(next_id) = new_dist; }
650        last_dist = new_dist;
651    }
652
653    for val in u.iter_mut() {
654        *val /= last_dist;
655    }
656
657    u
658}
659
660struct FindMaxErrorReturn {
661    error: Float,
662    index: usize
663}
664
665// find maximum squared distance error between real points and curve
666#[inline]
667fn find_max_error(points: &[Point], curve: &[Point;4], u: &[Float]) -> FindMaxErrorReturn {
668
669    let mut index = points.len() / 2.0 as usize;
670    let mut max_dist = 0.0;
671
672    for (i, (p, u)) in points.iter().zip(u.iter()).enumerate() {
673        let point_on_curve = evaluate_4(curve, *u);
674        let dist = point_on_curve.subtract(*p);
675        let dist_squared = dist.x.mul_add(dist.x, dist.y.powi(2)); // compute squared distance
676
677        if dist_squared >= max_dist {
678            max_dist = dist_squared;
679            index = i;
680        }
681    }
682
683    FindMaxErrorReturn {
684        error: max_dist,
685        index: index
686    }
687}