//! Duplicating at a spacing: a copy Alt-dragged off a layer snaps to that layer too (its edges, //! centres and equal spacing), and Duplicate repeats the last duplicate's offset while its copies //! are selected, so Alt-drag (or Duplicate and move) then Cmd+D steps out evenly spaced copies. use serde_json::{Value, json}; use crate::Session; use crate::params::id_of; use crate::tools::{PointerEvent, PointerKind}; use figcraft_doc::NodeId; fn ex(s: &mut Session, cmd: &str, p: Value) -> Value { s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd}: {e}")) } fn rect(s: &mut Session, x: f64, y: f64) -> NodeId { id_of(&ex(s, "shape.rectangle", json!({"x": x, "y": y, "width": 100, "height": 100}))["id"]).unwrap() } /// Alt-drag from `from` to `to`: returns the copy (the selection afterwards). fn alt_drag(s: &mut Session, from: (f64, f64), to: (f64, f64)) -> NodeId { let ev = |kind, (x, y): (f64, f64)| PointerEvent { alt: true, ..PointerEvent::at(kind, x, y, 1.0) }; s.pointer(&ev(PointerKind::Down, from)).unwrap(); for k in 1..=4 { let t = f64::from(k) / 4.0; s.pointer(&ev(PointerKind::Move, (from.0 + (to.0 - from.0) * t, from.1 + (to.1 - from.1) * t))).unwrap(); } s.pointer(&ev(PointerKind::Up, to)).unwrap(); let sel = s.selection(); assert_eq!(sel.len(), 1, "one copy selected"); sel[0] } /// Duplicate the selection: the copy. fn cmd_d(s: &mut Session) -> NodeId { let out = ex(s, "edit.duplicate", json!({})); id_of(&out["ids"][0]).unwrap() } fn at(s: &Session, id: NodeId) -> (f64, f64) { let b = s.doc().unwrap().world_bounds(id).unwrap(); (b.x0, b.y0) } #[test] fn an_alt_dragged_copy_snaps_to_the_layer_it_came_from() { let mut s = Session::new(); let a = rect(&mut s, 0.0, 0.0); // 2 px below the original's top edge: within the snap distance. let copy = alt_drag(&mut s, (50.0, 50.0), (250.0, 52.0)); assert_ne!(copy, a); assert_eq!(at(&s, copy), (200.0, 0.0), "the copy's top lines up with the original's"); assert_eq!(at(&s, a), (0.0, 0.0), "the original stays"); } #[test] fn an_alt_dragged_copy_spaces_equally_with_the_layer_it_came_from() { let mut s = Session::new(); rect(&mut s, 0.0, 0.0); rect(&mut s, 200.0, 0.0); // B's copy dropped 2 px short of the 100 gap that A and B have. let copy = alt_drag(&mut s, (250.0, 50.0), (448.0, 50.0)); assert_eq!(at(&s, copy), (400.0, 0.0)); } #[test] fn duplicate_repeats_the_step_set_by_moving_the_copy() { let mut s = Session::new(); let a = rect(&mut s, 0.0, 0.0); ex(&mut s, "select.set", json!({"ids": [a]})); let c1 = cmd_d(&mut s); assert_eq!(at(&s, c1), (0.0, 0.0), "the first copy lands in place"); ex(&mut s, "node.set", json!({"ids": [c1], "props": {"x": 150, "y": 20}})); let c2 = cmd_d(&mut s); assert_eq!(at(&s, c2), (300.0, 40.0), "the next one as far again"); let c3 = cmd_d(&mut s); assert_eq!(at(&s, c3), (450.0, 60.0), "and again"); } #[test] fn duplicate_after_an_alt_drag_repeats_its_step() { let mut s = Session::new(); rect(&mut s, 0.0, 0.0); let copy = alt_drag(&mut s, (50.0, 50.0), (180.0, 50.0)); assert_eq!(at(&s, copy), (130.0, 0.0)); let next = cmd_d(&mut s); assert_eq!(at(&s, next), (260.0, 0.0)); } #[test] fn duplicate_only_repeats_for_the_last_copies() { let mut s = Session::new(); let a = rect(&mut s, 0.0, 0.0); ex(&mut s, "select.set", json!({"ids": [a]})); let c1 = cmd_d(&mut s); ex(&mut s, "node.set", json!({"ids": [c1], "props": {"x": 150}})); // Another selection forgets it: the original duplicates in place. ex(&mut s, "select.set", json!({"ids": [a]})); ex(&mut s, "select.set", json!({"ids": [c1]})); let c2 = cmd_d(&mut s); assert_eq!(at(&s, c2), (150.0, 0.0), "the step was forgotten when the selection changed"); // An offset given explicitly wins over a remembered one. ex(&mut s, "node.set", json!({"ids": [c2], "props": {"x": 300}})); let out = ex(&mut s, "edit.duplicate", json!({"dx": 10})); let c3 = id_of(&out["ids"][0]).unwrap(); assert_eq!(at(&s, c3), (310.0, 0.0)); } #[test] fn undo_doesnt_let_a_new_layer_take_a_remembered_step() { let mut s = Session::new(); let a = rect(&mut s, 0.0, 0.0); ex(&mut s, "select.set", json!({"ids": [a]})); let c1 = cmd_d(&mut s); ex(&mut s, "node.set", json!({"ids": [c1], "props": {"x": 150}})); ex(&mut s, "edit.undo", json!({})); ex(&mut s, "edit.undo", json!({})); assert!(!s.doc().unwrap().contains(c1)); // Undo gives ids back: the new rectangle may well get the undone copy's. let r = rect(&mut s, 500.0, 500.0); ex(&mut s, "select.set", json!({"ids": [r]})); let c = cmd_d(&mut s); assert_eq!(at(&s, c), (500.0, 500.0), "a plain duplicate, in place"); } #[test] fn the_step_is_measured_in_page_space_inside_a_rotated_frame() { let mut s = Session::new(); let f = id_of(&ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 600, "height": 600}))["id"]).unwrap(); ex(&mut s, "node.set", json!({"ids": [f], "props": {"rotation": 30}})); let a = id_of(&ex(&mut s, "shape.rectangle", json!({"x": 50, "y": 50, "width": 40, "height": 40, "parent": f}))["id"]).unwrap(); ex(&mut s, "select.set", json!({"ids": [a]})); let c1 = cmd_d(&mut s); ex(&mut s, "node.set", json!({"ids": [c1], "props": {"x": 150}})); let (p0, p1) = (at(&s, a), at(&s, c1)); let c2 = cmd_d(&mut s); let p2 = at(&s, c2); let close = |a: f64, b: f64| (a - b).abs() < 1e-6; assert!(close(p2.0 - p1.0, p1.0 - p0.0) && close(p2.1 - p1.1, p1.1 - p0.1), "{p0:?} {p1:?} {p2:?}"); } #[test] fn an_alt_drag_into_auto_layout_sets_no_step() { let mut s = Session::new(); let f = id_of(&ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 400, "height": 100}))["id"]).unwrap(); for i in 0..2 { ex(&mut s, "shape.rectangle", json!({"x": i * 60, "y": 0, "width": 50, "height": 30, "parent": f})); } ex(&mut s, "layout.add", json!({"ids": [f]})); ex(&mut s, "select.none", json!({})); let kids = s.doc().unwrap().node(f).unwrap().children.clone(); let b = s.doc().unwrap().world_bounds(kids[0]).unwrap(); let from = (b.center().x, b.center().y); alt_drag(&mut s, from, (from.0 + 70.0, from.1)); assert!(s.ds().unwrap().last_duplicate.is_none(), "the layout placed the copy: no step to repeat"); let n = s.doc().unwrap().node(f).unwrap().children.len(); cmd_d(&mut s); assert_eq!(s.doc().unwrap().node(f).unwrap().children.len(), n + 1, "Duplicate still adds one to the flow"); } /// Drag `from` by `by` with no modifier: moving the selection, not copying it. fn drag_move(s: &mut Session, from: (f64, f64), by: (f64, f64)) { let ev = |kind, (x, y): (f64, f64)| PointerEvent::at(kind, x, y, 1.0); s.pointer(&ev(PointerKind::Down, from)).unwrap(); for k in 1..=4 { let t = f64::from(k) / 4.0; s.pointer(&ev(PointerKind::Move, (from.0 + by.0 * t, from.1 + by.1 * t))).unwrap(); } s.pointer(&ev(PointerKind::Up, (from.0 + by.0, from.1 + by.1))).unwrap(); } /// Drag from `from` to `to` in four steps, Alt held at step `k` when `alt(k)` (0: the press, 1–4: /// the moves, 5: the release). fn drag_alt_when(s: &mut Session, from: (f64, f64), to: (f64, f64), alt: impl Fn(u8) -> bool) { let ev = |k: u8, kind, (x, y): (f64, f64)| PointerEvent { alt: alt(k), ..PointerEvent::at(kind, x, y, 1.0) }; s.pointer(&ev(0, PointerKind::Down, from)).unwrap(); for k in 1..=4 { let t = f64::from(k) / 4.0; s.pointer(&ev(k, PointerKind::Move, (from.0 + (to.0 - from.0) * t, from.1 + (to.1 - from.1) * t))).unwrap(); } s.pointer(&ev(5, PointerKind::Up, to)).unwrap(); } fn layers(s: &Session) -> usize { s.doc().unwrap().node(s.page().unwrap()).unwrap().children.len() } fn last_step(s: &Session) -> String { s.ds().unwrap().history.undo.last().map(|e| e.label.clone()).unwrap_or_default() } #[test] fn alt_pressed_during_a_drag_turns_the_move_into_a_copy() { // #1385: as in other design apps, Alt counts whenever it goes down, not only at the press. let mut s = Session::new(); let a = rect(&mut s, 0.0, 0.0); // Alt from the third step: the original goes back, a copy lands where the drag ends and snaps // to the original (2 px below its top edge). drag_alt_when(&mut s, (50.0, 50.0), (250.0, 52.0), |k| k >= 3); let copy = s.selection()[0]; assert_ne!(copy, a); assert_eq!((at(&s, a), at(&s, copy)), ((0.0, 0.0), (200.0, 0.0))); assert_eq!((layers(&s), last_step(&s).as_str()), (2, "Duplicate")); assert!(s.undo().unwrap()); assert_eq!((layers(&s), at(&s, a)), (1, (0.0, 0.0)), "one undo step"); // Let go of Alt during the drag: no copy, the original moves (and no longer snaps to where it // started, as only a copy does). drag_alt_when(&mut s, (50.0, 50.0), (250.0, 52.0), |k| k < 3); assert_eq!((layers(&s), s.selection(), at(&s, a)), (1, vec![a], (200.0, 2.0))); assert_eq!(last_step(&s), "Move"); // Down and up again during one drag: a copy, as Alt is held at the end. s.undo().unwrap(); drag_alt_when(&mut s, (50.0, 50.0), (250.0, 52.0), |k| k == 1 || k >= 4); assert_eq!((layers(&s), at(&s, a)), (2, (0.0, 0.0))); // Alt changed only by the release (no move in between): the release decides. s.undo().unwrap(); drag_alt_when(&mut s, (50.0, 50.0), (250.0, 52.0), |k| k == 5); assert_eq!((layers(&s), at(&s, a), last_step(&s).as_str()), (2, (0.0, 0.0), "Duplicate"), "Alt down at the release: a copy"); s.undo().unwrap(); drag_alt_when(&mut s, (50.0, 50.0), (250.0, 52.0), |k| k < 5); assert_eq!((layers(&s), at(&s, a), last_step(&s).as_str()), (1, (200.0, 2.0), "Move"), "Alt up at the release: a move"); } /// The reported path (#567): Cmd+D, drag the copy with the mouse, Cmd+D again. The other tests /// here set the copy's position with `node.set` or Alt-drag it; dragging is how it is usually done, /// and the step is measured from where the copy ended up, whichever way it got there. #[test] fn duplicate_repeats_the_step_set_by_dragging_the_copy() { let mut s = Session::new(); let a = rect(&mut s, 0.0, 0.0); ex(&mut s, "select.set", json!({"ids": [a]})); let c1 = cmd_d(&mut s); assert_eq!(at(&s, c1), (0.0, 0.0), "the first copy lands in place"); drag_move(&mut s, (50.0, 50.0), (150.0, 20.0)); assert_eq!(at(&s, c1), (150.0, 20.0), "the drag moved the copy, it did not make another one"); assert_eq!(s.selection(), vec![c1], "and left it selected"); assert_eq!(at(&s, a), (0.0, 0.0), "the original stays"); let c2 = cmd_d(&mut s); assert_eq!(at(&s, c2), (300.0, 40.0), "the next one as far again"); let c3 = cmd_d(&mut s); assert_eq!(at(&s, c3), (450.0, 60.0), "and again"); } /// And a step set with the arrow keys, which is how a small offset is usually made exact. #[test] fn duplicate_repeats_a_nudged_step() { let mut s = Session::new(); let a = rect(&mut s, 0.0, 0.0); ex(&mut s, "select.set", json!({"ids": [a]})); let c1 = cmd_d(&mut s); ex(&mut s, "transform.nudge", json!({"dx": 40, "dy": 0})); assert_eq!(at(&s, c1), (40.0, 0.0)); let c2 = cmd_d(&mut s); assert_eq!(at(&s, c2), (80.0, 0.0), "the nudge is the step"); }