mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 22:20:50 -06:00
Fix #776: the Pen continues any open path and joins two paths
As in Illustrator, the Pen shows its continue cursor over an end of any
open path, selected or not, and a click there selects the path and draws
on from that end (reversing it from its first end). While drawing, it
shows a join cursor over an end of another open path, and a click joins
the two into one path, in one undo step, which finishes the path.
path.join takes {ids: [a, b], ends: ["first"|"last", "first"|"last"]}
to join the ends asked for (the Pen's join); without them it joins the
selected paths' nearest ends as before. Joining a path with itself, or
anything that isn't a path, is an error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
825d4fdb8b
commit
bd8fa795ed
@@ -432,6 +432,8 @@ pub enum Cursor {
|
||||
PenDelete,
|
||||
PenClose,
|
||||
PenContinue,
|
||||
/// While drawing, over an end of another open path (a click joins the two).
|
||||
PenJoin,
|
||||
/// Over the last anchor of the path being drawn (a click retracts its outgoing handle), or with
|
||||
/// Alt held over a selected path's handle or anchor (the Anchor Point tool's gesture).
|
||||
PenConvert,
|
||||
|
||||
+47
-3
@@ -11,8 +11,9 @@
|
||||
//! it a corner, dragging an anchor pulls out new symmetric handles and dragging a segment reshapes
|
||||
//! it. Cmd held lends the selection tool used last for a drag (the engine's
|
||||
//! `Session::pointer`); the path being drawn goes on afterwards while it stays selected.
|
||||
//! Enter/Esc (or switching tools) ends the path. Clicking the end of a selected open path continues
|
||||
//! it. The rubber-band preview shows the next segment (Enable Rubber Band for Pen Tool). Auto Add/Delete: between paths, a click on a
|
||||
//! Enter/Esc (or switching tools) ends the path. Clicking an end of an open path continues it
|
||||
//! (selecting it); while drawing, clicking an end of another open path joins the two into one
|
||||
//! (#776). The rubber-band preview shows the next segment (Enable Rubber Band for Pen Tool). Auto Add/Delete: between paths, a click on a
|
||||
//! segment of a selected path adds an anchor there and a click on one of its anchors deletes it
|
||||
//! (Shift held or General → Disable Auto Add/Delete starts a new path instead). On a selected
|
||||
//! blend's spine a click adds a point (on a point no key object sits on: deletes it).
|
||||
@@ -80,6 +81,29 @@ fn active_path(cx: &ToolContext) -> Option<(NodeId, Point, Point, Point)> {
|
||||
Some((id, first, last.p, last.h_out))
|
||||
}
|
||||
|
||||
/// An end of an open path (one subpath, not `except`) under `p`, within `tol`: the path the Pen
|
||||
/// continues or joins to, and whether it's the path's first anchor. Only paths that can be edited,
|
||||
/// not guides.
|
||||
fn open_end_at(cx: &ToolContext, p: Point, tol: f64, except: Option<NodeId>) -> Option<(NodeId, bool)> {
|
||||
let id = vectorcraft_doc::hit::hit_test(cx.doc, p, cx.hit_options())?.leaf;
|
||||
if Some(id) == except || !cx.doc.is_editable(id) {
|
||||
return None;
|
||||
}
|
||||
let NodeKind::Path { path, guide: false, .. } = &cx.doc.node(id)?.kind else { return None };
|
||||
let [sp] = path.subpaths.as_slice() else { return None };
|
||||
if sp.closed {
|
||||
return None;
|
||||
}
|
||||
let (first, last) = (sp.anchors.first()?.p, sp.anchors.last()?.p);
|
||||
if p.distance(last) <= tol {
|
||||
Some((id, false))
|
||||
} else if p.distance(first) <= tol {
|
||||
Some((id, true))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// The last anchor of `id`'s last subpath: (subpath, anchor).
|
||||
fn last_anchor(cx: &ToolContext, id: NodeId) -> Option<(usize, usize)> {
|
||||
let pd = cx.doc.node(id)?.path_data()?;
|
||||
@@ -160,6 +184,12 @@ impl Tool for PenTool {
|
||||
self.handle = Some((id, si, ai, last));
|
||||
return vec![Action::Begin("Convert Anchor Point".into()), set_out_handle(id, si, ai, last)];
|
||||
}
|
||||
// An end of another open path: the two become one, and the path is finished.
|
||||
if let Some((other, at_first)) = open_end_at(cx, p, tol, Some(id)) {
|
||||
self.stop();
|
||||
let end = if at_first { "first" } else { "last" };
|
||||
return vec![Action::Exec("path.join".into(), json!({"ids": [id.0, other.0], "ends": ["last", end]}))];
|
||||
}
|
||||
if let Some(acts) = self.alt_convert(cx, ev) {
|
||||
return acts;
|
||||
}
|
||||
@@ -184,6 +214,15 @@ impl Tool for PenTool {
|
||||
}
|
||||
return vec![];
|
||||
}
|
||||
// Continue any other open path from the end clicked, selecting it.
|
||||
if let Some((id, at_first)) = open_end_at(cx, p, tol, None) {
|
||||
(self.drawing, self.path) = (true, Some(id));
|
||||
let mut out = vec![Action::Exec("select.set".into(), json!({"ids": [id.0]}))];
|
||||
if at_first {
|
||||
out.push(Action::Exec("path.reverse".into(), json!({"ids": [id.0]})));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
if let Some(act) = auto_add_delete(cx, ev.pos, ev.mods, tol) {
|
||||
return vec![act];
|
||||
}
|
||||
@@ -306,6 +345,9 @@ impl Tool for PenTool {
|
||||
if p.distance(last) <= tol {
|
||||
return Cursor::PenConvert;
|
||||
}
|
||||
if open_end_at(cx, p, tol, Some(id)).is_some() {
|
||||
return Cursor::PenJoin;
|
||||
}
|
||||
}
|
||||
if alt_converts(cx, p, m) {
|
||||
return Cursor::PenConvert;
|
||||
@@ -316,7 +358,9 @@ impl Tool for PenTool {
|
||||
Some([_]) => return Cursor::PenAdd,
|
||||
_ => {}
|
||||
}
|
||||
if active_path(cx).is_some_and(|(_, first, last, _)| p.distance(last) <= tol || p.distance(first) <= tol) {
|
||||
if active_path(cx).is_some_and(|(_, first, last, _)| p.distance(last) <= tol || p.distance(first) <= tol)
|
||||
|| open_end_at(cx, p, tol, None).is_some()
|
||||
{
|
||||
return Cursor::PenContinue;
|
||||
}
|
||||
match auto_add_delete(cx, p, m, tol) {
|
||||
|
||||
Reference in New Issue
Block a user