Fix #1512: the Smooth tool smooths the stretch of path under the drag
A drag near an anchor refitted the whole segments on either side of it, so a short drag over a small bump on a long side turned the side into a curve from end to end. The segments leading into the anchors under the drag are now split where they enter the tool's reach, and only what is in reach is refitted.
This commit is contained in:
parent
a072e29016
commit
61cc3f6040
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use kurbo::{ParamCurve, ParamCurveNearest};
|
use kurbo::{CubicBez, ParamCurve, ParamCurveNearest};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use vectorcraft_color::{Color, Paint};
|
use vectorcraft_color::{Color, Paint};
|
||||||
use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, PressureProfile, Selection};
|
use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, PressureProfile, Selection};
|
||||||
|
|
@ -1048,17 +1048,46 @@ fn smooth_subpath(sp: &SubPath, near: &dyn Fn(Point) -> bool, tol: f64) -> Optio
|
||||||
open.anchors.push(open.anchors[0]);
|
open.anchors.push(open.anchors[0]);
|
||||||
m.push(false);
|
m.push(false);
|
||||||
open.closed = false;
|
open.closed = false;
|
||||||
let mut res = smooth_open(&open.anchors, &m, tol);
|
let mut res = smooth_open(&open.anchors, &m, near, tol);
|
||||||
let l = res.pop()?;
|
let l = res.pop()?;
|
||||||
if let Some(r0) = res.first_mut() {
|
if let Some(r0) = res.first_mut() {
|
||||||
r0.h_in = l.h_in;
|
r0.h_in = l.h_in;
|
||||||
}
|
}
|
||||||
return Some(SubPath::new(res, true));
|
return Some(SubPath::new(res, true));
|
||||||
}
|
}
|
||||||
Some(SubPath::new(smooth_open(&sp.anchors, &marked, tol), false))
|
Some(SubPath::new(smooth_open(&sp.anchors, &marked, near, tol), false))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn smooth_open(anchors: &[Anchor], marked: &[bool], tol: f64) -> Vec<Anchor> {
|
/// 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 n = anchors.len();
|
||||||
let mut runs = vec![];
|
let mut runs = vec![];
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
|
|
@ -1079,8 +1108,27 @@ fn smooth_open(anchors: &[Anchor], marked: &[bool], tol: f64) -> Vec<Anchor> {
|
||||||
if hi - lo < 2 {
|
if hi - lo < 2 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let new = refit_run(&out[lo..=hi], tol);
|
let mut piece = out[lo..=hi].to_vec();
|
||||||
out.splice(lo..=hi, new);
|
// 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
|
out
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -412,6 +412,29 @@ fn smooth_tool_reduces_jagged_path() {
|
||||||
assert_eq!(p.subpaths[0].anchors[0].p, Point::new(100.0, 200.0));
|
assert_eq!(p.subpaths[0].anchors[0].p, Point::new(100.0, 200.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// #1512: a short drag over a small bump smooths the stretch of path under it. The long straight
|
||||||
|
/// sides next to the bump stay straight lines from their far ends, which keep their place.
|
||||||
|
#[test]
|
||||||
|
fn a_short_smooth_drag_leaves_the_rest_of_long_segments_alone() {
|
||||||
|
let mut s = session();
|
||||||
|
let anchors: Vec<_> =
|
||||||
|
[(50.0, 200.0), (190.0, 200.0), (200.0, 192.0), (210.0, 200.0), (350.0, 200.0)].map(|(x, y)| json!({"x": x, "y": y})).to_vec();
|
||||||
|
let r = s.execute("path.create", &json!({"anchors": anchors})).unwrap();
|
||||||
|
let id = NodeId(r["id"].as_u64().unwrap());
|
||||||
|
let before = path(&s, id);
|
||||||
|
drag(&mut s, "smooth", &[(195.0, 194.0), (205.0, 194.0)], Mods::default());
|
||||||
|
let p = path(&s, id);
|
||||||
|
assert_ne!(p, before, "the bump is smoothed");
|
||||||
|
let a = &p.subpaths[0].anchors;
|
||||||
|
let (first, second, last) = (a[0], a[1], a[a.len() - 1]);
|
||||||
|
assert!(first.p == Point::new(50.0, 200.0) && !first.has_out() && !second.has_in(), "the left side is a line still: {first:?} {second:?}");
|
||||||
|
assert!(last.p == Point::new(350.0, 200.0) && !last.has_in() && !a[a.len() - 2].has_out(), "and the right one: {last:?}");
|
||||||
|
// Every other anchor is within the tool's reach of the drag (12 px either way).
|
||||||
|
for x in &a[1..a.len() - 1] {
|
||||||
|
assert!((182.0..=218.0).contains(&x.p.x) && (190.0..=201.0).contains(&x.p.y), "{x:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn join_tool_joins_two_lines() {
|
fn join_tool_joins_two_lines() {
|
||||||
let mut s = session();
|
let mut s = session();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue