mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 10:17:02 -06:00
Merge pull request #2982 from nrq6993-a11y/stream/vc-feat1-g1-join-smooth-corner
path.join: corner or smooth at the merged anchor
This commit is contained in:
@@ -74,7 +74,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Join",
|
||||
["Object", "Path"],
|
||||
Some("Cmd+J"),
|
||||
"{ids?: [path, path] (default: the selected paths), ends?: [\"first\"|\"last\", \"first\"|\"last\"] (which end of each path to join, as the Pen joins them; default: the nearest pair)} join two open paths into one (the first keeps its id, selected), or close a single open path; without ids, direct-selected anchors lying on one another along a path merge into one anchor instead",
|
||||
"{ids?: [path, path] (default: the selected paths), ends?: [\"first\"|\"last\", \"first\"|\"last\"] (which end of each path to join, as the Pen joins them; default: the nearest pair), to?: \"corner\"|\"smooth\" (the joined anchor: corner retracts both handles, smooth pulls handles in line with the neighbours)} join two open paths into one (the first keeps its id, selected), or close a single open path; without ids, direct-selected anchors lying on one another along a path merge into one anchor instead",
|
||||
has_selection,
|
||||
join
|
||||
),
|
||||
@@ -336,10 +336,19 @@ fn end_points(sp: &SubPath) -> Option<(Point, Point)> {
|
||||
Some((sp.anchors.first()?.p, sp.anchors.last()?.p))
|
||||
}
|
||||
|
||||
/// Corner or smooth on an anchor a join leaves behind.
|
||||
fn join_point(sp: &mut SubPath, ai: usize, smooth: bool) {
|
||||
if smooth {
|
||||
sp.smooth_anchor(ai);
|
||||
} else if let Some(a) = sp.anchors.get_mut(ai) {
|
||||
a.retract();
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge each run of `picked` neighbours of `sp` that lie on one another into one anchor, which
|
||||
/// keeps the first one's incoming handle and the last one's outgoing handle. Returns how many
|
||||
/// anchors went.
|
||||
fn merge_coincident(sp: &mut SubPath, picked: impl Fn(usize) -> bool) -> usize {
|
||||
fn merge_coincident(sp: &mut SubPath, picked: impl Fn(usize) -> bool, join_kind: Option<bool>) -> usize {
|
||||
let same = |a: &Anchor, b: &Anchor| a.p.distance(b.p) < 1e-6;
|
||||
let before = sp.anchors.len();
|
||||
// The closing pair of a closed path first: taking the last anchor moves no other index.
|
||||
@@ -352,6 +361,9 @@ fn merge_coincident(sp: &mut SubPath, picked: impl Fn(usize) -> bool) -> usize {
|
||||
{
|
||||
first.h_in = last.h_in;
|
||||
sp.anchors.pop();
|
||||
if let Some(smooth) = join_kind {
|
||||
join_point(sp, 0, smooth);
|
||||
}
|
||||
}
|
||||
// Back to front, so the indices `picked` knows stay true for the anchors still to visit.
|
||||
for i in (1..sp.anchors.len()).rev() {
|
||||
@@ -362,6 +374,9 @@ fn merge_coincident(sp: &mut SubPath, picked: impl Fn(usize) -> bool) -> usize {
|
||||
{
|
||||
a.h_out = b.h_out;
|
||||
sp.anchors.remove(i);
|
||||
if let Some(smooth) = join_kind {
|
||||
join_point(sp, i - 1, smooth);
|
||||
}
|
||||
}
|
||||
}
|
||||
before - sp.anchors.len()
|
||||
@@ -369,6 +384,12 @@ fn merge_coincident(sp: &mut SubPath, picked: impl Fn(usize) -> bool) -> usize {
|
||||
|
||||
fn join(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "path.join";
|
||||
let join_kind = match p.get("to").and_then(Value::as_str) {
|
||||
None => None,
|
||||
Some("smooth") => Some(true),
|
||||
Some("corner") => Some(false),
|
||||
Some(_) => return Err(bad(C, "`to` must be \"corner\" or \"smooth\"")),
|
||||
};
|
||||
let ids = match ids_param(p, "ids") {
|
||||
Some(ids) => {
|
||||
let st = s.doc()?;
|
||||
@@ -386,7 +407,8 @@ fn join(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut merged = vec![];
|
||||
for (id, picked) in &st.selection.anchors {
|
||||
let Some(mut path) = st.doc.node(*id).and_then(|n| n.path_data()).cloned() else { continue };
|
||||
let n: usize = path.subpaths.iter_mut().enumerate().map(|(si, sp)| merge_coincident(sp, |ai| picked.contains(&(si, ai)))).sum();
|
||||
let n: usize =
|
||||
path.subpaths.iter_mut().enumerate().map(|(si, sp)| merge_coincident(sp, |ai| picked.contains(&(si, ai)), join_kind)).sum();
|
||||
if n > 0 {
|
||||
merged.push((*id, path));
|
||||
}
|
||||
@@ -426,6 +448,11 @@ fn join(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
&& let Some(first) = sp.anchors.first_mut()
|
||||
{
|
||||
first.h_in = last.h_in;
|
||||
// Closed first, so the merged anchor smooths between both its neighbours.
|
||||
sp.closed = true;
|
||||
if let Some(smooth) = join_kind {
|
||||
join_point(sp, 0, smooth);
|
||||
}
|
||||
}
|
||||
sp.closed = true;
|
||||
Ok(())
|
||||
@@ -458,6 +485,7 @@ fn join(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if ry {
|
||||
y.reverse();
|
||||
}
|
||||
let junction = x.anchors.len() - 1;
|
||||
if let (Some(xe), Some(ys)) = (x.anchors.last().map(|a| a.p), y.anchors.first().map(|a| a.p))
|
||||
&& xe.distance(ys) < 1e-6
|
||||
{
|
||||
@@ -467,6 +495,9 @@ fn join(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
x.anchors.extend(y.anchors);
|
||||
if let Some(smooth) = join_kind {
|
||||
join_point(&mut x, junction, smooth);
|
||||
}
|
||||
pa.subpaths[sa] = x;
|
||||
d.remove(b)?;
|
||||
sel.set([a]);
|
||||
|
||||
@@ -1846,11 +1846,36 @@ fn join_takes_the_paths_and_the_ends_to_join() {
|
||||
json!({"ids": [a.0, 999]}),
|
||||
json!({"ids": [a.0, r.0], "ends": ["first"]}),
|
||||
json!({"ids": [a.0, b.0], "ends": ["first", "middle"]}),
|
||||
json!({"to": "round"}),
|
||||
] {
|
||||
assert!(s.execute("path.join", &bad).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
/// `path.join {to}` makes the joined anchor a corner or smooth point.
|
||||
#[test]
|
||||
fn join_to_corner_or_smooth_sets_the_merged_anchor() {
|
||||
let mut s = session();
|
||||
let r = rect(&mut s, 100.0, 100.0, 100.0, 80.0);
|
||||
s.execute("select.anchors", &json!({"id": r.0, "anchors": [[0, 0], [0, 1]], "mode": "set"})).unwrap();
|
||||
s.execute("path.average", &json!({"axis": "both"})).unwrap();
|
||||
s.execute("path.join", &json!({"to": "corner"})).unwrap();
|
||||
let a = &path(&s, r).subpaths[0].anchors[0];
|
||||
assert_eq!(a.kind, vectorcraft_geom::AnchorKind::Corner);
|
||||
assert_eq!((a.h_in, a.h_out), (a.p, a.p));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("path.average", &json!({"axis": "both"})).unwrap();
|
||||
s.execute("path.join", &json!({"to": "smooth"})).unwrap();
|
||||
let a = &path(&s, r).subpaths[0].anchors[0];
|
||||
assert_eq!(a.kind, vectorcraft_geom::AnchorKind::Smooth);
|
||||
let a = line(&mut s, 0.0, 0.0, 100.0, 0.0);
|
||||
let b = line(&mut s, 100.0, 0.0, 200.0, 0.0);
|
||||
s.execute("path.join", &json!({"ids": [a.0, b.0], "to": "smooth"})).unwrap();
|
||||
let j = &path(&s, a).subpaths[0].anchors[1];
|
||||
assert_eq!(j.kind, vectorcraft_geom::AnchorKind::Smooth);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pen_with_alt_converts_the_anchors_of_a_path_in_a_selected_group() {
|
||||
// #1273: a selected group (or compound path) shows its paths' anchors, so Alt with the Pen
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ tools, never checked side by side) isn't captured row by row.
|
||||
| Pencil: Fidelity, Keep selected, Alt to Smooth, edit selected paths, close by proximity | done | Alt key toggles to Smooth Tool (#2001); Edit Selected Paths continues a selected path from an end and, from a stroke that starts and ends on a selected path, redraws the section between (the shorter way round a closed path, #2850); with Keep selected (Pencil and Paintbrush, on by default) off, the path a stroke draws or continues is not selected afterwards; a filled Pencil path keeps a stroke of None (#1804, part of #1679) |
|
||||
| Pencil: Fidelity, Keep selected, Alt to Smooth, edit selected paths, close by proximity | partial | Alt to Smooth is missing; Edit Selected Paths continues a selected path from an end and, from a stroke that starts and ends on a selected path, redraws the section between (the shorter way round a closed path, #2850); with Keep selected (Pencil and Paintbrush, on by default) off, the path a stroke draws or continues is not selected afterwards; a filled Pencil path keeps a stroke of None (#1804, part of #1679) |
|
||||
| Join tool scrubbing, Average, Join | done | Join merges the direct-selected anchors lying on one another along a path, as after Average, into one (#1533) |
|
||||
| Average and Join in one step, with a corner or smooth choice for the joined anchor | missing | `path.join` and `path.average` are separate commands, and Join has no corner/smooth choice; [gap 1](gaps.md) |
|
||||
| Average and Join in one step, with a corner or smooth choice for the joined anchor | partial | `path.join` accepts `to: corner|smooth` on the merged anchor; Average and Join are still separate commands; [gap 1](gaps.md) |
|
||||
| Smooth tool smooths only the stretch of path under the drag | done | #1512 |
|
||||
| Simplify (slider, auto, corner angle threshold) | partial | Tolerance, default-on Preview and original/new point counts (#1272); Corner Point Angle Threshold, Convert to Straight Lines and Show Original Path; slider, auto-simplify and Retain my latest settings remain missing |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user