mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 17:32:42 -06:00
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.
163 lines
7.8 KiB
Rust
163 lines
7.8 KiB
Rust
//! Shape tools: the selection they keep on show moves by its centre point (Attributes › Show
|
||
//! Center) and turns from the rotate zone just outside a corner of its box, as the Selection
|
||
//! tool's do, through the whole engine (#5477).
|
||
|
||
use serde_json::json;
|
||
use vectorcraft_doc::{LiveShape, NodeKind};
|
||
use vectorcraft_geom::{Point, Rect, Vec2};
|
||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
|
||
|
||
use crate::*;
|
||
|
||
fn new_doc() -> Session {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||
s
|
||
}
|
||
|
||
fn view() -> ViewInfo {
|
||
ViewInfo { smart_guides: false, ..ViewInfo::default() }
|
||
}
|
||
|
||
fn gesture(s: &mut Session, mods: Mods, events: &[(PointerKind, f64, f64)]) {
|
||
for (k, x, y) in events {
|
||
s.pointer(&PointerEvent::new(*k, *x, *y).with_mods(mods), view()).unwrap();
|
||
}
|
||
}
|
||
|
||
const NONE: Mods = Mods { shift: false, alt: false, cmd: false, ctrl: false, space: false };
|
||
|
||
/// A 100 × 80 pt rectangle drawn from (100, 100) to (200, 180) with the Rectangle tool, still
|
||
/// selected: its centre point is at (150, 140).
|
||
fn drawn_rect(s: &mut Session) -> NodeId {
|
||
s.select_tool("rectangle", view()).unwrap();
|
||
gesture(s, NONE, &[(PointerKind::Down, 100.0, 100.0), (PointerKind::Drag, 200.0, 180.0), (PointerKind::Up, 200.0, 180.0)]);
|
||
*s.doc().unwrap().selection.objects.first().expect("the drawn rectangle is selected")
|
||
}
|
||
|
||
fn bounds(s: &Session, id: NodeId) -> Rect {
|
||
s.doc().unwrap().doc.bounds_of(&[id], false).expect("bounds")
|
||
}
|
||
|
||
fn angle(s: &Session, id: NodeId) -> f64 {
|
||
s.doc().unwrap().doc.oriented_bounds(&[id], false).map_or(0.0, |b| b.angle)
|
||
}
|
||
|
||
fn undo_steps(s: &Session) -> usize {
|
||
s.doc().unwrap().history.undo.len()
|
||
}
|
||
|
||
/// With the Rectangle tool still active, a drag from the shape's centre point moves it, in one
|
||
/// undo step; Alt-dragging it copies it; Escape turns a drag back.
|
||
#[test]
|
||
fn a_shape_tool_moves_the_shape_from_its_centre_point() {
|
||
let mut s = new_doc();
|
||
let id = drawn_rect(&mut s);
|
||
let before = bounds(&s, id);
|
||
let steps = undo_steps(&s);
|
||
gesture(
|
||
&mut s,
|
||
NONE,
|
||
&[(PointerKind::Down, 150.0, 140.0), (PointerKind::Drag, 170.0, 160.0), (PointerKind::Drag, 180.0, 170.0), (PointerKind::Up, 180.0, 170.0)],
|
||
);
|
||
assert_eq!(bounds(&s, id), before + Vec2::new(30.0, 30.0), "moved by the drag");
|
||
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, 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: &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();
|
||
let copy = *moved.first().expect("the copy is selected");
|
||
let at = bounds(&s, copy);
|
||
gesture(&mut s, NONE, &[(PointerKind::Down, 200.0, 140.0), (PointerKind::Drag, 250.0, 190.0)]);
|
||
s.tool_key(ToolKey::Escape, NONE, view()).unwrap();
|
||
gesture(&mut s, NONE, &[(PointerKind::Up, 250.0, 190.0)]);
|
||
assert_eq!(undo_steps(&s), steps, "no step");
|
||
assert_eq!(s.doc().unwrap().selection.objects, moved);
|
||
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]
|
||
fn a_shape_tool_turns_the_shape_from_just_outside_a_corner() {
|
||
let mut s = new_doc();
|
||
let id = drawn_rect(&mut s);
|
||
// 8.49 pt outside the top-left corner (100, 100), past the handle's 5 pt square.
|
||
gesture(
|
||
&mut s,
|
||
NONE,
|
||
&[(PointerKind::Down, 94.0, 94.0), (PointerKind::Drag, 150.0, 70.0), (PointerKind::Drag, 150.0, 70.0), (PointerKind::Up, 150.0, 70.0)],
|
||
);
|
||
let turned = angle(&s, id);
|
||
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();
|
||
let id = drawn_rect(&mut s);
|
||
gesture(&mut s, NONE, &[(PointerKind::Down, 110.0, 110.0), (PointerKind::Drag, 125.0, 125.0), (PointerKind::Up, 125.0, 125.0)]);
|
||
let n = s.doc().unwrap().doc.node(id).expect("the rectangle");
|
||
let NodeKind::Path { live: Some(live), .. } = &n.kind else { panic!("a live shape: {:?}", n.kind) };
|
||
let LiveShape::Rectangle { radii, .. } = &**live else { panic!("a rectangle") };
|
||
assert!(radii[0] > 0.0, "the corner rounded: {radii:?}");
|
||
assert_eq!(angle(&s, id), 0.0, "rounding does not turn it");
|
||
}
|
||
|
||
/// The Selection tool takes the centre point of an unfilled shape too: a drag there moves it
|
||
/// instead of starting a marquee.
|
||
#[test]
|
||
fn the_selection_tool_moves_an_unfilled_shape_from_its_centre_point() {
|
||
let mut s = new_doc();
|
||
let id = NodeId(s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 100, "height": 80})).unwrap()["id"].as_u64().unwrap());
|
||
s.execute("paint.setFill", &json!({"none": true})).unwrap();
|
||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||
s.select_tool("selection", view()).unwrap();
|
||
let before = bounds(&s, id);
|
||
gesture(&mut s, NONE, &[(PointerKind::Down, 150.0, 140.0), (PointerKind::Drag, 170.0, 160.0), (PointerKind::Up, 170.0, 160.0)]);
|
||
assert_eq!(bounds(&s, id), before + Vec2::new(20.0, 20.0));
|
||
// Away from it, a drag is still a marquee.
|
||
let steps = undo_steps(&s);
|
||
gesture(&mut s, NONE, &[(PointerKind::Down, 300.0, 300.0), (PointerKind::Drag, 350.0, 350.0), (PointerKind::Up, 350.0, 350.0)]);
|
||
assert_eq!(undo_steps(&s), steps, "a marquee changes no art");
|
||
assert!(s.doc().unwrap().selection.objects.is_empty(), "the marquee took nothing");
|
||
}
|