mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #3882 from dylanmtaylor/fix/audit-geom-pathops
Security: geometry, path edit and tool work bounds
This commit is contained in:
@@ -22,7 +22,7 @@ impl ArcPath {
|
||||
let mut segs = Vec::new();
|
||||
let mut cum = 0.0;
|
||||
for s in path.to_bezpath().segments() {
|
||||
let l = s.arclen(ACCURACY);
|
||||
let l = crate::path::measured_arclen(&s, ACCURACY);
|
||||
if l.is_finite() && l > 1e-9 {
|
||||
segs.push((s, cum, l));
|
||||
cum += l;
|
||||
@@ -51,7 +51,8 @@ impl ArcPath {
|
||||
let s = if self.closed && self.len > 0.0 { s.rem_euclid(self.len) } else { s.clamp(0.0, self.len) };
|
||||
let i = self.segs.partition_point(|(_, c, _)| *c <= s).saturating_sub(1);
|
||||
let Some(&(seg, c, l)) = self.segs.get(i) else { return (Point::ZERO, Vec2::new(1.0, 0.0)) };
|
||||
let t = seg.inv_arclen((s - c).min(l), ACCURACY).clamp(0.0, 1.0);
|
||||
// (The solver's precision is relative to the segment's length: a long one needs a looser one.)
|
||||
let t = seg.inv_arclen((s - c).min(l), ACCURACY.max(l * 1e-9)).clamp(0.0, 1.0);
|
||||
let p = seg.eval(t);
|
||||
let (t0, t1) = ((t - 1e-4).max(0.0), (t + 1e-4).min(1.0));
|
||||
let d = seg.eval(t1) - seg.eval(t0);
|
||||
|
||||
@@ -39,7 +39,11 @@ impl Side {
|
||||
|
||||
/// The side's length.
|
||||
fn length(&self) -> f64 {
|
||||
if self.straight() { self.curve.p0.distance(self.curve.p3) } else { self.curve.arclen(ARCLEN_ACCURACY) }
|
||||
if self.straight() {
|
||||
self.curve.p0.distance(self.curve.p3)
|
||||
} else {
|
||||
crate::path::measured_arclen(&kurbo::PathSeg::Cubic(self.curve), ARCLEN_ACCURACY)
|
||||
}
|
||||
}
|
||||
|
||||
/// The parameter `s` along the side from the corner, the point there and the direction the
|
||||
|
||||
+18
-1
@@ -561,10 +561,27 @@ impl PathData {
|
||||
}
|
||||
/// Total length of all segments.
|
||||
pub fn length(&self) -> f64 {
|
||||
self.to_bezpath().segments().map(|s| kurbo::ParamCurveArclen::arclen(&s, 1e-6)).sum()
|
||||
self.to_bezpath().segments().map(|s| measured_arclen(&s, 1e-6)).sum()
|
||||
}
|
||||
}
|
||||
|
||||
/// The largest x or y, in size, of a segment's control points for which its length is measured.
|
||||
/// Document coordinates are far inside it; past it (or not finite) the integrator's arithmetic
|
||||
/// overflows and it subdivides without end.
|
||||
const MEASURE_LIMIT: f64 = 1e9;
|
||||
|
||||
/// The length of `seg` to `accuracy`; 0 for a segment whose control points are not finite or past
|
||||
/// [`MEASURE_LIMIT`].
|
||||
pub(crate) fn measured_arclen(seg: &PathSeg, accuracy: f64) -> f64 {
|
||||
let ok = |p: Point| p.x.is_finite() && p.y.is_finite() && p.x.abs() <= MEASURE_LIMIT && p.y.abs() <= MEASURE_LIMIT;
|
||||
let fits = match seg {
|
||||
PathSeg::Line(l) => ok(l.p0) && ok(l.p1),
|
||||
PathSeg::Quad(q) => ok(q.p0) && ok(q.p1) && ok(q.p2),
|
||||
PathSeg::Cubic(c) => ok(c.p0) && ok(c.p1) && ok(c.p2) && ok(c.p3),
|
||||
};
|
||||
if fits { kurbo::ParamCurveArclen::arclen(seg, accuracy) } else { 0.0 }
|
||||
}
|
||||
|
||||
/// The largest x or y, in size, of a segment's control points for which [`segment_bounds`]
|
||||
/// measures the segment by its extrema. kurbo adds up to eight coordinates together when it finds a
|
||||
/// segment's extrema and evaluates the segment there, and a sum of eight coordinates of this size or
|
||||
|
||||
@@ -254,7 +254,17 @@ fn rdp(pts: &[Point], tol: f64) -> Vec<Point> {
|
||||
keep[0] = true;
|
||||
keep[pts.len() - 1] = true;
|
||||
let mut stack = vec![(0, pts.len() - 1)];
|
||||
// Distances measured at most: lopsided splits (a shallow zigzag) make it quadratic. Past it,
|
||||
// the points of the parts left are kept as they are.
|
||||
let mut budget = pts.len().saturating_mul(64).saturating_add(1 << 20);
|
||||
while let Some((a, b)) = stack.pop() {
|
||||
budget = match budget.checked_sub(b - a) {
|
||||
Some(left) => left,
|
||||
None => {
|
||||
keep[a..=b].fill(true);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let (pa, pb) = (pts[a], pts[b]);
|
||||
let chord = pb - pa;
|
||||
let len = chord.hypot();
|
||||
@@ -318,6 +328,23 @@ pub fn remove_redundant_points(path: &PathData, tolerance: f64) -> PathData {
|
||||
// Drop zero-length segments.
|
||||
segs.retain(|s| !(s.c.p0.distance(s.c.p3) < 1e-9 && s.c.p1.distance(s.c.p0) < 1e-9 && s.c.p2.distance(s.c.p0) < 1e-9));
|
||||
let min_segs = if sp.closed { 2 } else { 1 };
|
||||
// Joints are merged along the path in one pass (a merged segment is tried with the one
|
||||
// before it, then the next); the search below then only finds what a closed path's wrap
|
||||
// leaves. (Merging at the first joint of a dense line one at a time shifts the rest each time.)
|
||||
let total = segs.len();
|
||||
let mut merged = 0;
|
||||
let mut done: Vec<Seg> = Vec::with_capacity(total);
|
||||
for s in segs.drain(..) {
|
||||
done.push(s);
|
||||
while done.len() >= 2 && total - merged > min_segs {
|
||||
let n = done.len();
|
||||
let Some(m) = merge_pair(&done[n - 2], &done[n - 1], tol) else { break };
|
||||
done.truncate(n - 1);
|
||||
done[n - 2] = m;
|
||||
merged += 1;
|
||||
}
|
||||
}
|
||||
segs = done;
|
||||
let mut changed = true;
|
||||
while changed && segs.len() > min_segs {
|
||||
changed = false;
|
||||
@@ -521,9 +548,12 @@ pub fn join(paths: &[PathData], tolerance: f64) -> PathData {
|
||||
return PathData::new(closed);
|
||||
}
|
||||
while open.len() > 1 {
|
||||
// Find the closest pair of endpoints on different subpaths.
|
||||
// Find the closest pair of endpoints on different subpaths. Past this many subpaths every
|
||||
// pair at every merge is too much (cubic): the first one is joined to its nearest.
|
||||
const SEARCH_ALL: usize = 200;
|
||||
let first_only = open.len() > SEARCH_ALL;
|
||||
let mut best = (f64::INFINITY, 0, 0, false, false);
|
||||
for i in 0..open.len() {
|
||||
for i in 0..if first_only { 1 } else { open.len() } {
|
||||
for j in (i + 1)..open.len() {
|
||||
for ei in [false, true] {
|
||||
for ej in [false, true] {
|
||||
|
||||
@@ -388,7 +388,8 @@ fn subdivide(sp: &mut SubPath, prm: &LiquifyParams, c: Point, spacing: f64) {
|
||||
continue;
|
||||
}
|
||||
let len = cub.arclen(1e-3);
|
||||
let k = (len / spacing).ceil() as usize;
|
||||
// (No more pieces than the anchors left under the ceiling.)
|
||||
let k = ((len / spacing).ceil() as usize).min(MAX_ANCHORS.saturating_sub(sp.anchors.len()) + 1);
|
||||
if k <= 1 {
|
||||
seg += 1;
|
||||
continue;
|
||||
|
||||
@@ -549,6 +549,11 @@ impl PerspectiveGrid {
|
||||
let Some(h) = self.homography(plane) else { return vec![] };
|
||||
let d = self.domain(plane);
|
||||
let mut out = vec![];
|
||||
// A stored grid is not checked: no spacing, or an extent that isn't a number, has no lines.
|
||||
let (w, h_len) = (d.width(), d.height());
|
||||
if !(self.cell.is_finite() && self.cell > 0.0 && w.is_finite() && h_len.is_finite()) {
|
||||
return out;
|
||||
}
|
||||
let step = |len: f64| {
|
||||
let mut s = self.cell;
|
||||
while len / s > 120.0 {
|
||||
@@ -556,20 +561,22 @@ impl PerspectiveGrid {
|
||||
}
|
||||
s
|
||||
};
|
||||
let (su, sv) = (step(d.width()), step(d.height()));
|
||||
let mut u = 0.0;
|
||||
while u <= d.width() + 1e-9 {
|
||||
if let (Some(a), Some(b)) = (h.apply(Point::new(u, 0.0)), h.apply(Point::new(u, d.height()))) {
|
||||
let (su, sv) = (step(w), step(h_len));
|
||||
// Counted, not summed: a count in whole lines (at most 121 a way). The fixed 1e-9
|
||||
// tolerance can inflate the count when the whole domain is far smaller than it, so the
|
||||
// bound is capped on its own, before any loop runs.
|
||||
let count = |len: f64, s: f64| ((len + 1e-9) / s).floor().clamp(0.0, 120.0) as usize;
|
||||
for i in 0..=count(w, su) {
|
||||
let u = i as f64 * su;
|
||||
if let (Some(a), Some(b)) = (h.apply(Point::new(u, 0.0)), h.apply(Point::new(u, h_len))) {
|
||||
out.push((a, b));
|
||||
}
|
||||
u += su;
|
||||
}
|
||||
let mut v = 0.0;
|
||||
while v <= d.height() + 1e-9 {
|
||||
if let (Some(a), Some(b)) = (h.apply(Point::new(0.0, v)), h.apply(Point::new(d.width(), v))) {
|
||||
for i in 0..=count(h_len, sv) {
|
||||
let v = i as f64 * sv;
|
||||
if let (Some(a), Some(b)) = (h.apply(Point::new(0.0, v)), h.apply(Point::new(w, v))) {
|
||||
out.push((a, b));
|
||||
}
|
||||
v += sv;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -123,6 +123,16 @@ mod tests {
|
||||
assert_eq!(g.snap_offset(b, Vec2::new(18.0, 0.0)), Vec2::new(18.0, 0.0), "nothing within reach");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tiny_stored_grid_draws_a_bounded_number_of_lines() {
|
||||
// A stored grid can carry extreme values: the loop bound must not depend on a fixed
|
||||
// tolerance swallowing the whole domain.
|
||||
let g = PerspectiveGrid { cell: 1e-18, extent: 1e-18, height: 1e-18, ..PerspectiveGrid::normal(2, Rect::new(0.0, 0.0, 800.0, 600.0)) };
|
||||
for pl in Plane::ALL {
|
||||
assert!(g.lines(pl).len() <= 121 + 121, "at most 121 lines a way on {pl:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colours_and_rulers_come_from_the_definition() {
|
||||
let mut g = grid();
|
||||
|
||||
Reference in New Issue
Block a user