From bf2cf8842bc28e4fcef146bcd9f7a73b77afbbdb Mon Sep 17 00:00:00 2001 From: moppeeer Date: Wed, 14 Oct 2026 14:53:40 +0200 Subject: [PATCH 1/2] Direct Selection: pressing on a corner of an unselected object pulls just that corner Chris Nielsen's "The Adobe InDesign Toolbox" (10:10): with the White Arrow he clicks outside, then presses on a frame's corner and pulls it up, turning the rectangle into a lopsided triangle. DesignCraft only looked for anchors on objects already selected, so the press fell through to moving the whole frame. The Direct Selection tool now also looks at the anchors of the object under the pointer: on one, it selects the object and drags that point, in one undo step. Elsewhere on the object it still drags it whole; Shift- click still toggles the selection. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/engine/src/tests.rs | 42 ++++++++++++++++++++++++++++++++++++++ crates/tools/src/select.rs | 13 ++++++++++++ 3 files changed, 56 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index c60a04107..0473076a4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -128,6 +128,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-14** · The Direct Selection tool pressed on a corner or anchor of an object that isn't selected selects it and pulls just that point, as in InDesign (it moved the whole object). No readiness percentage change. - **2026-10-12** · Text threads by hand: click a selected text frame's in or out port to load the text cursor, then click a frame to thread it, click empty space for a column-wide frame or drag one (it snaps like frame drawing); Shift-click autoflows, Alt-click stays loaded on the diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 9d6308983..e40174c48 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -669,3 +669,45 @@ fn type_tool_frames_snap_to_the_margins() { assert!((r.x0 - margin.x0).abs() < 1e-6 && (r.y0 - margin.y0).abs() < 1e-6, "press point {r:?} vs {margin:?}"); assert!((r.x1 - margin.x1).abs() < 1e-6, "right edge {} vs {}", r.x1, margin.x1); } + +#[test] +fn direct_selection_pulls_one_corner_of_an_unselected_frame() { + // Chris Nielsen's "The Adobe InDesign Toolbox" (10:10): with the White Arrow, click outside, + // then press on a frame's corner and pull it: only that corner moves (a triangle, nearly). + use designcraft_tools::{PointerEvent, PointerKind, SnapView}; + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 612, "height": 792, "facingPages": false})).unwrap(); + let id = s.execute("frame.create", &json!({"rect": [156, 219, 455, 569], "content": "graphic"})).unwrap()["id"].as_u64().unwrap(); + s.execute("edit.deselectAll", &json!({})).unwrap(); + let corners = |s: &Session| { + let d = &s.doc().unwrap().doc; + let it = d.item(designcraft_doc::ItemId(id)).unwrap(); + it.path.subpaths[0].anchors.iter().map(|a| it.xf * a.p).collect::>() + }; + let before = corners(&s); + s.set_tool("directSelection"); + let v = ViewInfo { snap: SnapView::OFF, ..ViewInfo::at_zoom(1.0) }; + let layout = s.layout(); + let at = |x: f64, y: f64| layout.to_canvas(designcraft_doc::SpreadRef::Doc(0), designcraft_geom::Point::new(x, y)); + let (a, b) = (at(455.0, 219.0), at(455.0, 150.0)); + s.pointer(&PointerEvent::new(PointerKind::Down, a.x, a.y), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Drag, a.x, a.y - 30.0), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Drag, b.x, b.y), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Up, b.x, b.y), v).unwrap(); + let after = corners(&s); + let moved: Vec<_> = before.iter().zip(&after).filter(|(p, q)| (**p - **q).hypot() > 1e-6).collect(); + assert_eq!(moved.len(), 1, "one corner moved: {before:?} → {after:?}"); + assert!((*moved[0].1 - designcraft_geom::Point::new(455.0, 150.0)).hypot() < 1e-6, "{after:?}"); + assert_eq!(s.doc().unwrap().selection.items, [designcraft_doc::ItemId(id)], "the frame is selected"); + // One undo step puts the corner back. + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(corners(&s), before); + // Away from its corners, the White Arrow still drags the whole unselected frame. + s.execute("edit.deselectAll", &json!({})).unwrap(); + let (c, d) = (at(300.0, 219.0), at(300.0, 269.0)); + s.pointer(&PointerEvent::new(PointerKind::Down, c.x, c.y), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Drag, c.x, c.y + 20.0), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Drag, d.x, d.y), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Up, d.x, d.y), v).unwrap(); + assert!(corners(&s).iter().zip(&before).all(|(p, q)| (p.y - q.y - 50.0).abs() < 1e-6), "{:?}", corners(&s)); +} diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index b2ac97731..5dab5c128 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -507,6 +507,19 @@ impl Tool for SelectionTool { self.drag = Drag::Anchor { id, si, ai, handle, start: p, at }; return vec![Action::Begin(if handle.is_some() { "Move Direction Handle".into() } else { "Move Anchor".into() })]; } + // As in InDesign, the Direct Selection tool pressed on a corner or anchor of an object + // that isn't selected selects it and moves just that point (Chris Nielsen's "The + // InDesign Toolbox", 10:10: click outside, then pull a frame's corner). + if self.direct + && !ev.mods.shift + && let Some((_, hit)) = cx.hit(p) + && !cx.selection.contains(hit) + && let Some((id, si, ai, None)) = anchor_at_in(cx, &[hit], p) + { + let at = anchor_spread_point(cx, id, si, ai); + self.drag = Drag::Anchor { id, si, ai, handle: None, start: p, at }; + return vec![Action::Exec("selection.set".into(), json!({"ids": [id], "content": true})), Action::Begin("Move Anchor".into())]; + } if !self.direct && handle_at(cx, p).is_none() && let Some(center) = rotate_zone(cx, p) From 785c49a1d4e4a5669578397289f07e56fe0d26fd Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 15:08:12 +0100 Subject: [PATCH 2/2] rustfmt Co-Authored-By: Claude Opus 5.5 --- crates/tools/src/select.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index f073d063e..f6b42377d 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -703,19 +703,19 @@ impl SelectionTool { self.drag = Drag::Anchor { id, si, ai, handle, start: p, at }; return vec![Action::Begin(if handle.is_some() { "Move Direction Handle".into() } else { "Move Anchor".into() })]; } - // As in InDesign, the Direct Selection tool pressed on a corner or anchor of an object - // that isn't selected selects it and moves just that point (Chris Nielsen's "The - // InDesign Toolbox", 10:10: click outside, then pull a frame's corner). - if self.direct - && !ev.mods.shift - && let Some((_, hit)) = cx.hit(p) - && !cx.selection.contains(hit) - && let Some((id, si, ai, None)) = anchor_at_in(cx, &[hit], p) - { - let at = anchor_spread_point(cx, id, si, ai); - self.drag = Drag::Anchor { id, si, ai, handle: None, start: p, at }; - return vec![Action::Exec("selection.set".into(), json!({"ids": [id], "content": true})), Action::Begin("Move Anchor".into())]; - } + // As in InDesign, the Direct Selection tool pressed on a corner or anchor of an object + // that isn't selected selects it and moves just that point (Chris Nielsen's "The + // InDesign Toolbox", 10:10: click outside, then pull a frame's corner). + if self.direct + && !ev.mods.shift + && let Some((_, hit)) = cx.hit(p) + && !cx.selection.contains(hit) + && let Some((id, si, ai, None)) = anchor_at_in(cx, &[hit], p) + { + let at = anchor_spread_point(cx, id, si, ai); + self.drag = Drag::Anchor { id, si, ai, handle: None, start: p, at }; + return vec![Action::Exec("selection.set".into(), json!({"ids": [id], "content": true})), Action::Begin("Move Anchor".into())]; + } // A selected placed graphic's own handles resize it inside its frame (the // frame's anchors, under Direct Selection, come first). if let Some((id, h)) = content_handle_at(cx, p)