Files
vectorcraft/crates/engine/src/cmd/object/arrange.rs
T

386 lines
15 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Value> {
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<vectorcraft_doc::NodeId>, Vec<vectorcraft_doc::NodeId>)> = 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<Value> {
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<Value> {
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<NodeId> {
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<Value> {
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<Value> {
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<Vec<NodeId>> {
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<NodeId> {
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<Vec<NodeId>> {
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<NodeId> = 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<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
// 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<fn(&mut Session, &Value) -> Result<Value>> = 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()
}