mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 22:22:25 +00:00
Merge pull request #6614 from dotCooCoo/fix/simplify-keeps-small-closed-subpaths
Simplify: a closed subpath whose refit is one curve stays in the path
This commit is contained in:
@@ -84,11 +84,14 @@ pub fn simplify_with(path: &PathData, opts: &SimplifyOptions) -> PathData {
|
||||
PathData::new(out)
|
||||
}
|
||||
|
||||
/// [`simplify_with`] for one subpath; `None` when the refit leaves nothing of it.
|
||||
/// [`simplify_with`] for one subpath; `None` when the refit leaves nothing of it. A closed subpath
|
||||
/// whose refit is one segment, or two lines that enclose no area, is refitted in two parts, cut at
|
||||
/// the point farthest from its start, or in three when two still give that; one of a single segment
|
||||
/// stays as it is.
|
||||
fn simplify_subpath(sp: &SubPath, opts: &SimplifyOptions) -> Option<SubPath> {
|
||||
let mut segs = subpath_segs(sp);
|
||||
let n = segs.len();
|
||||
if n == 0 {
|
||||
if n == 0 || sp.closed && n < 2 {
|
||||
return Some(sp.clone());
|
||||
}
|
||||
if sp.closed {
|
||||
@@ -101,7 +104,34 @@ fn simplify_subpath(sp: &SubPath, opts: &SimplifyOptions) -> Option<SubPath> {
|
||||
// along its chord, and `no_worse` can return the original subpath, so the start can keep a kink.
|
||||
let across = if sp.closed && opts.smooth > 0.0 { unit(end_tangent(&segs[n - 1].c) + start_tangent(&segs[0].c)) } else { None };
|
||||
let across = across.filter(|_| (0..n).all(|i| turn_deg(&segs[(i + n - 1) % n], &segs[i]) <= opts.corner_angle_deg));
|
||||
let result = simplify_run_within(&segs, opts, MAX_RUN_SAMPLES, across);
|
||||
let mut result = simplify_run_within(&segs, opts, MAX_RUN_SAMPLES, across);
|
||||
let flat = |r: &[Seg]| r.len() < 2 || r.len() == 2 && r.iter().all(|s| s.line);
|
||||
if sp.closed && flat(&result) {
|
||||
// Each segment in two, and the loop cut where they meet: first at the point farthest from its
|
||||
// start, then also at the point farthest from the line between those two.
|
||||
let halves: Vec<Seg> = segs
|
||||
.iter()
|
||||
.flat_map(|s| {
|
||||
let (a, b) = s.c.subdivide();
|
||||
[Seg { c: a, line: s.line }, Seg { c: b, line: s.line }]
|
||||
})
|
||||
.collect();
|
||||
let m = halves.len();
|
||||
let at = |i: usize| halves[i].c.p0;
|
||||
let farthest = |from: &dyn Fn(Point) -> f64, skip: usize| (1..m).filter(|&i| i != skip).max_by(|&a, &b| from(at(a)).total_cmp(&from(at(b))));
|
||||
let p0 = at(0);
|
||||
let j = farthest(&|p| p.distance(p0), 0).unwrap_or(m / 2);
|
||||
let chord = at(j) - p0;
|
||||
let off_chord = |p: Point| if chord.hypot2() > 0.0 { chord.cross(p - p0).abs() } else { p.distance(p0) };
|
||||
let k = farthest(&off_chord, j).unwrap_or(m / 2);
|
||||
let refit = |cuts: &[usize]| -> Vec<Seg> {
|
||||
cuts.windows(2).flat_map(|w| simplify_run_within(&halves[w[0]..w[1]], opts, MAX_RUN_SAMPLES / 3, None)).collect()
|
||||
};
|
||||
result = refit(&[0, j, m]);
|
||||
if flat(&result) && k != j {
|
||||
result = refit(&[0, j.min(k), j.max(k), m]);
|
||||
}
|
||||
}
|
||||
let s = segs_to_subpath(&result, sp.closed)?;
|
||||
Some(if opts.straight_lines { s } else { no_worse(sp, s) })
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use kurbo::{ParamCurve, Point, Rect, Shape as _};
|
||||
use vectorcraft_geom::{AnchorKind, FillRule, PathData, SubPath};
|
||||
use vectorcraft_geom::{Anchor, AnchorKind, FillRule, PathData, SubPath};
|
||||
use vectorcraft_pathops::*;
|
||||
|
||||
const NZ: FillRule = FillRule::NonZero;
|
||||
@@ -408,6 +408,35 @@ fn simplify_straight_lines_option() {
|
||||
assert!(max_deviation(&c, &s) <= 1.0 + 1e-6);
|
||||
}
|
||||
|
||||
/// A closed subpath whose refit is one curve stays a closed subpath that encloses an area, in a
|
||||
/// compound path and on its own, with curves and with Convert to Straight Lines.
|
||||
#[test]
|
||||
fn simplify_keeps_a_closed_subpath_that_refits_to_one_curve() {
|
||||
let small = |cx: f64, cy: f64| circle(cx, cy, 2.0).subpaths.remove(0);
|
||||
let square = rect(0.0, 0.0, 1000.0, 1000.0).subpaths.remove(0);
|
||||
let compound = PathData::new(vec![square, small(500.0, 500.0)]);
|
||||
let lone = PathData::single(small(0.0, 0.0));
|
||||
// A straight edge and a curve that bows 2.25 pt from it.
|
||||
let bow = PathData::single(SubPath::new(
|
||||
vec![
|
||||
Anchor::with_handles(Point::new(0.0, 0.0), Point::new(0.0, 3.0), Point::new(0.0, 0.0)),
|
||||
Anchor::with_handles(Point::new(100.0, 0.0), Point::new(100.0, 0.0), Point::new(100.0, 3.0)),
|
||||
],
|
||||
true,
|
||||
));
|
||||
for straight_lines in [false, true] {
|
||||
for tolerance in [4.0, 10.0, 32.0] {
|
||||
let opts = SimplifyOptions { tolerance, straight_lines, ..Default::default() };
|
||||
for path in [&compound, &lone, &bow] {
|
||||
let s = simplify_with(path, &opts);
|
||||
assert_eq!(s.subpaths.len(), path.subpaths.len(), "{tolerance} pt, straight lines {straight_lines}: {s:?}");
|
||||
assert!(s.subpaths.iter().all(|sp| sp.closed && sp.anchors.len() >= 2 && sp.area().abs() > 1.0), "{s:?}");
|
||||
assert!(max_deviation(path, &s) <= tolerance + 1e-6, "{tolerance} pt: {}", max_deviation(path, &s));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The Simplify slider's levels: 1 pt at 50, doubled for every 10 levels down and halved for every
|
||||
/// 10 up; Auto-Simplify takes the lowest level whose tolerance is at most 1/400 of the size, and
|
||||
/// level 100 for a size under 12.5 pt.
|
||||
|
||||
Reference in New Issue
Block a user