diff --git a/sparse_strips/vello_common/src/flatten.rs b/sparse_strips/vello_common/src/flatten.rs index 65f987ee5..97a15ad60 100644 --- a/sparse_strips/vello_common/src/flatten.rs +++ b/sparse_strips/vello_common/src/flatten.rs @@ -3,7 +3,7 @@ //! Flattening filled and stroked paths. -use crate::flatten_simd::Callback; +use crate::flatten_simd::{Callback, LinePathEl}; use crate::kurbo::{self, Affine, PathEl, Stroke, StrokeCtx, StrokeOpts}; use alloc::vec::Vec; use fearless_simd::{Level, Simd, dispatch}; @@ -102,18 +102,13 @@ pub fn fill_impl( let mut lb = FlattenerCallback { line_buf, - start: kurbo::Point::default(), - p0: kurbo::Point::default(), + start: Point::ZERO, + p0: Point::ZERO, is_nan: false, - closed: false, }; crate::flatten_simd::flatten(simd, iter, TOL, &mut lb, flatten_ctx); - if !lb.closed { - close_path(lb.start, lb.p0, lb.line_buf); - } - // A path that contains NaN is ill-defined, so ignore it. if lb.is_nan { warn!("A path contains NaN, ignoring it."); @@ -154,50 +149,28 @@ pub fn expand_stroke( struct FlattenerCallback<'a> { line_buf: &'a mut Vec, - start: kurbo::Point, - p0: kurbo::Point, + start: Point, + p0: Point, is_nan: bool, - closed: bool, } impl Callback for FlattenerCallback<'_> { #[inline(always)] - fn callback(&mut self, el: PathEl) { - self.is_nan |= el.is_nan(); - + fn callback(&mut self, el: LinePathEl) { match el { - kurbo::PathEl::MoveTo(p) => { - if !self.closed && self.p0 != self.start { - close_path(self.start, self.p0, self.line_buf); - } + LinePathEl::MoveTo(p) => { + self.is_nan |= p.is_nan(); - self.closed = false; - self.start = p; - self.p0 = p; - } - kurbo::PathEl::LineTo(p) => { - let pt0 = Point::new(self.p0.x as f32, self.p0.y as f32); - let pt1 = Point::new(p.x as f32, p.y as f32); - self.line_buf.push(Line::new(pt0, pt1)); - self.p0 = p; + self.start = Point::new(p.x as f32, p.y as f32); + self.p0 = self.start; } - el @ (kurbo::PathEl::QuadTo(_, _) | kurbo::PathEl::CurveTo(_, _, _)) => { - unreachable!("Path has been flattened, so shouldn't contain {el:?}.") - } - kurbo::PathEl::ClosePath => { - self.closed = true; + LinePathEl::LineTo(p) => { + self.is_nan |= p.is_nan(); - close_path(self.start, self.p0, self.line_buf); + let p = Point::new(p.x as f32, p.y as f32); + self.line_buf.push(Line::new(self.p0, p)); + self.p0 = p; } } } } - -fn close_path(start: kurbo::Point, p0: kurbo::Point, line_buf: &mut Vec) { - let pt0 = Point::new(p0.x as f32, p0.y as f32); - let pt1 = Point::new(start.x as f32, start.y as f32); - - if pt0 != pt1 { - line_buf.push(Line::new(pt0, pt1)); - } -} diff --git a/sparse_strips/vello_common/src/flatten_simd.rs b/sparse_strips/vello_common/src/flatten_simd.rs index 08e2eabdb..265a509c0 100644 --- a/sparse_strips/vello_common/src/flatten_simd.rs +++ b/sparse_strips/vello_common/src/flatten_simd.rs @@ -13,11 +13,27 @@ use alloc::vec::Vec; use bytemuck::{Pod, Zeroable}; use fearless_simd::*; +/// The element of a path made of lines. +/// +/// Each subpath must start with a `MoveTo`. Closing of subpaths is not supported, and subpaths are +/// not closed implicitly when a new subpath (with `MoveTo`) is started. It is expected that closed +/// subpaths are watertight in the sense that the last `LineTo` matches exactly with the first +/// `MoveTo`. +/// +/// This intentionally allows for non-watertight subpaths, as, e.g., lines that are fully outside +/// of the viewport do not need to be drawn. +/// +/// See [`PathEl`] for a more general-purpose path element type. +pub(crate) enum LinePathEl { + MoveTo(Point), + LineTo(Point), +} + // Unlike kurbo, which takes a closure with a callback for outputting the lines, we use a trait // instead. The reason is that this way the callback can be inlined, which is not possible with // a closure and turned out to have a noticeable overhead. pub(crate) trait Callback { - fn callback(&mut self, el: PathEl); + fn callback(&mut self, el: LinePathEl); } /// See the docs for the kurbo implementation of flattening: @@ -35,97 +51,111 @@ pub(crate) fn flatten( flatten_ctx.flattened_cubics.clear(); let sqrt_tol = tolerance.sqrt(); - let mut last_pt = None; + let mut closed = true; + let mut start_pt = Point::ZERO; + let mut last_pt = Point::ZERO; for el in path { match el { PathEl::MoveTo(p) => { - last_pt = Some(p); - callback.callback(PathEl::MoveTo(p)); + if !closed && last_pt != start_pt { + callback.callback(LinePathEl::LineTo(start_pt)); + } + closed = false; + last_pt = p; + start_pt = p; + callback.callback(LinePathEl::MoveTo(p)); } PathEl::LineTo(p) => { - last_pt = Some(p); - callback.callback(PathEl::LineTo(p)); + debug_assert!(!closed, "Expected a `MoveTo` before a `LineTo`"); + last_pt = p; + callback.callback(LinePathEl::LineTo(p)); } PathEl::QuadTo(p1, p2) => { - if let Some(p0) = last_pt { - // An upper bound on the shortest distance of any point on the quadratic Bezier - // curve to the line segment [p0, p2] is 1/2 of the maximum of the - // endpoint-to-control-point distances. - // - // The derivation is similar to that for the cubic Bezier (see below). In - // short: - // - // q(t) = B0(t) p0 + B1(t) p1 + B2(t) p2 - // dist(q(t), [p0, p1]) <= B1(t) dist(p1, [p0, p1]) - // = 2 (1-t)t dist(p1, [p0, p1]). - // - // The maximum occurs at t=1/2, hence - // max(dist(q(t), [p0, p1] <= 1/2 dist(p1, [p0, p1])). - // - // A cheap upper bound for dist(p1, [p0, p1]) is max(dist(p1, p0), dist(p1, p2)). - // - // The following takes the square to elide the square root of the Euclidean - // distance. - if f64::max((p1 - p0).hypot2(), (p1 - p2).hypot2()) <= 4. * TOL_2 { - callback.callback(PathEl::LineTo(p2)); - } else { - let q = QuadBez::new(p0, p1, p2); - let params = q.estimate_subdiv(sqrt_tol); - let n = ((0.5 * params.val / sqrt_tol).ceil() as usize).max(1); - let step = 1.0 / (n as f64); - for i in 1..n { - let u = (i as f64) * step; - let t = q.determine_subdiv_t(¶ms, u); - let p = q.eval(t); - callback.callback(PathEl::LineTo(p)); - } - callback.callback(PathEl::LineTo(p2)); + debug_assert!(!closed, "Expected a `MoveTo` before a `QuadTo`"); + let p0 = last_pt; + // An upper bound on the shortest distance of any point on the quadratic Bezier + // curve to the line segment [p0, p2] is 1/2 of the maximum of the + // endpoint-to-control-point distances. + // + // The derivation is similar to that for the cubic Bezier (see below). In + // short: + // + // q}(t) = B0(t) p0 + B1(t) p1 + B2(t) p2 + // dist(q(t), [p0, p1]) <= B1(t) dist(p1, [p0, p1]) + // = 2 (1-t)t dist(p1, [p0, p1]). + // + // The maximum occurs at t=1/2, hence + // max(dist(q(t), [p0, p1] <= 1/2 dist(p1, [p0, p1])). + // + // A cheap upper bound for dist(p1, [p0, p1]) is max(dist(p1, p0), dist(p1, p2)). + // + // The following takes the square to elide the square root of the Euclidean + // distance. + if f64::max((p1 - p0).hypot2(), (p1 - p2).hypot2()) <= 4. * TOL_2 { + callback.callback(LinePathEl::LineTo(p2)); + } else { + let q = QuadBez::new(p0, p1, p2); + let params = q.estimate_subdiv(sqrt_tol); + let n = ((0.5 * params.val / sqrt_tol).ceil() as usize).max(1); + let step = 1.0 / (n as f64); + for i in 1..n { + let u = (i as f64) * step; + let t = q.determine_subdiv_t(¶ms, u); + let p = q.eval(t); + callback.callback(LinePathEl::LineTo(p)); } + callback.callback(LinePathEl::LineTo(p2)); } - last_pt = Some(p2); + last_pt = p2; } PathEl::CurveTo(p1, p2, p3) => { - if let Some(p0) = last_pt { - // An upper bound on the shortest distance of any point on the cubic Bezier - // curve to the line segment [p0, p3] is 3/4 of the maximum of the - // endpoint-to-control-point distances. - // - // With Bernstein weights Bi(t), we have - // c(t) = B0(t) p0 + B1(t) p1 + B2(t) p2 + B3(t) p3 - // with t from 0 to 1 (inclusive). - // - // Through convexivity of the Euclidean distance function and the line segment, - // we have - // dist(c(t), [p0, p3]) <= B1(t) dist(p1, [p0, p3]) + B2(t) dist(p2, [p0, p3]) - // <= B1(t) ||p1-p0|| + B2(t) ||p2-p3|| - // <= (B1(t) + B2(t)) max(||p1-p0||, ||p2-p3|||) - // = 3 ((1-t)t^2 + (1-t)^2t) max(||p1-p0||, ||p2-p3||). - // - // The inner polynomial has its maximum of 1/4 at t=1/2, hence - // max(dist(c(t), [p0, p3])) <= 3/4 max(||p1-p0||, ||p2-p3||). - // - // The following takes the square to elide the square root of the Euclidean - // distance. - if f64::max((p0 - p1).hypot2(), (p3 - p2).hypot2()) <= 16. / 9. * TOL_2 { - callback.callback(PathEl::LineTo(p3)); - } else { - let c = CubicBez::new(p0, p1, p2, p3); - let max = flatten_cubic_simd(simd, c, flatten_ctx, tolerance as f32); - - for p in &flatten_ctx.flattened_cubics[1..max] { - callback.callback(PathEl::LineTo(Point::new(p.x as f64, p.y as f64))); - } + debug_assert!(!closed, "Expected a `MoveTo` before a `CurveTo`"); + let p0 = last_pt; + // An upper bound on the shortest distance of any point on the cubic Bezier + // curve to the line segment [p0, p3] is 3/4 of the maximum of the + // endpoint-to-control-point distances. + // + // With Bernstein weights Bi(t), we have + // c(t) = B0(t) p0 + B1(t) p1 + B2(t) p2 + B3(t) p3 + // with t from 0 to 1 (inclusive). + // + // Through convexivity of the Euclidean distance function and the line segment, + // we have + // dist(c(t), [p0, p3]) <= B1(t) dist(p1, [p0, p3]) + B2(t) dist(p2, [p0, p3]) + // <= B1(t) ||p1-p0|| + B2(t) ||p2-p3|| + // <= (B1(t) + B2(t)) max(||p1-p0||, ||p2-p3|||) + // = 3 ((1-t)t^2 + (1-t)^2t) max(||p1-p0||, ||p2-p3||). + // + // The inner polynomial has its maximum of 1/4 at t=1/2, hence + // max(dist(c(t), [p0, p3])) <= 3/4 max(||p1-p0||, ||p2-p3||). + // + // The following takes the square to elide the square root of the Euclidean + // distance. + if f64::max((p0 - p1).hypot2(), (p3 - p2).hypot2()) <= 16. / 9. * TOL_2 { + callback.callback(LinePathEl::LineTo(p3)); + } else { + let c = CubicBez::new(p0, p1, p2, p3); + let max = flatten_cubic_simd(simd, c, flatten_ctx, tolerance as f32); + + for p in &flatten_ctx.flattened_cubics[1..max] { + callback.callback(LinePathEl::LineTo(Point::new(p.x as f64, p.y as f64))); } } - last_pt = Some(p3); + last_pt = p3; } PathEl::ClosePath => { - last_pt = None; - callback.callback(PathEl::ClosePath); + closed = true; + if last_pt != start_pt { + callback.callback(LinePathEl::LineTo(start_pt)); + } } } } + + if !closed && last_pt != start_pt { + callback.callback(LinePathEl::LineTo(start_pt)); + } } // The below methods are copied from kurbo and needed to implement flattening of normal quad curves.