Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 15 additions & 42 deletions sparse_strips/vello_common/src/flatten.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -102,18 +102,13 @@ pub fn fill_impl<S: Simd>(

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.");
Expand Down Expand Up @@ -154,50 +149,28 @@ pub fn expand_stroke(

struct FlattenerCallback<'a> {
line_buf: &'a mut Vec<Line>,
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<Line>) {
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));
}
}
176 changes: 103 additions & 73 deletions sparse_strips/vello_common/src/flatten_simd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -35,97 +51,111 @@ pub(crate) fn flatten<S: Simd>(
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(&params, 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(&params, 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.
Expand Down