Merge pull request #5983 from dotCooCoo/feat/artboard-context-menu

Artboards: the Artboard tool's canvas menu cuts, copies, pastes, renames, rearranges and exports artboards
This commit is contained in:
BFlat
2026-11-07 09:56:22 +08:00
committed by GitHub
51 changed files with 1342 additions and 104 deletions
+19 -10
View File
@@ -157,7 +157,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). 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. In Draw Inside (view.drawMode) the objects go inside the object drawn into, as new art does, and Paste Remembers Layers doesn't apply; edit.pasteInPlace and edit.pasteOnAllArtboards do the same, and in Draw Behind they go behind the selection as edit.pasteInBack puts them; edit.pasteInFront and edit.pasteInBack ignore the drawing mode → {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}]}",
"{center?: [x, y], artboardsAt?: [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, or centered on `artboardsAt` (the Artboard tool's canvas menu passes the right-click point; a value that is not [x, y] is an error) (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. In Draw Inside (view.drawMode) the objects go inside the object drawn into, as new art does, and Paste Remembers Layers doesn't apply; edit.pasteInPlace and edit.pasteOnAllArtboards do the same, and in Draw Behind they go behind the selection as edit.pasteInBack puts them; edit.pasteInFront and edit.pasteInBack ignore the drawing mode → {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)
),
@@ -962,19 +962,28 @@ fn onto_artboard(doc: &Document, p: &Value, clip: &Clipboard) -> Affine {
/// Insert clipboard resources atomically in the active editable context using the requested placement mode.
fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, choices: &SwatchChoices) -> Result<Value> {
let off = s.prefs.paste_offset;
let boards_at = match p.get("artboardsAt") {
Some(v) if mode == PasteMode::Offset => Some(
point_param(p, "artboardsAt")
.filter(|c| v.as_array().is_some_and(|a| a.len() == 2) && c.x.is_finite() && c.y.is_finite())
.ok_or_else(|| bad("edit.paste", "`artboardsAt` must be [x, y]"))?,
),
_ => None,
};
let st = s.doc()?;
let same_doc = clip.source_doc == Some(st.uid);
let parent = st.target_parent_for_path()?;
// 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(|all| (&clip.artboards, if mode == PasteMode::Offset { super::panelcmds::beside_artboards(&st.doc, all) } else { Vec2::ZERO }));
// or centered on `artboardsAt`, 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(|all| {
let dv = match (mode, boards_at) {
(PasteMode::Offset, Some(c)) => c - all.center(),
(PasteMode::Offset, None) => super::panelcmds::beside_artboards(&st.doc, all),
_ => Vec2::ZERO,
};
(&clip.artboards, dv)
});
// Drawing modes (#3705): Draw Inside puts what Paste, Paste in Place and Paste on All
// Artboards bring inside the object it draws into; Draw Behind puts what Paste in Place and
// Paste on All Artboards bring behind the selection, as Paste in Back does. Paste in Front and
+6 -1
View File
@@ -164,7 +164,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Fit to Artwork Bounds",
["Object", "Artboards"],
None,
"{index?: n | indices?: [n…] (0-based; the app passes the selected artboards: the Artboard tool's, else the active one; default: the first)} each artboard fits the art on it that shows (with its strokes and effects; hidden objects and layers, guides and template layers don't count), in one undo step: an object counts for every artboard its bounds overlap, and a group or clipping layer counts as one object; an artboard with no art, or a locked one, stays as it is, and the command fails when every artboard it names is locked or none has art",
"{index?: n | indices?: [n…] (0-based; the app passes the selected artboards: the Artboard tool's, else the active one; default: the first)} each artboard fits the art on it that shows (with its strokes and effects; hidden objects and layers, guides and template layers don't count), in one undo step (none when every artboard already fits): an object counts for every artboard its bounds overlap, and a group or clipping layer counts as one object; an artboard with no art, or a locked one, stays as it is, and the command fails when every artboard it names is locked or none has art",
has_doc,
artboard_fit_art
),
@@ -1151,6 +1151,11 @@ fn artboard_fit_art(s: &mut Session, p: &Value) -> Result<Value> {
if fits.is_empty() {
return Err(EngineError::Other("no artwork".into()));
}
// An artboard that already fits its art changes nothing, and no undo step is recorded for it.
fits.retain(|(_, was, now)| [now.x0 - was.x0, now.y0 - was.y0, now.x1 - was.x1, now.y1 - was.y1].iter().any(|d| d.abs() > 1e-6));
if fits.is_empty() {
return ok();
}
s.edit("Artboard Options", |d, _| {
for &(i, was, now) in &fits {
let a = d.artboards.get_mut(i).ok_or_else(|| EngineError::Other("no such artboard".into()))?;
+141 -21
View File
@@ -16,7 +16,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Duplicate Artboard",
["Window", "Artboards"],
None,
"{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}",
"{index?: n | indices?: [n…] (default: the artboards the Artboard tool has selected), selectedArtboards?: [n…] (the journal records that default here, for a replay), 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,18 +25,42 @@ pub fn specs() -> Vec<CommandSpec> {
"Copy Artboard",
[],
None,
"{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}",
"{index? | indices?, selectedArtboards?, art?} (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)
|s, p| {
boards_and_art_noted(s, p, "artboard.copy")?;
artboard_copy(s, p, false)
}
),
cmd!(
"artboard.cut",
"Cut Artboard",
[],
None,
"{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, or a locked one; art also on another locked artboard stays, and is left off the clipboard, unless prefs moveLockedWithArtboard); edit.cut runs this while the Artboard tool is chosen → {copied: objects}",
"{index? | indices?, selectedArtboards?, 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, or a locked one; art also on another locked artboard stays, and is left off the clipboard, unless prefs moveLockedWithArtboard); edit.cut runs this while the Artboard tool is chosen → {copied: objects}",
has_doc,
|s, p| artboard_copy(s, p, true)
|s, p| {
boards_and_art_noted(s, p, "artboard.cut")?;
artboard_copy(s, p, true)
}
),
cmd!(
"artboard.rename",
"Rename Artboards",
[],
None,
"{index? | indices?, selectedArtboards?: [n…] (as artboard.duplicate), name} (default: the artboards the Artboard tool has selected, else its active one) rename the artboards in one undo step: one takes `name` (trimmed), and several take it numbered in artboard order, `name-1`, `name-2` and so on; a locked artboard is renamed too. An empty name is an error, and when every artboard already has its new name nothing changes and no undo step is recorded → {renamed: [index…]}",
has_doc,
artboard_rename
),
cmd!(
"artboard.switchOrientation",
"Switch Orientation",
[],
None,
"{index? | indices?, selectedArtboards?: [n…] (as artboard.duplicate), scaleArt?: bool (default: the Artboard tool's Scale Artwork with Artboard option), patterns?, lockedAndHidden?: bool (as artboard.setProps)} (default: the artboards the Artboard tool has selected, else its active one) each artboard swaps its width and height and keeps its top-left corner, in one undo step; a square artboard and a locked one stay as they are, the call fails with `the artboard is locked` when every artboard it names is locked, and when nothing else changes no undo step is recorded → {switched: [index…]}",
has_doc,
artboard_switch_orientation
),
cmd!(
"text.setFormat",
@@ -82,24 +106,120 @@ fn artboard_reorder(s: &mut Session, p: &Value) -> Result<Value> {
/// 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)> {
check_switches(p, &["art"], cmd)?;
let indices = named_or_selected(s, p, cmd)?;
Ok((indices, bool_or(p, "art", s.tool_options_of("artboard")["moveArt"].as_bool().unwrap_or(true))))
}
/// [`boards_and_art`], with both values added to the running command's journal entry, so a replay
/// acts on the same artboards and art whatever the Artboard tool's state is then. `edit.cut` and
/// `edit.copy` call [`artboard_copy`] without this and take no such params.
fn boards_and_art_noted(s: &mut Session, p: &Value, cmd: &str) -> Result<(Vec<usize>, bool)> {
let (indices, art) = boards_and_art(s, p, cmd)?;
note_artboards(s, p, &indices);
s.note_journal("art", json!(art));
Ok((indices, art))
}
/// Add `selectedArtboards` to the running command's journal entry when `p` names no artboard, so a
/// replay acts on the artboards the Artboard tool had selected. An action leaves it out
/// (`REPLAY_ONLY`) and keeps an `index` or `indices` the call gave.
fn note_artboards(s: &mut Session, p: &Value, indices: &[usize]) {
if p.get("index").is_none() && p.get("indices").is_none() {
s.note_journal("selectedArtboards", json!(indices));
}
}
/// The artboards `cmd` acts on: `index` or `indices`, else `selectedArtboards` (a replay of the
/// journal), else those the Artboard tool has selected, else its active one.
fn named_or_selected(s: &Session, p: &Value, cmd: &str) -> Result<Vec<usize>> {
let t = s.tool_options_of("artboard");
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)?
if p.get("index").is_some() || p.get("indices").is_some() {
return artboard_indices(d, p, cmd);
}
if let Some(recorded) = p.get("selectedArtboards") {
return artboard_indices(d, &json!({ "indices": recorded }), cmd);
}
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)
}
/// Rename Artboards: one artboard takes the name, and several take it numbered in artboard order, in
/// one undo step. Nothing runs when each already has its new name.
fn artboard_rename(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "artboard.rename";
let name = match p.get("name") {
Some(Value::String(n)) if !n.trim().is_empty() => n.trim(),
Some(Value::String(_)) => return Err(bad(C, "`name` must not be empty")),
_ => return Err(bad(C, "`name` must be a string")),
};
Ok((indices, bool_or(p, "art", t["moveArt"].as_bool().unwrap_or(true))))
let indices = named_or_selected(s, p, C)?;
note_artboards(s, p, &indices);
let d = &s.doc()?.doc;
let several = indices.len() > 1;
let entries: Vec<(usize, String)> = indices
.iter()
.enumerate()
.map(|(k, i)| (*i, if several { format!("{name}-{}", k + 1) } else { name.to_string() }))
.filter(|(i, new)| d.artboards.get(*i).is_some_and(|a| a.name != *new))
.collect();
let renamed: Vec<usize> = entries.iter().map(|(i, _)| *i).collect();
if !entries.is_empty() {
let list: Vec<Value> = entries.iter().map(|(i, new)| json!({"index": i, "name": new})).collect();
s.execute("artboard.setProps", &json!({ "artboards": list }))?;
}
Ok(json!({ "renamed": renamed }))
}
/// Switch Orientation: each artboard that is neither square nor locked swaps its width and height
/// and keeps its top-left corner, in one undo step. Nothing runs when none of them changes.
fn artboard_switch_orientation(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "artboard.switchOrientation";
// Swapping width and height keeps the area, so `artboard.setProps` scales no strokes or corners.
const FROM_PREFS: [&str; 2] = ["patterns", "lockedAndHidden"];
check_switches(p, &["scaleArt"], C)?;
check_switches(p, &FROM_PREFS, C)?;
let indices = named_or_selected(s, p, C)?;
note_artboards(s, p, &indices);
let d = &s.doc()?.doc;
if indices.iter().all(|i| d.artboards.get(*i).is_some_and(|a| a.locked)) {
refuse_locked(d, &indices)?;
}
let turned: Vec<(usize, Rect)> = indices
.iter()
.filter_map(|i| d.artboards.get(*i).map(|a| (*i, a)))
.filter(|(_, a)| !a.locked && (a.rect.width() - a.rect.height()).abs() > 1e-6)
.map(|(i, a)| (i, a.rect))
.collect();
let switched: Vec<usize> = turned.iter().map(|(i, _)| *i).collect();
if !turned.is_empty() {
let list: Vec<Value> = turned.iter().map(|(i, r)| json!({"index": i, "width": r.height(), "height": r.width()})).collect();
let scale = bool_or(p, "scaleArt", s.tool_options_of("artboard")["scaleArt"].as_bool().unwrap_or(false));
s.note_journal("scaleArt", json!(scale));
let mut set = json!({ "artboards": list, "scaleArt": scale });
for key in FROM_PREFS {
if let Some(v) = p.get(key) {
set[key] = v.clone();
}
}
// What the step takes from the preferences joins this command's journal entry.
let (_, noted) = s.execute_step("artboard.setProps", &set)?;
for key in FROM_PREFS {
if let Some(v) = noted.get(key) {
s.note_journal(key, v.clone());
}
}
}
Ok(json!({ "switched": switched }))
}
/// The one artboard `cmd` acts on: `index`, an artboard of `d`, with the errors of
@@ -170,7 +290,7 @@ pub(crate) fn beside_artboards(d: &vectorcraft_doc::Document, rect: Rect) -> Vec
/// their places relative to each other, together right of the last artboard.
fn artboard_duplicate(s: &mut Session, p: &Value) -> Result<Value> {
check_switches(p, &["patterns"], "artboard.duplicate")?;
let (indices, art) = boards_and_art(s, p, "artboard.duplicate")?;
let (indices, art) = boards_and_art_noted(s, p, "artboard.duplicate")?;
let arts = artboards_art(s, &indices, art, false)?;
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() };
+9 -3
View File
@@ -24,9 +24,10 @@ pub const FIT_ACTIVE_ARTBOARD: [&str; 2] = ["artboard.fitToArt", "artboard.fitTo
/// `(command, param)` pairs the journal records only so a replay reproduces the original run
/// (dates from the clock: `cmd::clock_date`; the artboards the app passes to the
/// [`ON_ACTIVE_ARTBOARD`] and [`FIT_ACTIVE_ARTBOARD`] commands and to a batch; the view's center
/// the app passes to Edit Symbol); an action leaves them out ([`Session::journal_for_action`]).
const REPLAY_ONLY: [(&str, &str); 16] = [
/// [`ON_ACTIVE_ARTBOARD`] and [`FIT_ACTIVE_ARTBOARD`] commands and to a batch; the artboards the
/// Artboard tool had selected for its commands; the view's center the app passes to Edit Symbol);
/// an action leaves them out ([`Session::journal_for_action`]).
const REPLAY_ONLY: [(&str, &str); 21] = [
("file.new", "created"),
("document.save", "modified"),
("file.saveAs", "modified"),
@@ -39,6 +40,11 @@ const REPLAY_ONLY: [(&str, &str); 16] = [
("artboard.fitToArt", "index"),
("artboard.fitToArt", "indices"),
("artboard.fitToSelection", "index"),
("artboard.duplicate", "selectedArtboards"),
("artboard.copy", "selectedArtboards"),
("artboard.cut", "selectedArtboards"),
("artboard.rename", "selectedArtboards"),
("artboard.switchOrientation", "selectedArtboards"),
("select.allOnArtboard", "artboard"),
("symbol.edit", "artboard"),
("symbol.edit", "viewCenter"),
+143
View File
@@ -247,6 +247,149 @@ fn a_batch_step_records_what_it_took_from_the_preferences() {
assert_eq!(doc(&r), doc(&s));
}
/// A document of three artboards with a rectangle on the second, the Artboard tool chosen and that
/// artboard selected → the journal's length, where the entries under test start.
fn second_artboard_selected(s: &mut Session) -> usize {
s.execute("file.new", &json!({"width": 400, "height": 200, "created": null})).unwrap();
s.execute("artboard.new", &json!({"x": 1000, "y": 0, "width": 300, "height": 100})).unwrap();
s.execute("artboard.new", &json!({"x": 2000, "y": 0, "width": 100, "height": 300})).unwrap();
s.execute("shape.rectangle", &json!({"x": 1010, "y": 10, "width": 100, "height": 50})).unwrap();
s.execute("stroke.set", &json!({"weight": 2})).unwrap();
s.execute("select.none", &json!({})).unwrap();
s.select_tool("artboard", crate::tooling::ViewInfo::default()).unwrap();
s.set_tool_option("selected", &json!([1]));
s.journal.len()
}
/// A new session's Artboard tool has nothing selected, so a replay of a command that took its
/// artboards from the selection acts on the artboards it recorded, and an action does not keep them.
#[test]
fn artboard_commands_record_the_selected_artboards_for_a_replay_and_not_for_an_action() {
for (id, params, recorded, action) in [
("artboard.switchOrientation", json!({}), json!({"selectedArtboards": [1], "scaleArt": false}), json!({"scaleArt": false})),
("artboard.rename", json!({"name": "Back"}), json!({"name": "Back", "selectedArtboards": [1]}), json!({"name": "Back"})),
("artboard.duplicate", json!({}), json!({"selectedArtboards": [1], "art": true}), json!({"art": true})),
("artboard.cut", json!({}), json!({"selectedArtboards": [1], "art": true}), json!({"art": true})),
("artboard.copy", json!({}), json!({"selectedArtboards": [1], "art": true}), json!({"art": true})),
] {
let mut s = Session::new();
let start = second_artboard_selected(&mut s);
s.execute(id, &params).unwrap();
assert_eq!(s.journal.last().unwrap(), &(id.to_string(), recorded), "{id}");
assert_eq!(s.journal_for_action(start), [(id.to_string(), action)], "{id}");
assert_eq!(documents(&replayed(&s)), documents(&s), "{id}");
}
}
/// A copy of the artboard the tool had selected replays as a copy of that artboard: what it pastes
/// is the same.
#[test]
fn a_replayed_artboard_copy_pastes_the_selected_artboard() {
let mut s = Session::new();
second_artboard_selected(&mut s);
s.execute("artboard.copy", &json!({})).unwrap();
s.execute("artboard.switchOrientation", &json!({"index": 0})).unwrap();
s.execute("edit.paste", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().doc.artboards.len(), 4);
assert_eq!(documents(&replayed(&s)), documents(&s));
}
/// The artboards a call names are recorded as given and stay in an action, and a batch step records
/// the selected ones in its own params.
#[test]
fn artboard_commands_keep_a_named_artboard_and_note_a_batch_steps_artboards() {
let mut s = Session::new();
let start = second_artboard_selected(&mut s);
s.execute("artboard.switchOrientation", &json!({"index": 0})).unwrap();
assert_eq!(s.journal.last().unwrap().1, json!({"index": 0, "scaleArt": false}));
s.execute("artboard.duplicate", &json!({"indices": [0]})).unwrap();
assert_eq!(s.journal.last().unwrap().1, json!({"indices": [0], "art": true}));
let steps = json!([{"command": "artboard.switchOrientation", "params": {}}]);
s.execute("command.batch", &json!({ "commands": steps })).unwrap();
assert_eq!(s.journal.last().unwrap().1["commands"][0]["params"], json!({"selectedArtboards": [1], "scaleArt": false}));
assert_eq!(documents(&replayed(&s)), documents(&s));
let action = s.journal_for_action(start);
assert_eq!(action[0].1, json!({"index": 0, "scaleArt": false}));
assert_eq!(action[1].1, json!({"indices": [0], "art": true}));
assert_eq!(action[2].1["commands"][0]["params"], json!({"scaleArt": false}));
}
/// A recorded `selectedArtboards` goes ahead of the tool's selection, gives way to a named artboard,
/// and is checked as `indices` is.
#[test]
fn artboard_commands_read_the_recorded_selected_artboards() {
let mut s = Session::new();
second_artboard_selected(&mut s);
let run = |s: &mut Session, p| s.execute("artboard.switchOrientation", &p).map(|r| r["switched"].clone());
assert_eq!(run(&mut s, json!({"selectedArtboards": [2]})).unwrap(), json!([2]));
assert_eq!(run(&mut s, json!({"selectedArtboards": [2], "index": 0})).unwrap(), json!([0]));
for bad in [json!([9]), json!([]), json!("1"), json!(null)] {
assert!(run(&mut s, json!({ "selectedArtboards": bad })).is_err(), "{bad}");
}
}
/// `edit.copy` and `edit.cut` run the Artboard tool's copy and take no artboards, so their entries
/// stay as given.
#[test]
fn edit_copy_and_cut_with_the_artboard_tool_record_their_params_as_given() {
for id in ["edit.copy", "edit.cut"] {
let mut s = Session::new();
second_artboard_selected(&mut s);
s.execute(id, &json!({})).unwrap();
assert_eq!(s.journal.last().unwrap(), &(id.to_string(), json!({})), "{id}");
}
}
/// `artboard.duplicate` and the cut and copy record the tool's Move/Copy Artwork option: replayed in
/// a new session, where the option is on, the art stays behind as it did.
#[test]
fn artboard_commands_record_the_tools_move_art_option() {
for id in ["artboard.duplicate", "artboard.cut", "artboard.copy"] {
let mut s = Session::new();
second_artboard_selected(&mut s);
s.set_tool_option("moveArt", &json!(false));
s.execute(id, &json!({})).unwrap();
assert_eq!(s.journal.last().unwrap().1["art"], json!(false), "{id}");
assert_eq!(documents(&replayed(&s)), documents(&s), "{id}");
}
}
/// `artboard.switchOrientation` records the tool's Scale Artwork option: replayed in a new session,
/// where it is off, the art scales as it did.
#[test]
fn artboard_switch_orientation_records_the_tools_scale_art_option() {
let mut s = Session::new();
second_artboard_selected(&mut s);
s.set_tool_option("scaleArt", &json!(true));
s.execute("artboard.switchOrientation", &json!({})).unwrap();
assert_eq!(s.journal.last().unwrap().1["scaleArt"], json!(true));
assert_eq!(documents(&replayed(&s)), documents(&s));
}
/// `artboard.switchOrientation` records whether locked and hidden art goes along: replayed in a new
/// session, where the preference is off, the locked art scales as it did.
#[test]
fn artboard_switch_orientation_records_whether_locked_art_goes_along() {
let mut s = Session::new();
second_artboard_selected(&mut s);
let r = s.execute("shape.rectangle", &json!({"x": 1150, "y": 10, "width": 100, "height": 50})).unwrap();
s.execute("layer.setProps", &json!({"ids": [r["id"]], "locked": true})).unwrap();
s.prefs.move_locked_with_artboard = true;
s.set_tool_option("scaleArt", &json!(true));
s.execute("artboard.switchOrientation", &json!({})).unwrap();
assert_eq!(s.journal.last().unwrap().1["lockedAndHidden"], json!(true));
let r = replayed(&s);
assert!(!r.prefs.move_locked_with_artboard);
assert_eq!(documents(&r), documents(&s));
// The params' own values win over the preferences.
let mut t = Session::new();
second_artboard_selected(&mut t);
t.prefs.move_locked_with_artboard = true;
t.execute("artboard.switchOrientation", &json!({"scaleArt": true, "lockedAndHidden": false})).unwrap();
assert_eq!(t.journal.last().unwrap().1["lockedAndHidden"], json!(false));
assert!(t.execute("artboard.switchOrientation", &json!({"strokes": "yes"})).is_err());
}
/// Every open document of `s`, as JSON.
fn documents(s: &Session) -> Vec<serde_json::Value> {
s.documents().iter().map(|d| serde_json::to_value(&*d.doc).unwrap()).collect()
+144
View File
@@ -241,6 +241,150 @@ fn artboard_reorder_past_u32_goes_last() {
assert_eq!(s.doc().unwrap().doc.artboards.last().map(|a| a.name.as_str()), Some("Second"));
}
/// `artboard.rename`: one artboard takes the name trimmed, several take it numbered in artboard
/// order in one undo step, and a locked artboard is renamed too.
#[test]
fn artboard_rename_numbers_several_in_one_undo_step() {
let mut s = session();
s.execute("artboard.new", &json!({})).unwrap();
s.execute("artboard.new", &json!({})).unwrap();
s.execute("artboard.setProps", &json!({"index": 2, "locked": true})).unwrap();
let names = |s: &Session| s.doc().unwrap().doc.artboards.iter().map(|a| a.name.clone()).collect::<Vec<_>>();
let steps = s.doc().unwrap().history.undo.len();
assert_eq!(s.execute("artboard.rename", &json!({"index": 0, "name": " Cover "})).unwrap()["renamed"], json!([0]));
assert_eq!(names(&s)[0], "Cover");
assert_eq!(s.doc().unwrap().history.undo.len(), steps + 1);
// Listed out of order and twice: artboard order, once each.
assert_eq!(s.execute("artboard.rename", &json!({"indices": [2, 1, 2], "name": "Page"})).unwrap()["renamed"], json!([1, 2]));
assert_eq!(names(&s)[1..], ["Page-1", "Page-2"]);
assert_eq!(s.doc().unwrap().history.undo.len(), steps + 2, "one undo step");
s.execute("edit.undo", &json!({})).unwrap();
assert_ne!(names(&s)[1], "Page-1", "one undo restores both");
assert_ne!(names(&s)[2], "Page-2");
}
/// `artboard.rename` with nothing to change records no undo step, and without `index` or `indices`
/// acts on the artboards the Artboard tool has selected.
#[test]
fn artboard_rename_records_nothing_when_nothing_changes() {
let mut s = session();
s.execute("artboard.new", &json!({})).unwrap();
s.execute("artboard.rename", &json!({"indices": [0, 1], "name": "Page"})).unwrap();
let steps = s.doc().unwrap().history.undo.len();
assert_eq!(s.execute("artboard.rename", &json!({"indices": [0, 1], "name": "Page"})).unwrap()["renamed"], json!([]));
assert_eq!(s.doc().unwrap().history.undo.len(), steps);
s.select_tool("artboard", crate::tooling::ViewInfo::default()).unwrap();
s.set_tool_option("selected", &json!([1]));
assert_eq!(s.execute("artboard.rename", &json!({"name": "Back"})).unwrap()["renamed"], json!([1]));
assert_eq!(s.doc().unwrap().doc.artboards[1].name, "Back");
}
/// `artboard.rename` refuses an empty or missing name, a name that is not a string and an unknown
/// artboard, and leaves the document and its undo history as they were.
#[test]
fn artboard_rename_refuses_bad_params() {
let mut s = session();
let steps = s.doc().unwrap().history.undo.len();
for p in [
json!({"index": 0, "name": " "}),
json!({"index": 0}),
json!({"index": 0, "name": 7}),
json!({"index": 9, "name": "A"}),
json!({"indices": [], "name": "A"}),
json!({"indices": [0, 9], "name": "A"}),
json!({"index": "0", "name": "A"}),
] {
assert!(s.execute("artboard.rename", &p).is_err(), "{p}");
}
assert_eq!(s.doc().unwrap().history.undo.len(), steps);
assert_eq!(s.doc().unwrap().doc.artboards[0].name, "Artboard 1");
}
/// `artboard.switchOrientation` swaps width and height of each artboard named in one undo step,
/// each top-left corner staying put; a square one and a locked one stay as they are.
#[test]
fn artboard_switch_orientation_swaps_each_in_one_undo_step() {
let mut s = session();
s.execute("artboard.new", &json!({"x": 1000, "y": 10, "width": 300, "height": 100})).unwrap();
s.execute("artboard.new", &json!({"x": 2000, "y": 20, "width": 50, "height": 50})).unwrap();
s.execute("artboard.new", &json!({"x": 3000, "y": 30, "width": 40, "height": 60})).unwrap();
s.execute("artboard.setProps", &json!({"index": 3, "locked": true})).unwrap();
let steps = s.doc().unwrap().history.undo.len();
let r = s.execute("artboard.switchOrientation", &json!({"indices": [0, 1, 2, 3]})).unwrap();
assert_eq!(r["switched"], json!([0, 1]));
let rects: Vec<_> = s.doc().unwrap().doc.artboards.iter().map(|a| (a.rect.x0, a.rect.y0, a.rect.width(), a.rect.height())).collect();
assert_eq!(rects, [(0.0, 0.0, 600.0, 800.0), (1000.0, 10.0, 100.0, 300.0), (2000.0, 20.0, 50.0, 50.0), (3000.0, 30.0, 40.0, 60.0)]);
assert_eq!(s.doc().unwrap().history.undo.len(), steps + 1, "one undo step");
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().doc.artboards[1].rect.width(), 300.0);
assert_eq!(s.doc().unwrap().doc.artboards[0].rect.width(), 800.0, "one undo restores both");
}
/// `artboard.switchOrientation`: a square artboard records no undo step, every artboard locked is an
/// error, and bad params change nothing.
#[test]
fn artboard_switch_orientation_refuses_and_records_nothing() {
let mut s = session();
s.execute("artboard.new", &json!({"x": 2000, "y": 0, "width": 50, "height": 50})).unwrap();
s.execute("artboard.setProps", &json!({"index": 0, "locked": true})).unwrap();
let steps = s.doc().unwrap().history.undo.len();
assert_eq!(s.execute("artboard.switchOrientation", &json!({"index": 1})).unwrap()["switched"], json!([]));
assert_eq!(s.execute("artboard.switchOrientation", &json!({"index": 0})).unwrap_err().to_string(), "the artboard is locked");
for p in [json!({"index": 9}), json!({"indices": []}), json!({"index": "1"}), json!({"index": 1, "scaleArt": "yes"})] {
assert!(s.execute("artboard.switchOrientation", &p).is_err(), "{p}");
}
assert_eq!(s.doc().unwrap().history.undo.len(), steps);
}
/// `artboard.switchOrientation` takes the art along with `scaleArt`, and by default follows the
/// Artboard tool's Scale Artwork with Artboard option.
#[test]
fn artboard_switch_orientation_scales_art_with_the_tools_option() {
let mut s = session();
let id = rect(&mut s);
let width = |s: &Session| s.doc().unwrap().doc.bounds_of(&[id], false).unwrap().width();
let was = width(&s);
s.execute("artboard.switchOrientation", &json!({"index": 0})).unwrap();
assert_eq!(width(&s), was, "the art stays by default");
s.select_tool("artboard", crate::tooling::ViewInfo::default()).unwrap();
s.set_tool_option("scaleArt", &json!(true));
s.execute("artboard.switchOrientation", &json!({"index": 0})).unwrap();
assert_ne!(width(&s), was, "the tool's option scales the art");
}
/// `artboard.fitToArt` records no undo step when every artboard already fits its art.
#[test]
fn artboard_fit_to_art_records_nothing_when_it_already_fits() {
let mut s = session();
rect(&mut s);
s.execute("artboard.fitToArt", &json!({"index": 0})).unwrap();
let steps = s.doc().unwrap().history.undo.len();
let was = s.doc().unwrap().doc.artboards[0].rect;
s.execute("artboard.fitToArt", &json!({"index": 0})).unwrap();
assert_eq!(s.doc().unwrap().history.undo.len(), steps);
assert_eq!(s.doc().unwrap().doc.artboards[0].rect, was);
}
/// `edit.paste {artboardsAt}` centers the copied artboards on the point as they lay. A value that is
/// not a point is an error.
#[test]
fn paste_centers_copied_artboards_on_artboards_at() {
let mut s = session();
s.execute("artboard.new", &json!({"x": 1000, "y": 0, "width": 100, "height": 100})).unwrap();
s.execute("artboard.copy", &json!({"indices": [0, 1]})).unwrap();
let steps = s.doc().unwrap().history.undo.len();
for bad in [json!("here"), json!([1]), json!(["1", "2"]), json!([1, 2, 3]), json!(null)] {
assert!(s.execute("edit.paste", &json!({"artboardsAt": bad})).is_err(), "{bad}");
}
assert_eq!(s.doc().unwrap().history.undo.len(), steps);
let r = s.execute("edit.paste", &json!({"artboardsAt": [5000.0, 7000.0]})).unwrap();
assert_eq!(r["artboards"], json!([2, 3]));
let d = &s.doc().unwrap().doc;
let together = d.artboards[2].rect.union(d.artboards[3].rect).center();
assert_eq!((together.x, together.y), (5000.0, 7000.0));
assert_eq!(d.artboards[3].rect.x0 - d.artboards[2].rect.x0, 1000.0, "as they lay");
}
/// The artboards' left edges and names.
fn boards(s: &Session) -> Vec<(f64, String)> {
s.doc().unwrap().doc.artboards.iter().map(|a| (a.rect.x0, a.name.clone())).collect()
+1 -1
View File
@@ -202,7 +202,7 @@ impl VectorcraftApp {
{
self.last_pathfinder = Some(op.to_string());
}
if r.is_ok() && matches!(id, "edit.copy" | "edit.cut") {
if r.is_ok() && matches!(id, "edit.copy" | "edit.cut" | "artboard.copy" | "artboard.cut") {
self.publish_clipboard();
}
self.sync_views();
+9 -2
View File
@@ -6,7 +6,8 @@ use super::{Ui, VectorcraftApp, Xf, json, widgets};
/// objects under the pointer ([`vectorcraft_doc::hit::locked_objects_at`]) are kept in
/// `ui.context_locked` while the menu is open, for its Unlock submenu. With the Intertwine tool, a
/// right-click where three or more objects of the group overlap keeps the selection and puts what
/// it arranges in `ui.context_intertwine` ([`vectorcraft_tools::intertwine::menu_target`]).
/// it arranges in `ui.context_intertwine` ([`vectorcraft_tools::intertwine::menu_target`]). With the
/// Artboard tool, an artboard takes the place of the object ([`crate::panels::artboards::right_click`]).
pub(super) fn context_menu(app: &mut VectorcraftApp, resp: &egui::Response, xf: &Xf) {
if resp.secondary_clicked()
&& let Some(p) = resp.interact_pointer_pos()
@@ -19,7 +20,12 @@ pub(super) fn context_menu(app: &mut VectorcraftApp, resp: &egui::Response, xf:
.active()
.filter(|_| app.session.tool_id() == "intertwine")
.and_then(|st| vectorcraft_tools::intertwine::menu_target(&st.doc, &st.selection.objects, p).map(|(g, m)| (st.uid, g, p, m)));
if app.ui.context_intertwine.is_none()
let on_artboards = app.session.tool_id() == "artboard";
if on_artboards {
crate::panels::artboards::right_click(app, p);
}
if !on_artboards
&& app.ui.context_intertwine.is_none()
&& let Some(st) = app.session.active()
&& let Some(top) = selection_hit_at(app, p, xf.zoom).map(|h| h.top_object(st.isolation))
&& !st.selection.contains(top)
@@ -32,6 +38,7 @@ pub(super) fn context_menu(app: &mut VectorcraftApp, resp: &egui::Response, xf:
if resp.context_menu(|ui| crate::menus::context_menu_body(app, ui, &mut clicked)).is_none() {
app.ui.context_locked = None;
app.ui.context_intertwine = None;
app.ui.context_artboards = None;
}
if let Some((id, p)) = clicked {
crate::menus::invoke(app, &id, p);
+2
View File
@@ -91,6 +91,7 @@ pub mod raster_effects;
pub mod rearrange_artboards;
pub mod recolor;
mod recovery;
pub mod rename_artboards;
mod revolve;
pub(crate) mod revolve_gizmo;
pub mod saturate;
@@ -415,6 +416,7 @@ registry! {
NavigatorPanelOptions: [navigator_panel_options::KIND] => navigator_panel_options::SPEC,
PathfinderOptions: [pathfinder_options::KIND] => pathfinder_options::SPEC,
RearrangeArtboards: [rearrange_artboards::KIND] => rearrange_artboards::SPEC,
RenameArtboards: [rename_artboards::KIND] => rename_artboards::SPEC,
PathToolOptions: [crate::addons::pathtools::KIND] => crate::addons::pathtools::OPTIONS_SPEC,
DivideDashes: [crate::addons::pathtools::dashes::KIND] => crate::addons::pathtools::dashes::SPEC,
SetCorners: [crate::addons::pathtools::set_corners::KIND] => crate::addons::pathtools::set_corners::SPEC,
@@ -0,0 +1,104 @@
//! The Artboard tool's canvas menu › Rename: a name for the selected artboards. OK runs
//! `artboard.rename` as one undo step; with several artboards the name is numbered.
//!
//! Fields: `name` (the artboard's name with one artboard, empty with several) and `indices` (the
//! artboards the dialog opened on).
use serde_json::{Value, json};
use super::{DialogResult, DialogSpec, form, run_and_close};
use crate::state::Dialog;
use crate::{VectorcraftApp, widgets};
/// The dialog kind of the artboard Rename dialog.
pub const KIND: &str = "renameArtboards";
const CMD: &str = "artboard.rename";
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Rename").into(), body, confirm, min_width: 320.0, ..DialogSpec::FORM };
/// The fields the menu item opens with, for the selected artboards.
pub fn open_fields(app: &VectorcraftApp) -> Value {
let Some(st) = app.session.active() else { return json!({"name": "", "indices": []}) };
let indices = crate::panels::artboards::picked(app, st.doc.artboards.len());
let name = match indices.as_slice() {
[one] => st.doc.artboards.get(*one).map_or("", |a| a.name.as_str()),
_ => "",
};
json!({ "name": name, "indices": indices })
}
fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
widgets::label_row(ui, tl!("Name:"), 56.0, |ui| {
let r = form::text_edit_named(ui, d, "name", tl!("Name:"), 220.0);
if ui.memory(|m| m.focused().is_none()) {
r.request_focus();
}
});
false
}
/// An empty name keeps the names as they are, as the Artboards panel's name field does.
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> DialogResult {
let name = d.str("name");
if name.trim().is_empty() {
app.ui.dialog = None;
return Ok(Value::Null);
}
let indices = d.fields.get("indices").cloned().unwrap_or(Value::Null);
run_and_close(app, CMD, json!({ "indices": indices, "name": name }))
}
#[cfg(test)]
mod tests {
use super::*;
use vectorcraft_engine::Session;
fn app() -> VectorcraftApp {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 100, "height": 50, "artboards": 3})).unwrap();
app.select_tool("artboard");
app
}
fn names(app: &VectorcraftApp) -> Vec<String> {
app.session.active().unwrap().doc.artboards.iter().map(|a| a.name.clone()).collect()
}
/// With one artboard the field starts on its name; with several it starts empty, and OK numbers
/// the name in one undo step.
#[test]
fn the_menu_item_renames_the_selected_artboards() {
let mut app = app();
crate::menus::invoke(&mut app, CMD, json!({}));
let d = app.ui.dialog.as_ref().unwrap();
assert_eq!(d.kind, KIND);
assert_eq!(d.str("name"), "Artboard 1");
app.ui.dialog = None;
app.session.set_tool_option("selected", &json!([2, 0]));
crate::menus::invoke(&mut app, CMD, json!({}));
assert_eq!(app.ui.dialog.as_ref().unwrap().str("name"), "");
app.ui.dialog.as_mut().unwrap().fields.insert("name".into(), json!(" Page "));
let steps = app.session.active().unwrap().history.undo.len();
crate::dialogs::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
assert_eq!(names(&app), ["Page-1", "Artboard 2", "Page-2"]);
assert_eq!(app.session.active().unwrap().history.undo.len(), steps + 1, "one undo step");
}
/// OK with an empty name or the name the artboard already has records no undo step.
#[test]
fn nothing_changes_for_an_empty_or_unchanged_name() {
let mut app = app();
let steps = app.session.active().unwrap().history.undo.len();
for name in [" ", "Artboard 1"] {
crate::menus::invoke(&mut app, CMD, json!({}));
app.ui.dialog.as_mut().unwrap().fields.insert("name".into(), json!(name));
crate::dialogs::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
}
assert_eq!(names(&app), ["Artboard 1", "Artboard 2", "Artboard 3"]);
assert_eq!(app.session.active().unwrap().history.undo.len(), steps);
}
}
+4
View File
@@ -872,6 +872,7 @@ font-technology Variable Променливи
Duplicate Artboard Дублиране на работна област
Copy Artboard Копиране на работна област
Cut Artboard Изрязване на работна област
Rename Artboards Преименуване на работни области
Duplicate Artboards Дублиране на работни области
Duplicate Brush Дублиране на четка
Duplicate Character Style Дублиране на стил на знаци
@@ -2503,6 +2504,9 @@ digits Malayalam Малаялам
Real World: Реален свят:
Real-time Drawing and Editing Чертане и редактиране в реално време
Rearrange All Artboards Пренареждане на всички работни области
Rearrange Пренареждане
Select All Artboards Избиране на всички работни области
Switch Orientation Смяна на ориентацията
Rearrange All Пренареждане на всички
Layout Order: Ред на оформление:
Change to Left-to-Right Layout Превключване към подредба отляво надясно
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1924,6 +1924,9 @@ digits Malayalam Malajalam
More Paragraph options Više opcija pasusa
No document open Nijedan dokument nije otvoren
Quick Actions Brze akcije
Rearrange Preuredi
Select All Artboards Odaberi sve radne ploče
Switch Orientation Promijeni orijentaciju
Rearrange All Preuredi sve
Rulers & Grids Ravnala i mreže
Select a text object Odaberite tekstualni objekat
@@ -3446,6 +3449,7 @@ theme Appearance Izgled
Character / Paragraph Znak / pasus
Copy Artboard Kopiraj radnu ploču
Cut Artboard Izreži radnu ploču
Rename Artboards Preimenuj radne ploče
Move Artboard Up/Down Pomakni radnu ploču gore/dolje
Add Anchor (Pen) Dodaj sidro (pero)
Add Anchor Point Dodaj sidrenu tačku
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -962,6 +962,7 @@
Customize… Personalitza…
Cut Retalla
Cut Artboard Retalla el tauler
Rename Artboards Canvia el nom dels taulers
Cut Path Retalla el traçat
Cut Path at Selected Anchor Points Retalla el traçat als punts d’ancoratge seleccionats
Cutout Retall
@@ -3206,6 +3207,9 @@ digits Malayalam Malaiàlam
Real name Nom real
Real World: Món real:
Real-time Drawing and Editing Dibuix i edició en temps real
Rearrange Reorganitza
Select All Artboards Selecciona tots els taulers
Switch Orientation Canvia l’orientació
Rearrange All Reorganitza-ho tot
Rearrange All Artboards Reorganitza tots els taulers
Rearrange All Artboards… Reorganitza tots els taulers…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1950,6 +1950,9 @@ digits Malayalam Malajálamské
More Paragraph options Další možnosti odstavce
No document open Není otevřen žádný dokument
Quick Actions Rychlé akce
Rearrange Přeuspořádat
Select All Artboards Vybrat všechny pracovní plochy
Switch Orientation Přepnout orientaci
Rearrange All Přeuspořádat vše
Rulers & Grids Pravítka a mřížky
Select a text object Vyberte textový objekt
@@ -3485,6 +3488,7 @@ theme Appearance Motiv
Character / Paragraph Znak / Odstavec
Copy Artboard Kopírovat pracovní plochu
Cut Artboard Vyjmout pracovní plochu
Rename Artboards Přejmenovat pracovní plochy
Move Artboard Up/Down Posunout pracovní plochu nahoru/dolů
Add Anchor (Pen) Přidat kotevní bod (pero)
Add Anchor Point Přidat kotevní bod
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1756,6 +1756,9 @@ digits Malayalam Malayalam
More Paragraph options Flere afsnitsindstillinger
No document open Intet åbent dokument
Quick Actions Hurtige handlinger
Rearrange Omarrangér
Select All Artboards Markér alle tegnebrætter
Switch Orientation Skift retning
Rearrange All Omarrangér alle
Rulers & Grids Linealer og gitre
Select a text object Markér et tekstobjekt
@@ -3227,6 +3230,7 @@ theme Appearance Udseende
Character / Paragraph Tegn / Afsnit
Copy Artboard Kopiér kunstplade
Cut Artboard Klip kunstplade
Rename Artboards Omdøb tegnebrætter
Move Artboard Up/Down Flyt kunstplade op/ned
Add Anchor (Pen) Tilføj anker (Pen)
Add Anchor Point Tilføj ankerpunkt
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -866,6 +866,7 @@ font-technology Variable Variabel
Duplicate Artboard Zeichenfläche duplizieren
Copy Artboard Zeichenfläche kopieren
Cut Artboard Zeichenfläche ausschneiden
Rename Artboards Zeichenflächen umbenennen
Duplicate Artboards Zeichenflächen duplizieren
Duplicate Brush Pinsel duplizieren
Duplicate Character Style Zeichenformat duplizieren
@@ -2529,6 +2530,9 @@ digits Malayalam Malayalam
Real World: Reale Welt:
Real-time Drawing and Editing Zeichnen und Bearbeiten in Echtzeit
Rearrange All Artboards Alle Zeichenflächen neu anordnen
Rearrange Neu anordnen
Select All Artboards Alle Zeichenflächen auswählen
Switch Orientation Ausrichtung wechseln
Rearrange All Alle neu anordnen
Layout Order: Layoutreihenfolge:
Change to Left-to-Right Layout In Layout von links nach rechts ändern
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -844,6 +844,7 @@ font-technology Variable Μεταβλητές
Duplicate Artboard Δημιουργία αντιγράφου πίνακα σχεδίασης
Copy Artboard Αντιγραφή πίνακα σχεδίασης
Cut Artboard Αποκοπή πίνακα σχεδίασης
Rename Artboards Μετονομασία πινάκων σχεδίασης
Duplicate Artboards Δημιουργία αντιγράφων πινάκων σχεδίασης
Duplicate Brush Δημιουργία αντιγράφου πινέλου
Duplicate Character Style Δημιουργία αντιγράφου στυλ χαρακτήρων
@@ -3160,6 +3161,9 @@ digits Malayalam Μαλαγιάλαμ
Real World: Πραγματικός κόσμος:
Real-time Drawing and Editing Σχεδίαση και επεξεργασία σε πραγματικό χρόνο
Rearrange All Artboards Αναδιάταξη όλων των πινάκων σχεδίασης
Rearrange Αναδιάταξη
Select All Artboards Επιλογή όλων των πινάκων σχεδίασης
Switch Orientation Αλλαγή προσανατολισμού
Rearrange All Αναδιάταξη όλων
Layout Order: Σειρά διάταξης:
Change to Left-to-Right Layout Αλλαγή σε διάταξη από αριστερά προς τα δεξιά
Can't render this file because it contains an unexpected character in line 163 and column 5.
+4
View File
@@ -887,6 +887,7 @@ font-technology Variable Variables
Duplicate Artboard Duplicar mesa de trabajo
Copy Artboard Copiar mesa de trabajo
Cut Artboard Cortar mesa de trabajo
Rename Artboards Cambiar nombre de mesas de trabajo
Duplicate Artboards Duplicar mesas de trabajo
Duplicate Brush Duplicar pincel
Duplicate Character Style Duplicar estilo de carácter
@@ -2572,6 +2573,9 @@ digits Malayalam Malayálam
Real World: Mundo real:
Real-time Drawing and Editing Dibujo y edición en tiempo real
Rearrange All Artboards Reorganizar todas las mesas de trabajo
Rearrange Reorganizar
Select All Artboards Seleccionar todas las mesas de trabajo
Switch Orientation Cambiar orientación
Rearrange All Reorganizar todo
Layout Order: Orden de diseño:
Change to Left-to-Right Layout Cambiar a diseño de izquierda a derecha
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1911,6 +1911,9 @@ digits Malayalam Malajalam
More Paragraph options Lisää kappaleasetuksia
No document open Ei avointa asiakirjaa
Quick Actions Pikatoiminnot
Rearrange Järjestä uudelleen
Select All Artboards Valitse kaikki piirtoalustat
Switch Orientation Vaihda suuntaa
Rearrange All Järjestä kaikki uudelleen
Rulers & Grids Viivaimet ja ruudukot
Select a text object Valitse tekstiobjekti
@@ -3423,6 +3426,7 @@ theme Appearance Ulkoasu
Character / Paragraph Merkki / kappale
Copy Artboard Kopioi piirtoalusta
Cut Artboard Leikkaa piirtoalusta
Rename Artboards Nimeä piirtoalustat uudelleen
Move Artboard Up/Down Siirrä piirtoalustaa ylös/alas
Add Anchor (Pen) Lisää ankkuri (kynä)
Add Anchor Point Lisää ankkuripiste
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -890,6 +890,7 @@ font-technology Variable Variables
Duplicate Artboard Dupliquer le plan de travail
Copy Artboard Copier le plan de travail
Cut Artboard Couper le plan de travail
Rename Artboards Renommer les plans de travail
Duplicate Artboards Dupliquer les plans de travail
Duplicate Brush Dupliquer la forme
Duplicate Character Style Dupliquer le style de caractère
@@ -2564,6 +2565,9 @@ digits Malayalam Malayalam
Real World: Monde réel :
Real-time Drawing and Editing Dessin et édition en temps réel
Rearrange All Artboards Réorganiser tous les plans de travail
Rearrange Réorganiser
Select All Artboards Sélectionner tous les plans de travail
Switch Orientation Changer d’orientation
Rearrange All Tout réorganiser
Layout Order: Ordre de disposition :
Change to Left-to-Right Layout Passer à la disposition de gauche à droite
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1077,6 +1077,7 @@
Customize… अनुकूलित करें…
Cut काटें
Cut Artboard कट आर्टबोर्ड
Rename Artboards आर्टबोर्ड्स का नाम बदलें
Cut Path कट पाथ
Cut Path at Selected Anchor Points सिलेक्टेड एंकर अंक पर पाथ काटें
Cutout कटआउट
@@ -3487,6 +3488,9 @@
Real name वास्तविक नाम
Real World: रियल वर्ल्ड:
Real-time Drawing and Editing रियल टाइम ड्राइंग और संपादन
Rearrange फिर से व्यवस्थित करें
Select All Artboards सभी आर्टबोर्ड सिलेक्ट करें
Switch Orientation ओरिएंटेशन स्विच करें
Rearrange All सभी को फिर से व्यवस्थित करें
Rearrange All Artboards सभी आर्टबोर्ड को फिर से व्यवस्थित करें
Rearrange All Artboards… सभी आर्टबोर्ड को पुनर्व्यवस्थित करें…
Can't render this file because it is too large.
+4
View File
@@ -903,6 +903,7 @@ font-technology Variable Változó
Customize… Testreszabás…
Cut Kivágás
Cut Artboard Rajztábla kivágása
Rename Artboards Rajztáblák átnevezése
Cut Path Útvonal elvágása
Cut Path at Selected Anchor Points Útvonal elvágása a kijelölt horgonypontoknál
Cyan Cián
@@ -3114,6 +3115,9 @@ mark-style Japanese Japán
Real name Valódi név
Real World: Valós méret:
Real-time Drawing and Editing Valós idejű rajzolás és szerkesztés
Rearrange Átrendezés
Select All Artboards Összes rajztábla kijelölése
Switch Orientation Tájolás váltása
Rearrange All Összes átrendezése
Rearrange All Artboards Összes rajztábla átrendezése
Rearrange All Artboards… Összes rajztábla átrendezése…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1756,6 +1756,9 @@ digits Malayalam Malayalam
More Paragraph options Opsi paragraf lainnya
No document open Tidak ada dokumen terbuka
Quick Actions Tindakan cepat
Rearrange Atur ulang
Select All Artboards Seleksi semua artboard
Switch Orientation Ganti orientasi
Rearrange All Atur ulang semua
Rulers & Grids Penggaris & kisi
Select a text object Pilih objek teks
@@ -3227,6 +3230,7 @@ theme Appearance Tampilan
Character / Paragraph Karakter / Paragraf
Copy Artboard Salin artboard
Cut Artboard Potong artboard
Rename Artboards Ganti nama artboard
Move Artboard Up/Down Pindahkan artboard ke atas/bawah
Add Anchor (Pen) Tambah jangkar (pena)
Add Anchor Point Tambahkan titik jangkar
Can't render this file because it contains an unexpected character in line 2240 and column 5.
+4
View File
@@ -887,6 +887,7 @@ font-technology Variable Variabili
Duplicate Artboard Duplica tavola da disegno
Copy Artboard Copia tavola da disegno
Cut Artboard Taglia tavola da disegno
Rename Artboards Rinomina tavole da disegno
Duplicate Artboards Duplica tavole da disegno
Duplicate Brush Duplica pennello
Duplicate Character Style Duplica stile carattere
@@ -2605,6 +2606,9 @@ digits Malayalam Malayalam
Real World: Mondo reale:
Real-time Drawing and Editing Disegno e modifica in tempo reale
Rearrange All Artboards Ridisponi tutte le tavole da disegno
Rearrange Ridisponi
Select All Artboards Seleziona tutte le tavole da disegno
Switch Orientation Cambia orientamento
Rearrange All Ridisponi tutto
Layout Order: Ordine del layout:
Change to Left-to-Right Layout Passa al layout da sinistra a destra
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1441,6 +1441,7 @@ digits Malayalam マラヤーラム数字
Duplicate Artboard アートボードを複製
Copy Artboard アートボードをコピー
Cut Artboard アートボードをカット
Rename Artboards アートボード名を変更
Duplicate Artboards アートボードを複製
Duplicate Brush ブラシを複製
Duplicate Character Style 文字スタイルを複製
@@ -2619,6 +2620,9 @@ mark-style Japanese 日本式
Real World: 実寸:
Real-time Drawing and Editing リアルタイム描画および編集
Rearrange All Artboards すべてのアートボードを再配置
Rearrange 再配置
Select All Artboards すべてのアートボードを選択
Switch Orientation 方向を切り替え
Rearrange All すべて再配置
Layout Order: レイアウト順序:
Change to Left-to-Right Layout 左から右のレイアウトに変更
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1666,6 +1666,9 @@ digits Malayalam მალაიალამური
More Paragraph options აბზაცის სხვა პარამეტრები
No document open დოკუმენტი გახსნილი არ არის
Quick Actions სწრაფი მოქმედებები
Rearrange გადალაგება
Select All Artboards ყველა სამუშაო დაფის მონიშვნა
Switch Orientation ორიენტაციის გადართვა
Rearrange All ყველას გადალაგება
Rulers & Grids სახაზავები და ბადეები
Select a text object მონიშნეთ ტექსტური ობიექტი
@@ -3047,6 +3050,7 @@ theme Appearance იერსახე
Character / Paragraph სიმბოლო / აბზაცი
Copy Artboard სამუშაო დაფის კოპირება
Cut Artboard სამუშაო დაფის ამოჭრა
Rename Artboards სამუშაო დაფების სახელის შეცვლა
Move Artboard Up/Down სამუშაო დაფის აწევა/ჩამოწევა
Add Anchor (Pen) საყრდენი წერტილის დამატება (კალამი)
Add Anchor Point საყრდენი წერტილის დამატება
Can't render this file because it is too large.
+4
View File
@@ -1431,6 +1431,7 @@ digits Malayalam 말라얄람어
Duplicate Artboard 아트보드 복제
Copy Artboard 아트보드 복사
Cut Artboard 아트보드 잘라내기
Rename Artboards 아트보드 이름 바꾸기
Duplicate Artboards 아트보드 복제
Duplicate Brush 브러시 복제
Duplicate Character Style 문자 스타일 복제
@@ -2609,6 +2610,9 @@ mark-style Japanese 일본식
Real World: 실제 크기:
Real-time Drawing and Editing 실시간 그리기 및 편집
Rearrange All Artboards 모든 아트보드 재정렬
Rearrange 재정렬
Select All Artboards 모든 아트보드 선택
Switch Orientation 방향 전환
Rearrange All 모두 재정렬
Layout Order: 배치 순서:
Change to Left-to-Right Layout 왼쪽에서 오른쪽 배치로 변경
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1770,6 +1770,9 @@ digits Malayalam Malayalam
More Paragraph options Meer alineaopties
No document open Geen document geopend
Quick Actions Snelle acties
Rearrange Herschikken
Select All Artboards Alle tekengebieden selecteren
Switch Orientation Stand wisselen
Rearrange All Alles herschikken
Rulers & Grids Linialen & rasters
Select a text object Selecteer een tekstobject
@@ -3241,6 +3244,7 @@ theme Appearance Vormgeving
Character / Paragraph Teken / alinea
Copy Artboard Tekenbord kopiëren
Cut Artboard Tekenbord knippen
Rename Artboards Tekengebieden hernoemen
Move Artboard Up/Down Tekenbord omhoog/omlaag verplaatsen
Add Anchor (Pen) Anker toevoegen (Pen)
Add Anchor Point Ankerpunt toevoegen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1869,6 +1869,9 @@ digits Malayalam Malajalam
More Paragraph options Więcej opcji akapitu
No document open Brak otwartego dokumentu
Quick Actions Szybkie operacje
Rearrange Przestaw
Select All Artboards Zaznacz wszystkie obszary kompozycji
Switch Orientation Przełącz orientację
Rearrange All Przestaw wszystkie
Rulers & Grids Linijki i siatki
Select a text object Zaznacz obiekt tekstowy
@@ -3364,6 +3367,7 @@ theme Appearance Wygląd
Character / Paragraph Znak / akapit
Copy Artboard Kopiuj obszar kompozycji
Cut Artboard Wytnij obszar kompozycji
Rename Artboards Zmień nazwy obszarów kompozycji
Move Artboard Up/Down Przesuń obszar kompozycji w górę/dół
Add Anchor (Pen) Dodaj punkt (pióro)
Add Anchor Point Dodaj punkt kontrolny
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -790,6 +790,7 @@
Duplicate Artboard Duplicar Mesa de Trabalho
Copy Artboard Copiar Mesa de Trabalho
Cut Artboard Recortar Mesa de Trabalho
Rename Artboards Renomear Mesas de Trabalho
Duplicate Artboards Duplicar Mesas de Trabalho
Duplicate Brush Duplicar Pincel
Duplicate Character Style Duplicar Estilo de Caractere
@@ -2139,6 +2140,9 @@ digits Malayalam Malaiala
Real World: Mundo Real:
Real-time Drawing and Editing Desenho e Edição em Tempo Real
Rearrange All Artboards Reorganizar Todas as Mesas de Trabalho
Rearrange Reorganizar
Select All Artboards Selecionar Todas as Mesas de Trabalho
Switch Orientation Trocar Orientação
Rearrange All Reorganizar Tudo
Layout Order: Ordem do Layout:
Change to Left-to-Right Layout Mudar para Layout da Esquerda para a Direita
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -885,6 +885,7 @@ font-technology Variable Вариативные
Duplicate Artboard Дублировать монтажную область
Copy Artboard Копировать монтажную область
Cut Artboard Вырезать монтажную область
Rename Artboards Переименовать монтажные области
Duplicate Artboards Дублировать монтажные области
Duplicate Brush Дублировать кисть
Duplicate Character Style Дублировать стиль символа
@@ -2600,6 +2601,9 @@ digits Malayalam Малаялам
Real World: Реальный мир:
Real-time Drawing and Editing Рисование и редактирование в реальном времени
Rearrange All Artboards Переставить все монтажные области
Rearrange Переставить
Select All Artboards Выделить все монтажные области
Switch Orientation Переключить ориентацию
Rearrange All Переставить все
Layout Order: Порядок макета:
Change to Left-to-Right Layout Переключить на макет слева направо
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -874,6 +874,7 @@ font-technology Variable Variabla
Duplicate Artboard Duplicera rityta
Copy Artboard Kopiera rityta
Cut Artboard Klipp ut rityta
Rename Artboards Byt namn på ritytor
Duplicate Artboards Duplicera ritytor
Duplicate Brush Duplicera pensel
Duplicate Character Style Duplicera teckenstil
@@ -2509,6 +2510,9 @@ digits Malayalam Malayalam
Real World: Verkligt mått:
Real-time Drawing and Editing Ritning och redigering i realtid
Rearrange All Artboards Ordna om alla ritytor
Rearrange Ordna om
Select All Artboards Markera alla ritytor
Switch Orientation Byt orientering
Rearrange All Ordna om alla
Layout Order: Layoutordning:
Change to Left-to-Right Layout Byt till layout vänster till höger
Can't render this file because it contains an unexpected character in line 5 and column 25.
+4
View File
@@ -866,6 +866,7 @@ font-technology Variable Değişken
Duplicate Artboard Çalışma Yüzeyini Çoğalt
Copy Artboard Çalışma Yüzeyini Kopyala
Cut Artboard Çalışma Yüzeyini Kes
Rename Artboards Çalışma Yüzeylerini Yeniden Adlandır
Duplicate Artboards Çalışma Yüzeylerini Çoğalt
Duplicate Brush Yinelenen Fırça
Duplicate Character Style Yinelenen Karakter Stili
@@ -2382,6 +2383,9 @@ digits Malayalam Malayalam
Real World: Gerçek Dünya:
Real-time Drawing and Editing Gerçek Zamanlı Çizim ve Düzenleme
Rearrange All Artboards Tüm Çalışma Yüzeylerini Yeniden Düzenleme
Rearrange Yeniden Düzenle
Select All Artboards Tüm Çalışma Yüzeylerini Seç
Switch Orientation Yönü Değiştir
Rearrange All Tümünü Yeniden Düzenle
Layout Order: Yerleşim Sırası:
Change to Left-to-Right Layout Soldan Sağa Düzene Geç
Can't render this file because it contains an unexpected character in line 5 and column 25.
+4
View File
@@ -881,6 +881,7 @@ font-technology Variable Варіативні
Duplicate Artboard Дублювати монтажну область
Copy Artboard Копіювати монтажну область
Cut Artboard Вирізати монтажну область
Rename Artboards Перейменувати монтажні області
Duplicate Artboards Дублювати монтажні області
Duplicate Brush Дублювати пензель
Duplicate Character Style Дублювати стиль символу
@@ -2597,6 +2598,9 @@ digits Malayalam Малаялам
Real World: Реальний світ:
Real-time Drawing and Editing Малювання та редагування у реальному часі
Rearrange All Artboards Переставити всі монтажні області
Rearrange Переставити
Select All Artboards Виділити всі монтажні області
Switch Orientation Переключити орієнтацію
Rearrange All Переставити все
Layout Order: Порядок макету:
Change to Left-to-Right Layout Переключити на макет зліва направо
Can't render this file because it contains an unexpected character in line 177 and column 5.
+4
View File
@@ -799,6 +799,7 @@ font-technology Variable Biến thiên
Customize… Tùy chỉnh…
Cut Cắt
Cut Artboard Cắt bảng vẽ
Rename Artboards Đổi tên các bảng vẽ
Cut Path Cắt đường dẫn
Cut Path at Selected Anchor Points Cắt đường dẫn tại các điểm neo đã chọn
Cyan Xanh lơ
@@ -2894,6 +2895,9 @@ mark-style Japanese Kiểu Nhật
Real name Tên thật
Real World: Thực tế:
Real-time Drawing and Editing Vẽ và chỉnh sửa theo thời gian thực
Rearrange Sắp xếp lại
Select All Artboards Chọn tất cả các bảng vẽ
Switch Orientation Chuyển hướng
Rearrange All Sắp xếp lại tất cả
Rearrange All Artboards Sắp xếp lại tất cả bảng vẽ
Rearrange All Artboards… Sắp xếp lại tất cả bảng vẽ…
Can't render this file because it contains an unexpected character in line 199 and column 5.
+4
View File
@@ -829,6 +829,7 @@
Duplicate Artboard 复制画板
Copy Artboard 拷贝画板
Cut Artboard 剪切画板
Rename Artboards 重命名画板
Duplicate Artboards 复制画板
Duplicate Brush 复制画笔
Duplicate Character Style 制作字符样式副本
@@ -2478,6 +2479,9 @@ digits Malayalam 马拉雅拉姆数字
Real World: 真实世界:
Real-time Drawing and Editing 实时绘图和编辑
Rearrange All Artboards 重新排列所有画板
Rearrange 重新排列
Select All Artboards 选择所有画板
Switch Orientation 切换方向
Rearrange All 全部重新排列
Layout Order: 布局顺序:
Change to Left-to-Right Layout 更改为由左至右版面配置
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -849,6 +849,7 @@ font-technology Variable 可變
Duplicate Artboard 製作工作區域複本
Copy Artboard 複製工作區域
Cut Artboard 剪下工作區域
Rename Artboards 重新命名工作區域
Duplicate Artboards 製作工作區域複本
Duplicate Brush 製作筆刷複本
Duplicate Character Style 製作字元樣式複本
@@ -3467,6 +3468,9 @@ digits Malayalam 馬來亞拉姆數字
Real World: 真實世界:
Real-time Drawing and Editing 即時繪圖與編輯
Rearrange All Artboards 重新排列所有工作區域
Rearrange 重新排列
Select All Artboards 選取所有工作區域
Switch Orientation 切換方向
Rearrange All 全部重新排列
Layout Order: 版面配置順序:
Change to Left-to-Right Layout 變更為由左至右版面配置
Can't render this file because it contains an unexpected character in line 6 and column 47.
+98 -34
View File
@@ -8,6 +8,9 @@ use super::{Item, Sep, Value, VectorcraftApp, c, cp, json, sub, widgets};
pub fn context_items(app: &VectorcraftApp) -> Vec<Item> {
use vectorcraft_doc::NodeKind;
let Some(st) = app.session.active() else { return vec![] };
if app.session.tool_id() == "artboard" {
return prune(app, artboard_items(app));
}
let roots: Vec<&vectorcraft_doc::Node> = st.selection.objects.iter().filter_map(|id| st.doc.node(*id)).collect();
let any = |f: fn(&NodeKind) -> bool| roots.iter().any(|n| f(&n.kind));
let several = roots.len() >= 2;
@@ -30,40 +33,7 @@ pub fn context_items(app: &VectorcraftApp) -> Vec<Item> {
Sep,
]);
}
// Managing artboards from the canvas: with the Artboard tool, the menu locks and unlocks them
// (`artboard.setProps` with `locked` for each artboard named; an entry with none to change is left out).
if app.session.tool_id() == "artboard" {
let n = st.doc.artboards.len();
let sel = crate::panels::artboards::picked(app, n);
let others: Vec<usize> = (0..n).filter(|i| !sel.contains(i)).collect();
let entry = |label, idx: Vec<usize>, locked: bool| {
let list: Vec<Value> = idx.iter().map(|i| json!({ "index": i, "locked": locked })).collect();
(!list.is_empty()).then(|| cp(label, "artboard.setProps", json!({ "artboards": list })))
};
let lock: Vec<Item> =
[entry("Lock Selected", sel.clone(), true), entry("Lock All Except Selected", others, true), entry("Lock All", (0..n).collect(), true)]
.into_iter()
.flatten()
.collect();
let unlock: Vec<Item> = [entry("Unlock Selected", sel, false), entry("Unlock All", (0..n).collect(), false)].into_iter().flatten().collect();
for (label, items) in [("Lock Artboards", lock), ("Unlock Artboards", unlock)] {
if !items.is_empty() {
v.push(sub(label, items));
}
}
v.push(Sep);
}
// The locked objects under the pointer at the right-click, each unlocked as the Layers panel's
// lock column does. `unlock_item_label` shows each object's name.
let unlock: Vec<Item> = app
.ui
.context_locked
.iter()
.filter(|(uid, _)| *uid == st.uid)
.flat_map(|(_, ids)| ids)
.filter(|id| st.doc.node(**id).is_some_and(|n| n.locked))
.map(|id| cp("Unlock", "layer.setProps", json!({ "ids": [id.0], "locked": false })))
.collect();
let unlock = unlock_items(app);
if !unlock.is_empty() {
v.extend([sub("Unlock", unlock), Sep]);
}
@@ -138,6 +108,100 @@ pub fn context_items(app: &VectorcraftApp) -> Vec<Item> {
prune(app, v)
}
/// The locked objects under the pointer at the right-click, each unlocked as the Layers panel's
/// lock column does. `unlock_item_label` shows each object's name.
fn unlock_items(app: &VectorcraftApp) -> Vec<Item> {
let Some(st) = app.session.active() else { return vec![] };
app.ui
.context_locked
.iter()
.filter(|(uid, _)| *uid == st.uid)
.flat_map(|(_, ids)| ids)
.filter(|id| st.doc.node(**id).is_some_and(|n| n.locked))
.map(|id| cp("Unlock", "layer.setProps", json!({ "ids": [id.0], "locked": false })))
.collect()
}
/// The canvas context menu with the Artboard tool, in place of the entries for objects and the view: the list and order of the public
/// [Use the artboard context menu](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/use-artboard-context-menu.html)
/// page, with the page's Lock Artboards submenu as two, Lock Artboards and Unlock Artboards. `docs/mcp.md`
/// names the entries that act on the artboards the tool has selected. Exit Isolation Mode and the
/// Unlock submenu for locked objects under the pointer follow them.
fn artboard_items(app: &VectorcraftApp) -> Vec<Item> {
use crate::panels::artboards as boards;
let Some(st) = app.session.active() else { return vec![] };
let all = &st.doc.artboards;
let n = all.len();
let is_locked = |i: &usize| all.get(*i).is_some_and(|a| a.locked);
let sel: Vec<usize> = boards::picked(app, n).into_iter().filter(|i| *i < n).collect();
let free: Vec<usize> = sel.iter().copied().filter(|i| !is_locked(i)).collect();
let facts = boards::open_menu_facts(app);
let mut v = vec![];
// Cut refuses a locked artboard and every artboard of the document.
if !sel.is_empty() && free.len() == sel.len() && sel.len() < n {
v.push(c("Cut", "artboard.cut"));
}
if !sel.is_empty() {
v.push(c("Copy", "artboard.copy"));
}
v.push(match facts {
// Copied artboards go to the right-click's point, and so do copied objects.
Some(f) => cp("Paste", "edit.paste", json!({ "center": [f.at.x, f.at.y], "artboardsAt": [f.at.x, f.at.y] })),
None => c("Paste", "edit.paste"),
});
v.extend([Sep, c("Undo", "edit.undo"), c("Redo", "edit.redo"), Sep, c("New Artboard", "artboard.new")]);
if !sel.is_empty() {
v.extend([c("Duplicate Artboards", "artboard.duplicate"), c("Rename", "artboard.rename")]);
}
let deleted = boards::delete_targets(app, n);
if !deleted.is_empty() {
v.push(cp("Delete Artboards", "artboard.delete", json!({ "indices": deleted })));
}
v.extend([c("Delete Empty Artboards", "artboard.deleteEmpty"), Sep]);
if sel.len() < n {
v.push(c("Select All Artboards", "artboard.selectAll"));
}
// `artboard.setProps` with `locked` for each artboard it changes; an entry with none is left out.
let others: Vec<usize> = (0..n).filter(|i| !sel.contains(i)).collect();
let entry = |label, idx: Vec<usize>, locked: bool| {
let list: Vec<Value> = idx.iter().filter(|i| is_locked(i) != locked).map(|i| json!({ "index": i, "locked": locked })).collect();
(!list.is_empty()).then(|| cp(label, "artboard.setProps", json!({ "artboards": list })))
};
let lock: Vec<Item> =
[entry("Lock Selected", sel.clone(), true), entry("Lock All Except Selected", others, true), entry("Lock All", (0..n).collect(), true)]
.into_iter()
.flatten()
.collect();
let unlock: Vec<Item> =
[entry("Unlock Selected", sel.clone(), false), entry("Unlock All", (0..n).collect(), false)].into_iter().flatten().collect();
for (label, items) in [("Lock Artboards", lock), ("Unlock Artboards", unlock)] {
if !items.is_empty() {
v.push(sub(label, items));
}
}
v.push(Sep);
if n > 1 {
v.push(c("Rearrange", "artboard.rearrange"));
}
if facts.map_or_else(|| boards::fit_has_art(app, &free), |f| f.any_art) {
v.push(c("Fit to Artwork Bounds", "artboard.fitToArt"));
}
if free.iter().filter_map(|i| all.get(*i)).any(|a| (a.rect.width() - a.rect.height()).abs() > 1e-6) {
v.push(c("Switch Orientation", "artboard.switchOrientation"));
}
if !sel.is_empty() {
v.extend([cp("Artboard Options", "tool.options", json!({ "tool": "artboard" })), Sep, c("Export", "file.exportForScreens")]);
}
if st.isolation.is_some() {
v.extend([Sep, c("Exit Isolation Mode", "object.exitIsolation")]);
}
let locked_art = unlock_items(app);
if !locked_art.is_empty() {
v.extend([Sep, sub("Unlock", locked_art)]);
}
v
}
/// `items` without the commands that can't run now (Undo and Redo stay, greyed out), submenus
/// left empty, and separators that no longer separate anything.
fn prune(app: &VectorcraftApp, items: Vec<Item>) -> Vec<Item> {
+7
View File
@@ -80,6 +80,7 @@ pub(super) fn menu_dialog(app: &VectorcraftApp, id: &str) -> Option<(&'static st
crate::addons::typetools::COMMAND => (crate::addons::typetools::KIND, crate::addons::typetools::fields(app)),
crate::addons::select::COMMAND => (crate::addons::select::KIND, crate::addons::select::fields(app)),
"artboard.rearrange" => (crate::dialogs::rearrange_artboards::KIND, crate::dialogs::rearrange_artboards::open_fields(app)),
"artboard.rename" => (crate::dialogs::rename_artboards::KIND, crate::dialogs::rename_artboards::open_fields(app)),
_ => return None,
})
}
@@ -173,6 +174,12 @@ pub fn invoke(app: &mut VectorcraftApp, id: &str, p: Value) {
}
return;
}
if let Some(r) = crate::panels::artboards::run_menu_item(app, id, &p) {
if let Err(e) = r {
app.status(e);
}
return;
}
// A Paste the user gives decodes a bitmap off the UI thread ([`crate::placing::adopt_for_paste`]).
app.placing.for_user = id.starts_with("edit.paste");
let r = app.run(id, p);
+15
View File
@@ -217,6 +217,11 @@ pub fn item_shortcut(id: &str, p: &Value) -> Option<&'static str> {
match (id, p.get("panel").and_then(Value::as_str)) {
("window.panel", Some(panel)) => crate::shortcut_editor::panel_shortcut(panel),
("effect.dialog" | "effect.apply", _) => p.get("effect").and_then(Value::as_str).and_then(crate::shortcut_editor::effect_shortcut),
// The Artboard tool's canvas menu passes the point its Paste goes to.
("edit.paste", _) => shortcut_of(id),
// That menu's Cut and Copy: with the tool chosen, the keys run `edit.cut` and `edit.copy`, which do the same.
("artboard.cut", _) => shortcut_of("edit.cut"),
("artboard.copy", _) => shortcut_of("edit.copy"),
_ if p.is_null() => shortcut_of(id),
_ => None,
}
@@ -234,6 +239,16 @@ pub const CONTEXT_LABELS: &[&str] = &[
"Unlock Artboards",
"Unlock Selected",
"Unlock",
"New Artboard",
"Duplicate Artboards",
"Rename",
"Delete Artboards",
"Delete Empty Artboards",
"Select All Artboards",
"Rearrange",
"Switch Orientation",
"Artboard Options",
"Export",
"Make Clipping Mask",
"Release Clipping Mask",
"Make Compound Path",
+1
View File
@@ -712,6 +712,7 @@ fn run_ui_part8(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<
"ui.rename" => crate::panels::layers::rename(app, p),
"ui.newLayer" => crate::dialogs::layer_options::open_new(app, p),
"ui.newArtboard" => crate::dialogs::artboard_options::open_new(app),
"artboard.selectAll" => crate::panels::artboards::select_all_command(app),
"ui.layersPanelOptions" => crate::dialogs::layers_panel_options::open(app),
"ui.navigatorPanelOptions" => crate::panels::navigator::set_options(app, p),
"ui.layersExpand" => crate::panels::layers::expand(app, p),
+7 -1
View File
@@ -187,6 +187,12 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
if SUBJECT_COMMANDS.contains(&id) {
return has_subject(app);
}
// The Artboard tool's canvas menu measured this when it opened.
if id == "artboard.deleteEmpty"
&& let Some(facts) = crate::panels::artboards::open_menu_facts(app)
{
return facts.any_empty;
}
if let Some(c) = vectorcraft_engine::find_command(id) {
// The system clipboard's contents can be pasted with an empty internal clipboard.
return (c.enabled)(&app.session).is_ok() || app.system_paste && id.starts_with("edit.paste");
@@ -245,7 +251,7 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
(d.kind == crate::dialogs::char_style_options::KIND || (d.kind == crate::dialogs::new_text_style::KIND && !d.bool("__para")))
&& crate::dialogs::char_style_options::can_reset(d)
}),
"ui.newArtboard" => app.session.active().is_some(),
"ui.newArtboard" | "artboard.selectAll" => app.session.active().is_some(),
"ui.sampleScreenColor" => crate::screen_color::available(app),
"ui.newBrush" => app.session.active().is_some(),
"ui.brushOptions" => crate::dialogs::brush_options::available(app),
+7 -1
View File
@@ -873,6 +873,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{} open Artboard Options for a new artboard, filled in as artboard.new {} would make it (Artboard N, the last artboard's size, 20 pt to its right): dialog `artboardOptions` with new: true, fields name, x, y, width, height, showCenterMark, showCrossHairs; OK runs artboard.new with the fields and makes the new artboard the active one; Cancel adds nothing",
),
(
"artboard.selectAll",
"Select All Artboards",
"",
"{} the Artboard tool, chosen first when another tool is in use, selects every artboard, and the active artboard stays active; the art's selection stays as it is, and no undo step is recorded → {selected: [index…]} (the active artboard first)",
),
(
"ui.layersPanelOptions",
"Panel Options…",
@@ -1044,7 +1050,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"ui.menuDialog",
"Menu Dialog",
"",
"{command: object.move|object.rotate|object.scale|object.reflect|object.shear|object.transformEach|path.average|path.averageJoin|object.path.offsetPath|object.path.simplify|object.path.smooth|object.path.splitIntoGrid|object.vectorHalftone|artboard.rearrange|object.envelope.makeWithWarp|object.envelope.resetWithWarp|object.envelope.makeWithMesh|object.envelope.resetWithMesh|object.envelope.options|addon.arrange.grid|addon.arrange.replace|addon.arrange.size} open the dialog that command's menu item opens (dialog kind: move, rotate, scale, reflect, shear, transformEach, …, vectorHalftone, rearrangeArtboards, envelopeWarp, envelopeMesh, envelopeOptions, arrangeGrid, arrangeReplace, arrangeSize; Scale and Transform Each have `corners` and `strokes`, from the preferences, which OK updates; Transform Each opens on the values it was last confirmed with (not `copy`), and its Copy button confirms with `copy: true`; the envelope dialogs start from the selected envelope, Make opens as Reset (`reset: true`) while one is selected)",
"{command: object.move|object.rotate|object.scale|object.reflect|object.shear|object.transformEach|path.average|path.averageJoin|object.path.offsetPath|object.path.simplify|object.path.smooth|object.path.splitIntoGrid|object.vectorHalftone|artboard.rearrange|artboard.rename|object.envelope.makeWithWarp|object.envelope.resetWithWarp|object.envelope.makeWithMesh|object.envelope.resetWithMesh|object.envelope.options|addon.arrange.grid|addon.arrange.replace|addon.arrange.size} open the dialog that command's menu item opens (dialog kind: move, rotate, scale, reflect, shear, transformEach, …, vectorHalftone, rearrangeArtboards, renameArtboards, envelopeWarp, envelopeMesh, envelopeOptions, arrangeGrid, arrangeReplace, arrangeSize; Scale and Transform Each have `corners` and `strokes`, from the preferences, which OK updates; Transform Each opens on the values it was last confirmed with (not `copy`), and its Copy button confirms with `copy: true`; the envelope dialogs start from the selected envelope, Make opens as Reset (`reset: true`) while one is selected)",
),
(
"ui.widthPointEdit",
+115 -24
View File
@@ -245,20 +245,10 @@ pub(crate) fn set_props(
}
}
/// Rename the artboards of `indices` in one undo step (`artboard.setProps`): one artboard takes
/// `name` (trimmed), and several take it numbered in panel order, `name-1`, `name-2` and so on.
/// Rename the artboards of `indices` in one undo step (`artboard.rename`): one artboard takes `name`
/// (trimmed), and several take it numbered in panel order, `name-1`, `name-2` and so on.
fn rename(app: &mut VectorcraftApp, indices: &[usize], name: &str) -> Result<Value, String> {
let name = name.trim();
let mut all = indices.to_vec();
all.sort_unstable();
all.dedup();
let several = all.len() > 1;
let numbered = |k: usize| if several { format!("{name}-{}", k + 1) } else { name.to_string() };
let entries = all.iter().enumerate().map(|(k, i)| json!({"index": i, "name": numbered(k)})).collect();
match set_props_params(json!({}), entries) {
Some(p) => app.run("artboard.setProps", p),
None => Ok(Value::Null),
}
app.run("artboard.rename", json!({"indices": indices, "name": name}))
}
/// Open the name field on artboard `i`'s row. The name typed there goes to the artboards
@@ -268,32 +258,126 @@ fn start_rename(ctx: &egui::Context, app: &VectorcraftApp, i: usize) {
set_pstate(ctx, "ab-edit-all", renamed_with(app, i));
}
/// Delete the picked artboards that aren't locked and select a remaining one.
pub(crate) fn delete_selected(app: &mut VectorcraftApp) {
let n = app.session.active().map_or(0, |d| d.doc.artboards.len());
/// The artboards Delete Artboards deletes (of `n`): the picked ones that aren't locked, without
/// artboard 1 when that would be all of them. Empty when none can go, since one is always left.
pub(crate) fn delete_targets(app: &VectorcraftApp, n: usize) -> Vec<usize> {
let mut all = deletable(app, n);
if all.is_empty() {
return;
}
if n > 1 && all.len() == n {
// Deleting all artboards keeps artboard 1 (the public Delete artboards guide).
all.retain(|i| *i != 0);
}
if n > all.len() && app.run("artboard.delete", json!({ "indices": all })).is_ok() {
if n > all.len() { all } else { vec![] }
}
/// Delete the picked artboards that aren't locked and select a remaining one.
pub(crate) fn delete_selected(app: &mut VectorcraftApp) {
let n = app.session.active().map_or(0, |d| d.doc.artboards.len());
let all = delete_targets(app, n);
if !all.is_empty() && app.run("artboard.delete", json!({ "indices": all })).is_ok() {
select(app, all.first().copied().unwrap_or(0).saturating_sub(1));
}
}
/// Artboards panel › Delete Empty Artboards: delete the artboards with no artwork, then make the
/// active artboard, where it is now, active again, or else the nearest one before it that is left.
pub(crate) fn delete_empty(app: &mut VectorcraftApp) {
pub(crate) fn delete_empty(app: &mut VectorcraftApp) -> Result<Value, String> {
let n = app.session.active().map_or(0, |d| d.doc.artboards.len());
let active = selected(app, n);
let Ok(r) = app.run("artboard.deleteEmpty", json!({})) else { return };
let r = app.run("artboard.deleteEmpty", json!({}))?;
let deleted: Vec<usize> = r["deleted"].as_array().into_iter().flatten().filter_map(Value::as_u64).map(|i| i as usize).collect();
// The last artboard up to the active one that is left, numbered as it is now.
let kept = (0..=active).rev().find(|i| !deleted.contains(i)).unwrap_or(0);
select(app, kept.saturating_sub(deleted.iter().filter(|d| **d < kept).count()));
Ok(r)
}
/// What the Artboard tool's canvas menu needs to know about the document's art, which takes a walk
/// over it: [`right_click`] measures it once, and the menu reads it on every frame it is open.
#[derive(Clone, Debug)]
pub struct MenuFacts {
uid: u64,
revision: u64,
picked: Vec<usize>,
/// The right-click's point, in document coordinates: where the menu's Paste puts artboards.
pub(crate) at: vectorcraft_geom::Point,
/// Whether Delete Empty Artboards has an artboard to delete.
pub(crate) any_empty: bool,
/// Whether Fit to Artwork Bounds has art to fit a selected artboard to.
pub(crate) any_art: bool,
}
/// Whether an unlocked artboard of `indices` has art that Fit to Artwork Bounds fits it to.
pub(crate) fn fit_has_art(app: &VectorcraftApp, indices: &[usize]) -> bool {
app.session.active().is_some_and(|st| {
indices
.iter()
.filter_map(|i| st.doc.artboards.get(*i))
.any(|a| !a.locked && vectorcraft_render::encode::art_bounds_on(&st.doc, a.rect).is_some())
})
}
/// A right-click on the canvas at `p` with the Artboard tool: an artboard under the pointer that
/// isn't selected becomes the only selected one, and the menu's facts are measured.
pub(crate) fn right_click(app: &mut VectorcraftApp, p: vectorcraft_geom::Point) {
let Some(st) = app.session.active() else { return };
let (uid, n, hit) = (st.uid, st.doc.artboards.len(), st.doc.artboard_at(p));
if let Some(i) = hit.filter(|i| !picked(app, n).contains(i)) {
select(app, i);
}
let all = picked(app, n);
let any_empty = vectorcraft_engine::find_command("artboard.deleteEmpty").is_some_and(|c| (c.enabled)(&app.session).is_ok());
let any_art = fit_has_art(app, &all);
let revision = app.session.active().map_or(0, |st| st.revision);
app.ui.context_artboards = Some(MenuFacts { uid, revision, picked: all, at: p, any_empty, any_art });
}
/// The facts of the open canvas menu, while they still describe the document and the selection.
pub(crate) fn open_menu_facts(app: &VectorcraftApp) -> Option<&MenuFacts> {
let st = app.session.active()?;
let n = st.doc.artboards.len();
app.ui.context_artboards.as_ref().filter(|f| f.uid == st.uid && f.revision == st.revision && f.picked == picked(app, n))
}
/// Run an artboard command chosen from a menu or the command palette while the Artboard tool is in
/// use, with the view and the tool's selection following it as they do in the Artboards panel: a
/// new artboard becomes active, the copies are selected, and a deletion makes a remaining artboard
/// active. None for any other command or tool.
pub(crate) fn run_menu_item(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<Value, String>> {
if app.session.tool_id() != "artboard" {
return None;
}
let n = app.session.active().map_or(0, |d| d.doc.artboards.len());
Some(match id {
"artboard.new" => app.run(id, p.clone()).inspect(|r| {
if let Some(i) = r["index"].as_u64() {
select(app, i as usize);
}
}),
"artboard.duplicate" => {
let all = picked(app, n);
app.run(id, p.clone()).inspect(|r| select_copies(app, &all, r))
}
"artboard.delete" => app.run(id, p.clone()).inspect(|_| {
let listed = p["indices"].as_array().and_then(|l| l.iter().filter_map(Value::as_u64).min());
let first = listed.or(p["index"].as_u64()).unwrap_or(0) as usize;
select(app, first.saturating_sub(1));
}),
"artboard.deleteEmpty" => delete_empty(app),
_ => return None,
})
}
/// `artboard.selectAll`: the Artboard tool, chosen first when another tool is in use, selects every
/// artboard → the artboards selected, the active one first.
pub(crate) fn select_all_command(app: &mut VectorcraftApp) -> Result<Value, String> {
if app.session.active().is_none() {
return Err("no document".into());
}
if app.session.tool_id() != "artboard" {
app.select_tool("artboard");
}
select_all(app);
Ok(json!({ "selected": app.session.tool_options()["selected"] }))
}
/// Shift-click on artboard `i`'s row while the Artboard tool is in use: the tool selects the
@@ -397,8 +481,15 @@ fn duplicate(app: &mut VectorcraftApp, i: usize) {
pub(crate) fn duplicate_picked(app: &mut VectorcraftApp) {
let n = app.session.active().map_or(0, |d| d.doc.artboards.len());
let all = picked(app, n);
let active = selected(app, n);
let Ok(r) = app.run("artboard.duplicate", json!({ "indices": all })) else { return };
select_copies(app, &all, &r);
}
/// After `artboard.duplicate` of the artboards `all` returned `r`: select the copies, the active
/// artboard's copy active.
fn select_copies(app: &mut VectorcraftApp, all: &[usize], r: &Value) {
let n = app.session.active().map_or(0, |d| d.doc.artboards.len());
let active = selected(app, n);
let copies: Vec<usize> =
r["indices"].as_array().into_iter().flatten().filter_map(Value::as_u64).filter_map(|i| usize::try_from(i).ok()).collect();
let Some(first) = all.iter().position(|i| *i == active).and_then(|k| copies.get(k)).or(copies.first()).copied() else { return };
@@ -619,7 +710,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
start_rename(ui.ctx(), app, if all.contains(&sel) { sel } else { all.first().copied().unwrap_or(sel) });
}
if menu_item(ui, tl!("Delete Empty Artboards"), crate::menus::enabled(app, "artboard.deleteEmpty"), false) {
delete_empty(app);
delete_empty(app).ok();
}
ui.separator();
if menu_item(ui, tl!("Convert to Artboards"), crate::menus::enabled(app, "artboard.convertToArtboards"), false) {
@@ -1320,3 +1320,346 @@ fn the_artboard_tools_context_menu_locks_and_unlocks_artboards() {
run(&mut app, "Unlock Artboards", "Unlock All");
assert_eq!(locked(&app), [false, false, false]);
}
/// A document of `boards` artboards of 100 × 50 pt with the Artboard tool chosen.
fn menu_app(boards: usize) -> VectorcraftApp {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 100, "height": 50, "units": "Points", "artboards": boards})).unwrap();
app.select_tool("artboard");
app
}
/// The labels of `items`, submenus by their own label, separators left out.
fn menu_labels(items: &[crate::menus::Item]) -> Vec<&'static str> {
use crate::menus::Item;
items
.iter()
.filter_map(|i| match i {
Item::Cmd(l, ..) | Item::Sub(l, _) => Some(*l),
_ => None,
})
.collect()
}
/// The entry of the Artboard tool's canvas menu named `label`, chosen as a click on it chooses it.
fn choose(app: &mut VectorcraftApp, label: &str) {
use crate::menus::Item;
let items = crate::menus::context_items(app);
let Some(Item::Cmd(l, id, p)) = items.iter().find(|i| matches!(i, Item::Cmd(l, ..) if *l == label)) else { panic!("no {label}") };
let (id, p) = crate::menus::click_target(l, id, p);
crate::menus::invoke(app, &id, p);
}
fn undo_steps(app: &VectorcraftApp) -> usize {
app.session.active().unwrap().history.undo.len()
}
fn board_names(app: &VectorcraftApp) -> Vec<String> {
app.session.active().unwrap().doc.artboards.iter().map(|a| a.name.clone()).collect()
}
/// The Artboard tool's canvas menu lists the public page's entries in the page's order, with
/// nothing for objects or the view; another tool's menu lists none of them.
#[test]
fn the_artboard_tools_menu_lists_the_pages_entries_in_order() {
let mut app = menu_app(3);
app.session.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();
app.session.execute("edit.copy", &json!({})).unwrap();
assert_eq!(
menu_labels(&crate::menus::context_items(&app)),
[
"Cut",
"Copy",
"Paste",
"Undo",
"Redo",
"New Artboard",
"Duplicate Artboards",
"Rename",
"Delete Artboards",
"Delete Empty Artboards",
"Select All Artboards",
"Lock Artboards",
"Rearrange",
"Fit to Artwork Bounds",
"Switch Orientation",
"Artboard Options",
"Export",
]
);
app.select_tool("selection");
let other = menu_labels(&crate::menus::context_items(&app));
for label in
["New Artboard", "Duplicate Artboards", "Delete Artboards", "Select All Artboards", "Lock Artboards", "Rearrange", "Switch Orientation"]
{
assert!(!other.contains(&label), "{label} in {other:?}");
}
}
/// An entry whose command can't run is left out: Cut with every artboard selected, or a locked one;
/// Delete Artboards in a document of one; Select All Artboards with all selected; Rearrange with one;
/// Fit to Artwork Bounds without art; Switch Orientation on a square artboard.
#[test]
fn the_artboard_tools_menu_leaves_out_what_cannot_run() {
let shown = |app: &VectorcraftApp, label: &str| menu_labels(&crate::menus::context_items(app)).contains(&label);
let mut app = menu_app(1);
for label in ["Cut", "Delete Artboards", "Delete Empty Artboards", "Select All Artboards", "Rearrange", "Fit to Artwork Bounds"] {
assert!(!shown(&app, label), "{label} with one empty artboard");
}
assert!(shown(&app, "Copy") && shown(&app, "Switch Orientation"));
app.session.execute("artboard.setProps", &json!({"index": 0, "width": 50})).unwrap();
assert!(!shown(&app, "Switch Orientation"), "a square artboard");
let mut app = menu_app(2);
app.session.set_tool_option("selected", &json!([0, 1]));
assert!(!shown(&app, "Cut"), "every artboard selected");
assert!(!shown(&app, "Select All Artboards"));
app.session.set_tool_option("selected", &json!([1]));
app.session.execute("artboard.setProps", &json!({"index": 1, "locked": true})).unwrap();
for label in ["Cut", "Delete Artboards", "Switch Orientation"] {
assert!(!shown(&app, label), "{label} on a locked artboard");
}
assert!(shown(&app, "Rename"), "a locked artboard can be renamed");
}
/// With no selection of its own the tool has its active artboard selected, and the menu acts on it.
#[test]
fn the_artboard_tools_menu_acts_on_the_active_artboard_without_a_selection() {
let mut app = menu_app(3);
select(&mut app, 1);
choose(&mut app, "Switch Orientation");
let doc = &app.session.active().unwrap().doc;
assert_eq!((doc.artboards[1].rect.width(), doc.artboards[1].rect.height()), (50.0, 100.0));
assert_eq!(doc.artboards[0].rect.width(), 100.0);
}
/// Each entry runs its command on the selected artboards, in one undo step, and the view follows.
#[test]
fn the_artboard_tools_menu_entries_run_on_the_selected_artboards() {
let mut app = menu_app(4);
app.session.set_tool_option("selected", &json!([2, 1]));
app.view_mut().unwrap().artboard = 2;
let steps = undo_steps(&app);
choose(&mut app, "Switch Orientation");
let sizes = |app: &VectorcraftApp| app.session.active().unwrap().doc.artboards.iter().map(|a| a.rect.width()).collect::<Vec<_>>();
assert_eq!(sizes(&app), [100.0, 50.0, 50.0, 100.0]);
assert_eq!(undo_steps(&app), steps + 1, "one undo step");
choose(&mut app, "Duplicate Artboards");
assert_eq!(board_names(&app).len(), 6);
assert_eq!(undo_steps(&app), steps + 2, "one undo step");
assert_eq!(app.session.tool_options()["selected"], json!([5, 4]), "the copies are selected, artboard 3's copy active");
assert_eq!(app.view().unwrap().artboard, 5);
choose(&mut app, "Delete Artboards");
assert_eq!(board_names(&app).len(), 4);
assert_eq!(undo_steps(&app), steps + 3, "one undo step");
assert_eq!(selected(&app, 4), 3, "the artboard before the first one deleted is active");
assert_eq!(app.session.tool_options()["selected"], json!([3]), "and the only one selected");
choose(&mut app, "New Artboard");
assert_eq!(board_names(&app).len(), 5);
assert_eq!(selected(&app, 5), 4, "the new artboard is active");
assert_eq!(app.session.tool_options()["selected"], json!([4]), "and the only one selected");
choose(&mut app, "Select All Artboards");
assert_eq!(app.session.tool_options()["selected"], json!([4, 0, 1, 2, 3]));
assert_eq!(undo_steps(&app), steps + 4, "selecting records no undo step");
assert_eq!(selected(&app, 5), 4, "the active artboard stays active");
}
/// Cut and Copy from the menu take the selected artboards, in one undo step for the cut.
#[test]
fn the_artboard_tools_menu_cuts_and_copies_the_selected_artboards() {
let mut app = menu_app(4);
app.session.set_tool_option("selected", &json!([1, 2]));
app.view_mut().unwrap().artboard = 1;
choose(&mut app, "Copy");
assert_eq!(board_names(&app).len(), 4);
assert_eq!(app.session.clipboard.artboards.len(), 2);
let steps = undo_steps(&app);
choose(&mut app, "Cut");
assert_eq!(board_names(&app), ["Artboard 1", "Artboard 4"]);
assert_eq!(undo_steps(&app), steps + 1, "one undo step");
}
/// The menu's Cut and Copy record which artboards they took, so a replay in a new session, where
/// the Artboard tool isn't chosen, acts on the same artboards.
#[test]
fn the_artboard_tools_menu_replays_its_cut_and_copy_in_a_new_session() {
let mut app = menu_app(4);
app.session.set_tool_option("selected", &json!([1, 2]));
app.view_mut().unwrap().artboard = 1;
choose(&mut app, "Copy");
let (cmd, p) = app.session.journal.last().unwrap().clone();
assert_eq!((cmd.as_str(), &p["selectedArtboards"]), ("artboard.copy", &json!([1, 2])));
choose(&mut app, "Cut");
let (cmd, p) = app.session.journal.last().unwrap().clone();
assert_eq!((cmd.as_str(), &p["selectedArtboards"]), ("artboard.cut", &json!([1, 2])));
let mut replay = Session::new();
for (id, p) in &app.session.journal {
replay.execute(id, p).unwrap();
}
let names: Vec<String> = replay.active().unwrap().doc.artboards.iter().map(|a| a.name.clone()).collect();
assert_eq!(names, ["Artboard 1", "Artboard 4"]);
assert_eq!(replay.clipboard.artboards.len(), 2);
}
/// The menu's Cut and Copy publish the copied art to the system clipboard, as Edit › Cut and Copy do.
#[test]
fn the_artboard_tools_menu_publishes_its_cut_and_copy() {
let mut app = menu_app(2);
let on = app.session.active().unwrap().doc.artboards[1].rect;
app.session.execute("shape.rectangle", &json!({"x": on.x0 + 10.0, "y": on.y0 + 10.0, "width": 20, "height": 20})).unwrap();
app.session.set_tool_option("selected", &json!([1]));
app.view_mut().unwrap().artboard = 1;
for label in ["Copy", "Cut"] {
app.clipboard_out = None;
choose(&mut app, label);
assert!(app.clipboard_out.as_deref().is_some_and(|svg| svg.contains("<svg")), "{label}");
}
}
/// The menu's Cut and Copy show the keys that run them with the tool chosen.
#[test]
fn the_artboard_tools_menu_shows_the_cut_and_copy_shortcuts() {
use crate::menus::Item;
let app = menu_app(2);
let items = crate::menus::context_items(&app);
for (label, keys) in [("Cut", "edit.cut"), ("Copy", "edit.copy")] {
let Some(Item::Cmd(_, id, p)) = items.iter().find(|i| matches!(i, Item::Cmd(l, ..) if *l == label)) else { panic!("no {label}") };
let want = crate::menus::shortcut_of(keys);
assert!(want.is_some(), "{keys}");
assert_eq!(crate::menus::item_shortcut(id, p), want, "{label}");
}
}
/// Fit to Artwork Bounds fits the selected artboards only, in one undo step, and Undo from the menu
/// takes the step back.
#[test]
fn the_artboard_tools_menu_fits_the_selected_artboards_and_undoes() {
let mut app = menu_app(2);
let rects = |app: &VectorcraftApp| app.session.active().unwrap().doc.artboards.iter().map(|a| a.rect).collect::<Vec<_>>();
let before = rects(&app);
let on = |r: vectorcraft_geom::Rect| json!({"x": r.x0 + 10.0, "y": r.y0 + 10.0, "width": 20, "height": 20});
app.session.execute("shape.rectangle", &on(before[0])).unwrap();
app.session.execute("shape.rectangle", &on(before[1])).unwrap();
app.session.set_tool_option("selected", &json!([1]));
app.view_mut().unwrap().artboard = 1;
let steps = undo_steps(&app);
choose(&mut app, "Fit to Artwork Bounds");
let fitted = rects(&app);
assert_eq!(fitted[0], before[0], "the artboard that isn't selected stays");
assert_eq!((fitted[1].width(), fitted[1].height()), (21.0, 21.0), "the rectangle with its 1 pt stroke");
assert_eq!(undo_steps(&app), steps + 1, "one undo step");
choose(&mut app, "Undo");
assert_eq!(rects(&app), before);
}
/// Delete Empty Artboards leaves a remaining artboard active, and the dialog entries open their
/// dialogs: Rename, Rearrange, Artboard Options and Export.
#[test]
fn the_artboard_tools_menu_deletes_empty_artboards_and_opens_dialogs() {
let mut app = menu_app(3);
app.session.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();
for (label, kind) in [
("Rename", crate::dialogs::rename_artboards::KIND),
("Rearrange", crate::dialogs::rearrange_artboards::KIND),
("Artboard Options", "artboardOptions"),
("Export", crate::dialogs::EXPORT_FOR_SCREENS),
] {
choose(&mut app, label);
assert_eq!(app.ui.dialog.take().map(|d| d.kind).as_deref(), Some(kind), "{label}");
}
select(&mut app, 2);
choose(&mut app, "Delete Empty Artboards");
assert_eq!(board_names(&app), ["Artboard 1"]);
assert_eq!(selected(&app, 1), 0);
}
/// A lock entry lists only the artboards it changes and is left out when it has none, so it never
/// records an undo step that changes nothing.
#[test]
fn the_artboard_tools_lock_entries_have_something_to_change() {
use crate::menus::Item;
let mut app = menu_app(2);
app.session.execute("artboard.setProps", &json!({"index": 0, "locked": true})).unwrap();
let items = crate::menus::context_items(&app);
let subs = |menu: &str| match items.iter().find(|i| matches!(i, Item::Sub(t, _) if *t == menu)) {
Some(Item::Sub(_, subs)) => menu_labels(subs),
_ => vec![],
};
assert_eq!(subs("Lock Artboards"), ["Lock All Except Selected", "Lock All"], "the selected artboard is locked already");
assert_eq!(subs("Unlock Artboards"), ["Unlock Selected", "Unlock All"]);
}
/// A right-click on an artboard that isn't selected selects it alone; on a selected one the selection
/// stays; off every artboard it stays too.
#[test]
fn a_right_click_with_the_artboard_tool_selects_the_artboard_under_the_pointer() {
use vectorcraft_geom::Point;
let mut app = menu_app(3);
let doc = &app.session.active().unwrap().doc;
let (a, c) = (doc.artboards[0].rect.center(), doc.artboards[2].rect.center());
app.session.set_tool_option("selected", &json!([0, 1]));
right_click(&mut app, a);
assert_eq!(app.session.tool_options()["selected"], json!([0, 1]), "a selected artboard keeps the selection");
right_click(&mut app, Point::new(-500.0, -500.0));
assert_eq!(app.session.tool_options()["selected"], json!([0, 1]), "off every artboard");
right_click(&mut app, c);
assert_eq!(app.session.tool_options()["selected"], json!([2]));
assert_eq!(app.view().unwrap().artboard, 2);
}
/// Paste from the Artboard tool's menu centers the copied artboards on the right-click's point. With
/// no point it puts them right of the last artboard.
#[test]
fn the_artboard_tools_paste_goes_to_the_right_click() {
use vectorcraft_geom::Point;
let mut app = menu_app(2);
app.run("edit.copy", json!({})).unwrap();
right_click(&mut app, Point::new(-300.0, 700.0));
choose(&mut app, "Paste");
let doc = &app.session.active().unwrap().doc;
assert_eq!(doc.artboards.len(), 3);
let at = doc.artboards[2].rect.center();
assert_eq!((at.x, at.y), (-300.0, 700.0));
app.ui.context_artboards = None;
choose(&mut app, "Paste");
let doc = &app.session.active().unwrap().doc;
let right = doc.artboards.iter().take(3).map(|a| a.rect.x1).fold(f64::MIN, f64::max);
assert_eq!(doc.artboards[3].rect.x0, right + 20.0);
}
/// While the menu is open it reads what the right-click measured and does not walk the art again:
/// what it measured stands until the document or the selection changes.
#[test]
fn the_open_menu_reads_what_the_right_click_measured() {
use vectorcraft_geom::Point;
let mut app = menu_app(2);
right_click(&mut app, Point::new(10.0, 10.0));
let facts = app.ui.context_artboards.as_mut().unwrap();
assert!(facts.any_empty && !facts.any_art);
// Facts that differ from the document show which one the menu reads.
facts.any_empty = false;
facts.any_art = true;
let labels = menu_labels(&crate::menus::context_items(&app));
assert!(!labels.contains(&"Delete Empty Artboards") && labels.contains(&"Fit to Artwork Bounds"), "{labels:?}");
// An edit makes them out of date, and the menu measures again.
app.session.execute("artboard.setProps", &json!({"index": 1, "name": "Other"})).unwrap();
let labels = menu_labels(&crate::menus::context_items(&app));
assert!(labels.contains(&"Delete Empty Artboards") && !labels.contains(&"Fit to Artwork Bounds"), "{labels:?}");
}
/// `artboard.selectAll` chooses the Artboard tool first when another tool is in use, and fails
/// without a document.
#[test]
fn select_all_artboards_chooses_the_tool_and_needs_a_document() {
let mut app = menu_app(2);
app.select_tool("selection");
assert_eq!(app.run("artboard.selectAll", json!({})).unwrap()["selected"], json!([0, 1]));
assert_eq!(app.session.tool_id(), "artboard");
let mut empty = VectorcraftApp::new(Session::new(), Default::default());
assert_eq!(empty.run("artboard.selectAll", json!({})).unwrap_err(), "no document");
}
+5
View File
@@ -397,6 +397,10 @@ pub struct UiState {
/// is open. Not saved.
#[serde(skip)]
pub context_intertwine: Option<(u64, vectorcraft_doc::NodeId, vectorcraft_geom::Point, vectorcraft_doc::NodeId)>,
/// What the last right-click on the canvas with the Artboard tool measured, for the context menu
/// while it is open ([`crate::panels::artboards::MenuFacts`]). Not saved.
#[serde(skip)]
pub context_artboards: Option<crate::panels::artboards::MenuFacts>,
/// Which link statuses the active document's images and placed documents have, for Select ›
/// Linked, Embedded, Missing and Modified to be on only when some do (refreshed by
/// [`crate::addons::select::link_status::refresh`]). Not saved.
@@ -965,6 +969,7 @@ impl Default for UiState {
shape_modes_compound: false,
context_locked: None,
context_intertwine: None,
context_artboards: None,
link_statuses: Default::default(),
slot_tool: Default::default(),
floating_flyouts: vec![],
+4 -1
View File
@@ -371,10 +371,13 @@ fn every_context_label_is_a_menu_string() {
app.ui.context_locked = Some((app.session.active().unwrap().uid, vec![a]));
shown.extend(labels(&menus::context_items(&app)));
// The Lock Artboards and Unlock Artboards submenus of the Artboard tool.
app.run("artboard.new", json!({"x": 400, "y": 0, "width": 100, "height": 100})).unwrap();
app.run("artboard.new", json!({"x": 400, "y": 0, "width": 100, "height": 80})).unwrap();
app.run("artboard.setProps", json!({"index": 0, "locked": true})).unwrap();
app.select_tool("artboard");
shown.extend(labels(&menus::context_items(&app)));
// Lock Selected with the artboard that isn't locked active.
crate::panels::artboards::select(&mut app, 1);
shown.extend(labels(&menus::context_items(&app)));
let missing: Vec<_> = shown.iter().filter(|l| !strings.contains(**l)).collect();
assert!(missing.is_empty(), "context menu labels missing from menu_strings: {missing:?}");
for label in menus::CONTEXT_LABELS {
+2 -2
View File
@@ -94,7 +94,7 @@ the two can still win that race.
| `document.inspect` | `{depth?, childLimit?, ids?, kind?, limit?, offset?}` | layer tree, selection, the key object (`key`, null without one; a key ruler guide as `keyGuide`), history, paint defaults; `depth` / `childLimit` slice the layer tree as `document.node {summary: true}` does; `ids`, `kind`, `limit` and `offset` list just some nodes as `nodes` (with `total`) instead of the whole tree, see [mcp.md](mcp.md). Each node reports `opacity` below 1 and `blend` when it isn't Normal; objects and compound paths report `fill`, `stroke` and `strokeWidth`, a compound path's members `compoundMember: true` instead |
| `ui.inspect` | | tool, UI state, `screenMode` (0 normal, 1 full screen with menu bar, 2 full screen, 3 Presentation Mode), `taskBar` (`pinned`, and `rect` `[x, y, w, h]` while the Contextual Task Bar shows), `freeTransformWidget` (`[x, y, w, h]` while the Free Transform widget shows), view, canvas rect, window size, perf, background saves and exports still running, `nativeMenuBar` (true when the menus are in the macOS menu bar rather than the window; `ui.menu.list` lists the in-window menus either way), `fileDialog` (`"open"`, `"place"`, `"save"` or `"folder"` while a native file dialog is open, `null` otherwise; see below) |
| `ui.menu.list` / `ui.menu.invoke` | `{command, params}` | the full menu tree / invoke an item |
| `ui.contextMenu.list` | | the canvas context menu for the current selection, flattened like `ui.menu.list` (`path` holds its submenus). `ui.click {x, y, button: "right"}` on the canvas opens it, after selecting the object there unless it is already selected. While a menu opened over locked objects is open, it has an `Unlock` submenu with one item for each locked object under the pointer, topmost first and at most 20, labeled with the object's name, which runs `layer.setProps {ids: [id], locked: false}`. A locked group gets one item, and the locked objects inside it get none. Hidden objects, objects on hidden, locked or template layers, and in isolation mode objects outside the isolated group get no item |
| `ui.contextMenu.list` | | the canvas context menu for the current selection, flattened like `ui.menu.list` (`path` holds its submenus). `ui.click {x, y, button: "right"}` on the canvas opens it, after selecting the object there unless it is already selected. With the Artboard tool it lists the artboard entries (see `docs/mcp.md`), after selecting the artboard there unless it is already selected. While a menu opened over locked objects is open, it has an `Unlock` submenu with one item for each locked object under the pointer, topmost first and at most 20, labeled with the object's name, which runs `layer.setProps {ids: [id], locked: false}`. A locked group gets one item, and the locked objects inside it get none. Hidden objects, objects on hidden, locked or template layers, and in isolation mode objects outside the isolated group get no item |
| `ui.tool.select` / `ui.tool.list` | `{tool}` | |
| `ui.pointer` | `{events:[{kind: down|drag|up|move|doubleclick, x, y, space?: "doc"|"screen", mods?}]}` | drive the active tool exactly like the mouse: `mods.cmd` held at a press with any tool but a selection tool drags with the selection tool chosen last (Direct Selection with the Pen until one is), and the release gives the tool back as it was; the reply lists `selection`, `key` (the key object, null without one) and `tool`. With the Shape Builder or the Live Paint Bucket and a selection of more than 128 segments, the app finds the regions off the UI thread, so a `move` replies before the region under the pointer is highlighted, and the highlight shows in the first frame after the regions are found |
| `ui.key` / `ui.text` | `{key, shift?, alt?, cmd?}` / `{text}` | synthetic keyboard input |
@@ -645,7 +645,7 @@ dialog, a drop) opens the `textImport` dialog (fields `platform`: `windows`/`mac
`removeLineReturns`, `removeParagraphReturns`, `replaceSpaces`, `spaces`). `ui.dialog.confirm` places the file as area
type with those options (`file.place {text: {…}}`).
System clipboard formats: through the desktop app, `edit.copy` and `edit.cut` publish what `clipboard.flavours`
System clipboard formats: through the desktop app, `edit.copy`, `edit.cut`, `artboard.copy` and `artboard.cut` publish what `clipboard.flavours`
lists to the system clipboard (once they are written, the status bar shows the first of the PNG's and PDF's
`warnings`, such as an image that couldn't be decoded), and every Paste command first loads what another app copied (`clipboard.importSvg`,
`importPdf`, `importEps`, `importText` or `importImage`, centred in the view; the desktop app reads PDF and EPS from
+1 -1
View File
@@ -436,7 +436,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
- **Done (2026-10-29, #4522):** the Pathfinder panel menu's Make, Release and Expand Compound Shape, which were dimmed placeholders, and the panel's Expand button act on live compound shapes ([Create compound shapes with Pathfinder](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/create-compound-shapes-with-pathfinder.html)): Alt-clicking a Shape Mode makes one in that mode, a Shape Mode sets the mode of the members selected, and the compound shape draws, exports and saves as the area its members' modes make (`object.compoundShape.*`, `crates/engine/src/tests/compound_shape.rs`). Left: blends, envelopes and warps as members, question marks on the buttons for mixed member modes, clicks that hit the area rather than the members. Estimate unchanged.
- **Done (2026-10-25, #3426):** Pathfinder panel › Pathfinder Options… ([Pathfinder panel overview](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/pathfinder-panel-overview.html)): Precision (0.001–10 pt; results are refit to within it and pieces thinner than it are dropped), Remove Redundant Points and Divide and Outline Will Remove Unpainted Artwork (pieces of objects with no fill), kept in the preferences (`object.pathfinder.options`, group `pathfinder`) and used by every Shape Mode and Pathfinder button and Repeat; the page gives no defaults, so ours (0.01 pt, both off) keep the earlier results byte for byte. Trap is left as it was. Estimate unchanged.
- **Done (2026-10-25):** Artboards panel › Delete Empty Artboards ([Delete artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/delete-artboards.html), [artboard context menu](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/use-artboard-context-menu.html)): `artboard.deleteEmpty` deletes every unlocked artboard with no artwork, with its guides, in one undo step, and keeps artboard 1 when every artboard is empty and none is locked; an artboard is empty when no object (hidden, locked and template art included; guides not) has visual bounds that meet or touch it, a group counting by its members and a clip group or clipping layer by its members inside the clip (our reading of "no artwork"; the pages give no finer rule). The canvas context menu doesn't list it yet. Estimate unchanged.
- **Done (2026-10-25):** Artboards panel › Delete Empty Artboards ([Delete artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/delete-artboards.html), [artboard context menu](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/use-artboard-context-menu.html)): `artboard.deleteEmpty` deletes every unlocked artboard with no artwork, with its guides, in one undo step, and keeps artboard 1 when every artboard is empty and none is locked; an artboard is empty when no object (hidden, locked and template art included; guides not) has visual bounds that meet or touch it, a group counting by its members and a clip group or clipping layer by its members inside the clip (our reading of "no artwork"; the pages give no finer rule). The Artboard tool's canvas context menu lists it too. Estimate unchanged.
- **Done (2026-10-25):** the Transform panel menu's Transform Object Only, Transform Pattern Only and Transform Both
(`transformPanelPatterns`) choose what the panel's fields, flips and menu flips and the Properties panel's Transform
section transform: the object with its pattern tiles left as they are, the tiles alone, or both; Use Registration
+50 -2
View File
@@ -3398,7 +3398,8 @@ the art on that artboard, in one undo step. The art is what an export draws, mea
effects on paths, compound paths and symbol instances. An object counts for every artboard its bounds overlap, also
when it reaches past the artboard's edge, and a group or a clipping layer counts whole. An artboard with no art on it,
or a locked one, keeps its size and place. The call fails with `the artboard is locked` when every artboard it names is
locked, and with `no artwork` when none of the others has art.
locked, and with `no artwork` when none of the others has art. When every artboard already fits its art, no undo step
is recorded.
`artboard.fitToSelection {index?}` (Object › Artboards › Fit to Selected Art) fits artboard `index` to the selected
art. Both commands take 0-based indexes and resize the first artboard when the call names none. The desktop app adds
the selected artboards (those the Artboard tool has selected, else the active one) as `indices` to the
@@ -3941,7 +3942,8 @@ Then `edit.paste` adds a copy of each artboard, together right of the last one a
in the layers it came from, in one undo step (`edit.pasteInPlace`, `edit.pasteInFront` and `edit.pasteInBack` put
them where they were; `edit.pasteOnAllArtboards` pastes only the art), in this document or another; the result's
`artboards` lists the new artboards' indices and `artboard` is the first of them, and the Artboard tool, when chosen,
selects them. `artboard.duplicate {index? | indices?, art?}` does the same in one step → `{index, indices, ids}`.
selects them. `edit.paste {artboardsAt: [x, y]}` instead centers the copied artboards on that point, as they lay, and a
value that is not `[x, y]` fails with `` `artboardsAt` must be [x, y] ``. `artboard.duplicate {index? | indices?, art?}` does the same in one step → `{index, indices, ids}`.
Window › Artboards › Duplicate Artboards passes the selected artboards (those the Artboard tool has selected, else the
active one) as `indices`, and a row dragged onto New Artboard passes its artboard as `index`.
`artboard.move {copy: true, moveArt: true}` is the Artboard tool's Alt-drag. `artboard.duplicate` and `artboard.move`
@@ -4002,6 +4004,52 @@ it; a group counts by its members, a clip group or clipping layer by its members
artboard left, artboard 1 can go). With
nothing to delete the command is disabled and fails with `no empty artboards to delete`.
Rename Artboards: `artboard.rename {index? | indices?, name}` renames artboards in one undo step, by default those the
Artboard tool has selected, else its active one. One artboard takes `name`, trimmed. Several take it numbered in artboard
order, `name-1`, `name-2` and so on. A locked artboard is renamed too. An empty name fails with
`` `name` must not be empty ``, a name that is not a string fails with `` `name` must be a string ``, and an artboard
that is not in the document fails with `no such artboard`. When every artboard already has its new name, the command
records no undo step → `{renamed: [index…]}`. With no params, a menu click opens the `renameArtboards` dialog (fields
`name` and `indices`), and OK with an empty name changes nothing. The Artboards panel's name field runs this command too.
Switch Orientation: `artboard.switchOrientation {index? | indices?, scaleArt?, patterns?, lockedAndHidden?}` swaps the width and height of each
artboard in one undo step, by default those the Artboard tool has selected, else its active one. Each top-left corner
stays put, as with the Properties panel's Portrait and Landscape buttons. This rule for several artboards is VectorCraft's
own. A square artboard and a locked one stay as they are, and the call fails with `the artboard is locked` when every
artboard it names is locked. When no artboard changes, the command records no undo step → `{switched: [index…]}`.
`scaleArt` scales the art with each artboard and defaults to the Artboard tool's Scale Artwork with Artboard option.
`patterns` and `lockedAndHidden` work as they do on `artboard.setProps`.
When a call to `artboard.duplicate`, `artboard.copy`, `artboard.cut`, `artboard.rename` or `artboard.switchOrientation`
names no artboard, its journal entry records the artboards it acted on as `selectedArtboards`. A replay of the journal
acts on those artboards, and a recorded action leaves the value out and acts on the selection when it plays.
`artboard.selectAll {}` chooses the Artboard tool when another tool is in use and selects every artboard. The active
artboard stays active, the art's selection stays as it is, and no undo step is recorded → `{selected: [index…]}`, the
active artboard first.
The canvas context menu with the Artboard tool lists the entries of the public
[Use the artboard context menu](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/use-artboard-context-menu.html)
page in the page's order, and no entries for objects or the view: Cut (`artboard.cut`), Copy (`artboard.copy`), Paste
(`edit.paste`), Undo, Redo, New Artboard (`artboard.new`), Duplicate Artboards (`artboard.duplicate`), Rename
(`artboard.rename`), Delete Artboards (`artboard.delete` with `indices`), Delete Empty Artboards
(`artboard.deleteEmpty`), Select All Artboards (`artboard.selectAll`), Lock Artboards and Unlock Artboards
(`artboard.setProps` with `locked`), Rearrange (`artboard.rearrange`), Fit to Artwork Bounds (`artboard.fitToArt`),
Switch Orientation (`artboard.switchOrientation`), Artboard Options (`tool.options {tool: "artboard"}`) and Export
(`file.exportForScreens`). Exit Isolation Mode and the Unlock submenu for locked objects under the pointer follow them.
Cut, Copy, Duplicate Artboards, Rename, Delete Artboards, Lock Selected, Unlock Selected, Fit to Artwork Bounds, Switch
Orientation and Export act on the artboards the tool has selected, else the active one. Artboard Options opens on the
active artboard. New Artboard, Paste, Undo, Redo, Delete Empty Artboards, Select All Artboards and Rearrange do not act on
the selected artboards. Lock All and Unlock All act on every artboard, and Lock All Except Selected acts on the artboards
that are not selected. A right-click on an artboard that is not selected first selects it alone. An entry that cannot
run is left out: Cut with a locked artboard or every artboard selected, Delete Artboards with no artboard to delete, Select All Artboards with every artboard selected, Rearrange with
one artboard, Fit to Artwork Bounds with no art on a selected unlocked artboard, Switch Orientation with no selected
artboard to change, and a lock entry with no artboard to change. The menu measures the art for Delete Empty Artboards and
Fit to Artwork Bounds once, at the right-click. Paste centers copied artboards on the right-click's point
(`edit.paste {artboardsAt: [x, y]}`) and centers copied objects there too (`center`). Chosen from a menu or the command
palette with the tool in use, `artboard.new` makes the new artboard active, `artboard.duplicate` selects the copies,
and `artboard.delete` and `artboard.deleteEmpty` make a remaining artboard active.
Locked artboards: `artboard.setProps {index, locked: true}` locks an artboard and `{index, locked: false}` unlocks it,
each in one undo step. The Artboards panel's lock column, left of a row's options icon, shows a lock on a locked
artboard, and a click there runs this command. A locked artboard keeps its place and size. `artboard.setProps` with a
+1
View File
@@ -923,6 +923,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
Object › Artboards › Fit to Selected Art resizes the active artboard (`artboard.fitToSelection {index}`). It used to resize artboard 1 whichever artboard was active. When an action plays, its Fit to Artwork Bounds and Fit to Selected Art steps that name no artboard resize the artboard that is active then.
Object › Artboards › Fit to Artwork Bounds and the Artboards panel menu's Fit to Artwork Bounds fit each selected artboard (those the Artboard tool has selected, else the active one) to the art on it, as the public [Use the artboard context menu](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/use-artboard-context-menu.html) page describes (`artboard.fitToArt {indices}`). An object partly on an artboard counts for each artboard it overlaps, and an artboard with no art on it keeps its size and place. They used to fit the active artboard to all the art in the document.
The Artboards panel menu's Duplicate Artboards copies each selected artboard (those the Artboard tool has selected, else the active one) with its art, in one undo step, and the copies are then selected, as the public [Use the artboard context menu](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/use-artboard-context-menu.html) page describes. With the Artboard tool, Copy and Cut take every selected artboard with its art, as the public [Cut, copy, and paste artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/cut-copy-and-paste-artboards.html) page describes, and Paste brings them all back in one undo step (`artboard.duplicate`, `artboard.copy` and `artboard.cut` with `indices`). The copies keep their places relative to each other, together right of the last artboard. All three used to act on the active artboard only.
With the Artboard tool, the canvas context menu lists the entries of the public [Use the artboard context menu](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/use-artboard-context-menu.html) page in its order: Cut, Copy, Paste at the pointer, Undo, Redo, New Artboard, Duplicate Artboards, Rename (`artboard.rename`), Delete Artboards, Delete Empty Artboards, Select All Artboards (`artboard.selectAll`), Lock Artboards and Unlock Artboards, Rearrange, Fit to Artwork Bounds, Switch Orientation (`artboard.switchOrientation`), Artboard Options and Export. A right-click on an artboard that is not selected selects it first.
The Artboards panel menu's New Artboard adds the artboard in one undo step and makes it the active one, as the panel's button, the Control bar and Properties do; with the Artboard tool it is then the only one selected. This is VectorCraft's own rule. The menu item used to leave the active artboard where it was.
With the Artboard tool, Paste and Paste in Place make the first pasted artboard the active one (the navigator's and the Artboards panel's), and Cut makes the artboard before the first cut one active (the new first artboard when artboard 1 was cut). A command that chooses another document (`document.activate`) starts the tool on that document's active artboard, as choosing its tab does, and File › Revert keeps the active artboard and starts the tool on it. Paste used to leave the active artboard where it was while the tool had the pasted artboard active, and Cut left the active artboard on the artboard after the cut one while the tool had the one before it.
With the Artboard tool, an action that reverts the document or switches to another document and back (`command.batch`), and the Actions panel menu's Batch… over a folder whose files it closes or over data sets with a Revert step (`action.batch`), keep the active artboard and start the tool on it, as File › Revert does. They used to make artboard 1 the active one.