Shape tools: the centre point's cursor comes after the box's handles

The Selection tool's cursor now checks the bounding-box handles before a
selected object's centre point, as a press does (on a thin box the centre lies
within a handle's reach). The shape tools' tests cover a Live Corners widget
that lands on another selected shape's centre (the widget rounds), Alt pressed
mid-drag (the preview copies and the copy cursor shows), and the engine tests
check the copy's place and both gestures' Undo and Redo. The cursor catalog says
which arrow a drag of the selection shows.
This commit is contained in:
FractalEncrypt
2026-11-07 05:15:03 +07:00
committed by bflatastic
parent cd88b15912
commit c36971afe9
4 changed files with 79 additions and 10 deletions
+18 -3
View File
@@ -64,14 +64,23 @@ fn a_shape_tool_moves_the_shape_from_its_centre_point() {
assert_eq!(undo_steps(&s), steps + 1, "one undo step");
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(bounds(&s, id), before);
// Alt-dragging from the centre leaves a copy where the original was.
// Alt-dragging from the centre leaves a copy where the original was, in one undo step.
let steps = undo_steps(&s);
gesture(
&mut s,
Mods { alt: true, ..NONE },
&[(PointerKind::Down, 150.0, 140.0), (PointerKind::Drag, 200.0, 140.0), (PointerKind::Up, 200.0, 140.0)],
);
let objects = s.doc().unwrap().doc.layers[0].children().map(|c| c.len()).unwrap_or_default();
assert_eq!(objects, 2, "a copy");
let objects = |s: &Session| s.doc().unwrap().doc.layers[0].children().map(|c| c.len()).unwrap_or_default();
assert_eq!(objects(&s), 2, "a copy");
assert_eq!(undo_steps(&s), steps + 1, "one undo step for the copy");
let copy = *s.doc().unwrap().selection.objects.first().expect("the copy is selected");
assert_eq!(bounds(&s, id), before, "the original stays");
assert_eq!(bounds(&s, copy), before + Vec2::new(50.0, 0.0), "the copy moved");
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(objects(&s), 1, "undone");
s.execute("edit.redo", &json!({})).unwrap();
assert_eq!(objects(&s), 2, "redone");
// Escape turns the drag back, and the release leaves the art alone.
let steps = undo_steps(&s);
let moved = s.doc().unwrap().selection.objects.clone();
@@ -101,6 +110,12 @@ fn a_shape_tool_turns_the_shape_from_just_outside_a_corner() {
assert!(turned.abs() > 1.0, "turned: {turned}");
let centre = bounds(&s, id).center();
assert!((centre - Point::new(150.0, 140.0)).hypot() < 1e-6, "about its centre: {centre:?}");
// One undo step for the turn, and Redo turns it again.
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(angle(&s, id), 0.0, "undone");
s.execute("edit.redo", &json!({})).unwrap();
assert!((angle(&s, id) - turned).abs() < 1e-9, "redone");
s.execute("edit.undo", &json!({})).unwrap();
// A press on a Live Corners widget rounds it, as before (#5477): the widget keeps the corner,
// and sits 10 pt inside it.
let mut s = new_doc();
+2 -2
View File
@@ -26,7 +26,7 @@ id = "Arrow"
group = "Selection"
shown_when = [
"The Selection tool over empty canvas or over an unselected object (no Alt).",
"Dragging with the Selection tool (a move), or with Direct Selection (an anchor, a segment or a handle).",
"Dragging with the Selection tool (a move), with Direct Selection (an anchor, a segment or a handle), or with a shape tool (from the selection's centre point).",
"Any tool with no cursor of its own (Touch Type, Rotate View).",
"The resting state of Live Paint Selection, Reshape (away from a path), Free Transform, Puppet Warp and Crop Image (nothing selected or outside the box), the Perspective Grid (locked or away from handles) and Perspective Selection.",
]
@@ -94,7 +94,7 @@ id = "Move"
group = "Selection"
shown_when = [
"The Selection tool over a selected object, or over any object with Alt held (once an Alt-drag starts, SelectionCopy shows).",
"The shape tools over a selected object's centre point (Attributes › Show Center), and while dragging it (SelectionCopy with Alt, ArrowHollow while it snaps).",
"The shape tools over a selected object's centre point (Attributes › Show Center), before a press there moves it (while the drag runs the arrow is plain, SelectionCopy with Alt, ArrowHollow while it snaps).",
"Reshape over a path or while dragging.",
"Inside the Free Transform and Crop Image boxes.",
"Puppet Warp over a pin.",
+6 -5
View File
@@ -1109,11 +1109,6 @@ impl Tool for SelectionTool {
if near_selected && pie::over_widget(cx, p) {
return Cursor::ArrowHollow;
}
// A selected object's centre point (Show Center): a press there moves it, where the art
// may not reach.
if near_selected && hit_center(cx, p, cx.point_tol()).is_some() {
return Cursor::Move;
}
if over_rotate_all(cx, p) {
return Cursor::Rotate;
}
@@ -1133,6 +1128,12 @@ impl Tool for SelectionTool {
None => {}
}
}
// A selected object's centre point (Show Center): a press there moves it, where the art
// may not reach. The box's handles take a press before it, so they take the pointer too
// (on a thin box the centre lies within their reach).
if near_selected && hit_center(cx, p, cx.point_tol()).is_some() {
return Cursor::Move;
}
if let Some(c) = self.guide.cursor(cx, p) {
return c;
}
+53
View File
@@ -462,6 +462,15 @@ fn a_shape_tool_moves_the_selection_by_its_centre_point_and_turns_it_from_a_corn
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Up, 170.0, 160.0)), vec![Action::Commit], "{id}");
assert!(!t.busy() && !t.transforming(), "{id}");
}
// Alt pressed once the drag has begun copies, and the copy cursor shows, as the Selection
// tool's do.
let mut t = ShapeTool::new("rectangle");
t.pointer(&c, &PointerEvent::new(PointerKind::Down, centre.x, centre.y));
t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 160.0, 150.0));
let alt = Mods { alt: true, ..Mods::default() };
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 170.0, 150.0).with_mods(alt));
assert!(matches!(a.last(), Some(Action::Preview(_, v)) if v["copy"] == json!(true)), "{a:?}");
assert_eq!(t.cursor(&c, Point::new(170.0, 150.0), alt), Cursor::SelectionCopy);
// View › Hide Bounding Box hides the box, not the centre point: a press there still moves.
let hidden = ToolContext { show_bbox: false, ..cx(&d, &s, &p) };
let mut t = ShapeTool::new("rectangle");
@@ -497,3 +506,47 @@ fn a_shape_tool_moves_the_selection_by_its_centre_point_and_turns_it_from_a_corn
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 350.0, 340.0));
assert_eq!(a.first(), Some(&Action::Begin("Rectangle".into())), "{a:?}");
}
/// Two selected live rectangles, the second placed so that its top-left Live Corners widget (10 pt
/// in along the corner's bisector) lands on the first one's centre point.
fn two_live_rects() -> (vectorcraft_doc::Document, Selection) {
use vectorcraft_doc::{Appearance, LiveShape, Node, NodeKind};
let (mut d, first) = doc_with_live_rect();
let inside = 10.0 / std::f64::consts::SQRT_2;
let shape = LiveShape::Rectangle {
w: 100.0,
h: 100.0,
radii: [0.0; 4],
kinds: Default::default(),
radii_relative: false,
xf: Affine::translate((150.0 - inside, 150.0 - inside)),
};
let (layer, id) = (d.layers[0].id, d.alloc_id());
let mut n = Node::path(id, shape.to_path(), Appearance::default());
if let NodeKind::Path { live: slot, .. } = &mut n.kind {
*slot = Some(Box::new(shape));
}
d.insert(Some(layer), 1, n).unwrap();
let mut s = Selection::default();
s.add(first);
s.add(id);
(d, s)
}
/// A press takes a Live Corners widget before a centre point, as the Selection tool does: with two
/// shapes selected, one's corner widget may sit on another's centre, and it rounds rather than
/// moving the selection.
#[test]
fn a_live_corner_widget_wins_over_another_shapes_centre_point() {
let (d, s) = two_live_rects();
let p = paint();
let c = cx(&d, &s, &p);
let at = Point::new(150.0, 150.0);
assert!(corners::over_widget(&c, at, false), "a widget is on the centre point");
assert!(hit_center(&c, at, c.point_tol()).is_some(), "and so is the centre point");
let mut t = ShapeTool::new("rectangle");
assert_eq!(t.cursor(&c, at, Mods::default()), Cursor::CornerRadius, "the widget takes the pointer");
assert!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, at.x, at.y)).is_empty());
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 160.0, 160.0));
assert_eq!(a.first(), Some(&Action::Begin("Corner Radius".into())), "{a:?}");
}