From 7ad98ce9b81fa3b855ad26a75337c72f6b6873a2 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 15:05:15 -0800 Subject: [PATCH] Perspective: a repeated id attaches, moves or transforms an object once perspective.attach, perspective.release, perspective.move and perspective.transform took their `ids` as given, so an id listed twice was processed twice. Attach warped an object that was already projected a second time, and a move or transform of an object not yet in perspective attached it and then moved it twice. The Shift and Snap to Grid reads of a move also used a document attached twice. The ids are now taken once each, in the order first given. --- .../engine/src/cmd/distortcmds/perspective.rs | 7 ++++++- crates/engine/src/tests/distort.rs | 21 ++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) 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();