Skip to content

Commit aade1a9

Browse files
LaurenzVtomcur
andauthored
Don't manually flatten curves for which the control points are strictly within the tolerance threshold (#1214)
A revival from an older PR. Confirmed to give a decent speed boost for 8x8 and 16x16 shapes. --------- Co-authored-by: Tom Churchman <[email protected]>
1 parent 17f3bc0 commit aade1a9

File tree

2 files changed

+76
-29
lines changed

2 files changed

+76
-29
lines changed

sparse_strips/vello_common/src/flatten.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub use crate::flatten_simd::FlattenCtx;
1313

1414
/// The flattening tolerance.
1515
const TOL: f64 = 0.25;
16+
pub(crate) const TOL_2: f64 = TOL * TOL;
1617

1718
/// A point.
1819
#[derive(Clone, Copy, Debug, PartialEq)]

sparse_strips/vello_common/src/flatten_simd.rs

Lines changed: 75 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55
//! well as some code that was copied from kurbo, which is needed to reimplement the
66
//! full `flatten` method.
77
8+
use crate::flatten::TOL_2;
9+
#[cfg(not(feature = "std"))]
10+
use crate::kurbo::common::FloatFuncs as _;
811
use crate::kurbo::{CubicBez, ParamCurve, PathEl, Point, QuadBez};
912
use alloc::vec;
1013
use alloc::vec::Vec;
1114
use bytemuck::{Pod, Zeroable};
1215
use fearless_simd::*;
1316

14-
#[cfg(not(feature = "std"))]
15-
use crate::kurbo::common::FloatFuncs as _;
16-
1717
// Unlike kurbo, which takes a closure with a callback for outputting the lines, we use a trait
1818
// instead. The reason is that this way the callback can be inlined, which is not possible with
1919
// a closure and turned out to have a noticeable overhead.
@@ -49,38 +49,84 @@ pub(crate) fn flatten<S: Simd>(
4949
}
5050
PathEl::QuadTo(p1, p2) => {
5151
if let Some(p0) = last_pt {
52-
let q = QuadBez::new(p0, p1, p2);
53-
let params = q.estimate_subdiv(sqrt_tol);
54-
let n = ((0.5 * params.val / sqrt_tol).ceil() as usize).max(1);
55-
let step = 1.0 / (n as f64);
56-
for i in 1..n {
57-
let u = (i as f64) * step;
58-
let t = q.determine_subdiv_t(&params, u);
59-
let p = q.eval(t);
60-
callback.callback(PathEl::LineTo(p));
52+
// An upper bound on the shortest distance of any point on the quadratic Bezier
53+
// curve to the line segment [p0, p2] is 1/2 of the maximum of the
54+
// endpoint-to-control-point distances.
55+
//
56+
// The derivation is similar to that for the cubic Bezier (see below). In
57+
// short:
58+
//
59+
// q(t) = B0(t) p0 + B1(t) p1 + B2(t) p2
60+
// dist(q(t), [p0, p1]) <= B1(t) dist(p1, [p0, p1])
61+
// = 2 (1-t)t dist(p1, [p0, p1]).
62+
//
63+
// The maximum occurs at t=1/2, hence
64+
// max(dist(q(t), [p0, p1] <= 1/2 dist(p1, [p0, p1])).
65+
//
66+
// A cheap upper bound for dist(p1, [p0, p1]) is max(dist(p1, p0), dist(p1, p2)).
67+
//
68+
// The following takes the square to elide the square root of the Euclidean
69+
// distance.
70+
if f64::max((p1 - p0).hypot2(), (p1 - p2).hypot2()) <= 4. * TOL_2 {
71+
callback.callback(PathEl::LineTo(p2));
72+
} else {
73+
let q = QuadBez::new(p0, p1, p2);
74+
let params = q.estimate_subdiv(sqrt_tol);
75+
let n = ((0.5 * params.val / sqrt_tol).ceil() as usize).max(1);
76+
let step = 1.0 / (n as f64);
77+
for i in 1..n {
78+
let u = (i as f64) * step;
79+
let t = q.determine_subdiv_t(&params, u);
80+
let p = q.eval(t);
81+
callback.callback(PathEl::LineTo(p));
82+
}
83+
callback.callback(PathEl::LineTo(p2));
6184
}
62-
callback.callback(PathEl::LineTo(p2));
6385
}
6486
last_pt = Some(p2);
6587
}
6688
PathEl::CurveTo(p1, p2, p3) => {
6789
if let Some(p0) = last_pt {
68-
let c = CubicBez::new(p0, p1, p2, p3);
69-
let max = simd.vectorize(
70-
#[inline(always)]
71-
|| {
72-
flatten_cubic_simd(
73-
simd,
74-
c,
75-
flatten_ctx,
76-
tolerance as f32,
77-
&mut flattened_cubics,
78-
)
79-
},
80-
);
81-
82-
for p in &flattened_cubics[1..max] {
83-
callback.callback(PathEl::LineTo(Point::new(p.x as f64, p.y as f64)));
90+
// An upper bound on the shortest distance of any point on the cubic Bezier
91+
// curve to the line segment [p0, p3] is 3/4 of the maximum of the
92+
// endpoint-to-control-point distances.
93+
//
94+
// With Bernstein weights Bi(t), we have
95+
// c(t) = B0(t) p0 + B1(t) p1 + B2(t) p2 + B3(t) p3
96+
// with t from 0 to 1 (inclusive).
97+
//
98+
// Through convexivity of the Euclidean distance function and the line segment,
99+
// we have
100+
// dist(c(t), [p0, p3]) <= B1(t) dist(p1, [p0, p3]) + B2(t) dist(p2, [p0, p3])
101+
// <= B1(t) ||p1-p0|| + B2(t) ||p2-p3||
102+
// <= (B1(t) + B2(t)) max(||p1-p0||, ||p2-p3|||)
103+
// = 3 ((1-t)t^2 + (1-t)^2t) max(||p1-p0||, ||p2-p3||).
104+
//
105+
// The inner polynomial has its maximum of 1/4 at t=1/2, hence
106+
// max(dist(c(t), [p0, p3])) <= 3/4 max(||p1-p0||, ||p2-p3||).
107+
//
108+
// The following takes the square to elide the square root of the Euclidean
109+
// distance.
110+
if f64::max((p0 - p1).hypot2(), (p3 - p2).hypot2()) <= 16. / 9. * TOL_2 {
111+
callback.callback(PathEl::LineTo(p3));
112+
} else {
113+
let c = CubicBez::new(p0, p1, p2, p3);
114+
let max = simd.vectorize(
115+
#[inline(always)]
116+
|| {
117+
flatten_cubic_simd(
118+
simd,
119+
c,
120+
flatten_ctx,
121+
tolerance as f32,
122+
&mut flattened_cubics,
123+
)
124+
},
125+
);
126+
127+
for p in &flattened_cubics[1..max] {
128+
callback.callback(PathEl::LineTo(Point::new(p.x as f64, p.y as f64)));
129+
}
84130
}
85131
}
86132
last_pt = Some(p3);

0 commit comments

Comments
 (0)