diff --git a/crates/engine/src/cmd/distortcmds/perspective.rs b/crates/engine/src/cmd/distortcmds/perspective.rs index 647f4a4342..90d58541a6 100644 --- a/crates/engine/src/cmd/distortcmds/perspective.rs +++ b/crates/engine/src/cmd/distortcmds/perspective.rs @@ -104,9 +104,14 @@ fn plane_param(g: &PerspectiveGrid, p: &Value, cmd: &str) -> Result { Ok(plane) } +/// The objects a perspective command acts on: the outermost of `ids` (each once, in the order first +/// given), else the selection's roots. pub(super) fn roots(s: &Session, p: &Value) -> Result> { match named_ids(p)? { - Some(v) => Ok(outermost(&s.doc()?.doc, v)), + Some(v) => { + let mut seen = std::collections::HashSet::new(); + Ok(outermost(&s.doc()?.doc, v.into_iter().filter(|id| seen.insert(*id)).collect())) + } None => selected_roots(s), } } diff --git a/crates/engine/src/tests/distort.rs b/crates/engine/src/tests/distort.rs index d033fadee6..6f56a09b68 100644 --- a/crates/engine/src/tests/distort.rs +++ b/crates/engine/src/tests/distort.rs @@ -1,6 +1,6 @@ //! Width points, Liquify, Puppet Warp and Perspective Grid commands and tools. -use serde_json::json; +use serde_json::{Value, json}; use vectorcraft_geom::{PathData, Point, Rect, Shape}; use vectorcraft_tools::distort::perspective::{PerspectiveGrid, Plane}; use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey}; @@ -625,6 +625,25 @@ fn attach_projects_onto_the_plane_and_releases() { assert_eq!(path(&s, a).bounds().unwrap(), Rect::new(450.0, 300.0, 550.0, 400.0)); } +#[test] +fn a_repeated_id_attaches_moves_and_transforms_an_object_once() { + let edited = |cmd: &str, ids: &[u64], extra: Value| { + let mut s = session(); + s.execute("perspective.grid.preset", &json!({"kind": 2})).unwrap(); + let a = rect(&mut s, 450.0, 300.0, 100.0, 100.0); + let ids: Vec = ids.iter().map(|_| a.0).collect(); + let mut p = json!({"ids": ids, "plane": "right"}); + p.as_object_mut().unwrap().extend(extra.as_object().unwrap().clone()); + s.execute(cmd, &p).unwrap(); + path(&s, a) + }; + let moved = json!({"from": [480.0, 330.0], "to": [500.0, 350.0], "constrain": true}); + let scaled = json!({"matrix": [1.5, 0.0, 0.0, 1.5, 10.0, 0.0]}); + for (cmd, extra) in [("perspective.attach", json!({})), ("perspective.move", moved), ("perspective.transform", scaled)] { + assert_eq!(edited(cmd, &[0, 0], extra.clone()), edited(cmd, &[0], extra), "{cmd}"); + } +} + #[test] fn rectangle_tool_draws_in_perspective_on_the_active_plane() { let mut s = session();