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 <noreply@anthropic.com>
This commit is contained in:
moppeeerandClaude Opus 5.5 committed 2026-10-14 14:53:40 +02:00
1 parent 43a46c04bf
commit bf2cf8842b
3 files changed
+56

No files matched your search

+1
View File
@@ -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
+42
View File
@@ -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::<Vec<_>>()
};
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));
}
+13
View File
@@ -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)