mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 08:29:14 +00:00
Align: the Align panel aligns and distributes the selected artboards
With the Artboard tool chosen, the Align panel's Align Objects, Distribute Objects and Distribute Spacing buttons, their UI commands and the Control bar's Align and Distribute buttons act on the artboards the tool has selected, as the public page describes: https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/align-and-distribute-artboards.html - artboard.align, artboard.distribute and artboard.distributeSpacing move whole artboards with their guides, and with the art inside them when Move Artwork with Artboard is on, in one undo step. No undo step is recorded when no artboard moves. - Align To offers Align to Selection and Align to Artboard. Align to Artboard keeps the active artboard in place, and the Distribute buttons then send the Distribute Spacing value. A locked artboard stays where it is. These rules are VectorCraft's own. - The Control bar shows the Align buttons, the Align To menu and the Distribute buttons after the artboard controls while it has room. Docs: docs/mcp.md, docs/ui-parity.md.
This commit is contained in:
@@ -4,6 +4,7 @@ pub mod actionset;
|
||||
mod addons;
|
||||
mod apiref;
|
||||
pub(crate) mod appearance;
|
||||
mod artboardalign;
|
||||
mod assets;
|
||||
mod attributes;
|
||||
pub mod brushlib;
|
||||
@@ -334,6 +335,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
||||
v.extend(variablecmds::specs());
|
||||
v.extend(docinfo::specs());
|
||||
v.extend(panelcmds::specs());
|
||||
v.extend(artboardalign::specs());
|
||||
v.extend(buildcmds::specs());
|
||||
v.extend(shaper::specs());
|
||||
v.extend(brushsym::specs());
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
//! Align and Distribute for artboards: what the Align panel's buttons run while the Artboard tool
|
||||
//! is chosen.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::Scaling;
|
||||
use vectorcraft_geom::{Rect, Vec2};
|
||||
|
||||
use super::object::choice;
|
||||
use super::panelcmds::{artboard_index, artboards_art, locked_and_hidden, named_or_selected, note_artboards};
|
||||
use super::{Affine, CommandSpec, NodeId, Result, Session, bad, check_switches, cmd, has_doc, switch_or};
|
||||
use crate::EngineError;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
"artboard.align",
|
||||
"Align",
|
||||
[],
|
||||
None,
|
||||
"{index? | indices?: [n…], selectedArtboards?: [n…] (as artboard.duplicate; default: the artboards the Artboard tool has selected, else its active one), horizontal?: \"left\"|\"center\"|\"right\", vertical?: \"top\"|\"center\"|\"bottom\" (one of the two is needed), to?: \"selection\"|\"artboard\" (default \"selection\": the bounds of the artboards together; \"artboard\": artboard `artboard`, which stays where it is), artboard?: n (0-based; the artboard to align to with to: \"artboard\"; default: the Artboard tool's active one; it need not be one of the artboards aligned), activeArtboard?: n (the journal records that default here, for a replay; `artboard` goes ahead of it), moveArt?: bool (the art fully inside each artboard moves with it; default: the Artboard tool's Move Artwork with Artboard option), patterns?: bool, lockedAndHidden?: bool (as artboard.move)} (Align panel › Align Objects with the Artboard tool chosen) each artboard moves until its edge or center is on that edge or center of what it aligns to, with its guides, in one undo step; a locked artboard stays where it is and still counts in the bounds, and the call fails with `the artboard is locked` when every artboard it names is locked; when no artboard moves, no undo step is recorded → {moved: [index…]}",
|
||||
has_doc,
|
||||
align
|
||||
),
|
||||
cmd!(
|
||||
"artboard.distribute",
|
||||
"Distribute",
|
||||
[],
|
||||
None,
|
||||
"{index? | indices?, selectedArtboards? (as artboard.align), horizontal?: \"left\"|\"center\"|\"right\" | vertical?: \"top\"|\"center\"|\"bottom\" (one is needed; horizontal goes first), to?: \"selection\"|\"artboard\", artboard?: n, activeArtboard?: n (as artboard.align; with `spacing` the artboard must be one of those distributed), spacing?: pt (used with to: \"artboard\"), moveArt?, patterns?, lockedAndHidden? (as artboard.align)} (Align panel › Distribute Objects with the Artboard tool chosen) the named edges or centers of the artboards are spaced evenly between the first and the last along the axis, which stay where they are (three or more artboards); with to: \"artboard\", the artboards are spaced the same way and then moved together so that artboard `artboard`, when it is one of them, stays where it is; with to: \"artboard\" and `spacing`, that artboard stays and the edges or centers are placed `spacing` apart, outward from it in their order (two or more); a locked artboard stays where it is and keeps its place in the order, and the call fails with `the artboard is locked` when every artboard it names is locked; a spacing that would move an artboard past the canvas is an error; with fewer artboards, or when none moves, no undo step is recorded → {moved: [index…]}",
|
||||
has_doc,
|
||||
distribute
|
||||
),
|
||||
cmd!(
|
||||
"artboard.distributeSpacing",
|
||||
"Distribute Spacing",
|
||||
[],
|
||||
None,
|
||||
"{index? | indices?, selectedArtboards? (as artboard.align), axis?: \"horizontal\"|\"vertical\" (default horizontal), to?: \"selection\"|\"artboard\", artboard?: n, activeArtboard?: n (as artboard.distribute), spacing?: pt (used with to: \"artboard\"), moveArt?, patterns?, lockedAndHidden? (as artboard.align)} (Align panel › Distribute Spacing with the Artboard tool chosen) the gaps between the artboards along the axis become equal, the first and the last staying where they are (three or more artboards); with to: \"artboard\", the gaps become equal the same way and the artboards then move together so that artboard `artboard`, when it is one of them, stays where it is; with to: \"artboard\" and `spacing`, that artboard stays and the others are placed with a gap of `spacing` between neighbors (two or more; a negative spacing overlaps them); a locked artboard stays where it is and keeps its place in the order, and the call fails with `the artboard is locked` when every artboard it names is locked; a spacing that would move an artboard past the canvas is an error; with fewer artboards, or when none moves, no undo step is recorded → {moved: [index…]}",
|
||||
has_doc,
|
||||
distribute_spacing
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// What an artboard Align or Distribute command acts on, read from its params.
|
||||
struct Boards {
|
||||
/// The artboards named, in artboard order, each with its rectangle.
|
||||
items: Vec<(usize, Rect)>,
|
||||
/// With `to: "artboard"`, the artboard to align to and its rectangle.
|
||||
reference: Option<(usize, Rect)>,
|
||||
/// Whether the art inside each artboard moves with it.
|
||||
move_art: bool,
|
||||
/// Whether locked and hidden art moves too.
|
||||
locked_art: bool,
|
||||
}
|
||||
|
||||
/// Reads the params that `artboard.align`, `artboard.distribute` and `artboard.distributeSpacing`
|
||||
/// (command `cmd`) share, and adds the artboards and the art choice to the journal entry. The
|
||||
/// artboard to align to is `artboard`, else `activeArtboard` (a replay of the journal), else the
|
||||
/// Artboard tool's active one, which the journal entry then records as `activeArtboard`. It fails
|
||||
/// when every artboard named is locked.
|
||||
fn boards(s: &mut Session, p: &Value, cmd: &str) -> Result<Boards> {
|
||||
check_switches(p, &["patterns"], cmd)?;
|
||||
let to_artboard = choice(p, cmd, "to", &["selection", "artboard"])? == Some("artboard");
|
||||
let indices = named_or_selected(s, p, cmd)?;
|
||||
let given = |key: &str| -> Result<Option<usize>> {
|
||||
match p.get(key).filter(|v| !v.is_null()) {
|
||||
Some(v) => artboard_index(&s.doc()?.doc, &json!({ "index": v }), cmd).map(Some),
|
||||
None => Ok(None),
|
||||
}
|
||||
};
|
||||
let (named, recorded) = (given("artboard")?, given("activeArtboard")?);
|
||||
let move_art = switch_or(p, "moveArt", s.tool_options_of("artboard")["moveArt"].as_bool().unwrap_or(true), cmd)?;
|
||||
let locked_art = locked_and_hidden(s, p, move_art, cmd)?;
|
||||
note_artboards(s, p, &indices);
|
||||
s.note_journal("moveArt", json!(move_art));
|
||||
let reference = match (to_artboard, named.or(recorded)) {
|
||||
(false, _) => None,
|
||||
(true, Some(i)) => Some(i),
|
||||
(true, None) => {
|
||||
let active = s.tool_options_of("artboard")["active"].clone();
|
||||
Some(artboard_index(&s.doc()?.doc, &json!({ "index": active }), cmd)?)
|
||||
}
|
||||
};
|
||||
if let Some(i) = reference.filter(|_| named.is_none()) {
|
||||
s.note_journal("activeArtboard", json!(i));
|
||||
}
|
||||
let d = &s.doc()?.doc;
|
||||
let rect = |i: usize| d.artboards.get(i).map(|a| (i, a.rect)).ok_or_else(|| EngineError::Other("no such artboard".into()));
|
||||
let items = indices.iter().map(|i| rect(*i)).collect::<Result<Vec<_>>>()?;
|
||||
if indices.iter().all(|i| d.artboards.get(*i).is_some_and(|a| a.locked)) {
|
||||
return Err(EngineError::Other("the artboard is locked".into()));
|
||||
}
|
||||
Ok(Boards { items, reference: reference.map(rect).transpose()?, move_art, locked_art })
|
||||
}
|
||||
|
||||
/// `spacing` of the Distribute commands: a finite number, or `None` when it is missing or null.
|
||||
fn spacing(p: &Value, cmd: &str) -> Result<Option<f64>> {
|
||||
p.get("spacing")
|
||||
.filter(|v| !v.is_null())
|
||||
.map(|v| v.as_f64().filter(|g| g.is_finite()).ok_or_else(|| bad(cmd, "`spacing` must be a number")))
|
||||
.transpose()
|
||||
}
|
||||
|
||||
/// The place in `items` of the artboard `b` aligns to, which the Distribute commands keep where it
|
||||
/// is. `None` without one, or when it is not one of `items`; with a `gap` that is an error
|
||||
/// (`no such artboard`), since the gap is measured from it.
|
||||
fn kept(b: &Boards, items: &[(usize, Rect)], gap: Option<f64>) -> Result<Option<usize>> {
|
||||
let Some((reference, _)) = b.reference else { return Ok(None) };
|
||||
match items.iter().position(|(i, _)| *i == reference) {
|
||||
None if gap.is_some() => Err(EngineError::Other("no such artboard".into())),
|
||||
at => Ok(at),
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves each artboard of `moves` by its offset as undo step `label`, with its guides and, when
|
||||
/// `b` says so, the art inside it. Locked artboards stay where they are. When no artboard moves
|
||||
/// more than 1e-9 pt on either axis, the document and its undo history stay as they are. The edit
|
||||
/// fails with nothing changed when an artboard would end up past the canvas, as every edit does.
|
||||
fn apply(s: &mut Session, label: &str, p: &Value, b: &Boards, moves: &[(usize, Vec2)]) -> Result<Value> {
|
||||
let d = &s.doc()?.doc;
|
||||
let moves: Vec<(usize, Vec2)> =
|
||||
moves.iter().copied().filter(|(i, dv)| (dv.x.abs() > 1e-9 || dv.y.abs() > 1e-9) && d.artboards.get(*i).is_some_and(|a| !a.locked)).collect();
|
||||
let moved: Vec<usize> = moves.iter().map(|(i, _)| *i).collect();
|
||||
if moves.is_empty() {
|
||||
return Ok(json!({ "moved": moved }));
|
||||
}
|
||||
let arts = artboards_art(s, &moved, b.move_art, b.locked_art, true)?;
|
||||
let ids: Vec<NodeId> = arts.iter().flat_map(|(_, a)| a.iter().copied()).collect();
|
||||
let sc = Scaling { patterns: super::object::transform_patterns(s, p, &ids)?, ..Scaling::default() };
|
||||
s.edit(label, |d, _| {
|
||||
for ((i, dv), (_, art)) in moves.iter().zip(&arts) {
|
||||
let a = d.artboards.get_mut(*i).ok_or_else(|| EngineError::Other("no such artboard".into()))?;
|
||||
a.rect = a.rect + *dv;
|
||||
let id = a.id;
|
||||
d.move_artboard_guides(id, *dv);
|
||||
for id in art {
|
||||
if let Some(n) = d.node_mut(*id) {
|
||||
n.transform(Affine::translate(*dv), sc);
|
||||
super::object::keep_pixel_aligned(n, sc);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({ "moved": moved }))
|
||||
}
|
||||
|
||||
fn align(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "artboard.align";
|
||||
let h = choice(p, C, "horizontal", &["left", "center", "right"])?;
|
||||
let v = choice(p, C, "vertical", &["top", "center", "bottom"])?;
|
||||
if h.is_none() && v.is_none() {
|
||||
return Err(bad(C, "give horizontal or vertical"));
|
||||
}
|
||||
let b = boards(s, p, C)?;
|
||||
let Some(r) = b.reference.map(|(_, r)| r).or_else(|| b.items.iter().map(|(_, r)| *r).reduce(|a, b| a.union(b))) else {
|
||||
return Ok(json!({ "moved": [] }));
|
||||
};
|
||||
let moves: Vec<(usize, Vec2)> = b
|
||||
.items
|
||||
.iter()
|
||||
.map(|(i, a)| {
|
||||
let dx = match h {
|
||||
Some("left") => r.x0 - a.x0,
|
||||
Some("center") => r.center().x - a.center().x,
|
||||
Some("right") => r.x1 - a.x1,
|
||||
_ => 0.0,
|
||||
};
|
||||
let dy = match v {
|
||||
Some("top") => r.y0 - a.y0,
|
||||
Some("center") => r.center().y - a.center().y,
|
||||
Some("bottom") => r.y1 - a.y1,
|
||||
_ => 0.0,
|
||||
};
|
||||
(*i, Vec2::new(dx, dy))
|
||||
})
|
||||
.collect();
|
||||
apply(s, "Align", p, &b, &moves)
|
||||
}
|
||||
|
||||
fn distribute(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "artboard.distribute";
|
||||
let h = choice(p, C, "horizontal", &["left", "center", "right"])?;
|
||||
let v = choice(p, C, "vertical", &["top", "center", "bottom"])?;
|
||||
let (horiz, line): (bool, fn(&Rect) -> f64) = match (h, v) {
|
||||
(Some("left"), _) => (true, |r| r.x0),
|
||||
(Some("right"), _) => (true, |r| r.x1),
|
||||
(Some(_), _) => (true, |r| r.center().x),
|
||||
(None, Some("top")) => (false, |r| r.y0),
|
||||
(None, Some("bottom")) => (false, |r| r.y1),
|
||||
(None, Some(_)) => (false, |r| r.center().y),
|
||||
(None, None) => return Err(bad(C, "give horizontal or vertical")),
|
||||
};
|
||||
let gap = spacing(p, C)?;
|
||||
let b = boards(s, p, C)?;
|
||||
let mut items = b.items.clone();
|
||||
items.sort_by(|x, y| line(&x.1).total_cmp(&line(&y.1)));
|
||||
let kept = kept(&b, &items, gap)?;
|
||||
// With a spacing: the place in the order and the line of the artboard it is measured from.
|
||||
let from = kept.zip(gap).and_then(|(at, gap)| items.get(at).map(|(_, r)| (at, gap, line(r))));
|
||||
let n = items.len();
|
||||
// Two artboards are the ends already; with a spacing the second is placed from the first.
|
||||
if n < if from.is_some() { 2 } else { 3 } {
|
||||
return Ok(json!({ "moved": [] }));
|
||||
}
|
||||
let (Some((_, first)), Some((_, last))) = (items.first(), items.last()) else { return Ok(json!({ "moved": [] })) };
|
||||
let (first, last) = (line(first), line(last));
|
||||
let deltas: Vec<f64> = items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(k, (_, r))| {
|
||||
let target = match from {
|
||||
Some((at, gap, origin)) => origin + gap * (k as f64 - at as f64),
|
||||
None => first + (last - first) * k as f64 / (n - 1) as f64,
|
||||
};
|
||||
target - line(r)
|
||||
})
|
||||
.collect();
|
||||
// The artboard aligned to stays where it is, and the others keep their places from it.
|
||||
let hold = kept.and_then(|at| deltas.get(at)).copied().unwrap_or(0.0);
|
||||
let moves: Vec<(usize, Vec2)> = items.iter().zip(&deltas).map(|((i, _), d)| (*i, along(horiz, d - hold))).collect();
|
||||
apply(s, "Distribute", p, &b, &moves)
|
||||
}
|
||||
|
||||
fn distribute_spacing(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "artboard.distributeSpacing";
|
||||
let horiz = choice(p, C, "axis", &["horizontal", "vertical"])? != Some("vertical");
|
||||
let gap = spacing(p, C)?;
|
||||
let b = boards(s, p, C)?;
|
||||
let start = |r: &Rect| if horiz { r.x0 } else { r.y0 };
|
||||
let size = |r: &Rect| if horiz { r.width() } else { r.height() };
|
||||
let mut items = b.items.clone();
|
||||
items.sort_by(|x, y| start(&x.1).total_cmp(&start(&y.1)));
|
||||
let kept = kept(&b, &items, gap)?;
|
||||
let gap = gap.filter(|_| kept.is_some());
|
||||
let n = items.len();
|
||||
if n < if gap.is_some() { 2 } else { 3 } {
|
||||
return Ok(json!({ "moved": [] }));
|
||||
}
|
||||
let (Some((_, first)), Some((_, last))) = (items.first(), items.last()) else { return Ok(json!({ "moved": [] })) };
|
||||
let (lo, hi) = (start(first), start(last) + size(last));
|
||||
let gap = gap.unwrap_or_else(|| (hi - lo - items.iter().map(|(_, r)| size(r)).sum::<f64>()) / (n - 1) as f64);
|
||||
let mut pos = lo;
|
||||
let mut deltas = Vec::with_capacity(n);
|
||||
for (i, r) in &items {
|
||||
deltas.push((*i, pos - start(r)));
|
||||
pos += size(r) + gap;
|
||||
}
|
||||
// The artboard aligned to stays where it is, and the others keep their places from it.
|
||||
let hold = kept.and_then(|at| deltas.get(at)).map_or(0.0, |d| d.1);
|
||||
let moves: Vec<(usize, Vec2)> = deltas.into_iter().map(|(i, d)| (i, along(horiz, d - hold))).collect();
|
||||
apply(s, "Distribute Spacing", p, &b, &moves)
|
||||
}
|
||||
|
||||
/// An offset of `d` along the axis: x when `horiz`, else y.
|
||||
fn along(horiz: bool, d: f64) -> Vec2 {
|
||||
if horiz { Vec2::new(d, 0.0) } else { Vec2::new(0.0, d) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,277 @@
|
||||
//! Align and Distribute for artboards.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::NodeId;
|
||||
|
||||
use crate::Session;
|
||||
use crate::tooling::ViewInfo;
|
||||
|
||||
/// A document with three artboards, each with a 10 pt square 5 pt inside its top-left corner:
|
||||
/// 0 at (0, 0) 100 × 100, 1 at (150, 40) 60 × 80 and 2 at (400, 10) 100 × 50 → the squares' ids.
|
||||
fn three_boards() -> (Session, [u64; 3]) {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
s.execute("artboard.new", &json!({"x": 150, "y": 40, "width": 60, "height": 80})).unwrap();
|
||||
s.execute("artboard.new", &json!({"x": 400, "y": 10, "width": 100, "height": 50})).unwrap();
|
||||
let mut sq = |x: u64, y: u64| s.execute("shape.rectangle", &json!({"x": x, "y": y, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap();
|
||||
let ids = [sq(5, 5), sq(155, 45), sq(405, 15)];
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
(s, ids)
|
||||
}
|
||||
|
||||
/// The top-left corner of each artboard.
|
||||
fn corners(s: &Session) -> Vec<(f64, f64)> {
|
||||
s.doc().unwrap().doc.artboards.iter().map(|a| (a.rect.x0, a.rect.y0)).collect()
|
||||
}
|
||||
|
||||
/// The top-left corner of object `id`.
|
||||
fn corner(s: &Session, id: u64) -> (f64, f64) {
|
||||
let b = s.doc().unwrap().doc.node(NodeId(id)).unwrap().geometric_bounds().unwrap();
|
||||
(b.x0, b.y0)
|
||||
}
|
||||
|
||||
fn undo_steps(s: &Session) -> usize {
|
||||
s.doc().unwrap().history.undo.len()
|
||||
}
|
||||
|
||||
const ALL: [u64; 3] = [0, 1, 2];
|
||||
|
||||
/// Align Objects lines the artboards up on the bounds of all of them, each with the art inside
|
||||
/// it, in one undo step.
|
||||
#[test]
|
||||
fn align_moves_the_artboards_to_their_common_edge_with_their_art() {
|
||||
let (mut s, [a, b, c]) = three_boards();
|
||||
let before = corners(&s);
|
||||
let steps = undo_steps(&s);
|
||||
let out = s.execute("artboard.align", &json!({"indices": ALL, "vertical": "top"})).unwrap();
|
||||
assert_eq!(out, json!({"moved": [1, 2]}));
|
||||
assert_eq!(corners(&s), [(0.0, 0.0), (150.0, 0.0), (400.0, 0.0)]);
|
||||
assert_eq!([corner(&s, a), corner(&s, b), corner(&s, c)], [(5.0, 5.0), (155.0, 5.0), (405.0, 5.0)], "each square stays on its artboard");
|
||||
assert_eq!(undo_steps(&s), steps + 1);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!((corners(&s), corner(&s, b)), (before, (155.0, 45.0)), "one undo step");
|
||||
s.execute("artboard.align", &json!({"indices": ALL, "horizontal": "right", "vertical": "bottom"})).unwrap();
|
||||
assert_eq!(corners(&s), [(400.0, 20.0), (440.0, 40.0), (400.0, 70.0)], "the right edges at 500 and the bottom edges at 120");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("artboard.align", &json!({"indices": ALL, "horizontal": "center", "moveArt": false})).unwrap();
|
||||
assert_eq!(corners(&s), [(200.0, 0.0), (220.0, 40.0), (200.0, 10.0)], "the centers at 250");
|
||||
assert_eq!(corner(&s, a), (5.0, 5.0), "moveArt false leaves the art");
|
||||
}
|
||||
|
||||
/// Align to an artboard: that artboard stays where it is and the others line up on it. It need not
|
||||
/// be one of the artboards aligned.
|
||||
#[test]
|
||||
fn align_to_an_artboard_keeps_that_artboard_in_place() {
|
||||
let (mut s, _) = three_boards();
|
||||
let out = s.execute("artboard.align", &json!({"indices": ALL, "horizontal": "left", "to": "artboard", "artboard": 2})).unwrap();
|
||||
assert_eq!(out, json!({"moved": [0, 1]}));
|
||||
assert_eq!(corners(&s), [(400.0, 0.0), (400.0, 40.0), (400.0, 10.0)]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("artboard.align", &json!({"index": 0, "vertical": "center", "to": "artboard", "artboard": 1})).unwrap();
|
||||
assert_eq!(corners(&s), [(0.0, 30.0), (150.0, 40.0), (400.0, 10.0)], "artboard 1's center is at y 80");
|
||||
}
|
||||
|
||||
/// With no artboard named, the commands act on the artboards the Artboard tool has selected and
|
||||
/// align to its active one; the journal records both for a replay, and an action leaves them out.
|
||||
#[test]
|
||||
fn the_artboard_tools_selection_is_the_default_and_is_recorded() {
|
||||
let (mut s, [_, _, c]) = three_boards();
|
||||
s.select_tool("artboard", ViewInfo::default()).unwrap();
|
||||
s.set_tool_option("selected", &json!([1, 2]));
|
||||
let start = s.journal.len();
|
||||
assert_eq!(s.execute("artboard.align", &json!({"horizontal": "left"})).unwrap(), json!({"moved": [2]}));
|
||||
assert_eq!(corners(&s), [(0.0, 0.0), (150.0, 40.0), (150.0, 10.0)]);
|
||||
assert_eq!(corner(&s, c), (155.0, 15.0), "the tool's Move Artwork with Artboard option is on");
|
||||
let recorded = json!({"horizontal": "left", "selectedArtboards": [1, 2], "moveArt": true, "lockedAndHidden": false});
|
||||
assert_eq!(s.journal.last().unwrap(), &("artboard.align".to_string(), recorded));
|
||||
assert_eq!(
|
||||
s.journal_for_action(start),
|
||||
[("artboard.align".to_string(), json!({"horizontal": "left", "moveArt": true, "lockedAndHidden": false}))]
|
||||
);
|
||||
s.execute("artboard.align", &json!({"vertical": "top", "to": "artboard"})).unwrap();
|
||||
assert_eq!(corners(&s)[2], (150.0, 40.0), "artboard 2 lines up on the active artboard, 1");
|
||||
assert_eq!(s.journal.last().unwrap().1["activeArtboard"], json!(1));
|
||||
assert_eq!(s.journal_for_action(start)[1].1.get("activeArtboard"), None, "an action aligns to the artboard active when it plays");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("artboard.align", &json!({"vertical": "top", "to": "artboard", "activeArtboard": 0})).unwrap();
|
||||
assert_eq!(corners(&s)[1..], [(150.0, 0.0), (150.0, 0.0)], "a replay aligns to the artboard it recorded, 0");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("artboard.align", &json!({"vertical": "bottom", "to": "artboard", "artboard": 0, "activeArtboard": 2})).unwrap();
|
||||
assert_eq!(corners(&s)[1..], [(150.0, 20.0), (150.0, 50.0)], "`artboard` goes ahead of the recorded one");
|
||||
let action = s.journal_for_action(start);
|
||||
let entry = &action.last().unwrap().1;
|
||||
assert_eq!((entry.get("artboard"), entry.get("activeArtboard")), (Some(&json!(0)), None), "an action keeps the artboard a call named");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("artboard.align", &json!({"vertical": "top", "to": "artboard"})).unwrap();
|
||||
s.set_tool_option("moveArt", &json!(false));
|
||||
s.execute("artboard.align", &json!({"horizontal": "right"})).unwrap();
|
||||
assert_eq!((corners(&s)[1], corner(&s, c)), ((190.0, 40.0), (155.0, 45.0)), "with the option off the art stays");
|
||||
}
|
||||
|
||||
/// A command that moves no artboard leaves the document and its undo history as they are.
|
||||
#[test]
|
||||
fn nothing_to_move_records_no_undo_step() {
|
||||
let (mut s, _) = three_boards();
|
||||
s.execute("artboard.align", &json!({"indices": ALL, "vertical": "top"})).unwrap();
|
||||
let (steps, before) = (undo_steps(&s), corners(&s));
|
||||
for (id, p) in [
|
||||
("artboard.align", json!({"indices": ALL, "vertical": "top"})),
|
||||
("artboard.align", json!({"index": 1, "horizontal": "left", "vertical": "bottom"})),
|
||||
("artboard.align", json!({"index": 1, "horizontal": "center", "to": "artboard", "artboard": 1})),
|
||||
("artboard.distribute", json!({"indices": [0, 2], "horizontal": "center"})),
|
||||
("artboard.distribute", json!({"index": 2, "vertical": "top", "to": "artboard", "artboard": 2, "spacing": 30})),
|
||||
("artboard.distributeSpacing", json!({"indices": [0, 1], "axis": "horizontal"})),
|
||||
("artboard.distributeSpacing", json!({"indices": [1], "axis": "vertical"})),
|
||||
] {
|
||||
assert_eq!(s.execute(id, &p).unwrap(), json!({"moved": []}), "{id} {p}");
|
||||
assert_eq!((undo_steps(&s), corners(&s)), (steps, before.clone()), "{id} {p}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A locked artboard stays where it is and still counts in the bounds and in the order. When every
|
||||
/// artboard named is locked, the command fails.
|
||||
#[test]
|
||||
fn a_locked_artboard_stays_where_it_is() {
|
||||
let (mut s, _) = three_boards();
|
||||
s.execute("artboard.setProps", &json!({"index": 1, "locked": true})).unwrap();
|
||||
assert_eq!(s.execute("artboard.align", &json!({"indices": ALL, "vertical": "bottom"})).unwrap(), json!({"moved": [0, 2]}));
|
||||
assert_eq!(corners(&s), [(0.0, 20.0), (150.0, 40.0), (400.0, 70.0)], "the bottom edges at 120, the locked artboard's");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.execute("artboard.distributeSpacing", &json!({"indices": ALL})).unwrap(), json!({"moved": []}), "the middle artboard is locked");
|
||||
let out =
|
||||
s.execute("artboard.distribute", &json!({"indices": ALL, "horizontal": "left", "to": "artboard", "artboard": 0, "spacing": 100})).unwrap();
|
||||
assert_eq!((out, corners(&s)), (json!({"moved": [2]}), vec![(0.0, 0.0), (150.0, 40.0), (200.0, 10.0)]), "the third left edge goes to 200");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let (steps, before) = (undo_steps(&s), corners(&s));
|
||||
for (id, p) in [
|
||||
("artboard.align", json!({"index": 1, "horizontal": "left", "to": "artboard", "artboard": 0})),
|
||||
("artboard.distribute", json!({"index": 1, "horizontal": "left"})),
|
||||
("artboard.distributeSpacing", json!({"indices": [1]})),
|
||||
] {
|
||||
assert_eq!(s.execute(id, &p).unwrap_err().to_string(), "the artboard is locked", "{id}");
|
||||
assert_eq!((undo_steps(&s), corners(&s)), (steps, before.clone()), "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Distribute Objects spaces the named edges or centers evenly between the outermost artboards;
|
||||
/// with a spacing and an artboard to align to, it places them that far apart from that artboard.
|
||||
#[test]
|
||||
fn distribute_spaces_the_edges_or_centers() {
|
||||
let (mut s, [_, b, _]) = three_boards();
|
||||
assert_eq!(s.execute("artboard.distribute", &json!({"indices": ALL, "horizontal": "center"})).unwrap(), json!({"moved": [1]}));
|
||||
assert_eq!(corners(&s), [(0.0, 0.0), (220.0, 40.0), (400.0, 10.0)], "the centers at 50, 250 and 450");
|
||||
assert_eq!(corner(&s, b), (225.0, 45.0));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("artboard.distribute", &json!({"indices": ALL, "vertical": "bottom"})).unwrap();
|
||||
assert_eq!(corners(&s), [(0.0, -10.0), (150.0, 40.0), (400.0, 10.0)], "the bottom edges at 60, 90 and 120");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let p = json!({"indices": ALL, "horizontal": "left", "to": "artboard", "artboard": 1, "spacing": 100});
|
||||
assert_eq!(s.execute("artboard.distribute", &p).unwrap(), json!({"moved": [0, 2]}));
|
||||
assert_eq!(corners(&s), [(50.0, 0.0), (150.0, 40.0), (250.0, 10.0)], "the left edges 100 apart, artboard 1 in place");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let two = json!({"indices": [0, 2], "horizontal": "right", "to": "artboard", "artboard": 2, "spacing": 150});
|
||||
assert_eq!(s.execute("artboard.distribute", &two).unwrap(), json!({"moved": [0]}));
|
||||
assert_eq!(corners(&s)[0], (250.0, 0.0), "two artboards are enough with a spacing: the right edges at 350 and 500");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let held = json!({"indices": ALL, "horizontal": "center", "to": "artboard", "artboard": 1});
|
||||
assert_eq!(s.execute("artboard.distribute", &held).unwrap(), json!({"moved": [0, 2]}));
|
||||
assert_eq!(corners(&s), [(-70.0, 0.0), (150.0, 40.0), (330.0, 10.0)], "the centers 200 apart, artboard 1 in place");
|
||||
}
|
||||
|
||||
/// Distribute Spacing makes the gaps between the artboards equal; with a spacing and an artboard
|
||||
/// to align to, the gaps are that wide and that artboard stays.
|
||||
#[test]
|
||||
fn distribute_spacing_makes_the_gaps_equal() {
|
||||
let (mut s, _) = three_boards();
|
||||
assert_eq!(s.execute("artboard.distributeSpacing", &json!({"indices": ALL})).unwrap(), json!({"moved": [1]}));
|
||||
assert_eq!(corners(&s), [(0.0, 0.0), (220.0, 40.0), (400.0, 10.0)], "gaps of 120 between 0 and 500");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("artboard.distributeSpacing", &json!({"indices": ALL, "axis": "vertical"})).unwrap();
|
||||
assert_eq!(corners(&s), [(0.0, 0.0), (150.0, 40.0), (400.0, 45.0)], "top to bottom: artboards 0, 2 and 1, overlapping by 55");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let p = json!({"indices": ALL, "axis": "horizontal", "to": "artboard", "artboard": 1, "spacing": 20});
|
||||
assert_eq!(s.execute("artboard.distributeSpacing", &p).unwrap(), json!({"moved": [0, 2]}));
|
||||
assert_eq!(corners(&s), [(30.0, 0.0), (150.0, 40.0), (230.0, 10.0)], "gaps of 20 on both sides of artboard 1");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("artboard.distributeSpacing", &json!({"indices": [0, 2], "to": "artboard", "artboard": 0, "spacing": -25})).unwrap();
|
||||
assert_eq!(corners(&s)[2], (75.0, 10.0), "a negative spacing overlaps them");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let held = json!({"indices": ALL, "to": "artboard", "artboard": 1});
|
||||
assert_eq!(s.execute("artboard.distributeSpacing", &held).unwrap(), json!({"moved": [0, 2]}));
|
||||
assert_eq!(corners(&s), [(-70.0, 0.0), (150.0, 40.0), (330.0, 10.0)], "gaps of 120, artboard 1 in place");
|
||||
}
|
||||
|
||||
/// A value outside what a param takes is an error, and nothing changes.
|
||||
#[test]
|
||||
fn bad_params_are_errors() {
|
||||
let (mut s, _) = three_boards();
|
||||
let (steps, before) = (undo_steps(&s), corners(&s));
|
||||
let with = |extra: Value| {
|
||||
let mut p = json!({"indices": ALL, "horizontal": "left"});
|
||||
for (k, v) in extra.as_object().unwrap() {
|
||||
p[k] = v.clone();
|
||||
}
|
||||
p
|
||||
};
|
||||
let shared = [
|
||||
json!({"horizontal": "middle"}),
|
||||
json!({"horizontal": 1}),
|
||||
json!({"to": "key"}),
|
||||
json!({"to": "artboard", "artboard": 9}),
|
||||
json!({"artboard": "1"}),
|
||||
json!({"artboard": -1}),
|
||||
json!({"moveArt": "yes"}),
|
||||
json!({"patterns": 1}),
|
||||
json!({"lockedAndHidden": "no"}),
|
||||
json!({"indices": [9]}),
|
||||
json!({"indices": []}),
|
||||
json!({"indices": "0"}),
|
||||
];
|
||||
for id in ["artboard.align", "artboard.distribute", "artboard.distributeSpacing"] {
|
||||
let axis = if id == "artboard.distributeSpacing" { json!({"horizontal": null, "axis": "horizontal"}) } else { json!({}) };
|
||||
for extra in &shared {
|
||||
let mut p = with(axis.clone());
|
||||
for (k, v) in extra.as_object().unwrap() {
|
||||
p[k] = v.clone();
|
||||
}
|
||||
if id == "artboard.distributeSpacing" && extra.get("horizontal").is_some() {
|
||||
p = json!({"indices": ALL, "axis": extra["horizontal"]});
|
||||
}
|
||||
assert!(s.execute(id, &p).is_err(), "{id} {p}");
|
||||
}
|
||||
}
|
||||
for (id, p) in [
|
||||
("artboard.align", json!({"indices": ALL})),
|
||||
("artboard.align", json!({"indices": ALL, "vertical": "middle"})),
|
||||
("artboard.distribute", json!({"indices": ALL})),
|
||||
("artboard.distribute", with(json!({"spacing": "10"}))),
|
||||
("artboard.distribute", with(json!({"spacing": true}))),
|
||||
("artboard.distribute", json!({"indices": [0, 1], "horizontal": "left", "to": "artboard", "artboard": 2, "spacing": 10})),
|
||||
("artboard.distributeSpacing", json!({"indices": ALL, "spacing": [10]})),
|
||||
("artboard.distributeSpacing", json!({"indices": [0, 1], "to": "artboard", "artboard": 2, "spacing": 10})),
|
||||
] {
|
||||
assert!(s.execute(id, &p).is_err(), "{id} {p}");
|
||||
}
|
||||
// Spacings that would put an artboard past the canvas, or overflow to infinity.
|
||||
for (id, p) in [
|
||||
("artboard.distribute", with(json!({"to": "artboard", "artboard": 0, "spacing": 1e9}))),
|
||||
("artboard.distribute", with(json!({"to": "artboard", "artboard": 1, "spacing": 1e308}))),
|
||||
("artboard.distributeSpacing", json!({"indices": ALL, "to": "artboard", "artboard": 0, "spacing": 1e9})),
|
||||
("artboard.distributeSpacing", json!({"indices": ALL, "to": "artboard", "artboard": 1, "spacing": 1e308})),
|
||||
] {
|
||||
let e = s.execute(id, &p).unwrap_err().to_string();
|
||||
assert!(e.contains("exceed the canvas"), "{id} {p}: {e}");
|
||||
}
|
||||
assert_eq!((undo_steps(&s), corners(&s)), (steps, before));
|
||||
}
|
||||
|
||||
/// An artboard's own guides move with it.
|
||||
#[test]
|
||||
fn an_artboards_guides_move_with_it() {
|
||||
let (mut s, _) = three_boards();
|
||||
s.execute("guide.add", &json!({"vertical": true, "pos": 160, "artboard": 1})).unwrap();
|
||||
s.execute("artboard.align", &json!({"indices": [0, 1], "horizontal": "left"})).unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let positions: Vec<f64> = d.ruler_guides().iter().map(|g| g.pos).collect();
|
||||
assert_eq!(positions, [10.0], "the guide stays 10 pt inside artboard 1");
|
||||
}
|
||||
@@ -122,7 +122,7 @@ pub(crate) fn boards_and_art_noted(s: &mut Session, p: &Value, cmd: &str) -> Res
|
||||
/// 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]) {
|
||||
pub(crate) 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));
|
||||
}
|
||||
@@ -130,7 +130,7 @@ fn note_artboards(s: &mut Session, p: &Value, indices: &[usize]) {
|
||||
|
||||
/// 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>> {
|
||||
pub(crate) 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;
|
||||
if p.get("index").is_some() || p.get("indices").is_some() {
|
||||
|
||||
@@ -85,9 +85,12 @@ pub(super) const ALLOWED: &[&str] = &[
|
||||
"appearance.showAllHidden",
|
||||
"appearance.targetContents",
|
||||
"appearance.transfer",
|
||||
"artboard.align",
|
||||
"artboard.convertToArtboards",
|
||||
"artboard.delete",
|
||||
"artboard.deleteEmpty",
|
||||
"artboard.distribute",
|
||||
"artboard.distributeSpacing",
|
||||
"artboard.duplicate",
|
||||
"artboard.fitToArt",
|
||||
"artboard.fitToSelection",
|
||||
|
||||
@@ -28,7 +28,7 @@ pub const FIT_ACTIVE_ARTBOARD: [&str; 2] = ["artboard.fitToArt", "artboard.fitTo
|
||||
/// Artboard tool had selected for its commands and for Paste in Place, in Front and in Back; the artboards and art Cut and Copy took with that
|
||||
/// tool chosen; the view's center the app passes to Edit Symbol); an action leaves them out
|
||||
/// ([`Session::journal_for_action`]).
|
||||
const REPLAY_ONLY: [(&str, &str); 29] = [
|
||||
const REPLAY_ONLY: [(&str, &str); 35] = [
|
||||
("file.new", "created"),
|
||||
("edit.newDocumentFromState", "created"),
|
||||
("document.save", "modified"),
|
||||
@@ -50,6 +50,12 @@ const REPLAY_ONLY: [(&str, &str); 29] = [
|
||||
("artboard.cut", "selectedArtboards"),
|
||||
("artboard.rename", "selectedArtboards"),
|
||||
("artboard.switchOrientation", "selectedArtboards"),
|
||||
("artboard.align", "selectedArtboards"),
|
||||
("artboard.align", "activeArtboard"),
|
||||
("artboard.distribute", "selectedArtboards"),
|
||||
("artboard.distribute", "activeArtboard"),
|
||||
("artboard.distributeSpacing", "selectedArtboards"),
|
||||
("artboard.distributeSpacing", "activeArtboard"),
|
||||
("edit.copy", "selectedArtboards"),
|
||||
("edit.copy", "art"),
|
||||
("edit.cut", "selectedArtboards"),
|
||||
|
||||
@@ -261,91 +261,91 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"align.horizontalLeft",
|
||||
"Horizontal Align Left",
|
||||
"",
|
||||
"{} the Align panel's Horizontal Align Left button: object.align {horizontal: \"left\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
|
||||
"{} the Align panel's Horizontal Align Left button: object.align {horizontal: \"left\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice; with the Artboard tool chosen, artboard.align with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.horizontalCenter",
|
||||
"Horizontal Align Center",
|
||||
"",
|
||||
"{} the Align panel's Horizontal Align Center button: object.align {horizontal: \"center\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
|
||||
"{} the Align panel's Horizontal Align Center button: object.align {horizontal: \"center\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice; with the Artboard tool chosen, artboard.align with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.horizontalRight",
|
||||
"Horizontal Align Right",
|
||||
"",
|
||||
"{} the Align panel's Horizontal Align Right button: object.align {horizontal: \"right\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
|
||||
"{} the Align panel's Horizontal Align Right button: object.align {horizontal: \"right\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice; with the Artboard tool chosen, artboard.align with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.horizontalVerticalCenter",
|
||||
"Horizontal & Vertical Align Center",
|
||||
"",
|
||||
"{} the Align panel's Horizontal & Vertical Align Center button: object.align {horizontal: \"center\", vertical: \"center\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
|
||||
"{} the Align panel's Horizontal & Vertical Align Center button: object.align {horizontal: \"center\", vertical: \"center\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice; with the Artboard tool chosen, artboard.align with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.verticalTop",
|
||||
"Vertical Align Top",
|
||||
"",
|
||||
"{} the Align panel's Vertical Align Top button: object.align {vertical: \"top\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
|
||||
"{} the Align panel's Vertical Align Top button: object.align {vertical: \"top\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice; with the Artboard tool chosen, artboard.align with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.verticalCenter",
|
||||
"Vertical Align Center",
|
||||
"",
|
||||
"{} the Align panel's Vertical Align Center button: object.align {vertical: \"center\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
|
||||
"{} the Align panel's Vertical Align Center button: object.align {vertical: \"center\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice; with the Artboard tool chosen, artboard.align with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.verticalBottom",
|
||||
"Vertical Align Bottom",
|
||||
"",
|
||||
"{} the Align panel's Vertical Align Bottom button: object.align {vertical: \"bottom\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
|
||||
"{} the Align panel's Vertical Align Bottom button: object.align {vertical: \"bottom\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice; with the Artboard tool chosen, artboard.align with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.distributeVerticalTop",
|
||||
"Vertical Distribute Top",
|
||||
"",
|
||||
"{} the Align panel's Vertical Distribute Top button: object.distribute {vertical: \"top\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
|
||||
"{} the Align panel's Vertical Distribute Top button: object.distribute {vertical: \"top\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key; with the Artboard tool chosen, the artboard command of the same name (artboard.distribute, artboard.distributeSpacing) with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) and spacing: the Distribute Spacing value while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.distributeVerticalCenter",
|
||||
"Vertical Distribute Center",
|
||||
"",
|
||||
"{} the Align panel's Vertical Distribute Center button: object.distribute {vertical: \"center\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
|
||||
"{} the Align panel's Vertical Distribute Center button: object.distribute {vertical: \"center\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key; with the Artboard tool chosen, the artboard command of the same name (artboard.distribute, artboard.distributeSpacing) with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) and spacing: the Distribute Spacing value while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.distributeVerticalBottom",
|
||||
"Vertical Distribute Bottom",
|
||||
"",
|
||||
"{} the Align panel's Vertical Distribute Bottom button: object.distribute {vertical: \"bottom\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
|
||||
"{} the Align panel's Vertical Distribute Bottom button: object.distribute {vertical: \"bottom\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key; with the Artboard tool chosen, the artboard command of the same name (artboard.distribute, artboard.distributeSpacing) with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) and spacing: the Distribute Spacing value while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.distributeHorizontalLeft",
|
||||
"Horizontal Distribute Left",
|
||||
"",
|
||||
"{} the Align panel's Horizontal Distribute Left button: object.distribute {horizontal: \"left\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
|
||||
"{} the Align panel's Horizontal Distribute Left button: object.distribute {horizontal: \"left\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key; with the Artboard tool chosen, the artboard command of the same name (artboard.distribute, artboard.distributeSpacing) with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) and spacing: the Distribute Spacing value while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.distributeHorizontalCenter",
|
||||
"Horizontal Distribute Center",
|
||||
"",
|
||||
"{} the Align panel's Horizontal Distribute Center button: object.distribute {horizontal: \"center\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
|
||||
"{} the Align panel's Horizontal Distribute Center button: object.distribute {horizontal: \"center\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key; with the Artboard tool chosen, the artboard command of the same name (artboard.distribute, artboard.distributeSpacing) with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) and spacing: the Distribute Spacing value while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.distributeHorizontalRight",
|
||||
"Horizontal Distribute Right",
|
||||
"",
|
||||
"{} the Align panel's Horizontal Distribute Right button: object.distribute {horizontal: \"right\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
|
||||
"{} the Align panel's Horizontal Distribute Right button: object.distribute {horizontal: \"right\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key; with the Artboard tool chosen, the artboard command of the same name (artboard.distribute, artboard.distributeSpacing) with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) and spacing: the Distribute Spacing value while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.distributeVerticalSpace",
|
||||
"Vertical Distribute Space",
|
||||
"",
|
||||
"{} the Align panel's Vertical Distribute Space button: object.distributeSpacing {axis: \"vertical\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
|
||||
"{} the Align panel's Vertical Distribute Space button: object.distributeSpacing {axis: \"vertical\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key; with the Artboard tool chosen, the artboard command of the same name (artboard.distribute, artboard.distributeSpacing) with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) and spacing: the Distribute Spacing value while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.distributeHorizontalSpace",
|
||||
"Horizontal Distribute Space",
|
||||
"",
|
||||
"{} the Align panel's Horizontal Distribute Space button: object.distributeSpacing {axis: \"horizontal\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
|
||||
"{} the Align panel's Horizontal Distribute Space button: object.distributeSpacing {axis: \"horizontal\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key; with the Artboard tool chosen, the artboard command of the same name (artboard.distribute, artboard.distributeSpacing) with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) and spacing: the Distribute Spacing value while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"type.bold",
|
||||
|
||||
@@ -282,6 +282,26 @@ fn groups_before_transform(app: &mut VectorcraftApp, ui: &mut Ui, objects: usize
|
||||
transform
|
||||
}
|
||||
|
||||
/// Draws the Control bar's Align group after the artboard controls, with the Artboard tool in use:
|
||||
/// the Align buttons, the Align To menu and, for two or more selected artboards, the Distribute
|
||||
/// buttons. They align and distribute the selected artboards
|
||||
/// ([`crate::panels::align::run_button`]). Each shows only while the bar has room for it, and none
|
||||
/// while the panel menu has the group off.
|
||||
fn artboard_align_group(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if !group_on(app, "alignDistribute") || app.session.active().is_none() || ui.available_width() < separator_width(ui) + buttons_width(ui, 7) {
|
||||
return;
|
||||
}
|
||||
separator(ui);
|
||||
crate::panels::align::align_buttons(app, ui, BUTTON);
|
||||
if ui.available_width() >= crate::panels::align::align_to_menu_width(BUTTON) + ui.spacing().item_spacing.x {
|
||||
crate::panels::align::align_to_menu(app, ui, BUTTON);
|
||||
if crate::panels::align::artboards_selected(app) > 1 && ui.available_width() >= GROUP_GAP + buttons_width(ui, 6) {
|
||||
ui.add_space(GROUP_GAP);
|
||||
crate::panels::align::distribute_buttons(app, ui, BUTTON);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The edges of the window the Control bar docks to (`window.control.dock {edge}`): the top, then
|
||||
/// the bottom.
|
||||
pub const CONTROL_BAR_EDGES: [&str; 2] = ["top", "bottom"];
|
||||
@@ -516,6 +536,7 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
// of the object controls.
|
||||
let (artboard_options, transform) = (group_on(app, "artboardOptions"), group_on(app, "transform"));
|
||||
if crate::panels::artboards::control_bar(app, ui, artboard_options, transform) {
|
||||
artboard_align_group(app, ui);
|
||||
return;
|
||||
}
|
||||
if anchors != AnchorControls::None && group_on(app, "anchors") {
|
||||
|
||||
@@ -2669,6 +2669,49 @@ fn menus_stop_above_a_control_bar_docked_at_the_bottom() {
|
||||
assert!(f < bar_top - 1.0, "the status and hint bars show above the Control bar: menus stop above them, {f}");
|
||||
}
|
||||
|
||||
/// With the Artboard tool in use, the Control bar shows the Align buttons after the artboard
|
||||
/// controls while it has room, and the Distribute buttons for two or more selected artboards. A
|
||||
/// click aligns the selected artboards as the Align panel's button does. The group is left out
|
||||
/// while the panel menu has it off.
|
||||
#[test]
|
||||
fn the_control_bar_aligns_the_selected_artboards_with_the_artboard_tool() {
|
||||
crate::i18n::set_current(crate::i18n::Lang::EN);
|
||||
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
|
||||
app.run("artboard.new", json!({"x": 150, "y": 40, "width": 60, "height": 80})).unwrap();
|
||||
app.select_tool("artboard");
|
||||
let ctx = egui::Context::default();
|
||||
ctx.enable_accesskit();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(2400.0, 300.0));
|
||||
let frame = |app: &mut crate::VectorcraftApp, events: Vec<egui::Event>| {
|
||||
let mut out = ctx.run_ui(egui::RawInput { events, screen_rect: Some(screen), ..Default::default() }, |ui| super::control_bar(app, ui));
|
||||
out.textures_delta.clear();
|
||||
out.platform_output.accesskit_update.map(|u| u.nodes).unwrap_or_default()
|
||||
};
|
||||
let button = |app: &mut crate::VectorcraftApp, name: &str| {
|
||||
frame(app, vec![]);
|
||||
let nodes = frame(app, vec![]);
|
||||
let n = nodes.iter().find(|(_, n)| n.label() == Some(name)).map(|(_, n)| n)?;
|
||||
n.bounds().map(|b| egui::Rect::from_min_max(egui::pos2(b.x0 as f32, b.y0 as f32), egui::pos2(b.x1 as f32, b.y1 as f32)))
|
||||
};
|
||||
app.session.set_tool_option("selected", &json!([1]));
|
||||
assert!(button(&mut app, "Vertical Align Top").is_some(), "one artboard: the Align buttons");
|
||||
assert!(button(&mut app, "Horizontal Distribute Left").is_none(), "one artboard: no Distribute buttons");
|
||||
app.session.set_tool_option("selected", &json!([1, 0]));
|
||||
assert!(button(&mut app, "Horizontal Distribute Left").is_some(), "two artboards: the Distribute buttons");
|
||||
let r = button(&mut app, "Vertical Align Top").expect("Vertical Align Top");
|
||||
let press =
|
||||
|pressed| egui::Event::PointerButton { pos: r.center(), button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
for events in [vec![egui::Event::PointerMoved(r.center()), press(true)], vec![press(false)]] {
|
||||
frame(&mut app, events);
|
||||
}
|
||||
let tops: Vec<f64> = app.session.active().unwrap().doc.artboards.iter().map(|a| a.rect.y0).collect();
|
||||
assert_eq!(tops, [0.0, 0.0], "{}", app.ui.status);
|
||||
app.run("window.control.options", json!({"group": "alignDistribute", "visible": false})).unwrap();
|
||||
assert!(button(&mut app, "Vertical Align Top").is_none(), "the group is off");
|
||||
}
|
||||
|
||||
/// Two or more selected objects show all six Distribute Objects buttons in the Control bar;
|
||||
/// a single object keeps the bar as it was. A click distributes as the Align panel's button does.
|
||||
#[test]
|
||||
|
||||
@@ -225,7 +225,7 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
|
||||
}
|
||||
id if crate::panels::paragraph::ALIGN_COMMANDS.iter().any(|c| c.0 == id) => crate::panels::character::text_style(app).is_some(),
|
||||
"type.applyBullets" | "type.applyNumbers" | "type.convertListToText" => crate::panels::character::text_style(app).is_some(),
|
||||
id if let Some(b) = crate::panels::align::button(id) => enabled(app, b.command()),
|
||||
id if let Some(b) = crate::panels::align::button(id) => enabled(app, b.command_now(app)),
|
||||
"file.reveal" => app.services.reveal.is_some() && app.session.active().is_some_and(|d| d.path.is_some()),
|
||||
"file.cancelExport" | "file.exportInBackground" => app.background.jobs.iter().any(|j| j.progress.is_some() && !j.cancelled()),
|
||||
"file.cancelOpen" => !app.opening.pending.is_empty(),
|
||||
|
||||
@@ -262,91 +262,91 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"align.horizontalLeft",
|
||||
"Horizontal Align Left",
|
||||
"",
|
||||
"{} the Align panel's Horizontal Align Left button: object.align {horizontal: \"left\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
|
||||
"{} the Align panel's Horizontal Align Left button: object.align {horizontal: \"left\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice; with the Artboard tool chosen, artboard.align with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.horizontalCenter",
|
||||
"Horizontal Align Center",
|
||||
"",
|
||||
"{} the Align panel's Horizontal Align Center button: object.align {horizontal: \"center\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
|
||||
"{} the Align panel's Horizontal Align Center button: object.align {horizontal: \"center\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice; with the Artboard tool chosen, artboard.align with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.horizontalRight",
|
||||
"Horizontal Align Right",
|
||||
"",
|
||||
"{} the Align panel's Horizontal Align Right button: object.align {horizontal: \"right\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
|
||||
"{} the Align panel's Horizontal Align Right button: object.align {horizontal: \"right\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice; with the Artboard tool chosen, artboard.align with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.horizontalVerticalCenter",
|
||||
"Horizontal & Vertical Align Center",
|
||||
"",
|
||||
"{} the Align panel's Horizontal & Vertical Align Center button: object.align {horizontal: \"center\", vertical: \"center\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
|
||||
"{} the Align panel's Horizontal & Vertical Align Center button: object.align {horizontal: \"center\", vertical: \"center\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice; with the Artboard tool chosen, artboard.align with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.verticalTop",
|
||||
"Vertical Align Top",
|
||||
"",
|
||||
"{} the Align panel's Vertical Align Top button: object.align {vertical: \"top\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
|
||||
"{} the Align panel's Vertical Align Top button: object.align {vertical: \"top\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice; with the Artboard tool chosen, artboard.align with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.verticalCenter",
|
||||
"Vertical Align Center",
|
||||
"",
|
||||
"{} the Align panel's Vertical Align Center button: object.align {vertical: \"center\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
|
||||
"{} the Align panel's Vertical Align Center button: object.align {vertical: \"center\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice; with the Artboard tool chosen, artboard.align with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.verticalBottom",
|
||||
"Vertical Align Bottom",
|
||||
"",
|
||||
"{} the Align panel's Vertical Align Bottom button: object.align {vertical: \"bottom\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice",
|
||||
"{} the Align panel's Vertical Align Bottom button: object.align {vertical: \"bottom\"} with to: \"key\" while there is a key, else \"artboard\" for one selected object without selected anchor points, else the Align To choice; with the Artboard tool chosen, artboard.align with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.distributeVerticalTop",
|
||||
"Vertical Distribute Top",
|
||||
"",
|
||||
"{} the Align panel's Vertical Distribute Top button: object.distribute {vertical: \"top\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
|
||||
"{} the Align panel's Vertical Distribute Top button: object.distribute {vertical: \"top\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key; with the Artboard tool chosen, the artboard command of the same name (artboard.distribute, artboard.distributeSpacing) with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) and spacing: the Distribute Spacing value while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.distributeVerticalCenter",
|
||||
"Vertical Distribute Center",
|
||||
"",
|
||||
"{} the Align panel's Vertical Distribute Center button: object.distribute {vertical: \"center\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
|
||||
"{} the Align panel's Vertical Distribute Center button: object.distribute {vertical: \"center\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key; with the Artboard tool chosen, the artboard command of the same name (artboard.distribute, artboard.distributeSpacing) with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) and spacing: the Distribute Spacing value while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.distributeVerticalBottom",
|
||||
"Vertical Distribute Bottom",
|
||||
"",
|
||||
"{} the Align panel's Vertical Distribute Bottom button: object.distribute {vertical: \"bottom\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
|
||||
"{} the Align panel's Vertical Distribute Bottom button: object.distribute {vertical: \"bottom\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key; with the Artboard tool chosen, the artboard command of the same name (artboard.distribute, artboard.distributeSpacing) with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) and spacing: the Distribute Spacing value while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.distributeHorizontalLeft",
|
||||
"Horizontal Distribute Left",
|
||||
"",
|
||||
"{} the Align panel's Horizontal Distribute Left button: object.distribute {horizontal: \"left\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
|
||||
"{} the Align panel's Horizontal Distribute Left button: object.distribute {horizontal: \"left\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key; with the Artboard tool chosen, the artboard command of the same name (artboard.distribute, artboard.distributeSpacing) with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) and spacing: the Distribute Spacing value while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.distributeHorizontalCenter",
|
||||
"Horizontal Distribute Center",
|
||||
"",
|
||||
"{} the Align panel's Horizontal Distribute Center button: object.distribute {horizontal: \"center\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
|
||||
"{} the Align panel's Horizontal Distribute Center button: object.distribute {horizontal: \"center\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key; with the Artboard tool chosen, the artboard command of the same name (artboard.distribute, artboard.distributeSpacing) with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) and spacing: the Distribute Spacing value while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.distributeHorizontalRight",
|
||||
"Horizontal Distribute Right",
|
||||
"",
|
||||
"{} the Align panel's Horizontal Distribute Right button: object.distribute {horizontal: \"right\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
|
||||
"{} the Align panel's Horizontal Distribute Right button: object.distribute {horizontal: \"right\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key; with the Artboard tool chosen, the artboard command of the same name (artboard.distribute, artboard.distributeSpacing) with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) and spacing: the Distribute Spacing value while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.distributeVerticalSpace",
|
||||
"Vertical Distribute Space",
|
||||
"",
|
||||
"{} the Align panel's Vertical Distribute Space button: object.distributeSpacing {axis: \"vertical\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
|
||||
"{} the Align panel's Vertical Distribute Space button: object.distributeSpacing {axis: \"vertical\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key; with the Artboard tool chosen, the artboard command of the same name (artboard.distribute, artboard.distributeSpacing) with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) and spacing: the Distribute Spacing value while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"align.distributeHorizontalSpace",
|
||||
"Horizontal Distribute Space",
|
||||
"",
|
||||
"{} the Align panel's Horizontal Distribute Space button: object.distributeSpacing {axis: \"horizontal\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key",
|
||||
"{} the Align panel's Horizontal Distribute Space button: object.distributeSpacing {axis: \"horizontal\"} with to: \"key\" while there is a key, else the Align To choice, and spacing: the Distribute Spacing value while there is a key; with the Artboard tool chosen, the artboard command of the same name (artboard.distribute, artboard.distributeSpacing) with the same direction on the artboards the tool has selected, to: \"artboard\" (the active artboard) and spacing: the Distribute Spacing value while Align to Artboard is chosen, else \"selection\"",
|
||||
),
|
||||
(
|
||||
"type.bold",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Align panel: Align Objects, Distribute Objects and Distribute Spacing (both with the spacing
|
||||
//! value when aligning to a key object or a key guide) and Align To (selection / key object /
|
||||
//! artboard).
|
||||
//! artboard). With the Artboard tool chosen, the buttons align and distribute the artboards the
|
||||
//! tool has selected.
|
||||
|
||||
use egui::Ui;
|
||||
use serde::Serialize;
|
||||
@@ -104,7 +105,7 @@ pub enum Button {
|
||||
}
|
||||
|
||||
impl Button {
|
||||
/// The engine command the button runs.
|
||||
/// The engine command the button runs on the selected objects.
|
||||
pub(crate) fn command(self) -> &'static str {
|
||||
match self {
|
||||
Button::Align(..) | Button::AlignCenters => "object.align",
|
||||
@@ -112,6 +113,51 @@ impl Button {
|
||||
Button::Spacing(_) => "object.distributeSpacing",
|
||||
}
|
||||
}
|
||||
|
||||
/// The engine command the button runs on the selected artboards ([`on_artboards`]).
|
||||
fn artboard_command(self) -> &'static str {
|
||||
match self {
|
||||
Button::Align(..) | Button::AlignCenters => "artboard.align",
|
||||
Button::Distribute(..) => "artboard.distribute",
|
||||
Button::Spacing(_) => "artboard.distributeSpacing",
|
||||
}
|
||||
}
|
||||
|
||||
/// The engine command a click on the button runs now: the artboard command with the Artboard
|
||||
/// tool chosen, else the object command.
|
||||
pub(crate) fn command_now(self, app: &VectorcraftApp) -> &'static str {
|
||||
if on_artboards(app) { self.artboard_command() } else { self.command() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the Align panel's buttons, and the Control bar's, align and distribute artboards: the
|
||||
/// Artboard tool is chosen. They then act on the artboards the tool has selected.
|
||||
pub(crate) fn on_artboards(app: &VectorcraftApp) -> bool {
|
||||
app.session.tool_id() == "artboard"
|
||||
}
|
||||
|
||||
/// The number of artboards the Artboard tool has selected ([`super::artboards::picked`]).
|
||||
pub(crate) fn artboards_selected(app: &VectorcraftApp) -> usize {
|
||||
let n = app.session.active().map_or(0, |d| d.doc.artboards.len());
|
||||
if n == 0 { 0 } else { super::artboards::picked(app, n).len() }
|
||||
}
|
||||
|
||||
/// Runs what a click on button `b` runs with the Artboard tool chosen: the artboard command on
|
||||
/// the artboards the tool has selected. With Align to Artboard chosen, it sends `to: "artboard"`,
|
||||
/// which keeps the tool's active artboard in place, and the Distribute buttons send the
|
||||
/// Distribute Spacing value. With Align to Selection chosen, it sends `to: "selection"`.
|
||||
fn run_on_artboards(app: &mut VectorcraftApp, b: Button) -> Result<Value, String> {
|
||||
let to_artboard = app.ui.align_to == AlignTo::Artboard;
|
||||
let mut p = match b {
|
||||
Button::Align(axis, v) | Button::Distribute(axis, v) => json!({axis: v}),
|
||||
Button::AlignCenters => json!({"horizontal": "center", "vertical": "center"}),
|
||||
Button::Spacing(axis) => json!({"axis": axis}),
|
||||
};
|
||||
p["to"] = json!(if to_artboard { "artboard" } else { "selection" });
|
||||
if to_artboard && matches!(b, Button::Distribute(..) | Button::Spacing(_)) {
|
||||
p["spacing"] = json!(app.ui.align_spacing);
|
||||
}
|
||||
app.run(b.artboard_command(), p)
|
||||
}
|
||||
|
||||
/// The Align panel's buttons as UI commands ([`crate::menus::UI_COMMANDS`]), in the panel's order:
|
||||
@@ -143,8 +189,12 @@ pub(crate) fn button(id: &str) -> Option<Button> {
|
||||
|
||||
/// Runs what a click on button `b` runs. An Align Objects button sends the `to` of
|
||||
/// [`with_align_to`]. A Distribute Objects or Distribute Spacing button sends the key while there
|
||||
/// is one, else the Align To choice, and the Distribute Spacing value while there is a key.
|
||||
/// is one, else the Align To choice, and the Distribute Spacing value while there is a key. With
|
||||
/// the Artboard tool chosen, the button acts on the selected artboards ([`run_on_artboards`]).
|
||||
pub fn run_button(app: &mut VectorcraftApp, b: Button) -> Result<Value, String> {
|
||||
if on_artboards(app) {
|
||||
return run_on_artboards(app, b);
|
||||
}
|
||||
let to = align_to(app).param();
|
||||
let p = match b {
|
||||
Button::Align(axis, v) => with_align_to(app, json!({axis: v})),
|
||||
@@ -163,14 +213,15 @@ pub fn run_button(app: &mut VectorcraftApp, b: Button) -> Result<Value, String>
|
||||
|
||||
/// The Distribute Spacing value ([`crate::UiState::align_spacing`]) while there is a key object
|
||||
/// or a key guide (#2143), which the Distribute Objects and Distribute Spacing buttons then send;
|
||||
/// else none.
|
||||
/// else none. With the Artboard tool chosen, the buttons send it while Align to Artboard is chosen.
|
||||
fn spacing_sent(app: &VectorcraftApp) -> Option<f64> {
|
||||
(align_to(app) == AlignTo::Key).then_some(app.ui.align_spacing)
|
||||
let sent = if on_artboards(app) { app.ui.align_to == AlignTo::Artboard } else { align_to(app) == AlignTo::Key };
|
||||
sent.then_some(app.ui.align_spacing)
|
||||
}
|
||||
|
||||
/// The six align buttons of the Control bar and the Properties panel, `size` points square.
|
||||
/// They align to what the Align panel's Align To says (the key object while there is one), as
|
||||
/// its own buttons do (#789).
|
||||
/// its own buttons do (#789), and the selected artboards with the Artboard tool chosen.
|
||||
pub fn align_buttons(app: &mut VectorcraftApp, ui: &mut Ui, size: f32) {
|
||||
for (i, (icon, tip, axis, v)) in [
|
||||
("align-start-vertical", "Horizontal Align Left", "horizontal", "left"),
|
||||
@@ -184,10 +235,10 @@ pub fn align_buttons(app: &mut VectorcraftApp, ui: &mut Ui, size: f32) {
|
||||
.enumerate()
|
||||
{
|
||||
if i == 3 && widgets::icon_button(ui, "dc-al-center", tl!("Horizontal & Vertical Align Center"), false, size).clicked() {
|
||||
app.run("object.align", with_align_to(app, json!({"horizontal": "center", "vertical": "center"}))).ok();
|
||||
run_button(app, Button::AlignCenters).ok();
|
||||
}
|
||||
if widgets::icon_button(ui, icon, tl!(tip), false, size).clicked() {
|
||||
app.run("object.align", with_align_to(app, json!({axis: v}))).ok();
|
||||
run_button(app, Button::Align(axis, v)).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,15 +297,23 @@ fn align_to_icon_name(v: AlignTo) -> (&'static str, &'static str) {
|
||||
}
|
||||
|
||||
/// The Align To choice that the Align panel and the Control bar show chosen: Artboard while one
|
||||
/// object is selected (the buttons then align it to the artboard), else [`align_to`].
|
||||
/// object is selected (the buttons then align it to the artboard), else [`align_to`]. With the
|
||||
/// Artboard tool chosen, it is the stored choice, Selection or Artboard.
|
||||
fn shown_align_to(app: &VectorcraftApp) -> AlignTo {
|
||||
if on_artboards(app) {
|
||||
return app.ui.align_to;
|
||||
}
|
||||
aligns_to(align_to(app), objects_aligned(app))
|
||||
}
|
||||
|
||||
/// Whether Align To choice `v` can be chosen. Align to Key Object needs a key object or a key
|
||||
/// guide, or something to make one of: two or more selected objects, or a guide selected with
|
||||
/// objects.
|
||||
/// objects. With the Artboard tool chosen it is off: Align to Artboard keeps the active artboard
|
||||
/// in place.
|
||||
fn can_choose(app: &VectorcraftApp, v: AlignTo) -> bool {
|
||||
if on_artboards(app) {
|
||||
return v != AlignTo::Key;
|
||||
}
|
||||
v != AlignTo::Key || selection_len(app) >= 2 || has_key(app) || guide_for_key(app).is_some()
|
||||
}
|
||||
|
||||
@@ -262,11 +321,16 @@ fn can_choose(app: &VectorcraftApp, v: AlignTo) -> bool {
|
||||
/// do. Align to Key Object makes the topmost selected object the key object, unless there is one
|
||||
/// already; with one object, a guide selected with it becomes the key. The stored choice stays as
|
||||
/// it was, and Align To shows it again once the key is cleared. Align to Selection and Align to
|
||||
/// Artboard store the choice and let go of the key object.
|
||||
/// Artboard store the choice and let go of the key object. With the Artboard tool chosen, the
|
||||
/// choice is stored and the selected objects keep their key.
|
||||
pub(crate) fn choose_align_to(app: &mut VectorcraftApp, v: AlignTo) {
|
||||
if !can_choose(app, v) {
|
||||
return;
|
||||
}
|
||||
if on_artboards(app) {
|
||||
app.ui.align_to = v;
|
||||
return;
|
||||
}
|
||||
if v == AlignTo::Key {
|
||||
if !has_key(app) {
|
||||
match (topmost(app).filter(|_| selection_len(app) >= 2), guide_for_key(app)) {
|
||||
@@ -332,13 +396,17 @@ pub(crate) fn align_to_menu(app: &mut VectorcraftApp, ui: &mut Ui, size: f32) {
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let n = selection_len(app);
|
||||
let guides = selected_guides(app);
|
||||
let can_align = n + guides >= 1;
|
||||
// With the Artboard tool chosen, the buttons act on the artboards it has selected.
|
||||
let boards = on_artboards(app).then(|| artboards_selected(app));
|
||||
let can_align = boards.map_or(n + guides >= 1, |b| b >= 1);
|
||||
// `spacing` is the Distribute Spacing value. The Distribute Objects and Distribute Spacing
|
||||
// buttons send it while there is a key object or a key guide (#2143).
|
||||
// buttons send it while there is a key object or a key guide (#2143), or for artboards while
|
||||
// Align to Artboard is chosen.
|
||||
let spacing = app.ui.align_spacing;
|
||||
let key = spacing_sent(app).is_some();
|
||||
// Two or more objects and ruler guides (#1597), or anchor points, which may all be on one path.
|
||||
let distributes = n + selected_guides(app) >= 2 || has_anchors(app);
|
||||
// Two or more objects and ruler guides (#1597), or anchor points, which may all be on one
|
||||
// path; or two or more artboards.
|
||||
let distributes = boards.map_or(n + selected_guides(app) >= 2 || has_anchors(app), |b| b >= 2);
|
||||
widgets::subheader(ui, tl!("Align Objects:"));
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 5.0;
|
||||
|
||||
@@ -721,3 +721,96 @@ fn align_to_glyph_bounds_toggles_point_and_area_text() {
|
||||
choose(&mut app, "Point Text");
|
||||
assert_eq!((prefs(&app), checked(&mut app)), ((false, true), vec!["Area Text".to_string()]));
|
||||
}
|
||||
|
||||
/// Three artboards, 0 at (0, 0) 100 × 100, 1 at (150, 40) 60 × 80 and 2 at (400, 10) 100 × 50, with
|
||||
/// the Artboard tool chosen and all three selected, artboard 1 active.
|
||||
fn selected_artboards() -> VectorcraftApp {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
|
||||
app.run("artboard.new", json!({"x": 150, "y": 40, "width": 60, "height": 80})).unwrap();
|
||||
app.run("artboard.new", json!({"x": 400, "y": 10, "width": 100, "height": 50})).unwrap();
|
||||
app.select_tool("artboard");
|
||||
app.session.set_tool_option("selected", &json!([1, 0, 2]));
|
||||
app
|
||||
}
|
||||
|
||||
/// The top-left corner of each artboard.
|
||||
fn artboard_corners(app: &VectorcraftApp) -> Vec<(f64, f64)> {
|
||||
app.session.active().unwrap().doc.artboards.iter().map(|a| (a.rect.x0, a.rect.y0)).collect()
|
||||
}
|
||||
|
||||
/// The command the app ran last, with its `to` and `spacing`.
|
||||
fn last_sent(app: &VectorcraftApp) -> (String, Value, Option<Value>) {
|
||||
let (id, p) = app.session.journal.last().unwrap();
|
||||
(id.clone(), p["to"].clone(), p.get("spacing").cloned())
|
||||
}
|
||||
|
||||
/// With the Artboard tool chosen, the Align Objects buttons align the selected artboards: to
|
||||
/// their bounds with Align to Selection, and to the active artboard, which stays in place, with
|
||||
/// Align to Artboard. Align to Key Object cannot be chosen.
|
||||
#[test]
|
||||
fn the_align_buttons_align_the_selected_artboards() {
|
||||
let mut app = selected_artboards();
|
||||
let ctx = egui::Context::default();
|
||||
let steps = app.session.active().unwrap().history.undo.len();
|
||||
let ([_, key, artboard], left) = panel_buttons(&mut app, &ctx);
|
||||
click(&ctx, &mut app, left.center(), show);
|
||||
assert_eq!(artboard_corners(&app), [(0.0, 0.0), (0.0, 40.0), (0.0, 10.0)], "{}", app.ui.status);
|
||||
assert_eq!(last_sent(&app), ("artboard.align".to_string(), json!("selection"), None));
|
||||
assert_eq!(app.session.active().unwrap().history.undo.len(), steps + 1, "one undo step");
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
click(&ctx, &mut app, key.center(), show);
|
||||
assert!(app.ui.align_to == AlignTo::Selection && !has_key(&app), "Align to Key Object is off");
|
||||
click(&ctx, &mut app, artboard.center(), show);
|
||||
assert_eq!(app.ui.align_to, AlignTo::Artboard);
|
||||
click(&ctx, &mut app, left.center(), show);
|
||||
assert_eq!(artboard_corners(&app), [(150.0, 0.0), (150.0, 40.0), (150.0, 10.0)], "artboard 1 stays: {}", app.ui.status);
|
||||
assert_eq!(last_sent(&app), ("artboard.align".to_string(), json!("artboard"), None));
|
||||
}
|
||||
|
||||
/// With the Artboard tool chosen, the Distribute Objects and Distribute Spacing buttons space the
|
||||
/// selected artboards. With Align to Artboard they send the Distribute Spacing value and the
|
||||
/// active artboard stays in place. They are off for one artboard.
|
||||
#[test]
|
||||
fn the_distribute_buttons_space_the_selected_artboards() {
|
||||
let mut app = selected_artboards();
|
||||
let ctx = egui::Context::default();
|
||||
let (left, space) = distribute_buttons(&mut app, &ctx);
|
||||
click(&ctx, &mut app, left.center(), show);
|
||||
assert_eq!(artboard_corners(&app), [(0.0, 0.0), (200.0, 40.0), (400.0, 10.0)], "{}", app.ui.status);
|
||||
assert_eq!(last_sent(&app), ("artboard.distribute".to_string(), json!("selection"), None));
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
click(&ctx, &mut app, space.center(), show);
|
||||
assert_eq!(artboard_corners(&app), [(0.0, 0.0), (220.0, 40.0), (400.0, 10.0)], "gaps of 120: {}", app.ui.status);
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
app.ui.align_to = AlignTo::Artboard;
|
||||
app.ui.align_spacing = 20.0;
|
||||
click(&ctx, &mut app, space.center(), show);
|
||||
assert_eq!(artboard_corners(&app), [(30.0, 0.0), (150.0, 40.0), (230.0, 10.0)], "gaps of 20: {}", app.ui.status);
|
||||
assert_eq!(last_sent(&app), ("artboard.distributeSpacing".to_string(), json!("artboard"), Some(json!(20.0))));
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
click(&ctx, &mut app, left.center(), show);
|
||||
assert_eq!(artboard_corners(&app), [(130.0, 0.0), (150.0, 40.0), (170.0, 10.0)], "left edges 20 apart: {}", app.ui.status);
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
app.session.set_tool_option("selected", &json!([1]));
|
||||
let ran = app.session.journal.len();
|
||||
for button in [left, space] {
|
||||
click(&ctx, &mut app, button.center(), show);
|
||||
}
|
||||
assert_eq!(app.session.journal.len(), ran, "one artboard: the Distribute buttons are off");
|
||||
}
|
||||
|
||||
/// A button's shortcut entry is enabled by, and runs, the artboard command with the Artboard tool
|
||||
/// chosen and the object command with another tool.
|
||||
#[test]
|
||||
fn a_button_runs_the_artboard_command_only_with_the_artboard_tool() {
|
||||
let mut app = selected_artboards();
|
||||
let b = Button::Spacing("horizontal");
|
||||
assert_eq!(b.command_now(&app), "artboard.distributeSpacing");
|
||||
assert_eq!(artboards_selected(&app), 3);
|
||||
app.select_tool("selection");
|
||||
assert_eq!(b.command_now(&app), "object.distributeSpacing");
|
||||
let before = artboard_corners(&app);
|
||||
run_button(&mut app, Button::Align("horizontal", "left")).ok();
|
||||
assert_eq!(artboard_corners(&app), before, "with the Selection tool the buttons leave the artboards alone");
|
||||
}
|
||||
|
||||
+48
-1
@@ -4768,6 +4768,12 @@ the Align To choice, and while there is a key object or a key guide they send th
|
||||
`spacing`. Edit › Keyboard Shortcuts lists them under Align, with no default shortcut, and
|
||||
`shortcuts.set {id, shortcut}` gives one a shortcut.
|
||||
|
||||
With the Artboard tool chosen, the same buttons and UI commands, and the Control bar's Align and Distribute buttons,
|
||||
run `artboard.align`, `artboard.distribute` and `artboard.distributeSpacing` on the artboards the tool has selected.
|
||||
Align To then offers Align to Selection and Align to Artboard. Align to Artboard sends `to: "artboard"`, which keeps the
|
||||
tool's active artboard in place, and the Distribute buttons then send the Distribute Spacing value as `spacing`. Align
|
||||
to Selection sends `to: "selection"`. The Distribute buttons need two or more selected artboards.
|
||||
|
||||
Edit › Keyboard Shortcuts and `shortcuts.list` leave out a command with no default shortcut when a key press cannot
|
||||
run it: no item of the menu bar runs it, it opens no dialog when run without params, and it is one of these:
|
||||
|
||||
@@ -5634,7 +5640,48 @@ artboard it names is locked. When no artboard changes, the command records no un
|
||||
`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`
|
||||
Align and distribute artboards ([Align and distribute artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/align-and-distribute-artboards.html)):
|
||||
three commands move whole artboards, by default those the Artboard tool has selected, else its active one. Each takes
|
||||
`index? | indices?`, `to?: "selection"|"artboard"`, `artboard?: n`, `moveArt?`, `patterns?` and `lockedAndHidden?`, and
|
||||
returns `{moved: [index…]}`.
|
||||
|
||||
- `artboard.align {horizontal?: "left"|"center"|"right", vertical?: "top"|"center"|"bottom"}` moves each artboard until
|
||||
the named edge or center is on that edge or center of the bounds of all the artboards named. One of the two
|
||||
directions is needed, and both can be given.
|
||||
- `artboard.distribute {horizontal? | vertical?, spacing?}` spaces the named edges or centers evenly between the first
|
||||
and the last artboard along the axis, which stay where they are. It needs three or more artboards.
|
||||
- `artboard.distributeSpacing {axis?: "horizontal"|"vertical", spacing?}` makes the gaps between the artboards along
|
||||
the axis equal. The first and the last artboard stay where they are. It needs three or more artboards, and `axis`
|
||||
defaults to `"horizontal"`.
|
||||
|
||||
`to: "artboard"` aligns to artboard `artboard`, which stays where it is. `artboard` defaults to the Artboard tool's
|
||||
active artboard. For `artboard.align` it can be an artboard that is not being aligned. The two Distribute commands
|
||||
space the artboards as they do without it and then move them together so that artboard `artboard`, when it is one of
|
||||
them, stays where it is. With `to: "artboard"` and `spacing`, `artboard.distribute` places the edges or centers
|
||||
`spacing` points apart, outward from that artboard in their order, and `artboard.distributeSpacing` leaves a gap of
|
||||
`spacing` points between neighbors; two artboards are enough, a negative `spacing` overlaps them, and an `artboard`
|
||||
that is not one of the artboards distributed fails with `no such artboard`. Without `to: "artboard"`, `spacing` is not
|
||||
used. These rules for `to`, `artboard` and `spacing` are VectorCraft's own.
|
||||
|
||||
When a call with `to: "artboard"` names no `artboard`, its journal entry records the active artboard it used as
|
||||
`activeArtboard`. A replay of the journal aligns to that artboard, and a recorded action leaves the value out and
|
||||
aligns to the artboard that is active when it plays. An `artboard` the call names stays in the action.
|
||||
|
||||
`moveArt` moves the art fully inside each artboard with it and defaults to the Artboard tool's Move Artwork with
|
||||
Artboard option. `patterns` and `lockedAndHidden` work as they do on `artboard.move`. An artboard's own guides move
|
||||
with it. A locked artboard stays where it is: it still counts in the bounds and keeps its place in the order, and a
|
||||
call fails with `the artboard is locked` when every artboard it names is locked. That rule is VectorCraft's own. Each
|
||||
command is one undo step, and records none when no artboard moves (fewer artboards than it needs included). A direction,
|
||||
`to`, `axis`, `artboard`, `spacing` or switch with a value outside the ones listed is an error, and nothing changes.
|
||||
A `spacing` that would move an artboard past the edge of the canvas fails with
|
||||
`result would exceed the canvas (coordinates out of range)`, and nothing changes.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"artboard.distributeSpacing","params":{"indices":[0,1,2],"to":"artboard","artboard":0,"spacing":20}}}
|
||||
```
|
||||
|
||||
When a call to `artboard.duplicate`, `artboard.copy`, `artboard.cut`, `artboard.rename`, `artboard.switchOrientation`,
|
||||
`artboard.align`, `artboard.distribute` or `artboard.distributeSpacing`
|
||||
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.
|
||||
|
||||
|
||||
@@ -196,6 +196,7 @@ tools, never checked side by side) isn't captured row by row.
|
||||
| Distribute ruler guides: the Distribute Objects and Distribute Spacing buttons space the selected guides across the axis with the selected objects, as items with no size; two or more guides selected alone are enough | done | #1597 in part; guides alone: select them, choose Align to Artboard and click Horizontal (Vertical) Distribute Center for equal columns (rows) ([public forum answer, 2017](https://community.adobe.com/questions-652/illustrator-equal-guides-822924)); with objects: a guide on each artboard edge, included in the selection, keeps the outermost lines there ([public forum answer, 2025](https://www.signs101.com/threads/is-there-a-way-to-distribute-shapes-like-this-in-illustrator.179556/)); a guide has no width, so the three options of an axis are the same for it; guides along the axis stay |
|
||||
| Ruler guides align to a key object: a click on an object selected with guides makes it the key (one object is enough); Align moves the selected guides to its edge or centre (vertical guides to left, centre or right, horizontal ones to top, centre or bottom) and the key stays put | done | #2143; aligning to the selection or the artboard leaves guides alone |
|
||||
| Align panel menu › Align to Glyph Bounds › Point Text and Area Text (checked while on): Align and Distribute measure point type or area type by the ink of its glyphs instead of its em box or frame | done | #4231; `alignGlyphBoundsPoint`, `alignGlyphBoundsArea` ([Align text](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/align-text.html)); `tests::glyphbounds`, `panels::align::tests::align_to_glyph_bounds_toggles_point_and_area_text`; the Character and Properties panels' Align to Glyph Bounds controls are not done |
|
||||
| Align and distribute artboards: with the Artboard tool chosen, the Align panel's Align Objects, Distribute Objects and Distribute Spacing buttons, and the Control bar's, move the selected artboards with the art inside them (Move Artwork with Artboard) | done | `artboard.align`, `artboard.distribute`, `artboard.distributeSpacing` ([Align and distribute artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/align-and-distribute-artboards.html)); VectorCraft's own rules: Align to Artboard keeps the active artboard in place and sends the Distribute Spacing value, Align to Key Object is off, a locked artboard stays where it is; `cmd::artboardalign::tests`, `panels::align::tests::the_align_buttons_align_the_selected_artboards` |
|
||||
| A marquee selects the objects whose paths it touches (anchors or segments): drawn inside a filled shape, over a background, it leaves that shape (Preview and Outline) | done | #1103 |
|
||||
| Click a segment independently of its endpoint anchors; only its direction handles show | done | #1278: hollow endpoint squares, selected edge emphasized; headless egui checked |
|
||||
| Drag a curved segment to bend it, a straight one to move its anchors | done | |
|
||||
|
||||
Reference in New Issue
Block a user