mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 11:47:21 -06:00
Fix #681: Rearrange All Artboards layouts, order and one-click access
- artboard.rearrange takes layout (gridByRow, gridByColumn, row, column;
byColumn stays as an alias of gridByColumn) and order (leftToRight,
rightToLeft); Columns is the row count of a grid by column and is
ignored by row and column. It returns {artboards, rows, columns}.
- A Rearrange All Artboards dialog: Layout and Layout Order toggles drawn
in code (widgets::layout_button, previewing the order), Columns/Rows,
Spacing, Move Artwork with Artboard and the artboard count.
- One click: Rearrange All in the Properties panel (Document section with
several artboards, Edit Artboards' Quick Actions); the Artboards panel's
Rearrange button and menu item now open the dialog too.
- Tests for every layout x order (artboards, art riding along, one undo
step), the dialog, the Properties button; the command sweep covers the
new params. Translations for ja, es, it, ru, fr, zh-hans, zh-hant and
pt-br; ROADMAP updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
23b7c68d90
commit
01df2c730b
@@ -156,7 +156,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Rearrange All Artboards…",
|
||||
["Object", "Artboards"],
|
||||
None,
|
||||
"{columns?: n (2), spacing?: pt (20), byColumn?: false, moveArtwork?: true (locked and hidden art only with prefs moveLockedWithArtboard)} lay artboards out in a grid",
|
||||
"{layout?: gridByRow|gridByColumn|row|column (gridByRow; byColumn?: true is gridByColumn), order?: leftToRight|rightToLeft (leftToRight: right-to-left mirrors the order along the rows), columns?: n (2; the rows for gridByColumn; row and column ignore it), spacing?: pt (20), moveArtwork?: true (locked and hidden art only with prefs moveLockedWithArtboard)} lay the artboards out in their order, the grid's top-left at the first artboard's → {artboards, rows, columns}",
|
||||
has_doc,
|
||||
rearrange_artboards
|
||||
),
|
||||
@@ -777,10 +777,32 @@ fn convert_to_artboards(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn rearrange_artboards(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let n_ab = s.doc()?.doc.artboards.len().max(1);
|
||||
let cols = (f64_or(p, "columns", 2.0).clamp(1.0, n_ab as f64)) as usize;
|
||||
const CMD: &str = "artboard.rearrange";
|
||||
use super::newdoc::ArtboardLayout as L;
|
||||
// A value given (not null) must be one of the names.
|
||||
let named = |key: &str| p.get(key).filter(|v| !v.is_null()).map(|v| v.as_str().unwrap_or_default());
|
||||
let layout = match named("layout") {
|
||||
Some(l) => L::parse(l).ok_or_else(|| bad(CMD, "layout must be gridByRow, gridByColumn, row or column"))?,
|
||||
None if bool_or(p, "byColumn", false) => L::GridByColumn,
|
||||
None => L::GridByRow,
|
||||
};
|
||||
let rtl = match named("order") {
|
||||
None => false,
|
||||
Some(o) if o.eq_ignore_ascii_case("leftToRight") => false,
|
||||
Some(o) if o.eq_ignore_ascii_case("rightToLeft") => true,
|
||||
Some(_) => return Err(bad(CMD, "order must be leftToRight or rightToLeft")),
|
||||
};
|
||||
let count = s.doc()?.doc.artboards.len();
|
||||
let n_ab = count.max(1);
|
||||
// Columns of a grid by row, rows of a grid by column (NaN casts to 0).
|
||||
let lines = (f64_or(p, "columns", 2.0).clamp(1.0, n_ab as f64) as usize).max(1);
|
||||
let (rows, cols) = match layout {
|
||||
L::GridByRow => (n_ab.div_ceil(lines), lines),
|
||||
L::GridByColumn => (lines, n_ab.div_ceil(lines)),
|
||||
L::Row => (1, n_ab),
|
||||
L::Column => (n_ab, 1),
|
||||
};
|
||||
let spacing = f64_or(p, "spacing", 20.0).clamp(-1.0e5, 1.0e5);
|
||||
let by_col = bool_or(p, "byColumn", false);
|
||||
let move_art = bool_or(p, "moveArtwork", true);
|
||||
let locked_and_hidden = s.prefs.move_locked_with_artboard;
|
||||
let scale_strokes = false;
|
||||
@@ -788,7 +810,7 @@ fn rearrange_artboards(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let rects: Vec<Rect> = d.artboards.iter().map(|a| a.rect).collect();
|
||||
let Some(first) = rects.first().copied() else { return Ok(()) };
|
||||
let sizes: Vec<(f64, f64)> = rects.iter().map(|r| (r.width(), r.height())).collect();
|
||||
let origins = super::newdoc::grid_origins(&sizes, first.origin(), cols, spacing, by_col, false);
|
||||
let origins = super::newdoc::grid_origins_rc(&sizes, first.origin(), (rows, cols), spacing, layout == L::GridByColumn, rtl);
|
||||
let deltas: Vec<Vec2> = rects.iter().zip(origins).map(|(r, o)| o - r.origin()).collect();
|
||||
if move_art {
|
||||
let tops: Vec<(NodeId, Point)> = d
|
||||
@@ -800,11 +822,11 @@ fn rearrange_artboards(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.filter_map(|n| Some((n.id, n.geometric_bounds()?.center())))
|
||||
.collect();
|
||||
for (id, c) in tops {
|
||||
if let Some(i) = rects.iter().position(|r| r.contains(c))
|
||||
&& deltas[i] != Vec2::ZERO
|
||||
if let Some(&dl) = rects.iter().position(|r| r.contains(c)).and_then(|i| deltas.get(i))
|
||||
&& dl != Vec2::ZERO
|
||||
&& let Some(n) = d.node_mut(id)
|
||||
{
|
||||
n.transform(Affine::translate(deltas[i]), scale_strokes);
|
||||
n.transform(Affine::translate(dl), scale_strokes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -818,5 +840,5 @@ fn rearrange_artboards(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
ok()
|
||||
Ok(json!({ "artboards": count, "rows": rows, "columns": cols }))
|
||||
}
|
||||
|
||||
@@ -345,9 +345,22 @@ pub fn color_mode_id(m: ColorMode) -> &'static str {
|
||||
/// each column as wide and each row as tall as its largest artboard; `by_column` fills columns
|
||||
/// top to bottom first, `rtl` puts the first column at the right.
|
||||
pub(crate) fn grid_origins(sizes: &[(f64, f64)], origin: Point, columns: usize, spacing: f64, by_column: bool, rtl: bool) -> Vec<Point> {
|
||||
let n = sizes.len();
|
||||
let cols = columns.clamp(1, n.max(1));
|
||||
let rows = n.div_ceil(cols).max(1);
|
||||
let n = sizes.len().max(1);
|
||||
let cols = columns.clamp(1, n);
|
||||
grid_origins_rc(sizes, origin, (n.div_ceil(cols), cols), spacing, by_column, rtl)
|
||||
}
|
||||
|
||||
/// [`grid_origins`] on a grid of `rows` × `cols` cells (at least enough for every artboard when
|
||||
/// filled the `by_column` way).
|
||||
pub(crate) fn grid_origins_rc(
|
||||
sizes: &[(f64, f64)],
|
||||
origin: Point,
|
||||
(rows, cols): (usize, usize),
|
||||
spacing: f64,
|
||||
by_column: bool,
|
||||
rtl: bool,
|
||||
) -> Vec<Point> {
|
||||
let (rows, cols) = (rows.max(1), cols.max(1));
|
||||
// Grid cell (row, column) of artboard i.
|
||||
let cell = |i: usize| {
|
||||
let (r, c) = if by_column { (i % rows, i / rows) } else { (i / cols, i % cols) };
|
||||
@@ -362,7 +375,7 @@ pub(crate) fn grid_origins(sizes: &[(f64, f64)], origin: Point, columns: usize,
|
||||
}
|
||||
}
|
||||
let offset = |spans: &[f64], k: usize| spans.iter().take(k).map(|s| s + spacing).sum::<f64>();
|
||||
(0..n)
|
||||
(0..sizes.len())
|
||||
.map(|i| {
|
||||
let (r, c) = cell(i);
|
||||
Point::new(origin.x + offset(&col_w, c), origin.y + offset(&row_h, r))
|
||||
|
||||
@@ -223,3 +223,63 @@ fn rearrange_keeps_its_grid() {
|
||||
s.execute("artboard.rearrange", &json!({"columns": 2, "spacing": 5})).unwrap();
|
||||
assert_eq!(boards(&s).iter().map(|r| (r.x0, r.y0)).collect::<Vec<_>>(), [(0.0, 0.0), (105.0, 0.0), (0.0, 55.0)]);
|
||||
}
|
||||
|
||||
/// #681: every layout and order of Rearrange All Artboards, five 100 × 50 artboards 10 pt apart
|
||||
/// with Columns (the rows of a grid by column) 2: each artboard lands in its (row, column) cell, the
|
||||
/// art on it rides along, and it all undoes in one step.
|
||||
#[test]
|
||||
fn rearrange_lays_out_by_layout_and_order() {
|
||||
type Cells = [(u32, u32); 5];
|
||||
let cells: [(&str, &str, Cells); 8] = [
|
||||
("gridByRow", "leftToRight", [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0)]),
|
||||
("gridByRow", "rightToLeft", [(0, 1), (0, 0), (1, 1), (1, 0), (2, 1)]),
|
||||
("gridByColumn", "leftToRight", [(0, 0), (1, 0), (0, 1), (1, 1), (0, 2)]),
|
||||
("gridByColumn", "rightToLeft", [(0, 2), (1, 2), (0, 1), (1, 1), (0, 0)]),
|
||||
("row", "leftToRight", [(0, 0), (0, 1), (0, 2), (0, 3), (0, 4)]),
|
||||
("row", "rightToLeft", [(0, 4), (0, 3), (0, 2), (0, 1), (0, 0)]),
|
||||
("column", "leftToRight", [(0, 0), (1, 0), (2, 0), (3, 0), (4, 0)]),
|
||||
("column", "rightToLeft", [(0, 0), (1, 0), (2, 0), (3, 0), (4, 0)]),
|
||||
];
|
||||
for (layout, order, want) in cells {
|
||||
// One row, 20 pt apart; a 10 pt square 10 pt into each artboard.
|
||||
let mut s = new_doc(json!({"width": 100, "height": 50, "artboards": 5}));
|
||||
let art: Vec<u64> = boards(&s)
|
||||
.iter()
|
||||
.map(|r| {
|
||||
s.execute("shape.rectangle", &json!({"x": r.x0 + 10.0, "y": r.y0 + 10.0, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap()
|
||||
})
|
||||
.collect();
|
||||
let before = boards(&s);
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
let p = json!({"layout": layout, "order": order, "columns": 2, "spacing": 10});
|
||||
let r = s.execute("artboard.rearrange", &p).unwrap();
|
||||
let (rows, cols) = want.iter().fold((0, 0), |(r, c), (wr, wc)| (r.max(wr + 1), c.max(wc + 1)));
|
||||
assert_eq!(r, json!({"artboards": 5, "rows": rows, "columns": cols}), "{layout} {order}");
|
||||
let got: Vec<(f64, f64)> = boards(&s).iter().map(|r| (r.x0, r.y0)).collect();
|
||||
let cells: Vec<(f64, f64)> = want.iter().map(|&(r, c)| (f64::from(c) * 110.0, f64::from(r) * 60.0)).collect();
|
||||
assert_eq!(got, cells, "{layout} {order}");
|
||||
assert!(boards(&s).iter().all(|r| r.width() == 100.0 && r.height() == 50.0));
|
||||
for (id, (x, y)) in art.iter().zip(&got) {
|
||||
let b = s.doc().unwrap().doc.node(NodeId(*id)).unwrap().geometric_bounds().unwrap();
|
||||
assert_eq!((b.x0, b.y0), (x + 10.0, y + 10.0), "{layout} {order}: the art moves with its artboard");
|
||||
}
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(boards(&s), before);
|
||||
}
|
||||
}
|
||||
|
||||
/// `byColumn: true` is Grid by Column; without `moveArtwork` the art stays; bad values are errors.
|
||||
#[test]
|
||||
fn rearrange_aliases_and_rejects_bad_values() {
|
||||
let mut s = new_doc(json!({"width": 100, "height": 50, "artboards": 3}));
|
||||
let id = s.execute("shape.rectangle", &json!({"x": 130, "y": 10, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("artboard.rearrange", &json!({"byColumn": true, "columns": 2, "spacing": 0, "moveArtwork": false})).unwrap();
|
||||
assert_eq!(boards(&s).iter().map(|r| (r.x0, r.y0)).collect::<Vec<_>>(), [(0.0, 0.0), (0.0, 50.0), (100.0, 0.0)]);
|
||||
assert_eq!(s.doc().unwrap().doc.node(NodeId(id)).unwrap().geometric_bounds().unwrap().x0, 130.0, "the art stays");
|
||||
let before = boards(&s);
|
||||
for p in [json!({"layout": "diagonal"}), json!({"order": "upward"}), json!({"layout": 3}), json!({"order": true})] {
|
||||
assert!(s.execute("artboard.rearrange", &p).is_err(), "{p}");
|
||||
}
|
||||
assert_eq!(boards(&s), before, "nothing moved");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user