mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 03:37:00 -06:00
285 lines
11 KiB
Rust
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 }))
|
|
}
|