Fix #798: the Curvature tool edits any selected path

The Curvature tool only edited the path it was drawing. It now edits any
selected path, whatever drew it, keeping its shape except where edited:
a click on a point makes it the current (direct-selected) point and a
drag moves it, re-curving only its two segments (the neighbours keep
their handle directions); Alt-click or double-click toggles it smooth or
corner and Delete removes it; a click on a segment adds a smooth point
without changing the shape, and a drag moves it in the same undo step.
With an end of an open path current, clicks go on from it (the old
segments keep their shape) and a click on the other end closes it.

The edits go through a new path.curvatureEdit command (move, insert,
extend, close), path.convertAnchor and path.removeAnchor, one undo step
each. The tool now claims Delete/Backspace while it has a point to
remove, so the key no longer deletes the whole path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-10 11:12:10 +07:00
co-authored by Claude Opus 5.5
parent 898c4f7677
commit 1db82cbc02
14 changed files with 870 additions and 43 deletions
+79
View File
@@ -36,6 +36,15 @@ pub fn specs() -> Vec<CommandSpec> {
has_doc,
curvature
),
cmd!(
"path.curvatureEdit",
"Curvature",
[],
None,
"{id, subpath?, op: \"move\"|\"insert\"|\"extend\"|\"close\", anchor?, segment?, t?, end?: \"start\"|\"end\", from?: \"keep\"|\"smooth\"|\"corner\", x?, y?} edit a path as the Curvature tool does, its shape kept elsewhere; the anchor edited becomes the direct-selected one → {anchor}. move: anchor to x, y, re-curving only its two segments; insert: a smooth anchor at t on segment, the shape unchanged (with x, y: then moved there); extend: an open subpath goes on from its end (default) or start to a new end at x, y; close: an open subpath closes from that end. The old end bends into the new segment as from says: keep its curve (default), smooth through its neighbours, or a corner",
has_doc,
curvature_edit
),
cmd!(
"path.removeAnchor",
"Delete Anchor Point",
@@ -593,6 +602,76 @@ fn curvature(s: &mut Session, p: &Value) -> Result<Value> {
add_look(s, "Curvature", path_kind(PathData::single(sp)), look, None)
}
/// An edit `path.curvatureEdit` makes.
enum CurvatureOp {
Move(usize, Point),
Insert(usize, f64, Option<Point>),
Extend(Point),
Close,
}
fn curvature_edit(s: &mut Session, p: &Value) -> Result<Value> {
use vectorcraft_tools::draw2::{EndCurve, curvature_close, curvature_extend, curvature_insert, curvature_move};
const C: &str = "path.curvatureEdit";
let id = id_param(p, "id").ok_or_else(|| bad(C, "missing id"))?;
let si = p.get("subpath").and_then(Value::as_u64).unwrap_or(0) as usize;
let at = match (p.get("x").and_then(Value::as_f64), p.get("y").and_then(Value::as_f64)) {
(Some(x), Some(y)) if x.is_finite() && y.is_finite() => Some(Point::new(x, y)),
(None, None) => None,
_ => return Err(bad(C, "x and y must be finite numbers")),
};
let to = || at.ok_or_else(|| bad(C, "missing x, y"));
let start = match str_param(p, "end") {
None | Some("end") => false,
Some("start") => true,
Some(e) => return Err(bad(C, format!("unknown end {e:?}"))),
};
let how = match str_param(p, "from") {
None => EndCurve::Keep,
Some(f) => EndCurve::parse(f).ok_or_else(|| bad(C, format!("unknown from {f:?}")))?,
};
let op = match str_param(p, "op") {
Some("move") => CurvatureOp::Move(usize_req(p, "anchor", C)?, to()?),
Some("insert") => CurvatureOp::Insert(usize_req(p, "segment", C)?, f64_or(p, "t", 0.5), at),
Some("extend") => CurvatureOp::Extend(to()?),
Some("close") => CurvatureOp::Close,
Some(o) => return Err(bad(C, format!("unknown op {o:?}"))),
None => return Err(bad(C, "missing op")),
};
let anchor = s.edit("Curvature", |d, sel| {
let path = path_mut(d, id)?;
let sp = path.subpaths.get_mut(si).ok_or_else(|| EngineError::Other("no such subpath".into()))?;
if matches!(op, CurvatureOp::Extend(_) | CurvatureOp::Close) && sp.closed {
return Err(EngineError::Other("path is already closed".into()));
}
let ai = match op {
CurvatureOp::Move(ai, q) => curvature_move(sp, ai, q).then_some(ai).ok_or_else(|| EngineError::Other("no such anchor".into()))?,
CurvatureOp::Insert(seg, t, q) => {
let ai = curvature_insert(sp, seg, t).ok_or_else(|| EngineError::Other("no such segment".into()))?;
if let Some(q) = q {
curvature_move(sp, ai, q);
}
ai
}
CurvatureOp::Extend(q) => {
curvature_extend(sp, start, q, how);
if start { 0 } else { sp.anchors.len() - 1 }
}
CurvatureOp::Close => {
if sp.anchors.len() < 3 {
return Err(EngineError::Other("closing needs three anchors".into()));
}
curvature_close(sp, start, how);
if start { sp.anchors.len() - 1 } else { 0 }
}
};
sel.set([id]);
sel.anchors.insert(id, [(si, ai)].into());
Ok(ai)
})?;
Ok(json!({ "anchor": anchor }))
}
// ---------- anchor tools ----------
fn remove_anchor(s: &mut Session, p: &Value) -> Result<Value> {