Files
vectorcraft/crates/engine/src/cmd/draw2/smooth.rs
T

285 lines
11 KiB
Rust

use super::common::{dist_to_polyline, ids_json, path_mut, path_of, points_param, poly_bounds, target_paths};
use super::*;
// ---------- Smooth ----------
/// 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 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;
out[0].p = f.p;
out[0].h_in = f.h_in;
out[last].p = l.p;
out[last].h_out = l.h_out;
for a in &mut out {
*a = Anchor::with_handles(a.p, a.h_in, a.h_out);
}
out
}
fn smooth_subpath(sp: &SubPath, near: &dyn Fn(Point) -> bool, tol: f64) -> Option<SubPath> {
let n = sp.anchors.len();
let marked: Vec<bool> = sp.anchors.iter().map(|a| near(a.p)).collect();
if n < 3 || !marked.iter().any(|m| *m) {
return None;
}
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 });
return f.subpaths.into_iter().next();
};
// Rotate so an untouched anchor comes first, duplicate it at the end and treat as open.
let mut open = sp.clone();
open.anchors.rotate_left(k);
let mut m = marked.clone();
m.rotate_left(k);
open.anchors.push(open.anchors[0]);
m.push(false);
open.closed = false;
let mut res = smooth_open(&open.anchors, &m, near, tol);
let l = res.pop()?;
if let Some(r0) = res.first_mut() {
r0.h_in = l.h_in;
}
return Some(SubPath::new(res, true));
}
Some(SubPath::new(smooth_open(&sp.anchors, &marked, near, tol), false))
}
/// The segment `c`, which ends in the drag's reach (`near`) and starts outside it, split where it
/// enters the reach: the part outside, then the part in reach. `None` when no more than a sliver
/// is outside.
fn split_at_reach(c: CubicBez, near: &dyn Fn(Point) -> bool) -> Option<(CubicBez, CubicBez)> {
const STEPS: u32 = 32;
let step = 1.0 / f64::from(STEPS);
// Back from the end in reach to the first sample outside, then halving to the crossing.
let outside = (1..=STEPS).map(|i| 1.0 - f64::from(i) * step).find(|t| !near(c.eval(*t)))?;
let (mut lo, mut hi) = (outside, outside + step);
for _ in 0..20 {
let mid = (lo + hi) / 2.0;
if near(c.eval(mid)) {
hi = mid;
} else {
lo = mid;
}
}
// A straight segment's parts stay straight, their handles on their ends.
let straight = c.p1 == c.p0 && c.p2 == c.p3;
let part = |range| {
let s = c.subsegment(range);
if straight { CubicBez::new(s.p0, s.p0, s.p3, s.p3) } else { s }
};
(hi > 0.02).then(|| (part(0.0..hi), part(hi..1.0)))
}
/// `anchors` with each run of `marked` ones refitted, together with the stretch of the segments
/// leading into the run that is in the drag's reach (`near`): a short drag smooths what is under
/// it, and the rest of a long segment next to it stays as it was (#1512).
fn smooth_open(anchors: &[Anchor], marked: &[bool], near: &dyn Fn(Point) -> bool, tol: f64) -> Vec<Anchor> {
let n = anchors.len();
let mut runs = vec![];
let mut i = 0;
while i < n {
if marked[i] {
let a = i;
while i + 1 < n && marked[i + 1] {
i += 1;
}
runs.push((a, i));
}
i += 1;
}
let mut out = anchors.to_vec();
for (a, b) in runs.into_iter().rev() {
let lo = a.saturating_sub(1);
let hi = (b + 1).min(n - 1);
if hi - lo < 2 {
continue;
}
let mut piece = out[lo..=hi].to_vec();
// The anchors outside the reach, kept with the part of their segment that is outside.
let (mut head, mut tail) = (None, None);
if hi > b
&& let [.., inner, outer] = piece.as_mut_slice()
&& let Some((far, reach)) = split_at_reach(CubicBez::new(outer.p, outer.h_in, inner.h_out, inner.p), near)
{
inner.h_out = reach.p2;
tail = Some(Anchor { h_in: far.p1, ..*outer });
*outer = Anchor::with_handles(far.p3, reach.p1, far.p2);
}
if lo < a
&& let [outer, inner, ..] = piece.as_mut_slice()
&& let Some((far, reach)) = split_at_reach(CubicBez::new(outer.p, outer.h_out, inner.h_in, inner.p), near)
{
inner.h_in = reach.p2;
head = Some(Anchor { h_out: far.p1, ..*outer });
*outer = Anchor::with_handles(far.p3, far.p2, reach.p1);
}
let new = refit_run(&piece, tol);
out.splice(lo..=hi, head.into_iter().chain(new).chain(tail));
}
out
}
pub(super) fn smooth_region(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "path.smoothRegion";
let pts = points_param(p, "points", C)?;
let radius = number_or(p, "radius", 12.0, C)?.max(0.0);
let tol = number_or(p, "fidelity", 2.5, C)?.clamp(vectorcraft_tools::draw2::SMOOTH_MIN_FIDELITY, 100.0);
let st = s.doc()?;
let near = |q: Point| dist_to_polyline(&pts, q) <= radius;
let mut plan = vec![];
let (mut before, mut after) = (0, 0);
for id in target_paths(&st.doc, &st.selection, poly_bounds(&pts, radius)) {
let (pd, _) = path_of(&st.doc, id)?;
let mut changed = false;
let subs: Vec<SubPath> = pd
.subpaths
.iter()
.map(|sp| match smooth_subpath(sp, &near, tol) {
Some(n) if n != *sp && n.anchors.len() >= 2 => {
changed = true;
n
}
_ => sp.clone(),
})
.collect();
if changed {
let np = PathData::new(subs);
before += pd.anchor_count();
after += np.anchor_count();
plan.push((id, np));
}
}
if plan.is_empty() {
return Ok(json!({ "ids": [], "before": 0, "after": 0 }));
}
let ids: Vec<NodeId> = plan.iter().map(|(id, _)| *id).collect();
s.edit("Smooth", |d, _| {
for (id, np) in plan {
*path_mut(d, id)? = np;
}
Ok(())
})?;
let mut v = ids_json(&ids);
v["before"] = json!(before);
v["after"] = json!(after);
Ok(v)
}
// ---------- Join tool ----------
pub(super) fn join_scrub(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "path.joinScrub";
let pts = points_param(p, "points", C)?;
let tol = number_or(p, "tolerance", 12.0, C)?.max(0.0);
let st = s.doc()?;
// Open endpoints near the scrub, ordered along it: (order, id, subpath, at_start).
let poly = {
let mut bp = BezPath::new();
bp.move_to(pts[0]);
for q in &pts[1..] {
bp.line_to(*q);
}
bp
};
let order = |q: Point| -> f64 {
poly.segments()
.enumerate()
.map(|(i, sg)| (i as f64 + sg.nearest(q, 1e-6).t, sg.nearest(q, 1e-6).distance_sq))
.min_by(|a, b| a.1.total_cmp(&b.1))
.map_or(0.0, |x| x.0)
};
let (first_pt, last_pt) = (pts.first().copied(), pts.last().copied());
let mut ends: Vec<(f64, NodeId, usize, bool, Point)> = vec![];
for id in target_paths(&st.doc, &st.selection, poly_bounds(&pts, tol)) {
let (pd, _) = path_of(&st.doc, id)?;
for (si, sp) in pd.subpaths.iter().enumerate() {
if sp.closed || sp.anchors.len() < 2 {
continue;
}
for (start, a) in [(true, sp.anchors[0].p), (false, sp.anchors[sp.anchors.len() - 1].p)] {
let on_scrub = dist_to_polyline(&pts, a) <= tol;
let near_start = first_pt.is_some_and(|p| a.distance(p) <= tol);
let near_end = last_pt.is_some_and(|p| a.distance(p) <= tol);
if !on_scrub && !near_start && !near_end {
continue;
}
let ord = if on_scrub {
order(a)
} else if near_start && (!near_end || first_pt.is_some_and(|p| a.distance(p) <= a.distance(last_pt.unwrap_or(p)))) {
-1.0
} else {
1e30
};
ends.push((ord, id, si, start, a));
}
}
}
ends.sort_by(|a, b| a.0.total_cmp(&b.0));
if ends.len() < 2 {
return Ok(json!({ "id": null }));
}
let (_, ida, sa, starta, pa) = ends[0];
let (_, idb, sb, startb, pb) = ends[1];
let mid = pa.midpoint(pb);
let merge = pa.distance(pb) <= tol;
s.edit("Join", |d, sel| {
if ida == idb && sa == sb {
let path = path_mut(d, ida)?;
let sp = &mut path.subpaths[sa];
if merge
&& sp.anchors.len() > 2
&& let Some(l) = sp.anchors.pop()
{
let d0 = mid - sp.anchors[0].p;
sp.anchors[0].translate(d0);
sp.anchors[0].h_in = l.h_in + (mid - l.p);
}
sp.closed = true;
sel.set([ida]);
return Ok(());
}
let (pdb, _) = path_of(d, idb)?;
let mut x = path_of(d, ida)?.0.subpaths[sa].clone();
let mut y = pdb.subpaths[sb].clone();
if starta {
x.reverse();
}
if !startb {
y.reverse();
}
if merge
&& !y.anchors.is_empty()
&& let Some(last) = x.anchors.last_mut()
{
let yf = y.anchors.remove(0);
let lp = last.p;
last.translate(mid - lp);
last.h_out = yf.h_out + (mid - yf.p);
*last = Anchor::with_handles(last.p, last.h_in, last.h_out);
}
x.anchors.extend(y.anchors);
if ida == idb {
let path = path_mut(d, ida)?;
path.subpaths[sa] = x;
path.subpaths.remove(sb);
} else {
path_mut(d, ida)?.subpaths[sa] = x;
let pbm = path_mut(d, idb)?;
pbm.subpaths.remove(sb);
if pbm.is_empty() {
d.remove(idb)?;
}
}
sel.set([ida]);
Ok(())
})?;
Ok(json!({ "id": ida.0 }))
}