//! 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::{make_layer, row_roots, rows_param}; use super::*; use crate::EngineError; pub fn specs() -> Vec { vec![ cmd!( "layer.merge", "Merge Selected", ["Window", "Layers"], None, "{ids?} (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. 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. 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?} (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?, 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?} hide every top-level layer but those holding the highlighted rows (else the current layer). One undo step → {count}", has_doc, |s, p| others(s, p, Flag::Visible, false) ), 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?} turn Preview off (outline view) for every top-level layer but those holding the highlighted rows (else the current layer) → {count}", has_doc, |s, p| others(s, p, Flag::Preview, false) ), 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?} lock every top-level layer but those holding the highlighted rows (else the current layer) → {count}", has_doc, |s, p| others(s, p, Flag::Locked, true) ), 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` to `v` on `ids` (layers that differ) in one undo step labelled `label` → how many /// changed. fn set_flag(s: &mut Session, ids: Vec, flag: Flag, v: bool, label: &str) -> Result { let d = &s.doc()?.doc; let ids: Vec = ids.into_iter().filter(|id| d.node(*id).is_some_and(|n| flag.get(n) != v)).collect(); if !ids.is_empty() { s.edit(label, |d, sel| { for id in &ids { if let Some(n) = d.node_mut(*id) { flag.set(n, v); } } // Hidden or locked art can't stay selected. let out: Vec = sel.objects.iter().copied().filter(|o| !d.is_editable(*o)).collect(); for o in out { sel.remove(o); } Ok(()) })?; } Ok(json!({ "count": ids.len() })) } /// The top-level layers holding the rows a command acts on. fn kept_layers(s: &Session, p: &Value) -> Result> { let ids = rows_param(s, p)?; let d = &s.doc()?.doc; Ok(ids.iter().filter_map(|id| d.layer_of(*id)).collect()) } fn others(s: &mut Session, p: &Value, flag: Flag, v: bool) -> Result { let keep = kept_layers(s, p)?; let ids: Vec = s.doc()?.doc.layers.iter().map(|l| l.id).filter(|l| !keep.contains(l)).collect(); set_flag(s, ids, flag, v, flag.label(v)) } fn all(s: &mut Session, flag: Flag, v: bool) -> Result { let mut ids = vec![]; s.doc()?.doc.walk(|n| { if n.is_layer() { ids.push(n.id); } }); set_flag(s, ids, flag, v, flag.label(v)) } fn template(s: &mut Session, p: &Value) -> Result { let ids = rows_param(s, p)?; let d = &s.doc()?.doc; let ids: Vec = 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::>(), "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 own: Vec = d.node(into).and_then(|n| n.children()).map(|c| c.iter().map(|c| c.id).filter(|c| !others.contains(c)).collect()).unwrap_or_default(); let mut moved = 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)); } else { moved.push(*id); } } // Where everything is now: the merged layer stacks its contents in this order. let keys: HashMap> = own.iter().chain(&moved).filter_map(|id| Some((*id, d.index_path(*id)?))).collect(); let mut taken = vec![]; for id in &moved { taken.push(d.remove(*id)?); } for id in others { if d.node(*id).is_some() { d.remove(*id)?; } } 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; Ok(()) } fn merge(s: &mut Session, p: &Value) -> Result { const C: &str = "layer.merge"; let ids = rows_param(s, p)?; let st = s.doc()?; let d = &st.doc; let mut seen = HashSet::new(); let ids: Vec = 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 = 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); 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 { const C: &str = "layer.flatten"; let st = s.doc()?; let d = &st.doc; let from = id_param(p, "id").or_else(|| 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 = d.layers.iter().filter(|l| l.id != into && l.visible && !l.is_template()).map(|l| l.id).collect(); let hidden: Vec = d.layers.iter().filter(|l| l.id != into && !l.visible && !l.is_template()).map(|l| l.id).collect(); s.edit("Flatten Artwork", |d, sel| { for h in &hidden { d.remove(*h)?; } merge_into(d, into, &others)?; sel.prune(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 { 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 = 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, mut 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 mut originals = vec![]; for o in &objects { originals.push(Arc::unwrap_or_clone(d.remove(*o)?)); } if group { d.remove(id)?; } let mut new = vec![]; let last = originals.len().saturating_sub(1); for k in 0..originals.len() { let mut layer = make_layer(d, None); let lid = layer.id; let held: Vec = 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 { d.reid(o) }).collect() } else { originals.get(k).cloned().into_iter().collect() }; if let Some(ch) = layer.children_mut() { ch.extend(held.into_iter().map(Arc::new)); } let at = if group { index } else { index.min(d.children(Some(parent)).map_or(0, Vec::len)) }; d.insert(Some(parent), at, layer)?; index = at.saturating_add(1); new.push(lid); } sel.prune(d); Ok(new) })?; s.doc_mut()?.layer_rows = new.clone(); Ok(json!({ "ids": new.iter().map(|i| i.0).collect::>() })) } fn reverse(s: &mut Session, p: &Value) -> Result { const C: &str = "layer.reverse"; let ids = rows_param(s, p)?; 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, Vec)> = 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> = 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 { 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::>() })) }