Shape tools: guides take the centre point's pointer too, and more regressions

The Selection tool's cursor now checks the ruler guides before a selected
object's centre point, as a press does. Tests: a ruler guide under the centre
point keeps the pointer and drags; on a box too thin for its centre to clear the
handles the handles take both; a shape-tool move from the centre snaps and shows
the hollow arrow; and Alt pressed mid-drag copies the shape through the engine.
This commit is contained in:
FractalEncrypt
2026-11-07 05:15:04 +07:00
committed by bflatastic
parent c36971afe9
commit 6d3c0fef99
4 changed files with 97 additions and 6 deletions
+14
View File
@@ -94,6 +94,20 @@ fn a_shape_tool_moves_the_shape_from_its_centre_point() {
assert_eq!(bounds(&s, copy), at, "back where it was");
}
/// Alt pressed once the drag has begun turns the move into a copy, as the Selection tool's.
#[test]
fn alt_pressed_mid_drag_copies_the_shape() {
let mut s = new_doc();
let id = drawn_rect(&mut s);
let before = bounds(&s, id);
gesture(&mut s, NONE, &[(PointerKind::Down, 150.0, 140.0), (PointerKind::Drag, 170.0, 140.0)]);
gesture(&mut s, Mods { alt: true, ..NONE }, &[(PointerKind::Drag, 200.0, 140.0), (PointerKind::Up, 200.0, 140.0)]);
assert_eq!(s.doc().unwrap().doc.layers[0].children().map(|c| c.len()).unwrap_or_default(), 2, "a copy");
assert_eq!(bounds(&s, id), before, "the original stays");
let copy = *s.doc().unwrap().selection.objects.first().expect("the copy is selected");
assert_eq!(bounds(&s, copy), before + Vec2::new(50.0, 0.0), "the copy moved by the whole drag");
}
/// A press in the rotate zone just outside a corner of the shape's box turns it about that box's
/// centre, as the Selection tool's does; a press on the corner itself still rounds it.
#[test]
+6 -6
View File
@@ -1128,15 +1128,15 @@ 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;
}
// A selected object's centre point (Show Center): a press there moves it, where the art
// may not reach. The box's handles and the ruler guides take a press before it, so they
// take the pointer too (on a thin box the centre lies within a handle's reach).
if near_selected && hit_center(cx, p, cx.point_tol()).is_some() {
return Cursor::Move;
}
// View → Snap to Point: over an anchor a press would grab the object by, the arrow turns
// hollow.
if cx.snap_to_point && matches!(Self::hover_point(cx, p), Some((_, "anchor"))) {
+51
View File
@@ -2199,3 +2199,54 @@ fn a_shift_press_on_the_centre_point_moves_or_deselects() {
vec![Action::Exec("select.toggle".into(), json!({"id": id.0}))]
);
}
/// On a box too thin for its centre point to clear the handles, the handles take the pointer and
/// the press, as they take the press itself: the centre is within their 5 pt reach.
#[test]
fn a_thin_shapes_centre_point_gives_the_handles_the_pointer() {
use vectorcraft_doc::{Appearance, LiveShape, Node, NodeKind};
let shape = LiveShape::Rectangle {
w: 100.0,
h: 8.0,
radii: [0.0; 4],
kinds: Default::default(),
radii_relative: false,
xf: vectorcraft_geom::Affine::translate((100.0, 100.0)),
};
let mut d = vectorcraft_doc::Document::new(500.0, 500.0);
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), 0, n).unwrap();
let mut s = Selection::default();
s.add(id);
let p = paint();
let c = cx(&d, &s, &p);
let centre = Point::new(150.0, 104.0);
assert!(hit_center(&c, centre, c.point_tol()).is_some(), "the centre point is there");
let mut t = SelectionTool::default();
assert_eq!(t.cursor(&c, centre, Mods::default()), Cursor::ResizeV, "the top and bottom handles take it");
assert_eq!(t.pointer(&c, &ev(PointerKind::Down, centre.x, centre.y)), vec![Action::Begin("Scale".into())]);
}
/// A ruler guide under a selected object's centre point keeps the pointer and the press, as it
/// keeps them over the art.
#[test]
fn a_ruler_guide_under_the_centre_point_keeps_the_pointer() {
let (mut d, id) = doc_with_live_rect();
d.guides.push(vectorcraft_doc::Guide::new(true, 150.0));
let mut s = Selection::default();
s.add(id);
let p = paint();
let c = cx(&d, &s, &p);
let centre = Point::new(150.0, 150.0);
assert!(hit_center(&c, centre, c.point_tol()).is_some());
let mut t = SelectionTool::default();
let cursor = t.cursor(&c, centre, Mods::default());
assert_ne!(cursor, Cursor::Move, "the guide's cursor, not the move arrow");
let down = t.pointer(&c, &ev(PointerKind::Down, centre.x, centre.y));
let drag = t.pointer(&c, &ev(PointerKind::Drag, 170.0, 150.0));
assert!(drag.iter().any(|a| matches!(a, Action::Preview(cmd, _) if cmd == "guide.move")), "the guide drags: {down:?} {drag:?}");
}
+26
View File
@@ -550,3 +550,29 @@ fn a_live_corner_widget_wins_over_another_shapes_centre_point() {
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 160.0, 160.0));
assert_eq!(a.first(), Some(&Action::Begin("Corner Radius".into())), "{a:?}");
}
/// A move of the selection from the centre point snaps like the Selection tool's, and the arrow
/// turns hollow while it does.
#[test]
fn a_move_from_the_centre_point_snaps_and_shows_the_hollow_arrow() {
use vectorcraft_doc::{Appearance, Node};
let (mut d, s) = unfilled_live_rect();
// Another rectangle whose left edge is at x = 300: the moved selection's right edge lands 4 pt
// short of it, within the Smart Guides' reach.
let (layer, id) = (d.layers[0].id, d.alloc_id());
d.insert(Some(layer), 1, Node::path(id, vectorcraft_geom::shapes::rectangle(Rect::new(300.0, 100.0, 350.0, 180.0)), Appearance::default_art()))
.unwrap();
let p = paint();
let c = cx(&d, &s, &p);
let mut t = ShapeTool::new("rectangle");
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 150.0, 150.0));
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 246.0, 150.0));
assert!(matches!(a.last(), Some(Action::Preview(_, v)) if v["matrix"][4] == json!(100.0)), "snapped to the edge: {a:?}");
assert_eq!(t.cursor(&c, Point::new(246.0, 150.0), Mods::default()), Cursor::ArrowHollow);
// Away from the neighbour, the move is the pointer's own and the arrow is plain.
let mut t = ShapeTool::new("rectangle");
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 150.0, 150.0));
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 160.0, 150.0));
assert!(matches!(a.last(), Some(Action::Preview(_, v)) if v["matrix"][4] == json!(10.0)), "{a:?}");
assert_eq!(t.cursor(&c, Point::new(160.0, 150.0), Mods::default()), Cursor::Arrow);
}