Merge PR #487: Pen: a click does what the cursor shows over an open path's end point

This commit is contained in:
aiongg
2026-10-16 02:51:57 +00:00
3 changed files with 53 additions and 15 deletions
+12 -12
View File
@@ -1,6 +1,8 @@
//! InDesign. Clicking an end point of an open path (the cursor shows it) continues that path from
//! there; dragging from it first reshapes its direction lines. Between paths, a click on a selected
//! path's anchor deletes it and a click on its outline adds an anchor; the cursor shows which.
//! Pen tool (P): click for corner points, drag for smooth points, click the first point to close,
//! Enter/Esc (or switching tools) to finish an open path. Each anchor is its own undo step.
//! Between paths, a click on a selected path's anchor deletes it and a click on its outline adds an
//! anchor; clicking an end point of an open path continues that path from there (dragging from it
//! first reshapes its direction lines). The cursor shows which.
use designcraft_doc::{ItemId, SpreadRef};
use designcraft_geom::{BezPath, Point};
@@ -76,6 +78,8 @@ enum Hover {
Delete(u64, usize, usize),
/// Add an anchor on a selected path's outline.
Add(ItemId),
/// Continue subpath `.1` of open path `.0` from its last (`.2`) or first anchor.
Continue(ItemId, usize, bool),
}
impl PenTool {
@@ -93,6 +97,9 @@ impl PenTool {
if !self.anchors.is_empty() {
return Hover::Place;
}
if let Some((id, si, at_end)) = endpoint_at(cx, p) {
return Hover::Continue(id, si, at_end);
}
let ids = &cx.selection.items;
if let Some((id, si, ai, None)) = anchor_at_in(cx, ids, p) {
return Hover::Delete(id, si, ai);
@@ -208,6 +215,7 @@ impl Tool for PenTool {
self.finish();
return out;
}
Hover::Continue(id, si, at_end) => return self.continue_from(cx, id, si, at_end),
Hover::Delete(id, si, ai) => {
return vec![Action::Exec("path.deleteAnchor".into(), json!({"id": id, "subpath": si, "anchor": ai}))];
}
@@ -216,12 +224,6 @@ impl Tool for PenTool {
}
Hover::Place => {}
}
// On an open path's end point: continue that path.
if self.anchors.is_empty()
&& let Some((id, si, at_end)) = endpoint_at(cx, ev.pos)
{
return self.continue_from(cx, id, si, at_end);
}
let p = self.snap_click(cx, snap_spread, sp, ev.mods.shift);
if self.anchors.is_empty() {
self.spread = Some(sr);
@@ -301,12 +303,10 @@ impl Tool for PenTool {
}
fn cursor(&self, cx: &ToolContext, p: Point, _m: Mods) -> Cursor {
if self.anchors.is_empty() && endpoint_at(cx, p).is_some() {
return Cursor::PenContinue;
}
match self.hover_at(cx, p) {
Hover::Place => Cursor::Pen,
Hover::Close => Cursor::PenClose,
Hover::Continue(..) => Cursor::PenContinue,
Hover::Delete(..) => Cursor::PenDelete,
Hover::Add(_) => Cursor::PenAdd,
}
+26
View File
@@ -1091,6 +1091,32 @@ fn pen_continues_an_open_path_from_either_end_point() {
assert_eq!(t.cursor(&cx, Point::new(at(200.0, 100.0).0, at(200.0, 100.0).1), Mods::default()), Cursor::Pen);
}
#[test]
fn pen_over_a_selected_open_paths_end_point_continues_it() {
let mut d = Document::new(&NewDocument::default());
let lid = d.default_layer();
let id = ItemId(d.alloc());
let anchors = vec![
designcraft_geom::Anchor::corner(Point::new(100.0, 100.0)),
designcraft_geom::Anchor::corner(Point::new(200.0, 100.0)),
designcraft_geom::Anchor::corner(Point::new(200.0, 200.0)),
];
let mut it = Item::new(id, lid, Shape::Path, designcraft_geom::PathData::single(designcraft_geom::SubPath::new(anchors, false)));
it.stroke = Stroke::default();
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
let (s, c, l) = (Selection::items(vec![id]), Cache::new(), CanvasLayout::new(&d, false));
let cx = ctx(&d, &s, &c, &l);
let off = l.xf(SpreadRef::Doc(0)).translation();
let at = |x: f64, y: f64| Point::new(x + off.x, y + off.y);
let mut t = create("pen");
// The end point wins over deleting the selected path's anchor; a middle anchor still deletes.
assert_eq!(t.cursor(&cx, at(200.0, 200.0), Mods::default()), Cursor::PenContinue);
assert_eq!(t.cursor(&cx, at(200.0, 100.0), Mods::default()), Cursor::PenDelete);
let p = at(200.0, 200.0);
assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, p.x, p.y)).is_empty(), "no anchor is deleted");
assert!(t.busy(), "the path is being continued");
}
#[test]
fn the_selection_tool_drags_a_path_type_bracket_along_its_path() {
let mut d = Document::new(&NewDocument::default());
+15 -3
View File
@@ -1,6 +1,6 @@
//! Tool cursors drawn in code: black glyphs with a white halo, so they read on any page colour.
//! The Selection tool's filled arrow, Direct Selection's hollow arrow, the pen nib with a badge for
//! what a click does (add, delete, close, convert), and a curved double arrow for rotating.
//! what a click does (add, delete, close, convert, continue), and a curved double arrow for rotating.
//! Cursors without a glyph stay system cursors.
//!
//! Linux and Windows show the glyphs as OS cursors ([`Images`]): the system moves them itself, so
@@ -96,6 +96,7 @@ enum Badge {
Delete,
Close,
Convert,
Continue,
}
/// The pen nib, tip at `o` (top left), with a badge below right.
@@ -121,6 +122,7 @@ fn pen(ink: &mut Ink, o: Pos2, badge: Badge) {
ink.line(b + vec2(0.0, 6.0), b + vec2(3.0, 0.0));
ink.line(b + vec2(3.0, 0.0), b + vec2(6.0, 6.0));
}
Badge::Continue => ink.line(b + vec2(1.0, 6.0), b + vec2(5.0, 0.0)),
}
}
@@ -152,6 +154,7 @@ fn glyph(c: Cursor, p: Pos2) -> Option<Vec<Shape>> {
Cursor::PenDelete => pen(&mut ink, p, Badge::Delete),
Cursor::PenClose => pen(&mut ink, p, Badge::Close),
Cursor::PenConvert => pen(&mut ink, p, Badge::Convert),
Cursor::PenContinue => pen(&mut ink, p, Badge::Continue),
Cursor::Rotate => rotate(&mut ink, p),
_ => return None,
}
@@ -321,8 +324,17 @@ mod tests {
use super::*;
/// Every cursor with a glyph.
const GLYPHS: [Cursor; 8] =
[Cursor::Arrow, Cursor::ArrowHollow, Cursor::Pen, Cursor::PenAdd, Cursor::PenDelete, Cursor::PenClose, Cursor::PenConvert, Cursor::Rotate];
const GLYPHS: [Cursor; 9] = [
Cursor::Arrow,
Cursor::ArrowHollow,
Cursor::Pen,
Cursor::PenAdd,
Cursor::PenDelete,
Cursor::PenClose,
Cursor::PenConvert,
Cursor::PenContinue,
Cursor::Rotate,
];
fn alpha(img: &CustomCursorImage, x: usize, y: usize) -> u8 {
img.rgba.get((y * img.size[0] as usize + x) * 4 + 3).copied().unwrap_or(0)