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.
This commit is contained in:
dotCooCoo
2026-11-14 15:20:27 -08:00
parent dafd993395
commit 7ad98ce9b8
2 changed files with 26 additions and 2 deletions
@@ -104,9 +104,14 @@ fn plane_param(g: &PerspectiveGrid, p: &Value, cmd: &str) -> Result<Plane> {
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<Vec<NodeId>> {
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),
}
}
+20 -1
View File
@@ -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<u64> = 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();