diff --git a/crates/tools/src/pen.rs b/crates/tools/src/pen.rs index 6fe1289b..4d953edc 100644 --- a/crates/tools/src/pen.rs +++ b/crates/tools/src/pen.rs @@ -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, } diff --git a/crates/tools/src/tests.rs b/crates/tools/src/tests.rs index ba49ff4b..aaa255ae 100644 --- a/crates/tools/src/tests.rs +++ b/crates/tools/src/tests.rs @@ -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()); diff --git a/crates/ui-egui/src/cursors.rs b/crates/ui-egui/src/cursors.rs index 15e63a73..566fe65f 100644 --- a/crates/ui-egui/src/cursors.rs +++ b/crates/ui-egui/src/cursors.rs @@ -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> { 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)