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 { 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 { let n = sp.anchors.len(); let marked: Vec = 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 { 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 { 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 = 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 = 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 { 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 })) }