use super::align::choice; use super::{Document, EngineError, Node, NodeId, NodeKind, Result, Session, Value, bad, id_param, json, ok, selected_roots}; pub(super) enum Arrange { Front, Forward, Backward, Back, } pub(super) fn arrange(s: &mut Session, how: Arrange) -> Result { let mut ids = selected_roots(s)?; if matches!(how, Arrange::Forward | Arrange::Front) { ids.reverse(); } let label = match how { Arrange::Front => "Bring to Front", Arrange::Forward => "Bring Forward", Arrange::Backward => "Send Backward", Arrange::Back => "Send to Back", }; // When nothing changes order, the document and its undo history stay as they are. crate::cmd::overprint::edit_counted(s, label, |d| { let mut moved = 0; // Arrange within each parent independently (selections may span layers/groups). let mut by_parent: Vec<(Option, Vec)> = vec![]; for id in &ids { let par = d.parent_of(*id); match by_parent.iter_mut().find(|(p, _)| *p == par) { Some((_, v)) => v.push(*id), None => by_parent.push((par, vec![*id])), } } for (par, mut group) in by_parent { // In a clip group or a clipping layer the bottom child is the clipping path: it stays // there, and the art above it never goes below it (#3188). let floor = usize::from(par.and_then(|p| d.node(p)).is_some_and(vectorcraft_doc::Node::clips)); if floor > 0 { group.retain(|id| d.position(*id).is_some_and(|(_, idx, _)| idx >= floor)); } for (k, id) in group.iter().enumerate() { let Some((_, idx, len)) = d.position(*id) else { continue }; if len <= floor { continue; } let last = len - 1; let to = match how { Arrange::Front => last.saturating_sub(k).max(floor), Arrange::Back => (floor + k).min(last), Arrange::Forward => (idx + 1).min(last.saturating_sub(k)).max(idx), Arrange::Backward => idx.saturating_sub(1).max(floor + k.min(idx.saturating_sub(floor))), }; if to != idx { d.move_node(*id, par, to)?; moved += 1; } } } Ok(moved) })?; ok() } pub(super) fn send_to_current_layer(s: &mut Session, _: &Value) -> Result { let ids = selected_roots(s)?; // Ids are reused after undo: the remembered current layer must still be a layer. let st = s.doc()?; let layer = st.current_layer().or_else(|| st.doc.default_layer()).ok_or_else(|| EngineError::Other("no current layer".into()))?; // When the current layer is hidden or locked, or is inside a hidden or locked layer, the // command fails with the error that `DocState::target_parent` gives for new art. if !st.doc.is_editable(layer) { return Err(EngineError::Other("the target layer is locked or hidden".into())); } // When the selected objects are already the topmost in the current layer, in paint order, the // document and its undo history stay as they are. if st.doc.children(Some(layer)).is_some_and(|c| c.iter().rev().map(|n| n.id).take(ids.len()).eq(ids.iter().rev().copied())) { return ok(); } s.edit("Send to Current Layer", |d, _| { let moved = d.remove_many(&ids)?; d.children_mut(Some(layer))?.extend(moved); Ok(()) })?; ok() } pub(super) fn group(s: &mut Session, _: &Value) -> Result { let ids = selected_roots(s)?; let gid = s.edit("Group", |d, sel| { let gid = group_nodes(d, &ids)?; sel.set([gid]); Ok(gid) })?; Ok(json!({ "id": gid.0 })) } /// 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 { if ids.is_empty() { return Err(EngineError::Other("nothing selected".into())); } let gid = d.alloc_id(); Ok(d.wrap(ids, |children| Node::group(gid, children))?) } pub(super) fn ungroup(s: &mut Session, _: &Value) -> Result { let ids = selected_roots(s)?; // With no group selected, the document, the selection and the undo history stay as they are. let d = &s.doc()?.doc; if !ids.iter().any(|id| d.node(*id).is_some_and(|n| matches!(n.kind, NodeKind::Group { .. }))) { return ok(); } 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 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 && k == 0 && let NodeKind::Path { clipping, .. } = &mut c.kind { *clipping = false; } // Each member keeps its own opacity; the group's opacity goes with the group. new_sel.push(c.id); out.push(c); } ungrouped.push((*id, out)); } d.insert_beside(ungrouped, true)?; sel.set(new_sel); Ok(()) })?; ok() } pub(super) fn lock(s: &mut Session, _: &Value) -> Result { let ids = selected_lock_hide_roots(s)?; // With every selected object already locked, the document and its undo history stay as they // are, and the objects are deselected. let d = &s.doc()?.doc; if ids.iter().all(|id| d.node(*id).is_none_or(|n| n.locked)) { s.select(|_, sel| sel.clear())?; return ok(); } s.edit("Lock", |d, sel| { for id in &ids { if let Some(n) = d.node_mut(*id) { n.locked = true; } } sel.clear(); Ok(()) })?; ok() } /// The selected objects ([`selected_roots`]) that Lock › Selection and Hide › Selection change: in /// isolation mode, only those inside the isolated object, where a path of the isolated compound /// path is an object of its own. fn selected_lock_hide_roots(s: &Session) -> Result> { match crate::cmd::ordinary_isolation(s.doc()?) { Some(root) => crate::cmd::edit::selected_roots_inside(s, root, false), None => selected_roots(s), } } fn collect_flag(d: &Document, f: impl Fn(&Node) -> bool) -> Vec { let mut v = vec![]; d.walk(|n| { if !n.is_layer() && f(n) { v.push(n.id) } }); v } /// The objects that `f` picks for `object.unlockAll` and `object.showAll` (`cmd`): in the whole /// document, inside the isolated object in isolation mode, or with `scope: "group"` inside the /// groups that hold the selected objects (in isolation mode, only the objects of those groups /// that are inside the isolated object). A selected group is the group that holds its own /// members, and the objects of a group inside another such group are taken once. With /// `scope: "group"` and no selected object in a group, it returns an error. fn collect_flag_in_scope(s: &Session, cmd: &str, p: &Value, f: impl Fn(&Node) -> bool) -> Result> { let st = s.doc()?; let d = &st.doc; if choice(p, cmd, "scope", &["document", "group"])? != Some("group") { if let Some(root) = crate::cmd::ordinary_isolation(st) { let mut ids = vec![]; if let Some(node) = d.node(root) { for child in node.children().into_iter().flatten() { child.walk(&mut |n| { if !n.is_layer() && f(n) { ids.push(n.id); } }); } } return Ok(ids); } return Ok(collect_flag(d, f)); } let is_group = |id: &NodeId| d.node(*id).is_some_and(|n| matches!(n.kind, NodeKind::Group { .. })); let mut groups: Vec = st.selection.objects.iter().filter_map(|id| d.ancestry(*id)?.into_iter().rev().find(is_group)).collect(); groups.sort(); groups.dedup(); if groups.is_empty() { return Err(EngineError::Other("nothing selected is in a group".into())); } let mut v = vec![]; for g in &groups { if d.ancestry(*g).is_some_and(|a| a.iter().any(|up| up != g && groups.contains(up))) { continue; } for c in d.node(*g).and_then(Node::children).into_iter().flatten() { c.walk(&mut |n| { if !n.is_layer() && f(n) { v.push(n.id) } }); } } // In isolation mode only the objects inside the isolated object are taken. if let Some(root) = crate::cmd::ordinary_isolation(st) { v.retain(|id| crate::cmd::inside_isolation(d, root, *id)); } Ok(v) } pub(super) fn unlock_all(s: &mut Session, p: &Value) -> Result { let ids = collect_flag_in_scope(s, "object.unlockAll", p, |n| n.locked)?; // With nothing locked, the document, the selection and the undo history stay as they are. if ids.is_empty() { return Ok(json!({ "count": 0 })); } s.edit("Unlock All", |d, sel| { for id in &ids { if let Some(n) = d.node_mut(*id) { n.locked = false; } } sel.set(ids.iter().copied()); Ok(()) })?; Ok(json!({ "count": ids.len() })) } pub(super) fn hide(s: &mut Session, _: &Value) -> Result { let ids = selected_lock_hide_roots(s)?; // With every selected object already hidden, the document and its undo history stay as they // are, and the objects are deselected. let d = &s.doc()?.doc; if ids.iter().all(|id| d.node(*id).is_none_or(|n| !n.visible)) { s.select(|_, sel| sel.clear())?; return ok(); } s.edit("Hide", |d, sel| { for id in &ids { if let Some(n) = d.node_mut(*id) { n.visible = false; } } sel.clear(); Ok(()) })?; ok() } pub(super) fn show_all(s: &mut Session, p: &Value) -> Result { let ids = collect_flag_in_scope(s, "object.showAll", p, |n| !n.visible)?; // With nothing hidden, the document, the selection and the undo history stay as they are. if ids.is_empty() { return Ok(json!({ "count": 0 })); } s.edit("Show All", |d, sel| { for id in &ids { if let Some(n) = d.node_mut(*id) { n.visible = true; } } sel.set(ids.iter().copied()); Ok(()) })?; Ok(json!({ "count": ids.len() })) } pub(super) fn isolate(s: &mut Session, p: &Value) -> Result { let id = id_param(p, "id") .or_else(|| s.active().and_then(|d| d.selection.objects.first().copied())) .ok_or_else(|| bad("object.isolate", "missing id"))?; let guides_locked = s.menu.guides_locked; let (isolation, prev) = { let st = s.doc_mut()?; // Groups, layers and single objects (a path, a compound path, an image…); type isn't // isolated: a double-click on it edits its text. if matches!(st.doc.node(id).ok_or(EngineError::NoNode(id))?.kind, NodeKind::Text(_)) { return Err(bad("object.isolate", "type objects can't be isolated")); } let isolation = st.doc.node(id).filter(|n| n.extras().shaper.is_some()).and_then(|n| n.children()).and_then(|c| c.first()).map_or(id, |n| n.id); // A hidden or locked object, one in a hidden or locked group or layer, and a guide path while // Lock Guides is on aren't isolated either: a click with the Selection tool doesn't select them. // A Shaper Group isolates its source group, so that group is the one checked. if !st.doc.is_editable(isolation) { return Err(bad("object.isolate", "the object is hidden or locked")); } if guides_locked && crate::is_guide_path(&st.doc, isolation) { return Err(EngineError::Other("guides are locked".into())); } (isolation, st.isolation) }; if let Some(prev) = prev && prev != isolation { crate::cmd::settle_interaction(s)?; let st = s.doc_mut()?; crate::cmd::distortcmds::finish_edit_text(st, prev); } let st = s.doc_mut()?; st.isolation = Some(isolation); st.selection.clear(); // New art on a locked artboard, now deselected, is protected. st.sync_new_art(); st.revision += 1; ok() } pub(super) fn exit_isolation(s: &mut Session, p: &Value) -> Result { // Opacity mask editing, pattern editing and symbol editing isolate their editing layer. Leaving // isolation mode then leaves that mode, as Stop Editing Opacity Mask and the isolation bar's // Done do. A group isolated inside the symbol's art goes back to the symbol's layer. let st = s.doc()?; if let Some(layer) = st.doc.symbol_edit.as_ref().map(|e| e.layer) && let Some(inner) = st.isolation.filter(|i| *i != layer) { crate::cmd::settle_interaction(s)?; let st = s.doc_mut()?; crate::cmd::distortcmds::finish_edit_text(st, inner); st.isolation = Some(layer); st.selection.set([inner]); st.revision += 1; return ok(); } let leave_mode: Option Result> = if st.doc.mask_edit.is_some() { Some(crate::cmd::maskedit::leave) } else if st.doc.pattern_edit.is_some() { Some(crate::cmd::patterncmds::pattern_done) } else if st.doc.symbol_edit.is_some() { Some(crate::cmd::symboledit::leave) } else { None }; if let Some(leave_mode) = leave_mode { if let Some(i) = st.isolation { crate::cmd::distortcmds::finish_edit_text(s.doc_mut()?, i); } leave_mode(s, p)?; return ok(); } let menu = s.menu.clone(); let prev = s.active().and_then(|d| d.isolation); if let Some(i) = prev { crate::cmd::settle_interaction(s)?; let st = s.doc_mut()?; st.isolation = None; crate::cmd::distortcmds::finish_edit_text(st, i); let target = st.doc.parent_of(i).filter(|p| st.doc.node(*p).is_some_and(|n| n.extras().shaper.is_some())).unwrap_or(i); st.selection.set([target]); // A guide path isolated while Lock Guides is on isn't selected (#1534). crate::deselect_hidden(&menu, &st.doc, &mut st.selection); // The art on a locked artboard can't be selected. st.protect_locked_artboards(&[], &Default::default(), None); } let st = s.doc_mut()?; st.revision += 1; ok() }