mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 19:22:16 +00:00
prepress: index Tidy::is_kept by x and y, validate a collinear run once (#851)
Findings 9ef8d922 and ec807eda (vectorcraft pathops boolean.rs analogues).
This commit is contained in:
@@ -641,11 +641,41 @@ fn tol_at(p: Point) -> f64 {
|
||||
1e-6 * (1.0 + p.x.abs().max(p.y.abs()))
|
||||
}
|
||||
|
||||
fn coord(p: Point, axis: usize) -> f64 {
|
||||
if axis == 0 { p.x } else { p.y }
|
||||
}
|
||||
|
||||
/// Arrange `points` as an implicit k-d tree: the median of each slice, by x and then y in turn, is
|
||||
/// its root, with the lesser points before it and the greater after.
|
||||
fn kd_build(points: &mut [Point], axis: usize) {
|
||||
if points.len() < 2 {
|
||||
return;
|
||||
}
|
||||
let mid = points.len() / 2;
|
||||
points.select_nth_unstable_by(mid, |a, b| coord(*a, axis).total_cmp(&coord(*b, axis)));
|
||||
let (before, after) = points.split_at_mut(mid);
|
||||
kd_build(before, 1 - axis);
|
||||
kd_build(&mut after[1..], 1 - axis);
|
||||
}
|
||||
|
||||
/// Whether a point of the tree `points` (made by [`kd_build`] from `axis`) lies in the box `lo..=hi`.
|
||||
/// A box this small meets a few branches however many points share an x.
|
||||
fn kd_any(points: &[Point], axis: usize, lo: Point, hi: Point) -> bool {
|
||||
let mid = points.len() / 2;
|
||||
let Some(&q) = points.get(mid) else { return false };
|
||||
if q.x >= lo.x && q.x <= hi.x && q.y >= lo.y && q.y <= hi.y {
|
||||
return true;
|
||||
}
|
||||
let c = coord(q, axis);
|
||||
(coord(lo, axis) <= c && kd_any(&points[..mid], 1 - axis, lo, hi)) || (coord(hi, axis) >= c && kd_any(&points[mid + 1..], 1 - axis, lo, hi))
|
||||
}
|
||||
|
||||
/// Output cleanup: the inputs' on-curve points (joints there stay), and their curves, to rebuild
|
||||
/// split pieces exactly.
|
||||
struct Tidy {
|
||||
sliver: f64,
|
||||
/// Sorted by x.
|
||||
/// The finite points as an implicit k-d tree (see [`kd_build`]), so a lookup doesn't scan the
|
||||
/// many points that share an x.
|
||||
keep: Vec<Point>,
|
||||
/// Input curves with slightly inflated bounds.
|
||||
curves: Vec<(Rect, CubicBez)>,
|
||||
@@ -668,7 +698,8 @@ impl Tidy {
|
||||
}
|
||||
}
|
||||
}
|
||||
keep.sort_by(|a, b| a.x.total_cmp(&b.x));
|
||||
keep.retain(|p| p.is_finite());
|
||||
kd_build(&mut keep, 0);
|
||||
let mut by_x: Vec<usize> = (0..curves.len()).collect();
|
||||
by_x.sort_by(|&i, &j| curves[i].0.x0.total_cmp(&curves[j].0.x0));
|
||||
let max_width = curves.iter().map(|(r, _)| r.width()).fold(0.0, f64::max);
|
||||
@@ -677,8 +708,7 @@ impl Tidy {
|
||||
|
||||
fn is_kept(&self, p: Point) -> bool {
|
||||
let tol = tol_at(p);
|
||||
let lo = self.keep.partition_point(|q| q.x < p.x - tol);
|
||||
self.keep[lo..].iter().take_while(|q| q.x <= p.x + tol).any(|q| (q.y - p.y).abs() <= tol)
|
||||
kd_any(&self.keep, 0, Point::new(p.x - tol, p.y - tol), Point::new(p.x + tol, p.y + tol))
|
||||
}
|
||||
|
||||
/// The input curves whose bounds hold both `a` and `b`.
|
||||
@@ -790,18 +820,41 @@ fn tidy_segments(segs: Vec<Seg>, tidy: &Tidy) -> Vec<Seg> {
|
||||
if let Some(k) = (0..n).find(|&i| !mergeable(&segs[(i + n - 1) % n], &segs[i])) {
|
||||
segs.rotate_left(k);
|
||||
}
|
||||
// Where the run of mergeable joints that starts at each segment ends, found once.
|
||||
let mut run_end = vec![n; n];
|
||||
for i in (0..n).rev() {
|
||||
run_end[i] = if i + 1 < n && mergeable(&segs[i], &segs[i + 1]) { run_end[i + 1] } else { i + 1 };
|
||||
}
|
||||
let mut out: Vec<Seg> = Vec::with_capacity(n);
|
||||
let mut i = 0;
|
||||
while i < n {
|
||||
let mut cur = segs[i];
|
||||
let mut j = i + 1;
|
||||
while j < n && mergeable(&segs[j - 1], &segs[j]) {
|
||||
match join(&segs[i..=j], tidy) {
|
||||
Some(m) => {
|
||||
let end = run_end[i];
|
||||
// A run is validated whole, once (its pieces split one line or curve). Only when it doesn't
|
||||
// join is the longest prefix that does looked for, by halving, instead of re-checking the
|
||||
// whole run for every piece added: that was quadratic in a long collinear run.
|
||||
if end > j {
|
||||
if let Some(m) = join(&segs[i..end], tidy) {
|
||||
cur = m;
|
||||
j = end;
|
||||
} else {
|
||||
let (mut ok, mut bad) = (i, end - 1);
|
||||
let mut best = None;
|
||||
while bad - ok > 1 {
|
||||
let mid = ok + (bad - ok) / 2;
|
||||
match join(&segs[i..=mid], tidy) {
|
||||
Some(m) => {
|
||||
ok = mid;
|
||||
best = Some(m);
|
||||
}
|
||||
None => bad = mid,
|
||||
}
|
||||
}
|
||||
if let Some(m) = best {
|
||||
cur = m;
|
||||
j += 1;
|
||||
j = ok + 1;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
out.push(cur);
|
||||
@@ -810,8 +863,16 @@ fn tidy_segments(segs: Vec<Seg>, tidy: &Tidy) -> Vec<Seg> {
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// How many times `join` ran on this thread, for the tests that bound the work.
|
||||
static JOINS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// `run` as one segment, when its pieces lie on one line or one input curve.
|
||||
fn join(run: &[Seg], tidy: &Tidy) -> Option<Seg> {
|
||||
#[cfg(test)]
|
||||
JOINS.with(|joins| joins.set(joins.get() + 1));
|
||||
let (first, last) = (run.first()?.c, run.last()?.c);
|
||||
let (a, b) = (first.p0, last.p3);
|
||||
let tol = tol_at(Point::new(a.x.abs().max(b.x.abs()), a.y.abs().max(b.y.abs())));
|
||||
@@ -1051,4 +1112,80 @@ mod tests {
|
||||
// Less the slivers left out.
|
||||
assert!(sum <= union * (1.0 + 1e-9) && sum > union * (1.0 - 1e-4), "{sum} vs {union}");
|
||||
}
|
||||
|
||||
/// Many anchors sharing an x (and some within tolerance of it): the k-d lookup answers as a scan of
|
||||
/// every anchor does.
|
||||
#[test]
|
||||
fn is_kept_agrees_with_a_scan_when_many_anchors_share_an_x() {
|
||||
let mut seed: u64 = 0x2545_f491_4f6c_dd1d;
|
||||
let mut next = || {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 7;
|
||||
seed ^= seed << 17;
|
||||
(seed >> 11) as f64 / (1u64 << 53) as f64
|
||||
};
|
||||
let mut paths = Vec::new();
|
||||
for k in 0..3000 {
|
||||
let x = [5.0, 5.0 + 3e-7, 100.0][k % 3];
|
||||
let mut bp = BezPath::new();
|
||||
bp.move_to((x, next() * 1000.0));
|
||||
bp.line_to((x + 1.0, next() * 1000.0));
|
||||
paths.push(bp);
|
||||
}
|
||||
let tidy = Tidy::keeping(DEFAULT_SLIVER, &paths);
|
||||
let anchors: Vec<Point> = paths.iter().flat_map(|p| p.segments().map(|s| to_seg(s).c.p0)).collect();
|
||||
let (mut kept, mut not_kept) = (0, 0);
|
||||
for k in 0..4000 {
|
||||
let a = anchors[k % anchors.len()];
|
||||
let p = match k % 4 {
|
||||
0 => a,
|
||||
1 => Point::new(a.x + 5e-7, a.y + 5e-7),
|
||||
2 => Point::new(a.x, a.y + 1e-3),
|
||||
_ => Point::new(next() * 120.0, next() * 1000.0),
|
||||
};
|
||||
let tol = tol_at(p);
|
||||
let scan = anchors.iter().any(|q| (q.x - p.x).abs() <= tol && (q.y - p.y).abs() <= tol);
|
||||
assert_eq!(tidy.is_kept(p), scan, "{p:?}");
|
||||
if scan { kept += 1 } else { not_kept += 1 }
|
||||
}
|
||||
assert!(kept > 1000 && not_kept > 1000, "{kept} {not_kept}");
|
||||
assert!(!Tidy::keeping(DEFAULT_SLIVER, &Vec::<BezPath>::new()).is_kept(Point::new(0.0, 0.0)));
|
||||
}
|
||||
|
||||
/// A contour with a 20,000-piece collinear side is joined by validating the run once, not once per
|
||||
/// piece (some 200,000,000 piece checks).
|
||||
#[test]
|
||||
fn a_long_collinear_run_is_validated_once() {
|
||||
let n = 20_000;
|
||||
let tidy = Tidy::keeping(DEFAULT_SLIVER, &Vec::<BezPath>::new());
|
||||
let mut segs: Vec<Seg> = (0..n).map(|i| Seg::line(Point::new(i as f64, 0.0), Point::new(i as f64 + 1.0, 0.0))).collect();
|
||||
segs.push(Seg::line(Point::new(n as f64, 0.0), Point::new(0.0, 1.0)));
|
||||
segs.push(Seg::line(Point::new(0.0, 1.0), Point::new(0.0, 0.0)));
|
||||
let before = JOINS.with(std::cell::Cell::get);
|
||||
let out = tidy_segments(segs, &tidy);
|
||||
let joins = JOINS.with(std::cell::Cell::get) - before;
|
||||
assert_eq!(out.len(), 3);
|
||||
assert!(joins <= 4, "{joins} joins");
|
||||
assert_eq!((out[0].c.p0, out[0].c.p3), (Point::new(0.0, 0.0), Point::new(n as f64, 0.0)));
|
||||
}
|
||||
|
||||
/// A run that bends partway still joins its longest straight part, in a few validations.
|
||||
#[test]
|
||||
fn a_run_that_bends_joins_its_longest_prefix() {
|
||||
let n = 5000;
|
||||
let tidy = Tidy::keeping(DEFAULT_SLIVER, &Vec::<BezPath>::new());
|
||||
let mut segs: Vec<Seg> = (0..n).map(|i| Seg::line(Point::new(i as f64, 0.0), Point::new(i as f64 + 1.0, 0.0))).collect();
|
||||
// A last piece that leaves the line by 0.01 (inside the tangent test, outside the join's tolerance).
|
||||
segs.push(Seg::line(Point::new(n as f64, 0.0), Point::new(n as f64 + 100.0, 1.0)));
|
||||
segs.push(Seg::line(Point::new(n as f64 + 100.0, 1.0), Point::new(0.0, 5.0)));
|
||||
let before = JOINS.with(std::cell::Cell::get);
|
||||
let out = tidy_segments(segs, &tidy);
|
||||
let joins = JOINS.with(std::cell::Cell::get) - before;
|
||||
assert!(out.len() <= 4 && joins < 200, "{} pieces, {joins} joins", out.len());
|
||||
assert!(
|
||||
out.iter().any(|s| s.c.p0 == Point::new(0.0, 0.0) && s.c.p3.x >= n as f64 - 1.0),
|
||||
"{:?}",
|
||||
out.iter().map(|s| (s.c.p0, s.c.p3)).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user