//! 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::*; /// A new 400 × 400 pt document. fn new_doc() -> Session { let mut s = Session::new(); s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap(); s } /// The view the gestures run in: page coordinates, with Smart Guides off so nothing snaps. fn view() -> ViewInfo { ViewInfo { smart_guides: false, ..ViewInfo::default() } } /// Feed the pointer events `(kind, x, y)`, all with `mods` held, to the active tool. 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(); } } /// No modifier held. 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") } /// The geometric bounds of `id`. fn bounds(s: &Session, id: NodeId) -> Rect { s.doc().unwrap().doc.bounds_of(&[id], false).expect("bounds") } /// The angle of `id`'s bounding box, in degrees counter-clockwise: 0 for art square to the page. fn angle(s: &Session, id: NodeId) -> f64 { s.doc().unwrap().doc.oriented_bounds(&[id], false).map_or(0.0, |b| b.angle) } /// How many undo steps the active document's history holds. 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"); }