mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 15:25:16 +00:00
Merge main into fix/custom-toolbar-add-tools
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use kurbo::{CubicBez, ParamCurve, PathSeg, Point, Rect, Vec2};
|
||||
use kurbo::{CubicBez, Line, ParamCurve, ParamCurveNearest, PathSeg, Point, Rect, Vec2};
|
||||
use vectorcraft_geom::path::measured_arclen;
|
||||
use vectorcraft_geom::{Anchor, AnchorKind, PathData, SubPath};
|
||||
|
||||
@@ -85,9 +85,10 @@ pub fn simplify_with(path: &PathData, opts: &SimplifyOptions) -> PathData {
|
||||
}
|
||||
|
||||
/// [`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.
|
||||
/// whose refit is one segment, or 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. When three parts still give
|
||||
/// no area, straight lines follow the subpath within the tolerance, and without straight lines it
|
||||
/// stays as it is, as does one of a single segment; one that is all in line keeps the refit.
|
||||
fn simplify_subpath(sp: &SubPath, opts: &SimplifyOptions) -> Option<SubPath> {
|
||||
let mut segs = subpath_segs(sp);
|
||||
let n = segs.len();
|
||||
@@ -105,7 +106,12 @@ fn simplify_subpath(sp: &SubPath, opts: &SimplifyOptions) -> Option<SubPath> {
|
||||
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 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);
|
||||
let in_line = |pts: &[Point]| {
|
||||
let Some(&a) = pts.first() else { return true };
|
||||
let far = pts.iter().map(|&p| p - a).max_by(|u, v| u.hypot2().total_cmp(&v.hypot2())).unwrap_or_default();
|
||||
pts.iter().all(|&p| far.cross(p - a).abs() <= 1e-6 * far.hypot())
|
||||
};
|
||||
let flat = |r: &[Seg]| r.len() < 2 || r.iter().all(|s| s.line) && in_line(&r.iter().map(|s| s.c.p0).collect::<Vec<_>>());
|
||||
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.
|
||||
@@ -131,6 +137,30 @@ fn simplify_subpath(sp: &SubPath, opts: &SimplifyOptions) -> Option<SubPath> {
|
||||
if flat(&result) && k != j {
|
||||
result = refit(&[0, j.min(k), j.max(k), m]);
|
||||
}
|
||||
let ctrl: Vec<Point> = segs.iter().flat_map(|s| [s.c.p0, s.c.p1, s.c.p2]).collect();
|
||||
if flat(&result) && !in_line(&ctrl) {
|
||||
// Each segment split in 2, 4, 8 and so on, up to 1,024 pieces and 65,536 in all, until every
|
||||
// piece is within the tolerance of the line between its ends and those lines enclose an
|
||||
// area: with straight lines they are the result, and without them the loop stays as it is.
|
||||
// A loop whose points are all in line keeps the lines of the refit.
|
||||
let near = |c: &CubicBez| [c.p1, c.p2].iter().all(|&p| Line::new(c.p0, c.p3).nearest(p, 1e-9).distance_sq.sqrt() <= opts.tolerance);
|
||||
for k in 1..=10u32 {
|
||||
let parts = 1u32 << k;
|
||||
if segs.len().saturating_mul(parts as usize) > 1 << 16 {
|
||||
break;
|
||||
}
|
||||
let at = |i: u32| f64::from(i) / f64::from(parts);
|
||||
let pieces: Vec<CubicBez> = segs.iter().flat_map(|s| (0..parts).map(move |i| s.c.subsegment(at(i)..at(i + 1)))).collect();
|
||||
let lines: Vec<Seg> = pieces.iter().map(|c| Seg::line(c.p0, c.p3)).collect();
|
||||
if pieces.iter().all(near) && !flat(&lines) {
|
||||
if !opts.straight_lines {
|
||||
return Some(sp.clone());
|
||||
}
|
||||
result = lines;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let s = segs_to_subpath(&result, sp.closed)?;
|
||||
Some(if opts.straight_lines { s } else { no_worse(sp, s) })
|
||||
|
||||
@@ -424,17 +424,59 @@ fn simplify_keeps_a_closed_subpath_that_refits_to_one_curve() {
|
||||
],
|
||||
true,
|
||||
));
|
||||
// A straight edge and an S curve whose midpoints all lie on that edge.
|
||||
let s_curve = |h: f64, x: f64| {
|
||||
PathData::single(SubPath::new(
|
||||
vec![
|
||||
Anchor::with_handles(Point::new(0.0, 0.0), Point::new(0.0, 0.0), Point::new(0.0, h)),
|
||||
Anchor::with_handles(Point::new(100.0, 0.0), Point::new(x, -h), Point::new(100.0, 0.0)),
|
||||
],
|
||||
true,
|
||||
))
|
||||
};
|
||||
let (s_low, s_high) = (s_curve(3.0, 0.0), s_curve(13.5, 100.0));
|
||||
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] {
|
||||
for path in [&compound, &lone, &bow, &s_low] {
|
||||
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));
|
||||
let lines = s.subpaths.iter().all(|sp| (0..sp.segment_count()).all(|i| sp.segment_is_line(i)));
|
||||
assert!(lines || !straight_lines, "{tolerance} pt: {s:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Lobes on both sides of the edge, 3.9 pt from it, stay within the tolerance.
|
||||
for straight_lines in [false, true] {
|
||||
let s = simplify_with(&s_high, &SimplifyOptions { tolerance: 4.0, straight_lines, ..Default::default() });
|
||||
assert!(max_deviation(&s_high, &s) <= 4.0 + 1e-6, "straight lines {straight_lines}: {}", max_deviation(&s_high, &s));
|
||||
}
|
||||
// Curves whose handles lie on the line between their anchors become lines.
|
||||
let thin = PathData::single(SubPath::new(
|
||||
vec![
|
||||
Anchor::with_handles(Point::new(0.0, 0.0), Point::new(20.0, 0.0), Point::new(30.0, 0.0)),
|
||||
Anchor::with_handles(Point::new(100.0, 0.0), Point::new(70.0, 0.0), Point::new(80.0, 0.0)),
|
||||
],
|
||||
true,
|
||||
));
|
||||
let s = simplify_with(&thin, &SimplifyOptions { tolerance: 4.0, straight_lines: true, ..Default::default() });
|
||||
assert!(s.subpaths.iter().all(|sp| (0..sp.segment_count()).all(|i| sp.segment_is_line(i))), "{s:?}");
|
||||
// Anchors in line on a closed path are still removed.
|
||||
let in_line = PathData::single(SubPath::new([0.0, 25.0, 50.0, 100.0].map(|x| Anchor::corner(Point::new(x, 0.0))).to_vec(), true));
|
||||
let s = simplify_with(&in_line, &SimplifyOptions { tolerance: 4.0, ..Default::default() });
|
||||
assert!(s.subpaths[0].anchors.len() < 4, "{s:?}");
|
||||
let many = PathData::single(SubPath::new((0..5000).map(|i| Anchor::corner(Point::new(f64::from(i), 0.0))).collect(), true));
|
||||
for tolerance in [4.0, f64::INFINITY, f64::NAN] {
|
||||
for path in [&in_line, &thin, &many] {
|
||||
simplify_with(path, &SimplifyOptions { tolerance, straight_lines: true, ..Default::default() });
|
||||
}
|
||||
}
|
||||
// A long, thin rectangle keeps its corners.
|
||||
let long = rect(0.0, 0.0, 1e9, 0.5);
|
||||
let s = simplify_with(&long, &SimplifyOptions { tolerance: 0.1, straight_lines: true, ..Default::default() });
|
||||
assert!(max_deviation(&long, &s) <= 0.1 + 1e-6, "{s:?}");
|
||||
}
|
||||
|
||||
/// The Simplify slider's levels: 1 pt at 50, doubled for every 10 levels down and halved for every
|
||||
|
||||
Reference in New Issue
Block a user