diff --git a/crates/engine/src/tests/shapetools.rs b/crates/engine/src/tests/shapetools.rs index 8f2b181e2..3ecca4bef 100644 --- a/crates/engine/src/tests/shapetools.rs +++ b/crates/engine/src/tests/shapetools.rs @@ -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] diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index eee24b4e9..8fc737937 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -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"))) { diff --git a/crates/tools/src/select/tests.rs b/crates/tools/src/select/tests.rs index 7268d0678..c37a35455 100644 --- a/crates/tools/src/select/tests.rs +++ b/crates/tools/src/select/tests.rs @@ -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:?}"); +} diff --git a/crates/tools/src/shape/tests.rs b/crates/tools/src/shape/tests.rs index 60c3058e6..85a2d52fc 100644 --- a/crates/tools/src/shape/tests.rs +++ b/crates/tools/src/shape/tests.rs @@ -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); +}