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)
})?;