mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 21:57:12 -06:00
Smooth: the slider smooths bumps and jags into curves, and smooths only a direct-selected section
This commit is contained in:
@@ -45,7 +45,10 @@ fn fit_freehand(pts: &[Point], tol: f64, closed: bool) -> SubPath {
|
||||
if sp.anchors.len() < 3 {
|
||||
return sp;
|
||||
}
|
||||
let fitted = po::simplify_with(&PathData::single(sp.clone()), &SimplifyOptions { tolerance: tol, corner_angle_deg: 60.0, straight_lines: false });
|
||||
let fitted = po::simplify_with(
|
||||
&PathData::single(sp.clone()),
|
||||
&SimplifyOptions { tolerance: tol, corner_angle_deg: 60.0, straight_lines: false, ..Default::default() },
|
||||
);
|
||||
fitted.subpaths.into_iter().next().filter(|s| s.anchors.len() >= 2).unwrap_or(sp)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use super::{
|
||||
/// Refit the anchors of an open run `a[lo..=hi]` keeping its end points and outer handles.
|
||||
fn refit_run(anchors: &[Anchor], tol: f64) -> Vec<Anchor> {
|
||||
let piece = PathData::single(SubPath::new(anchors.to_vec(), false));
|
||||
let fitted = po::simplify_with(&piece, &SimplifyOptions { tolerance: tol, corner_angle_deg: 179.0, straight_lines: false });
|
||||
let fitted = po::simplify_with(&piece, &SimplifyOptions { tolerance: tol, corner_angle_deg: 179.0, straight_lines: false, ..Default::default() });
|
||||
let Some(mut out) = fitted.subpaths.into_iter().next().map(|s| s.anchors).filter(|a| a.len() >= 2) else { return anchors.to_vec() };
|
||||
let (f, l) = (anchors[0], anchors[anchors.len() - 1]);
|
||||
let last = out.len() - 1;
|
||||
@@ -32,8 +32,10 @@ fn smooth_subpath(sp: &SubPath, near: &dyn Fn(Point) -> bool, tol: f64) -> Optio
|
||||
if sp.closed {
|
||||
let Some(k) = marked.iter().position(|m| !m) else {
|
||||
// Everything is in range: smooth the whole closed path.
|
||||
let f =
|
||||
po::simplify_with(&PathData::single(sp.clone()), &SimplifyOptions { tolerance: tol, corner_angle_deg: 179.0, straight_lines: false });
|
||||
let f = po::simplify_with(
|
||||
&PathData::single(sp.clone()),
|
||||
&SimplifyOptions { tolerance: tol, corner_angle_deg: 179.0, straight_lines: false, ..Default::default() },
|
||||
);
|
||||
return f.subpaths.into_iter().next();
|
||||
};
|
||||
// Rotate so an untouched anchor comes first, duplicate it at the end and treat as open.
|
||||
|
||||
@@ -161,7 +161,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Smooth…",
|
||||
["Object", "Path"],
|
||||
None,
|
||||
"{level?: 0..100 (50; the Smooth slider: 0 minimum smoothing, the path staying within 0.1% of its longer side, 100 maximum: refitted with curves within up to 2.5% of it, corners kept only where the path turns back, never with more anchor points), auto?: bool (Auto-Smooth: level 50), ids?: [id…] (default: the selection)} smooth each path under the objects, compound path members included, as one undo step → {ids, before, after} anchor counts, and the level used",
|
||||
"{level?: 0..100 (50; the Smooth slider: 0 minimum smoothing, the path staying within 0.1% of its longer side, 100 maximum: smoothed along its length by up to 5% of it and refitted with curves within up to 2.5% of it, corners kept only where the path turns back, never with more anchor points), auto?: bool (Auto-Smooth: level 50), ids?: [id…] (default: the selection)} smooth each path under the objects, compound path members included, as one undo step; without ids, a path with anchor points or segments direct-selected smooths only the sections between them, which stay selected → {ids, before, after} anchor counts, and the level used",
|
||||
has_selection,
|
||||
smooth
|
||||
),
|
||||
@@ -1089,6 +1089,7 @@ fn simplify(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
tolerance: tolerance.clamp(1e-4, 1e4),
|
||||
corner_angle_deg: num_param(p, "cornerAngle").unwrap_or(30.0).clamp(0.0, 180.0),
|
||||
straight_lines: bool_or(p, "straightLines", false),
|
||||
smooth: 0.0,
|
||||
};
|
||||
let whole = bool_or(p, "wholePaths", false);
|
||||
// A path with segments direct-selected simplifies only their runs, and keeps those runs (and the
|
||||
@@ -1099,13 +1100,7 @@ fn simplify(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return (po::simplify_with(path, &opts), None);
|
||||
};
|
||||
let part = po::simplify_part(path, &picked, &opts);
|
||||
let mut kept: BTreeSet<AnchorRef> = anchors.into_iter().flatten().filter_map(|a| part.anchors.get(a).copied()).collect();
|
||||
for &(si, i) in &part.segments {
|
||||
let m = part.path.subpaths.get(si).map_or(1, |sp| sp.anchors.len().max(1));
|
||||
kept.extend([(si, i), (si, (i + 1) % m)]);
|
||||
}
|
||||
let segs = segments.map(|_| part.segments.clone());
|
||||
(part.path, Some((kept, segs)))
|
||||
refitted_part(part, anchors, segments)
|
||||
})?;
|
||||
let mut out = json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>(), "before": before, "after": after });
|
||||
if let Some(l) = level {
|
||||
@@ -1114,14 +1109,39 @@ fn simplify(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// A path with only the runs of `part` refitted, and what stays direct-selected: the anchors it had
|
||||
/// selected that are still there, and the refitted runs (their anchors, and their segments when
|
||||
/// segments were selected).
|
||||
fn refitted_part(
|
||||
part: po::SimplifiedPart,
|
||||
anchors: Option<&BTreeSet<AnchorRef>>,
|
||||
segments: Option<&BTreeSet<SegmentRef>>,
|
||||
) -> (PathData, PartSelection) {
|
||||
let mut kept: BTreeSet<AnchorRef> = anchors.into_iter().flatten().filter_map(|a| part.anchors.get(a).copied()).collect();
|
||||
for &(si, i) in &part.segments {
|
||||
let m = part.path.subpaths.get(si).map_or(1, |sp| sp.anchors.len().max(1));
|
||||
kept.extend([(si, i), (si, (i + 1) % m)]);
|
||||
}
|
||||
let segs = segments.map(|_| part.segments.clone());
|
||||
(part.path, Some((kept, segs)))
|
||||
}
|
||||
|
||||
/// Object › Path › Smooth (`object.path.smooth`): [`po::smooth_level`] on each path under the
|
||||
/// objects given or selected, as one undo step; Auto-Smooth (`auto`) uses [`po::SMOOTH_AUTO_LEVEL`].
|
||||
/// A path with anchor points or segments direct-selected (and no `ids` given) smooths only the runs
|
||||
/// between them, which stay selected, as Simplify does.
|
||||
fn smooth(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "object.path.smooth";
|
||||
let level = number_or(p, "level", po::SMOOTH_AUTO_LEVEL, C)?.round().clamp(0.0, 100.0);
|
||||
let level = if switch_or(p, "auto", false, C)? { po::SMOOTH_AUTO_LEVEL } else { level };
|
||||
let given = p.get("ids").is_some_and(|v| !v.is_null());
|
||||
let roots = checked_ids_or_selection(s, p, C)?;
|
||||
let (ids, before, after) = edit_paths(s, &roots, "Smooth", |path, _, _| (po::smooth_level(path, level), None))?;
|
||||
let (ids, before, after) = edit_paths(s, &roots, "Smooth", |path, anchors, segments| {
|
||||
let Some(picked) = selected_segments(path, anchors, segments).filter(|_| !given) else {
|
||||
return (po::smooth_level(path, level), None);
|
||||
};
|
||||
refitted_part(po::simplify_part(path, &picked, &po::smooth_options(path, level)), anchors, segments)
|
||||
})?;
|
||||
Ok(json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>(), "before": before, "after": after, "level": level }))
|
||||
}
|
||||
|
||||
|
||||
@@ -161,3 +161,56 @@ fn bad_params_are_refused_and_change_nothing() {
|
||||
// Levels past the ends count as the ends.
|
||||
assert_eq!(s.execute("object.path.smooth", &json!({"level": 250})).unwrap()["level"], json!(100.0));
|
||||
}
|
||||
|
||||
/// A wave 360 pt long with an anchor every 9 pt pushed up to 6 pt off it, as a shaky hand draws.
|
||||
fn shaky_wave(s: &mut Session) -> NodeId {
|
||||
let pts: Vec<(f64, f64)> =
|
||||
(0..=40u32).map(|i| (20.0 + f64::from(i) * 9.0, 150.0 + 60.0 * (f64::from(i) / 6.0).sin() + f64::from((i * 37) % 13) - 6.0)).collect();
|
||||
polyline(s, &pts, false)
|
||||
}
|
||||
|
||||
/// The largest turn (degrees) at any anchor between two segments of `path`: a kink when large.
|
||||
fn sharpest_turn(path: &PathData) -> f64 {
|
||||
path.subpaths
|
||||
.iter()
|
||||
.flat_map(|sp| (1..sp.segment_count()).map(move |i| (sp.segment(i - 1), sp.segment(i))))
|
||||
.map(|(a, b)| {
|
||||
let (out, inn) = ((a.p3 - a.p2).normalize(), (b.p1 - b.p0).normalize());
|
||||
out.dot(inn).clamp(-1.0, 1.0).acos().to_degrees()
|
||||
})
|
||||
.fold(0.0, f64::max)
|
||||
}
|
||||
|
||||
/// Auto-Smooth takes a shaky line to one smooth curve: no kinks left at its anchors, far fewer of
|
||||
/// them, and the wave still where it was.
|
||||
#[test]
|
||||
fn auto_smooth_leaves_no_jags_in_a_shaky_line() {
|
||||
let mut s = session();
|
||||
let id = shaky_wave(&mut s);
|
||||
let before = path_of(&s, id);
|
||||
s.execute("object.path.smooth", &json!({"auto": true})).unwrap();
|
||||
let after = path_of(&s, id);
|
||||
assert!(sharpest_turn(&after) < 12.0, "a kink of {}°", sharpest_turn(&after));
|
||||
assert!(after.anchor_count() <= before.anchor_count() / 3, "{} anchors", after.anchor_count());
|
||||
assert!(farthest(&after, &before) < 8.0, "{}", farthest(&after, &before));
|
||||
}
|
||||
|
||||
/// With anchor points of a path direct-selected, Smooth changes only the section between them: the
|
||||
/// anchors outside it stay exactly where they were, and the section is smoothed and stays selected.
|
||||
#[test]
|
||||
fn direct_selected_anchors_smooth_only_their_section() {
|
||||
let mut s = session();
|
||||
let id = shaky_wave(&mut s);
|
||||
let before = path_of(&s, id);
|
||||
let anchors: Vec<Value> = (10..=30).map(|i| json!([0, i])).collect();
|
||||
s.execute("select.anchors", &json!({"id": id.0, "anchors": anchors})).unwrap();
|
||||
s.execute("object.path.smooth", &json!({"level": 100})).unwrap();
|
||||
let after = path_of(&s, id);
|
||||
let (a, b) = (&before.subpaths[0].anchors, &after.subpaths[0].anchors);
|
||||
assert!(b.len() < a.len(), "{} → {}", a.len(), b.len());
|
||||
// Anchors 0..=10 and 30..=40 are untouched, the section between them refitted.
|
||||
assert_eq!(a[..=10].iter().map(|x| x.p).collect::<Vec<_>>(), b[..=10].iter().map(|x| x.p).collect::<Vec<_>>());
|
||||
assert_eq!(a[30..].iter().map(|x| x.p).collect::<Vec<_>>(), b[b.len() - 11..].iter().map(|x| x.p).collect::<Vec<_>>());
|
||||
let sel = &s.doc().unwrap().selection;
|
||||
assert!(sel.anchors.get(&id).is_some_and(|set| !set.is_empty()), "the section stays direct-selected");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user