Part of #1429: Edit and Object commands on many objects take time linear in the document

Delete, Cut, Group, Make Compound Path, Make Clipping Mask, Send to
Current Layer, Ungroup and Release Compound Path removed, inserted or
moved one object at a time. duplicate_in inserted one copy at a time;
Duplicate, Copy in the Move, Rotate, Scale, Reflect and Shear dialogs,
Transform Again after a copy, Transform Each with Copy, the perspective
copies and each move of an Alt-drag call it. Each insertion or removal
moved the later objects of the same layer or group to other indices.
The next lookup by id then walked the tree, and after every fourth such
walk Document::index_path rebuilt the whole lookup cache. Each command
did work that grew with the number of objects times the size of the
document: with 25,000 rectangles selected, a release build of
vectorcraft-cli took 7.2 s to group them and 11.5 s to copy them with
object.transform.

Document::remove_many, Document::wrap and Document::insert_beside
remove nodes, wrap them in a new node, or put nodes directly above or
in place of their anchors, in one pass over the children of each
parent. Each finds the nodes with one index_paths call, checks every id
before it changes anything, and clears the lookup cache once. The
commands above use them. The objects keep their order, the copies take
their ids in the same order, and the selection, the undo step and the
journal are the same. duplicate_in still copies one root at a time when
a root is listed twice or lies inside another root. A release build
with this change groups the 25,000 rectangles in 27 ms and copies them
in 121 ms.

Docs: docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-12 23:42:43 +07:00
committed by bflatastic
parent 25c33b6927
commit ffa948dcc3
7 changed files with 677 additions and 51 deletions
+29 -12
View File
@@ -384,9 +384,7 @@ fn clear(s: &mut Session, p: &Value) -> Result<Value> {
return ok();
}
s.edit("Clear", |d, sel| {
for id in &ids {
d.remove(*id)?;
}
d.remove_many(&ids)?;
if with_guides {
let gone = sel.guides.clone();
d.retain_guides(sel, |i, _| !gone.contains(&i));
@@ -560,16 +558,35 @@ fn duplicate(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>() }))
}
/// Duplicate `roots` directly above each original, transform the copies, select them.
/// Duplicate `roots` directly above each original, transform the copies, select them. The copies
/// take new ids in the order of `roots`. Roots listed twice, or inside another root, are copied one
/// at a time: each copy is made from the document as the copies before it left it.
pub(crate) fn duplicate_in(d: &mut Document, sel: &mut vectorcraft_doc::Selection, roots: &[NodeId], xf: Affine) -> Result<Vec<NodeId>> {
let mut out = vec![];
for id in roots {
let Some((par, idx, _)) = d.position(*id) else { continue };
let Some(n) = d.node(*id).cloned() else { continue };
let mut c = d.reid(&n);
c.transform(xf, true);
out.push(d.insert(par, idx + 1, c)?);
}
let paths = d.index_paths(roots.iter().copied());
let mut found: Vec<_> = roots.iter().filter_map(|id| paths.get(id)).collect();
found.sort_unstable();
let out = if found.windows(2).all(|w| !w[1].starts_with(w[0])) {
let mut copies = Vec::with_capacity(roots.len());
for id in roots {
let Some(n) = paths.get(id).and_then(|p| d.node_at(p)).cloned() else { continue };
let mut c = d.reid(&n);
c.transform(xf, true);
copies.push((*id, vec![c]));
}
let out: Vec<NodeId> = copies.iter().flat_map(|(_, c)| c.iter().map(|n| n.id)).collect();
d.insert_beside(copies, false)?;
out
} else {
let mut out = vec![];
for id in roots {
let Some((par, idx, _)) = d.position(*id) else { continue };
let Some(n) = d.node(*id).cloned() else { continue };
let mut c = d.reid(&n);
c.transform(xf, true);
out.push(d.insert(par, idx + 1, c)?);
}
out
};
sel.set(out.iter().copied());
Ok(out)
}
+29 -38
View File
@@ -505,9 +505,8 @@ fn send_to_current_layer(s: &mut Session, _: &Value) -> Result<Value> {
return ok();
}
s.edit("Send to Current Layer", |d, _| {
for id in &ids {
d.move_node(*id, Some(layer), usize::MAX)?;
}
let moved = d.remove_many(&ids)?;
d.children_mut(Some(layer))?.extend(moved);
Ok(())
})?;
ok()
@@ -525,14 +524,11 @@ fn group(s: &mut Session, _: &Value) -> Result<Value> {
/// Put `ids` (top-level objects, in paint order) in a new group where the front-most one was.
pub(crate) fn group_nodes(d: &mut Document, ids: &[NodeId]) -> Result<NodeId> {
let Some(top) = ids.last().copied() else { return Err(EngineError::Other("nothing selected".into())) };
let (par, idx, _) = d.position(top).ok_or(EngineError::NoNode(top))?;
let gid = d.alloc_id();
d.insert(par, idx + 1, Node::group(gid, vec![]))?;
for id in ids {
d.move_node(*id, Some(gid), usize::MAX)?;
if ids.is_empty() {
return Err(EngineError::Other("nothing selected".into()));
}
Ok(gid)
let gid = d.alloc_id();
Ok(d.wrap(ids, |children| Node::group(gid, children))?)
}
fn ungroup(s: &mut Session, _: &Value) -> Result<Value> {
@@ -544,31 +540,30 @@ fn ungroup(s: &mut Session, _: &Value) -> Result<Value> {
}
s.edit("Ungroup", |d, sel| {
let mut new_sel = vec![];
let mut ungrouped = vec![];
for id in &ids {
let Some(n) = d.node(*id) else { continue };
let NodeKind::Group { children, clip } = &n.kind else {
new_sel.push(*id);
continue;
};
let children = children.clone();
let clip = *clip;
let (par, idx, _) = d.position(*id).ok_or(EngineError::NoNode(*id))?;
let opacity = n.opacity;
d.remove(*id)?;
for (k, c) in children.into_iter().enumerate() {
let mut c = (*c).clone();
let mut out = Vec::with_capacity(children.len());
for (k, c) in children.iter().enumerate() {
let mut c = (**c).clone();
// The clipping path keeps its paint (none, unless painted after Make).
if clip
if *clip
&& k == 0
&& let NodeKind::Path { clipping, .. } = &mut c.kind
{
*clipping = false;
}
c.opacity *= opacity;
c.opacity *= n.opacity;
new_sel.push(c.id);
d.insert(par, idx + k, c)?;
out.push(c);
}
ungrouped.push((*id, out));
}
d.insert_beside(ungrouped, true)?;
sel.set(new_sel);
Ok(())
})?;
@@ -666,10 +661,8 @@ fn show_all(s: &mut Session, _: &Value) -> Result<Value> {
fn compound_make(s: &mut Session, _: &Value) -> Result<Value> {
let ids = selected_roots(s)?;
let Some(backmost) = ids.first().copied() else { return Err(EngineError::Other("nothing selected".into())) };
let top = ids.last().copied().unwrap_or(backmost);
let id = s.edit("Make Compound Path", |d, sel| {
let appearance = d.node(backmost).ok_or(EngineError::NoNode(backmost))?.appearance.clone();
let (par, idx, _) = d.position(top).ok_or(EngineError::NoNode(top))?;
let mut paths: Vec<Arc<Node>> = vec![];
for id in &ids {
let n = d.node(*id).cloned().ok_or(EngineError::NoNode(*id))?;
@@ -689,11 +682,7 @@ fn compound_make(s: &mut Session, _: &Value) -> Result<Value> {
let cid = d.alloc_id();
let mut c = Node::new(cid, NodeKind::Compound { children: vec![], rule: FillRule::NonZero });
c.appearance = appearance;
d.insert(par, idx + 1, c)?;
for id in &ids {
d.remove(*id)?;
}
let ch = d.node_mut(cid).and_then(|n| n.children_mut()).ok_or(EngineError::NoNode(cid))?;
let ch = c.children_mut().ok_or(EngineError::NoNode(cid))?;
// The paths in front run against the backmost one, so where they overlap it they cut holes
// under the non-zero fill rule (#1197).
let direction = |n: &Node| n.path_data().and_then(|pd| pd.subpaths.first()).map_or(0.0, |sp| sp.signed_area().signum());
@@ -710,6 +699,7 @@ fn compound_make(s: &mut Session, _: &Value) -> Result<Value> {
}
ch.push(p);
}
d.wrap(&ids, |_| c)?;
sel.set([cid]);
Ok(cid)
})?;
@@ -736,17 +726,17 @@ fn compound_release(s: &mut Session, _: &Value) -> Result<Value> {
}
s.edit("Release Compound Path", |d, sel| {
let mut out = vec![];
let mut released = vec![];
for id in &ids {
let Some(n) = d.node(*id).cloned().filter(releases) else { continue };
let (par, idx, _) = d.position(*id).ok_or(EngineError::NoNode(*id))?;
d.remove(*id)?;
let mut parts = vec![];
match &n.kind {
NodeKind::Compound { children, .. } => {
for (k, c) in children.iter().enumerate() {
for c in children {
let mut c = (**c).clone();
c.appearance = n.appearance.clone();
out.push(c.id);
d.insert(par, idx + k, c)?;
parts.push(c);
}
}
// Each subpath becomes a path of its own with the path's paint, in the same
@@ -758,12 +748,14 @@ fn compound_release(s: &mut Session, _: &Value) -> Result<Value> {
(*path, *live) = (PathData::single(sp.clone()), None);
}
out.push(c.id);
d.insert(par, idx + k, c)?;
parts.push(c);
}
}
_ => {}
}
released.push((*id, parts));
}
d.insert_beside(released, true)?;
sel.set(out);
Ok(())
})?;
@@ -811,13 +803,12 @@ fn clip_make(s: &mut Session, _: &Value) -> Result<Value> {
let Some(&top) = ids.last() else { return Err(EngineError::Other("nothing selected".into())) };
let gid = s.edit("Make Clipping Mask", |d, sel| {
make_clipping_path(d, top)?;
let (par, idx, _) = d.position(top).ok_or(EngineError::NoNode(top))?;
let gid = d.alloc_id();
d.insert(par, idx + 1, Node::new(gid, NodeKind::Group { children: vec![], clip: true }))?;
d.move_node(top, Some(gid), 0)?;
for id in &ids[..ids.len() - 1] {
d.move_node(*id, Some(gid), usize::MAX)?;
}
// The topmost object, the clipping path, goes first, and the others follow in paint order.
d.wrap(&ids, |mut children| {
children.rotate_right(1);
Node::new(gid, NodeKind::Group { children, clip: true })
})?;
sel.set([gid]);
Ok(gid)
})?;
+2
View File
@@ -1740,6 +1740,8 @@ mod tests_knockout;
#[cfg(test)]
mod tests_labspots;
#[cfg(test)]
mod tests_large_edits;
#[cfg(test)]
mod tests_layerclip;
#[cfg(test)]
mod tests_layers;
+361
View File
@@ -0,0 +1,361 @@
//! Commands that remove, insert or copy many objects, and an Alt-drag that copies them, each finish
//! within 20 s on one layer of 25,000 to 100,000 objects, and objects in two layers and inside a
//! group keep their order and ids.
use std::sync::Arc;
use std::time::Duration;
use serde_json::{Value, json};
use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind};
use vectorcraft_geom::{FillRule, Rect, shapes};
use vectorcraft_tools::{Mods, PointerEvent, PointerKind};
use super::*;
/// Squares in a row of [`square_at`].
const ROW: usize = 200;
/// Square `k`: 10 pt wide, in rows of 200 squares 20 pt apart.
fn square_at(k: usize) -> Rect {
let (x, y) = ((k % ROW) as f64 * 20.0, (k / ROW) as f64 * 20.0);
Rect::new(x, y, x + 10.0, y + 10.0)
}
/// Square `k` ([`square_at`]) as a path with a new id.
fn square(d: &mut Document, k: usize) -> Node {
Node::path(d.alloc_id(), shapes::rectangle(square_at(k)), Appearance::default_art())
}
/// A session whose document holds `make(d, k)` for each `k` below `n` on its one layer, all of it
/// selected.
fn session(n: usize, mut make: impl FnMut(&mut Document, usize) -> Node) -> Session {
let mut d = Document::new(ROW as f64 * 20.0, (n / ROW + 1) as f64 * 20.0);
let art: Vec<Arc<Node>> = (0..n).map(|k| Arc::new(make(&mut d, k))).collect();
if let Some(ch) = Arc::make_mut(&mut d.layers[0]).children_mut() {
*ch = art;
}
let mut s = Session::new();
s.add_document(d, None);
s.execute("select.all", &json!({})).unwrap();
s
}
/// The ids of `n`'s children.
fn ids_of(n: &Node) -> Vec<NodeId> {
n.children().into_iter().flatten().map(|c| c.id).collect()
}
/// The ids of the objects on layer `i`.
fn layer(s: &Session, i: usize) -> Vec<NodeId> {
ids_of(&s.doc().unwrap().doc.layers[i])
}
/// The ids of the children of `id`.
fn children(s: &Session, id: NodeId) -> Vec<NodeId> {
ids_of(s.doc().unwrap().doc.node(id).unwrap())
}
/// `n` ids counting up from `first`.
fn ids_from(first: u64, n: usize) -> Vec<NodeId> {
(first..).take(n).map(NodeId).collect()
}
/// The ids of `a` and `b` in turn: each id of `a` followed by the id of `b` at the same place.
fn in_turn(a: &[NodeId], b: &[NodeId]) -> Vec<NodeId> {
a.iter().zip(b).flat_map(|(a, b)| [*a, *b]).collect()
}
/// Runs `f` on `s` on a thread of its own and fails the test when it takes more than 20 s.
fn soon<T: Send + 'static>(mut s: Session, what: &'static str, f: impl FnOnce(&mut Session) -> T + Send + 'static) -> (Session, T) {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let r = f(&mut s);
tx.send((s, r)).ok();
});
rx.recv_timeout(Duration::from_secs(20)).unwrap_or_else(|_| panic!("{what} did not finish within 20 s"))
}
/// Runs command `cmd` with `params` through [`soon`] and returns its result.
fn run_soon(s: Session, cmd: &'static str, params: Value) -> (Session, Value) {
let (s, r) = soon(s, cmd, move |s| s.execute(cmd, &params));
(s, r.unwrap())
}
/// The ids in the `ids` array of a command's result.
fn ids_in(r: &Value) -> Vec<NodeId> {
r["ids"].as_array().unwrap().iter().map(|v| NodeId(v.as_u64().unwrap())).collect()
}
/// Runs Undo and checks that it gives back `before`.
fn undo_gives_back(s: &mut Session, before: &Document) {
s.execute("edit.undo", &json!({})).unwrap();
assert!(*s.doc().unwrap().doc == *before, "Undo gives back the document");
}
/// Whether the selected objects are `ids`, in that order.
fn selected(s: &Session, ids: &[NodeId]) -> bool {
s.doc().unwrap().selection.objects == ids
}
/// The geometric bounds of object `id`.
fn bounds(s: &Session, id: NodeId) -> Rect {
s.doc().unwrap().doc.node(id).unwrap().geometric_bounds().unwrap()
}
#[test]
fn delete_of_100000_objects_finishes_within_20_s() {
let s = session(100_000, square);
let before = s.doc().unwrap().doc.clone();
let (mut s, _) = run_soon(s, "edit.clear", json!({}));
assert!(layer(&s, 0).is_empty());
assert!(selected(&s, &[]));
undo_gives_back(&mut s, &before);
}
#[test]
fn group_of_50000_objects_finishes_within_20_s() {
let s = session(50_000, square);
let (before, ids) = (s.doc().unwrap().doc.clone(), layer(&s, 0));
let (mut s, r) = run_soon(s, "object.group", json!({}));
let g = NodeId(r["id"].as_u64().unwrap());
assert_eq!(layer(&s, 0), [g]);
assert!(children(&s, g) == ids, "the group holds the objects in their order");
assert!(selected(&s, &[g]));
undo_gives_back(&mut s, &before);
}
#[test]
fn clipping_mask_of_50000_objects_finishes_within_20_s() {
let s = session(50_000, square);
let (before, ids) = (s.doc().unwrap().doc.clone(), layer(&s, 0));
let (mut s, r) = run_soon(s, "object.clippingMask.make", json!({}));
let g = NodeId(r["id"].as_u64().unwrap());
assert_eq!(layer(&s, 0), [g]);
// The topmost object comes first, as the clipping path, and the others follow in their order.
let mut want = ids.clone();
want.rotate_right(1);
assert!(children(&s, g) == want, "the clip group holds the objects in their order");
let doc = &s.doc().unwrap().doc;
assert!(matches!(doc.node(g).unwrap().kind, NodeKind::Group { clip: true, .. }));
assert!(matches!(doc.node(want[0]).unwrap().kind, NodeKind::Path { clipping: true, .. }));
assert!(selected(&s, &[g]));
undo_gives_back(&mut s, &before);
}
#[test]
fn duplicate_of_50000_objects_finishes_within_20_s() {
let s = session(50_000, square);
let (before, ids, first) = (s.doc().unwrap().doc.clone(), layer(&s, 0), s.doc().unwrap().doc.peek_next_id());
let (mut s, r) = run_soon(s, "edit.duplicate", json!({"dx": 5}));
// Each copy goes directly above its object and takes the next id, in the objects' order.
let copies = ids_from(first, ids.len());
assert!(ids_in(&r) == copies);
assert!(layer(&s, 0) == in_turn(&ids, &copies), "each copy directly above its object");
assert!(selected(&s, &copies));
assert_eq!(bounds(&s, copies[0]), square_at(0) + vectorcraft_geom::Vec2::new(5.0, 0.0));
undo_gives_back(&mut s, &before);
}
#[test]
fn transform_with_copy_of_50000_objects_finishes_within_20_s() {
let s = session(50_000, square);
let (before, ids, first) = (s.doc().unwrap().doc.clone(), layer(&s, 0), s.doc().unwrap().doc.peek_next_id());
let (mut s, r) = run_soon(s, "object.transform", json!({"matrix": [1, 0, 0, 1, 5, 0], "copy": true}));
let copies = ids_from(first, ids.len());
assert!(ids_in(&r) == copies);
assert!(layer(&s, 0) == in_turn(&ids, &copies), "each copy directly above its object");
assert!(selected(&s, &copies));
let last = ids.len() - 1;
assert_eq!(bounds(&s, copies[last]), square_at(last) + vectorcraft_geom::Vec2::new(5.0, 0.0));
assert_eq!(bounds(&s, ids[last]), square_at(last));
undo_gives_back(&mut s, &before);
}
#[test]
fn transform_each_with_copy_of_50000_objects_finishes_within_20_s() {
let s = session(50_000, square);
let (before, ids, first) = (s.doc().unwrap().doc.clone(), layer(&s, 0), s.doc().unwrap().doc.peek_next_id());
let (mut s, r) = run_soon(s, "object.transformEach", json!({"moveH": 5, "copy": true}));
let copies = ids_from(first, ids.len());
assert!(ids_in(&r) == copies);
assert!(layer(&s, 0) == in_turn(&ids, &copies), "each copy directly above its object");
assert!(selected(&s, &copies));
assert_eq!(bounds(&s, copies[1]), square_at(1) + vectorcraft_geom::Vec2::new(5.0, 0.0));
undo_gives_back(&mut s, &before);
}
#[test]
fn compound_path_of_100000_objects_finishes_within_20_s() {
let s = session(100_000, square);
let (before, ids) = (s.doc().unwrap().doc.clone(), layer(&s, 0));
let (mut s, r) = run_soon(s, "object.compoundPath.make", json!({}));
let c = NodeId(r["id"].as_u64().unwrap());
assert_eq!(layer(&s, 0), [c]);
assert!(children(&s, c) == ids, "the compound path holds the paths in their order");
assert!(selected(&s, &[c]));
undo_gives_back(&mut s, &before);
}
#[test]
fn send_to_current_layer_of_100000_objects_finishes_within_20_s() {
let mut s = session(100_000, square);
let ids = layer(&s, 0);
let to = NodeId(s.execute("layer.new", &json!({})).unwrap()["id"].as_u64().unwrap());
s.execute("select.all", &json!({})).unwrap();
s.execute("layer.setCurrent", &json!({"id": to.0})).unwrap();
let before = s.doc().unwrap().doc.clone();
let (mut s, _) = run_soon(s, "object.arrange.sendToCurrentLayer", json!({}));
assert!(layer(&s, 0).is_empty());
assert!(children(&s, to) == ids, "the layer holds the objects in their order");
assert!(selected(&s, &ids));
undo_gives_back(&mut s, &before);
}
#[test]
fn ungroup_of_25000_groups_finishes_within_20_s() {
// Each group holds two squares, and ungrouping one adds an object to the layer.
let s = session(25_000, |d, k| {
let pair = [square(d, 2 * k), square(d, 2 * k + 1)].map(Arc::new);
Node::group(d.alloc_id(), pair.to_vec())
});
let before = s.doc().unwrap().doc.clone();
let squares: Vec<NodeId> = layer(&s, 0).into_iter().flat_map(|g| children(&s, g)).collect();
let (mut s, _) = run_soon(s, "object.ungroup", json!({}));
assert!(layer(&s, 0) == squares, "each group's objects where the group was");
assert!(selected(&s, &squares));
undo_gives_back(&mut s, &before);
}
#[test]
fn compound_path_release_of_25000_compound_paths_finishes_within_20_s() {
let s = session(25_000, |d, k| {
let pair = [square(d, 2 * k), square(d, 2 * k + 1)].map(Arc::new);
Node::new(d.alloc_id(), NodeKind::Compound { children: pair.to_vec(), rule: FillRule::NonZero })
});
let before = s.doc().unwrap().doc.clone();
let paths: Vec<NodeId> = layer(&s, 0).into_iter().flat_map(|c| children(&s, c)).collect();
let (mut s, _) = run_soon(s, "object.compoundPath.release", json!({}));
assert!(layer(&s, 0) == paths, "each compound path's paths where it was");
assert!(selected(&s, &paths));
undo_gives_back(&mut s, &before);
}
/// With Alt held, a Selection tool press on one of 30,000 selected squares, four moves of 20 pt
/// and the release copy the squares once, as one undo step.
#[test]
fn alt_drag_of_30000_objects_finishes_within_20_s() {
let s = session(30_000, square);
let (before, ids, first) = (s.doc().unwrap().doc.clone(), layer(&s, 0), s.doc().unwrap().doc.peek_next_id());
// The press lands on a square in the middle of the art, away from the handles of the
// selection's box.
let grab = square_at(15_100).center();
let (mut s, r) = soon(s, "the Alt-drag", move |s| {
let (v, alt) = (ViewInfo::default(), Mods { alt: true, ..Mods::default() });
s.select_tool("selection", v)?;
let at = |kind, dx: f64| PointerEvent::new(kind, grab.x + dx, grab.y).with_mods(alt);
s.pointer(&at(PointerKind::Down, 0.0), v)?;
for dx in [20.0, 40.0, 60.0, 80.0] {
s.pointer(&at(PointerKind::Drag, dx), v)?;
}
s.pointer(&at(PointerKind::Up, 80.0), v)
});
r.unwrap();
let copies = ids_from(first, ids.len());
assert!(layer(&s, 0) == in_turn(&ids, &copies), "each copy directly above its square");
assert!(selected(&s, &copies));
assert_eq!(s.doc().unwrap().history.undo.last().map(|e| e.label.as_str()), Some("Copy"));
// The squares stay; their copies move together.
let moved = bounds(&s, copies[0]).origin() - square_at(0).origin();
assert!(moved.x > 0.0, "{moved:?}");
for (k, (id, copy)) in ids.iter().zip(&copies).enumerate() {
assert_eq!(bounds(&s, *id), square_at(k));
assert_eq!(bounds(&s, *copy), square_at(k) + moved);
}
undo_gives_back(&mut s, &before);
}
/// The ids under `n`: a child that can hold children is a list of its id and theirs.
fn outline(n: &Node) -> Value {
let ids = n.children().into_iter().flatten().map(|c| match c.children() {
Some(_) => {
let mut v = vec![json!(c.id.0)];
v.extend(outline(c).as_array().cloned().unwrap_or_default());
Value::Array(v)
}
None => json!(c.id.0),
});
Value::Array(ids.collect())
}
/// [`outline`] of each layer.
fn layers(s: &Session) -> Vec<Value> {
s.doc().unwrap().doc.layers.iter().map(|l| outline(l)).collect()
}
/// A session whose first layer holds a, b, c and a group of d, e and f, and whose second layer
/// holds g and h, with a, c, e, g and h selected. Returns the session and the ids of a to h and of
/// the group.
fn two_layers() -> (Session, [u64; 9]) {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
let rect = |s: &mut Session| {
let x = 20.0 * s.doc().unwrap().doc.node_count() as f64;
s.execute("shape.rectangle", &json!({"x": x, "y": 10, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap()
};
let [a, b, c, d, e, f] = [(); 6].map(|()| rect(&mut s));
s.execute("select.set", &json!({"ids": [d, e, f]})).unwrap();
let group = s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap();
s.execute("layer.new", &json!({})).unwrap();
let [g, h] = [(); 2].map(|()| rect(&mut s));
s.execute("select.set", &json!({"ids": [a, c, e, g, h]})).unwrap();
assert_eq!(layers(&s), [json!([a, b, c, [group, d, e, f]]), json!([g, h])]);
(s, [a, b, c, d, e, f, g, h, group])
}
/// After Group, Delete and Duplicate of objects in two layers and inside a group, the layers hold
/// the objects and the new ones in the order, and with the ids, that each check lists, and Undo
/// gives back the document.
#[test]
fn group_delete_and_duplicate_across_layers_and_inside_a_group_keep_order_and_ids() {
let (mut s, [a, b, c, d, e, f, g, h, group]) = two_layers();
let before = s.doc().unwrap().doc.clone();
let next = before.peek_next_id();
let r = s.execute("object.group", &json!({})).unwrap();
assert_eq!(r["id"], json!(next));
assert_eq!(layers(&s), [json!([b, [group, d, f]]), json!([[next, a, c, e, g, h]])]);
assert!(selected(&s, &[NodeId(next)]));
undo_gives_back(&mut s, &before);
s.execute("edit.clear", &json!({})).unwrap();
assert_eq!(layers(&s), [json!([b, [group, d, f]]), json!([])]);
undo_gives_back(&mut s, &before);
let r = s.execute("edit.duplicate", &json!({})).unwrap();
let [a2, c2, e2, g2, h2] = [0, 1, 2, 3, 4].map(|k| next + k);
assert_eq!(r["ids"], json!([a2, c2, e2, g2, h2]));
assert_eq!(layers(&s), [json!([a, a2, b, c, c2, [group, d, e, e2, f]]), json!([g, g2, h, h2])]);
assert!(selected(&s, &ids_from(next, 5)));
undo_gives_back(&mut s, &before);
}
/// `object.transform` copies ids listed twice, and an object listed before a group around it.
/// Each copy goes directly above its original, a later copy of the same object goes below an
/// earlier one, and the copy of the group holds a copy of the copy made inside the group before it.
#[test]
fn copies_of_ids_listed_twice_or_inside_another_listed_id_keep_their_order_and_ids() {
let (mut s, [a, b, c, d, e, f, g, h, group]) = two_layers();
let before = s.doc().unwrap().doc.clone();
let n = before.peek_next_id();
let r = s.execute("object.transform", &json!({"matrix": [1, 0, 0, 1, 5, 0], "copy": true, "ids": [e, group]})).unwrap();
assert_eq!(r["ids"], json!([n, n + 1]));
let copied = json!([n + 1, n + 2, n + 3, n + 4, n + 5]);
assert_eq!(layers(&s), [json!([a, b, c, [group, d, e, n, f], copied]), json!([g, h])]);
undo_gives_back(&mut s, &before);
let r = s.execute("object.transform", &json!({"matrix": [1, 0, 0, 1, 5, 0], "copy": true, "ids": [a, a]})).unwrap();
assert_eq!(r["ids"], json!([n, n + 1]));
assert_eq!(layers(&s), [json!([a, n + 1, n, b, c, [group, d, e, f]]), json!([g, h])]);
undo_gives_back(&mut s, &before);
}