Files
vectorcraft/crates/engine/src/cmd/layerpanel.rs
T
dotCooCoo 6f9e2d5c16 Part of #1429: Layers panel and artboard commands on many rows take time linear in the document
These commands removed, moved or copied one row or object at a time:
the Layers panel's Delete Selection, Duplicate Selection, Collect in
New Layer, row drags, Merge Selected, Flatten Artwork and Release to
Layers, and Cut Artboard, Duplicate Artboard and the Artboard tool's
Alt-drags that copy art. Each edit moved the later rows 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. Release to Layers also walked the document
twice for each new layer to name and color it. With 25,000 rectangles
in one layer, a release build of vectorcraft-cli took 11.9 s for
Duplicate Selection and 17.7 s for Release to Layers.

The commands now remove, move and copy the rows in one pass over each
layer or group, with Document::remove_many and Document::wrap, and
with copy_above, the copy pass of duplicate_in, which Duplicate
Selection and the artboard copies now share. Release to Layers names
its new layers from one walk of the document. Merge Selected checks
the target layer's objects against a set of the listed rows, and
Delete Selection checks the top-level layers against such a set. In a
document whose ids are unique, the rows keep their order, new rows and
layers take the same ids, names and colors, and the selection, the
undo step and the journal are the same. A release build with this
change takes 98 ms for Duplicate Selection and 26 ms for Release to
Layers.

Docs: docs/target-app-parity.md.
2026-10-13 05:21:04 +07:00

429 lines
17 KiB
Rust

//! The Layers panel menu's operations on rows: Merge Selected, Flatten Artwork, Release to Layers,
//! Reverse Order, Template, Hide/Outline/Lock Others and their Show/Preview/Unlock All
//! counterparts, Locate Object. Rows default to the highlighted ones, else the current layer (see
//! [`super::layer`]).
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use serde_json::{Value, json};
use vectorcraft_doc::{Document, Node, NodeId, NodeKind};
use super::layer::{NewLayers, row_roots, rows_param};
use super::*;
use crate::EngineError;
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!(
"layer.merge",
"Merge Selected",
["Window", "Layers"],
None,
"{ids?|id?} (default: the highlighted rows) merge two or more rows into the layer listed (highlighted) last: the other layers' contents and the other rows move into it, keeping their stacking order, and the emptied layers go. Every selected object left hidden or locked, such as one merged into a hidden or locked layer, is deselected (the key object too). One undo step → {id}",
has_doc,
merge
),
cmd!(
"layer.flatten",
"Flatten Artwork",
["Window", "Layers"],
None,
"{id?} (default: the highlighted row's top-level layer, else the current one's) move the art of every other visible top-level layer into this one, keeping the stacking order; hidden layers and their art are deleted, template layers stay. Every selected object left hidden or locked, such as one flattened into a locked layer, is deselected (the key object too). One undo step → {id, discarded}",
has_doc,
flatten
),
cmd!(
"layer.releaseToLayers",
"Release to Layers (Sequence)",
["Window", "Layers"],
None,
"{id?: layer or group (default: the highlighted row, else the current layer), build?: bool} each object in it goes into a new sublayer of its own, in stacking order (a group's objects go into sublayers of its layer, where the group was). `build`: Release to Layers (Build), each new sublayer holds copies of the objects up to its own, so the layers add up. One undo step → {ids}",
has_doc,
release
),
cmd!(
"layer.releaseToLayersBuild",
"Release to Layers (Build)",
["Window", "Layers"],
None,
"{id?} layer.releaseToLayers with build: true → {ids}",
has_doc,
|s, p| {
let mut p = p.clone();
if let Some(m) = p.as_object_mut() {
m.insert("build".into(), json!(true));
} else {
p = json!({"build": true});
}
release(s, &p)
}
),
cmd!(
"layer.reverse",
"Reverse Order",
["Window", "Layers"],
None,
"{ids?|id?} (default: the highlighted rows) reverse the stacking order of two or more rows that share a layer or group; each keeps the places the others held. One undo step",
has_doc,
reverse
),
cmd!(
"layer.template",
"Template",
["Window", "Layers"],
None,
"{ids?|id?, on?: bool (default: toggle the first)} make layers templates (locked, dimmed, left out of print and export) or ordinary layers again (layer.setProps template) → {on}",
has_doc,
template
),
cmd!(
"layer.hideOthers",
"Hide Others",
["Window", "Layers"],
None,
"{ids?|id?} hide every top-level layer but those holding the highlighted rows (else the current layer), which show. One undo step → {count}",
has_doc,
|s, p| others(s, p, Flag::Visible, false, "layer.hideOthers")
),
cmd!("layer.showAll", "Show All Layers", ["Window", "Layers"], None, "{} show every layer and sublayer → {count}", has_doc, |s, _| all(
s,
Flag::Visible,
true
)),
cmd!(
"layer.outlineOthers",
"Outline Others",
["Window", "Layers"],
None,
"{ids?|id?} turn Preview off (outline view) for every top-level layer but those holding the highlighted rows (else the current layer), which preview → {count}",
has_doc,
|s, p| others(s, p, Flag::Preview, false, "layer.outlineOthers")
),
cmd!(
"layer.previewAll",
"Preview All Layers",
["Window", "Layers"],
None,
"{} turn Preview on for every layer and sublayer → {count}",
has_doc,
|s, _| all(s, Flag::Preview, true)
),
cmd!(
"layer.lockOthers",
"Lock Others",
["Window", "Layers"],
None,
"{ids?|id?} lock every top-level layer but those holding the highlighted rows (else the current layer), which unlock → {count}",
has_doc,
|s, p| others(s, p, Flag::Locked, true, "layer.lockOthers")
),
cmd!(
"layer.unlockAll",
"Unlock All Layers",
["Window", "Layers"],
None,
"{} unlock every layer and sublayer → {count}",
has_doc,
|s, _| all(s, Flag::Locked, false)
),
cmd!(
"layer.locate",
"Locate Object",
["Window", "Layers"],
None,
"{id?} (default: the last selected object) highlight the object's row; the Layers panel opens the layers around it and scrolls to it → {id, ancestry: [layer…, id]}",
has_doc,
locate
),
]
}
/// A layer flag the panel menu sets on many layers.
#[derive(Clone, Copy)]
enum Flag {
Visible,
Locked,
Preview,
}
impl Flag {
fn get(self, n: &Node) -> bool {
match (self, &n.kind) {
(Flag::Visible, _) => n.visible,
(Flag::Locked, _) => n.locked,
(Flag::Preview, NodeKind::Layer { preview, .. }) => *preview,
(Flag::Preview, _) => true,
}
}
fn set(self, n: &mut Node, v: bool) {
match (self, &mut n.kind) {
(Flag::Visible, _) => n.visible = v,
(Flag::Locked, _) => n.locked = v,
(Flag::Preview, NodeKind::Layer { preview, .. }) => *preview = v,
(Flag::Preview, _) => {}
}
}
fn label(self, on: bool) -> &'static str {
match (self, on) {
(Flag::Visible, true) => "Show All Layers",
(Flag::Visible, false) => "Hide Others",
(Flag::Locked, true) => "Lock Others",
(Flag::Locked, false) => "Unlock All Layers",
(Flag::Preview, true) => "Preview All Layers",
(Flag::Preview, false) => "Outline Others",
}
}
}
/// Set `flag` on each of `changes` (id, value; layers that differ) in one undo step labelled `label` → how many
/// changed.
fn set_flag(s: &mut Session, changes: Vec<(NodeId, bool)>, flag: Flag, label: &str) -> Result<Value> {
let d = &s.doc()?.doc;
let changes: Vec<(NodeId, bool)> = changes.into_iter().filter(|(id, v)| d.node(*id).is_some_and(|n| flag.get(n) != *v)).collect();
if !changes.is_empty() {
s.edit(label, |d, sel| {
for (id, v) in &changes {
if let Some(n) = d.node_mut(*id) {
flag.set(n, *v);
}
}
sel.deselect_uneditable(d);
Ok(())
})?;
}
Ok(json!({ "count": changes.len() }))
}
/// The top-level layers holding the rows a command acts on.
fn kept_layers(s: &Session, p: &Value, command: &str) -> Result<HashSet<NodeId>> {
let ids = rows_param(s, p, command)?;
let d = &s.doc()?.doc;
Ok(ids.iter().filter_map(|id| d.layer_of(*id)).collect())
}
/// Every top-level layer but the kept ones set to `v`, and the kept ones to the opposite: Alt-click
/// on a hidden layer's eye shows just that layer (#1158).
fn others(s: &mut Session, p: &Value, flag: Flag, v: bool, command: &str) -> Result<Value> {
let keep = kept_layers(s, p, command)?;
let changes = s.doc()?.doc.layers.iter().map(|l| (l.id, !keep.contains(&l.id) == v)).collect();
set_flag(s, changes, flag, flag.label(v))
}
fn all(s: &mut Session, flag: Flag, v: bool) -> Result<Value> {
let mut ids = vec![];
s.doc()?.doc.walk(|n| {
if n.is_layer() {
ids.push((n.id, v));
}
});
set_flag(s, ids, flag, flag.label(v))
}
fn template(s: &mut Session, p: &Value) -> Result<Value> {
let ids = rows_param(s, p, "layer.template")?;
let d = &s.doc()?.doc;
let ids: Vec<NodeId> = ids.into_iter().filter(|id| d.node(*id).is_some_and(Node::is_layer)).collect();
let Some(first) = ids.first().and_then(|id| d.node(*id)) else { return Err(bad("layer.template", "select a layer")) };
let on = bool_or(p, "on", !first.is_template());
s.execute("layer.setProps", &json!({"ids": ids.iter().map(|i| i.0).collect::<Vec<_>>(), "template": on}))?;
Ok(json!({ "on": on }))
}
/// Move the contents of `others` (layers give their children, anything else itself; none inside
/// another) into layer `into`, keeping the document's stacking order, then delete the emptied
/// layers.
fn merge_into(d: &mut Document, into: NodeId, others: &[NodeId]) -> Result<()> {
let listed: HashSet<NodeId> = others.iter().copied().collect();
let own: Vec<NodeId> =
d.node(into).and_then(|n| n.children()).map(|c| c.iter().map(|c| c.id).filter(|c| !listed.contains(c)).collect()).unwrap_or_default();
let (mut moved, mut emptied) = (vec![], vec![]);
for id in others {
let n = d.node(*id).ok_or(EngineError::NoNode(*id))?;
if n.is_layer() {
moved.extend(n.children().into_iter().flatten().map(|c| c.id));
emptied.push(*id);
} else {
moved.push(*id);
}
}
// Where everything is now: the merged layer stacks its contents in this order.
let keys: HashMap<NodeId, Vec<usize>> = own.iter().chain(&moved).filter_map(|id| Some((*id, d.index_path(*id)?))).collect();
let taken = d.remove_many(&moved)?;
d.remove_many(&emptied)?;
let ch = d.children_mut(Some(into))?;
let mut all = std::mem::take(ch);
all.extend(taken);
all.sort_by(|a, b| keys.get(&a.id).cmp(&keys.get(&b.id)));
*ch = all;
// The merged layers' guides go to it too.
d.rehome_guides(into);
Ok(())
}
fn merge(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "layer.merge";
let ids = rows_param(s, p, C)?;
let st = s.doc()?;
let d = &st.doc;
let mut seen = HashSet::new();
let ids: Vec<NodeId> = ids.into_iter().filter(|id| d.node(*id).is_some() && seen.insert(*id)).collect();
if ids.len() < 2 {
return Err(bad(C, "highlight two or more rows to merge"));
}
let into =
ids.iter().rev().copied().find(|id| d.node(*id).is_some_and(Node::is_layer)).ok_or_else(|| bad(C, "highlight a layer to merge into"))?;
let around = d.ancestry(into).unwrap_or_default();
// (Rows inside the target layer merge too: only rows inside other listed rows go with those.)
let rest: Vec<NodeId> = ids.iter().copied().filter(|id| *id != into).collect();
let others = row_roots(d, &rest);
if others.iter().any(|o| around.contains(o)) {
return Err(bad(C, "a layer can't be merged into its own sublayer"));
}
s.edit("Merge Selected", |d, sel| {
merge_into(d, into, &others)?;
sel.prune(d);
sel.deselect_uneditable(d);
Ok(())
})?;
let st = s.doc_mut()?;
st.active_layer = Some(into);
st.layer_rows = vec![into];
Ok(json!({ "id": into.0 }))
}
fn flatten(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "layer.flatten";
let st = s.doc()?;
let d = &st.doc;
let from = match p.get("id") {
Some(id) => Some(checked_id(s, id, C)?),
None => st.highlighted_rows().last().copied().or(st.current_layer()),
};
let into = from.and_then(|id| d.layer_of(id)).ok_or_else(|| bad(C, "no layer"))?;
if !d.node(into).is_some_and(|n| n.visible) {
return Err(EngineError::Other("the layer to flatten into is hidden".into()));
}
let others: Vec<NodeId> = d.layers.iter().filter(|l| l.id != into && l.visible && !l.is_template()).map(|l| l.id).collect();
let hidden: Vec<NodeId> = d.layers.iter().filter(|l| l.id != into && !l.visible && !l.is_template()).map(|l| l.id).collect();
s.edit("Flatten Artwork", |d, sel| {
d.remove_many(&hidden)?;
// Hidden layers are discarded with their guides.
d.drop_guides_of_deleted_layers(sel);
merge_into(d, into, &others)?;
sel.prune(d);
sel.deselect_uneditable(d);
Ok(())
})?;
let st = s.doc_mut()?;
st.active_layer = Some(into);
st.layer_rows = vec![into];
Ok(json!({ "id": into.0, "discarded": hidden.len() }))
}
fn release(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "layer.releaseToLayers";
let build = bool_or(p, "build", false);
let st = s.doc()?;
let id = match id_param(p, "id") {
Some(id) => id,
None => match st.highlighted_rows().as_slice() {
[id] => *id,
_ => st.current_layer().ok_or_else(|| bad(C, "no layer"))?,
},
};
let n = st.doc.node(id).ok_or(EngineError::NoNode(id))?;
let group = match n.kind {
NodeKind::Layer { .. } => false,
NodeKind::Group { .. } => true,
_ => return Err(bad(C, "release a layer or a group")),
};
// Its objects, bottom first (sublayers stay as they are).
let objects: Vec<NodeId> = n.children().into_iter().flatten().filter(|c| !c.is_layer()).map(|c| c.id).collect();
if objects.is_empty() {
return Err(EngineError::Other("there are no objects to release".into()));
}
let new = s.edit(if build { "Release to Layers (Build)" } else { "Release to Layers (Sequence)" }, |d, sel| {
// A group's objects go into sublayers of the layer holding it, where it was.
let (parent, index) = if group {
let layer = d.layer_containing(id).ok_or(EngineError::NoNode(id))?;
let (par, i, _) = d.position(id).ok_or(EngineError::NoNode(id))?;
if par == Some(layer) { (layer, i) } else { (layer, usize::MAX) }
} else {
(id, 0)
};
let originals = d.remove_many(&objects)?;
if group {
d.remove(id)?;
}
let mut names = NewLayers::of(d);
let mut layers = Vec::with_capacity(originals.len());
let last = originals.len().saturating_sub(1);
for k in 0..originals.len() {
let mut layer = names.make(d, None);
let held: Vec<Arc<Node>> = if build {
// Copies of the objects below and itself; the top layer keeps the originals.
(0..=k).filter_map(|j| originals.get(j)).map(|o| if k == last { o.clone() } else { Arc::new(d.reid(o)) }).collect()
} else {
originals.get(k).cloned().into_iter().collect()
};
if let Some(ch) = layer.children_mut() {
ch.extend(held);
}
names.add(&layer);
layers.push(Arc::new(layer));
}
let new: Vec<NodeId> = layers.iter().map(|l| l.id).collect();
let ch = d.children_mut(Some(parent))?;
let at = index.min(ch.len());
ch.splice(at..at, layers);
sel.prune(d);
Ok(new)
})?;
s.doc_mut()?.layer_rows = new.clone();
Ok(json!({ "ids": new.iter().map(|i| i.0).collect::<Vec<_>>() }))
}
fn reverse(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "layer.reverse";
let ids = rows_param(s, p, C)?;
let d = &s.doc()?.doc;
let ids = row_roots(d, &ids);
// Rows grouped by the layer or group holding them (None: top-level layers).
let mut by_parent: Vec<(Option<NodeId>, Vec<usize>)> = vec![];
for id in &ids {
let Some((par, i, _)) = d.position(*id) else { continue };
match by_parent.iter_mut().find(|(p, _)| *p == par) {
Some((_, v)) => v.push(i),
None => by_parent.push((par, vec![i])),
}
}
by_parent.retain(|(_, v)| v.len() >= 2);
if by_parent.is_empty() {
return Err(bad(C, "highlight two or more rows in one layer or group"));
}
s.edit("Reverse Order", |d, _| {
for (par, mut slots) in by_parent {
slots.sort_unstable();
let ch = d.children_mut(par)?;
let nodes: Vec<Arc<Node>> = slots.iter().filter_map(|i| ch.get(*i).cloned()).collect();
for (slot, n) in slots.iter().zip(nodes.into_iter().rev()) {
if let Some(c) = ch.get_mut(*slot) {
*c = n;
}
}
}
Ok(())
})?;
ok()
}
fn locate(s: &mut Session, p: &Value) -> Result<Value> {
let st = s.doc_mut()?;
let id = id_param(p, "id").or_else(|| st.selection.objects.last().copied()).ok_or_else(|| bad("layer.locate", "nothing selected"))?;
let ancestry = st.doc.ancestry(id).ok_or(EngineError::NoNode(id))?;
st.layer_rows = vec![id];
st.revision += 1;
Ok(json!({ "id": id.0, "ancestry": ancestry.iter().map(|i| i.0).collect::<Vec<_>>() }))
}