mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 05:07:51 -06:00
Artboards: Duplicate Artboards, Copy and Cut take every selected artboard
With several artboards selected, the Artboards panel menu's Duplicate Artboards copied only the active one, and Copy and Cut with the Artboard tool took only the tool's active artboard. artboard.duplicate, artboard.copy and artboard.cut now take `indices`, and without `index` or `indices` they act on the artboards the Artboard tool has selected. Duplicate Artboards passes the selected artboards and selects the copies. Duplicate and Cut are one undo step each, and Paste adds every copied artboard back, with its art, in one undo step. The copies keep their places relative to each other, together right of the last artboard. A cut fails when it names every artboard. Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -80,22 +80,22 @@ pub struct Clipboard {
|
||||
pub para_styles: Vec<TextStyleDef>,
|
||||
/// Brushes the objects' strokes use.
|
||||
pub brushes: Vec<Brush>,
|
||||
/// The artboard the objects were copied from (none when they lie on no artboard): Paste on All
|
||||
/// Artboards keeps their offset to it.
|
||||
/// The artboard the objects were copied from (none when they lie on no artboard; the first of
|
||||
/// [`Self::artboards`] when there are any): Paste on All Artboards keeps their offset to it.
|
||||
pub source_artboard: Option<Rect>,
|
||||
/// Per object: the name of the layer it was copied from (Paste Remembers Layers).
|
||||
pub source_layers: Vec<Option<String>>,
|
||||
/// The open document the objects came from ([`DocState::uid`]): pasting back into it uses its
|
||||
/// own resources as they are now.
|
||||
pub source_doc: Option<u64>,
|
||||
/// An artboard copied with the Artboard tool (`artboard.copy`); `nodes` are then its art. Pasting
|
||||
/// adds a copy of it with the art.
|
||||
pub artboard: Option<vectorcraft_doc::Artboard>,
|
||||
/// The artboards copied with the Artboard tool (`artboard.copy`); `nodes` are then their art.
|
||||
/// Pasting adds a copy of each with the art.
|
||||
pub artboards: Vec<vectorcraft_doc::Artboard>,
|
||||
}
|
||||
|
||||
impl Clipboard {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.nodes.is_empty() && self.artboard.is_none()
|
||||
self.nodes.is_empty() && self.artboards.is_empty()
|
||||
}
|
||||
|
||||
/// Objects `roots` of `st`'s document with their resources, source artboard and layers.
|
||||
|
||||
@@ -83,7 +83,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Paste",
|
||||
["Edit"],
|
||||
Some("Cmd+V"),
|
||||
"{center?: [x, y], dx?, dy?, swatchConflict?} paste centred on `center` (the app passes the view centre), else offset by dx/dy (default: the Paste Offset preference). Pasting brings the image blobs, symbols, patterns, global and spot swatches (with the tint swatches of the tints used), gradient swatches, graphic styles, character and paragraph styles and brushes the objects use; one of the same name that differs comes in renamed. swatchConflict, for a swatch whose name the document gives another colour (clipboard.conflicts): \"merge\" (default: the objects take the document's swatch) | \"add\" (the pasted swatch comes in renamed) | {name: \"merge\"|\"add\"}. With Paste Remembers Layers on (layer.pasteRemembersLayers), objects go back into the layers they came from (by name; made when missing). An artboard copied with artboard.copy comes with its art, right of the last artboard (the other paste commands but Paste on All Artboards: where it was), its art back in the layers it came from → {ids, artboard: the new artboard's index or null, added: resources added, merged: conflicts merged, renamed: [{kind, from, to}]}",
|
||||
"{center?: [x, y], dx?, dy?, swatchConflict?} paste centred on `center` (the app passes the view centre), else offset by dx/dy (default: the Paste Offset preference). Pasting brings the image blobs, symbols, patterns, global and spot swatches (with the tint swatches of the tints used), gradient swatches, graphic styles, character and paragraph styles and brushes the objects use; one of the same name that differs comes in renamed. swatchConflict, for a swatch whose name the document gives another colour (clipboard.conflicts): \"merge\" (default: the objects take the document's swatch) | \"add\" (the pasted swatch comes in renamed) | {name: \"merge\"|\"add\"}. With Paste Remembers Layers on (layer.pasteRemembersLayers), objects go back into the layers they came from (by name; made when missing). Artboards copied with artboard.copy come with their art, together right of the last artboard as they lay (the other paste commands but Paste on All Artboards: where they were), their art back in the layers it came from, and the Artboard tool, when chosen, selects them → {ids, artboard: the first new artboard's index or null, artboards: the new artboards' indices, added: resources added, merged: conflicts merged, renamed: [{kind, from, to}]}",
|
||||
has_clipboard,
|
||||
|s, p| paste(s, p, PasteMode::Offset)
|
||||
),
|
||||
@@ -515,16 +515,19 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho
|
||||
let st = s.doc()?;
|
||||
let same_doc = clip.source_doc == Some(st.uid);
|
||||
let parent = st.target_parent()?;
|
||||
// A copied artboard comes back with its art: right of the last artboard, or where it was
|
||||
// (Paste in Place, in Front, in Back). Paste on All Artboards pastes only the art.
|
||||
let board = clip
|
||||
.artboard
|
||||
.as_ref()
|
||||
// Copied artboards come back with their art: together right of the last artboard, as they lay,
|
||||
// or where they were (Paste in Place, in Front, in Back). Paste on All Artboards pastes only the
|
||||
// art.
|
||||
let boards = clip
|
||||
.artboards
|
||||
.iter()
|
||||
.map(|a| a.rect)
|
||||
.reduce(|a, b| a.union(b))
|
||||
.filter(|_| mode != PasteMode::AllArtboards)
|
||||
.map(|a| (a, if mode == PasteMode::Offset { super::panelcmds::beside_artboards(&st.doc, a.rect) } else { Vec2::ZERO }));
|
||||
.map(|all| (&clip.artboards, if mode == PasteMode::Offset { super::panelcmds::beside_artboards(&st.doc, all) } else { Vec2::ZERO }));
|
||||
// Paste Remembers Layers (not while isolating a group: pastes stay in it); an artboard's art
|
||||
// always goes back into its layers.
|
||||
let remember = (st.doc.paste_remembers_layers || board.is_some()) && parent.is_some_and(|p| st.doc.node(p).is_some_and(Node::is_layer));
|
||||
let remember = (st.doc.paste_remembers_layers || boards.is_some()) && parent.is_some_and(|p| st.doc.node(p).is_some_and(Node::is_layer));
|
||||
// Front/back: relative to the selection (top-most / bottom-most selected object); with
|
||||
// nothing selected, the top / bottom of the current layer.
|
||||
let anchor = match mode {
|
||||
@@ -535,7 +538,7 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let placements: Vec<Affine> = match (mode, board) {
|
||||
let placements: Vec<Affine> = match (mode, boards) {
|
||||
(_, Some((_, dv))) => vec![Affine::translate(dv)],
|
||||
(PasteMode::Offset | PasteMode::Library, None) => vec![match point_param(p, "center") {
|
||||
Some(c) => clip.bounds().map_or(Affine::IDENTITY, |b| Affine::translate(c - b.center())),
|
||||
@@ -548,9 +551,13 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho
|
||||
// In place, in front, in back: onto the active artboard when the UI names it.
|
||||
_ => vec![onto_artboard(&st.doc, p, clip)],
|
||||
};
|
||||
let (ids, imported, artboard) = s.edit(mode.label(), |d, sel| {
|
||||
let (ids, imported, artboards) = s.edit(mode.label(), |d, sel| {
|
||||
let imported = clip.import_into(d, choices, same_doc);
|
||||
let artboard = board.map(|(a, dv)| super::panelcmds::push_artboard_copy(d, a, a.rect + dv));
|
||||
let artboards: Vec<usize> = boards
|
||||
.into_iter()
|
||||
.flat_map(|(all, dv)| all.iter().map(move |a| (a, dv)))
|
||||
.map(|(a, dv)| super::panelcmds::push_artboard_copy(d, a, a.rect + dv))
|
||||
.collect();
|
||||
let mut layers: BTreeMap<&str, NodeId> = BTreeMap::new();
|
||||
// Objects pasted into each parent so far (keeps their order in front and back pastes).
|
||||
let mut placed: BTreeMap<Option<NodeId>, usize> = BTreeMap::new();
|
||||
@@ -585,15 +592,16 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho
|
||||
// Pasted type arrives without the layout bounds cache: fill it so boxes and alignment are
|
||||
// right away (opening the file later would, but paste must not depend on a reopen).
|
||||
super::typecmd::refresh_bounds_of(d, &new_ids);
|
||||
Ok((new_ids, imported, artboard))
|
||||
Ok((new_ids, imported, artboards))
|
||||
})?;
|
||||
// The Artboard tool takes the pasted artboard.
|
||||
if let Some(i) = artboard.filter(|_| s.tool_id() == "artboard") {
|
||||
s.set_tool_option("active", &json!(i));
|
||||
// The Artboard tool selects the pasted artboards, the first one active.
|
||||
if !artboards.is_empty() && s.tool_id() == "artboard" {
|
||||
s.set_tool_option("selected", &json!(artboards));
|
||||
}
|
||||
Ok(json!({
|
||||
"ids": ids.iter().map(|i| i.0).collect::<Vec<_>>(),
|
||||
"artboard": artboard,
|
||||
"artboard": artboards.first(),
|
||||
"artboards": artboards,
|
||||
"added": imported.added,
|
||||
"merged": imported.merged,
|
||||
"renamed": imported.renamed,
|
||||
|
||||
@@ -16,7 +16,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Duplicate Artboard",
|
||||
["Window", "Artboards"],
|
||||
None,
|
||||
"{index?: n (default: the Artboard tool's artboard), art?: bool (default: the Artboard tool's moveArt option), patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)} copy placed right of the last artboard, with copies of the art fully inside it when `art` (locked and hidden art only with prefs moveLockedWithArtboard) → {index, ids: the art's copies}",
|
||||
"{index?: n | indices?: [n…] (default: the artboards the Artboard tool has selected), art?: bool (default: the Artboard tool's moveArt option), patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)} a copy of each artboard, the copies placed together right of the last artboard as they lay, with copies of the art fully inside each when `art` (locked and hidden art only with prefs moveLockedWithArtboard), in one undo step → {index: the first copy's index, indices: the copies' indices, ids: the art's copies}",
|
||||
has_doc,
|
||||
artboard_duplicate
|
||||
),
|
||||
@@ -25,7 +25,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Copy Artboard",
|
||||
[],
|
||||
None,
|
||||
"{index?, art?} (defaults as artboard.duplicate) put the artboard, and the art fully inside it when `art`, on the clipboard: edit.paste adds a copy right of the last artboard, edit.pasteInPlace/InFront/InBack where it was, in this or another document (one undo step; edit.pasteOnAllArtboards pastes only the art); edit.copy runs this while the Artboard tool is chosen → {copied: objects}",
|
||||
"{index? | indices?, art?} (defaults as artboard.duplicate) put the artboards, and the art fully inside each when `art`, on the clipboard: edit.paste adds a copy of each, together right of the last artboard as they lay, edit.pasteInPlace/InFront/InBack where they were, in this or another document (one undo step; edit.pasteOnAllArtboards pastes only the art); edit.copy runs this while the Artboard tool is chosen → {copied: objects}",
|
||||
has_doc,
|
||||
|s, p| artboard_copy(s, p, false)
|
||||
),
|
||||
@@ -34,7 +34,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Cut Artboard",
|
||||
[],
|
||||
None,
|
||||
"{index?, art?} artboard.copy, then delete the artboard and that art in one undo step (not the only artboard); edit.cut runs this while the Artboard tool is chosen → {copied: objects}",
|
||||
"{index? | indices?, art?} artboard.copy, then delete the artboards and that art in one undo step (one artboard is always left: the cut fails when it names every artboard); edit.cut runs this while the Artboard tool is chosen → {copied: objects}",
|
||||
has_doc,
|
||||
|s, p| artboard_copy(s, p, true)
|
||||
),
|
||||
@@ -68,16 +68,28 @@ fn artboard_reorder(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
ok()
|
||||
}
|
||||
|
||||
/// The artboard (`index`) and whether its art comes along (`art`) for the artboard copy
|
||||
/// commands: by default the Artboard tool's artboard and its Move/Copy Artwork with Artboard option.
|
||||
/// An `index` that names no artboard is an error.
|
||||
fn board_and_art(s: &Session, p: &Value, cmd: &str) -> Result<(usize, bool)> {
|
||||
/// The artboards (`index` or `indices`) and whether their art comes along (`art`) for the artboard
|
||||
/// copy commands: by default the artboards the Artboard tool has selected (its active one when none
|
||||
/// of them is in the document) and its Move/Copy Artwork with Artboard option.
|
||||
fn boards_and_art(s: &Session, p: &Value, cmd: &str) -> Result<(Vec<usize>, bool)> {
|
||||
let t = s.tool_options_of("artboard");
|
||||
let i = match p.get("index") {
|
||||
Some(_) => artboard_index(&s.doc()?.doc, p, cmd)?,
|
||||
None => t["active"].as_u64().unwrap_or(0) as usize,
|
||||
let d = &s.doc()?.doc;
|
||||
let indices = if p.get("index").is_some() || p.get("indices").is_some() {
|
||||
artboard_indices(d, p, cmd)?
|
||||
} else {
|
||||
let mut picked: Vec<u64> = t["selected"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(Value::as_u64)
|
||||
.filter(|i| usize::try_from(*i).is_ok_and(|i| i < d.artboards.len()))
|
||||
.collect();
|
||||
if picked.is_empty() {
|
||||
picked.push(t["active"].as_u64().unwrap_or(0));
|
||||
}
|
||||
artboard_indices(d, &json!({ "indices": picked }), cmd)?
|
||||
};
|
||||
Ok((i, bool_or(p, "art", t["moveArt"].as_bool().unwrap_or(true))))
|
||||
Ok((indices, bool_or(p, "art", t["moveArt"].as_bool().unwrap_or(true))))
|
||||
}
|
||||
|
||||
/// The one artboard `cmd` acts on: `index`, an artboard of `d`, with the errors of
|
||||
@@ -87,8 +99,6 @@ pub(crate) fn artboard_index(d: &vectorcraft_doc::Document, p: &Value, cmd: &str
|
||||
artboard_indices(d, &json!({ "index": i }), cmd)?.into_iter().next().ok_or_else(|| EngineError::Other("no such artboard".into()))
|
||||
}
|
||||
|
||||
/// The art that goes with artboard `i` when `art`: the objects fully inside it (locked and hidden
|
||||
/// ones only with prefs moveLockedWithArtboard), in paint order.
|
||||
/// The artboards `cmd` acts on: `indices` (several), else `index` (one), each an artboard of `d`,
|
||||
/// in order and once each.
|
||||
pub(crate) fn artboard_indices(d: &vectorcraft_doc::Document, p: &Value, cmd: &str) -> Result<Vec<usize>> {
|
||||
@@ -104,55 +114,87 @@ pub(crate) fn artboard_indices(d: &vectorcraft_doc::Document, p: &Value, cmd: &s
|
||||
Ok(indices)
|
||||
}
|
||||
|
||||
/// The art that goes with artboard `i` when `art`: the objects fully inside it (locked and hidden
|
||||
/// ones only with prefs moveLockedWithArtboard), in paint order.
|
||||
pub(crate) fn artboard_art(s: &Session, i: usize, art: bool) -> Result<Vec<NodeId>> {
|
||||
let d = &s.doc()?.doc;
|
||||
let rect = d.artboards.get(i).map(|a| a.rect).ok_or_else(|| EngineError::Other("no such artboard".into()))?;
|
||||
Ok(if art { d.art_on_artboard(rect, s.prefs.move_locked_with_artboard) } else { vec![] })
|
||||
}
|
||||
|
||||
/// Each of `indices` with the art that goes with it when `art` ([`artboard_art`]), an object
|
||||
/// counted once, with the first of them it is on (artboards can overlap).
|
||||
pub(crate) fn artboards_art(s: &Session, indices: &[usize], art: bool) -> Result<Vec<(usize, Vec<NodeId>)>> {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut arts = vec![];
|
||||
for &i in indices {
|
||||
let art = artboard_art(s, i, art)?.into_iter().filter(|id| seen.insert(*id)).collect();
|
||||
arts.push((i, art));
|
||||
}
|
||||
Ok(arts)
|
||||
}
|
||||
|
||||
/// How far a copy of `rect` moves to sit right of the last artboard (Duplicate Artboards, Paste).
|
||||
pub(crate) fn beside_artboards(d: &vectorcraft_doc::Document, rect: Rect) -> Vec2 {
|
||||
let right = d.artboards.iter().map(|a| a.rect.x1).fold(f64::MIN, f64::max);
|
||||
Vec2::new(right + 20.0 - rect.x0, 0.0)
|
||||
}
|
||||
|
||||
/// Duplicate Artboards: a copy of each artboard with its art, in one undo step. The copies keep
|
||||
/// their places relative to each other, together right of the last artboard.
|
||||
fn artboard_duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (i, art) = board_and_art(s, p, "artboard.duplicate")?;
|
||||
let art = artboard_art(s, i, art)?;
|
||||
let sc = Scaling { patterns: super::object::transform_patterns(s, p, &art)?, ..Scaling::default() };
|
||||
let (indices, art) = boards_and_art(s, p, "artboard.duplicate")?;
|
||||
let arts = artboards_art(s, &indices, art)?;
|
||||
let all: Vec<NodeId> = arts.iter().flat_map(|(_, a)| a.iter().copied()).collect();
|
||||
let sc = Scaling { patterns: super::object::transform_patterns(s, p, &all)?, ..Scaling::default() };
|
||||
let (index, ids) = s.edit("Duplicate Artboard", |d, _| {
|
||||
let dv = d.artboards.get(i).map(|a| beside_artboards(d, a.rect)).unwrap_or_default();
|
||||
copy_artboard(d, i, dv, &art, sc)
|
||||
let dv = arts
|
||||
.iter()
|
||||
.filter_map(|(i, _)| d.artboards.get(*i))
|
||||
.map(|a| a.rect)
|
||||
.reduce(|a, b| a.union(b))
|
||||
.map(|r| beside_artboards(d, r))
|
||||
.unwrap_or_default();
|
||||
let (mut index, mut ids) = (vec![], vec![]);
|
||||
for (i, art) in &arts {
|
||||
let (at, copies) = copy_artboard(d, *i, dv, art, sc)?;
|
||||
index.push(at);
|
||||
ids.extend(copies);
|
||||
}
|
||||
Ok((index, ids))
|
||||
})?;
|
||||
Ok(json!({"index": index, "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>()}))
|
||||
Ok(json!({"index": index.first(), "indices": index, "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>()}))
|
||||
}
|
||||
|
||||
/// Artboard Copy / Cut: the artboard and its art onto the clipboard (pasting brings both); a cut
|
||||
/// then deletes them.
|
||||
/// Artboard Copy / Cut: the artboards and their art onto the clipboard (pasting brings them all);
|
||||
/// a cut then deletes them.
|
||||
pub(crate) fn artboard_copy(s: &mut Session, p: &Value, cut: bool) -> Result<Value> {
|
||||
let (i, art) = board_and_art(s, p, if cut { "artboard.cut" } else { "artboard.copy" })?;
|
||||
let art = artboard_art(s, i, art)?;
|
||||
let (indices, art) = boards_and_art(s, p, if cut { "artboard.cut" } else { "artboard.copy" })?;
|
||||
let arts = artboards_art(s, &indices, art)?;
|
||||
let st = s.doc()?;
|
||||
if cut && st.doc.artboards.len() <= 1 {
|
||||
if cut && st.doc.artboards.len() <= indices.len() {
|
||||
return Err(EngineError::Other("a document needs at least one artboard".into()));
|
||||
}
|
||||
let board = st.doc.artboards.get(i).cloned().ok_or_else(|| EngineError::Other("no such artboard".into()))?;
|
||||
let clip = Clipboard { source_artboard: Some(board.rect), artboard: Some(board), ..Clipboard::copy(st, &art) };
|
||||
let art = st.doc.paint_order(arts.into_iter().flat_map(|(_, a)| a));
|
||||
let boards: Vec<_> = indices.iter().filter_map(|i| st.doc.artboards.get(*i).cloned()).collect();
|
||||
let clip = Clipboard { source_artboard: boards.first().map(|a| a.rect), artboards: boards, ..Clipboard::copy(st, &art) };
|
||||
let copied = clip.nodes.len();
|
||||
if cut {
|
||||
s.edit("Cut Artboard", |d, sel| {
|
||||
if i >= d.artboards.len() {
|
||||
if indices.iter().any(|i| *i >= d.artboards.len()) {
|
||||
return Err(EngineError::Other("no such artboard".into()));
|
||||
}
|
||||
// Its guides go with it.
|
||||
let gone = d.artboards.remove(i).id;
|
||||
d.retain_guides(sel, |_, g| g.artboard != Some(gone));
|
||||
// The last first, so the others keep their places; their guides go with them.
|
||||
for &i in indices.iter().rev() {
|
||||
let gone = d.artboards.remove(i).id;
|
||||
d.retain_guides(sel, |_, g| g.artboard != Some(gone));
|
||||
}
|
||||
d.remove_many(&art)?;
|
||||
sel.prune(d);
|
||||
Ok(())
|
||||
})?;
|
||||
if s.tool_id() == "artboard" {
|
||||
s.set_tool_option("active", &json!(i.saturating_sub(1)));
|
||||
s.set_tool_option("active", &json!(indices.first().map_or(0, |i| i.saturating_sub(1))));
|
||||
}
|
||||
}
|
||||
s.clipboard = clip;
|
||||
|
||||
Reference in New Issue
Block a user