Fix #4917: Sort Objects sorts the selection by one attribute and applies that order to another

Squashed from #5017 onto main.
This commit is contained in:
bflatastic
2026-11-01 08:11:17 +07:00
parent ef80709e16
commit cd277d5d84
36 changed files with 680 additions and 16 deletions
+3 -1
View File
@@ -9,7 +9,7 @@
**Stage: alpha** · next: beta, ~13 points (ready for real work ~62% → ~75%, and reliable `.ai` exchange) and ~170–270 h away
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Progress log row for the Arrange Tools add-on's Randomize Opacity; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Progress log row for the Arrange Tools add-on's Sort Objects; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe Illustrator workflow. It runs
on macOS, Windows, Linux, FreeBSD and the web (WASM), and agents can drive all of it over MCP, the CLI and a JSON
@@ -130,6 +130,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap
## Progress log
- 2026-11-01: Randomize Opacity joins the Arrange Tools add-on (Object › Plug-ins › Arrange Tools): each selected object gets its own opacity at random between a minimum and a maximum percent, with a seed that repeats the result, as one undo step (`addon.arrange.randomizeOpacity`, #4925).
- 2026-11-01: Sort Objects joins the Arrange Tools add-on (Object › Plug-ins › Arrange Tools): the selected objects are sorted by width, height, area, position, opacity or stacking order (small to large, large to small or at random) and take another of those in that order, trading their values or stepping them evenly, as one undo step (`addon.arrange.sort`, #4917).
- 2026-11-01: New Symbols from Layers joins the Document Tools add-on (Object › Plug-ins › Document Tools): one symbol of each top-level layer's art (the layers highlighted in the Layers panel when two or more are, else every layer), named after the layer, made as New Symbol makes one; Convert Layers to Symbols also puts an instance of each in place of its layer's art. Empty layers are skipped and reported; one undo step. A VectorCraft addition: headline numbers unchanged.
- 2026-11-01: Document Tools, a built-in add-on (Object › Plug-ins › Document Tools), starts with Save a Timestamped Copy: a copy of the document named `<name>_YYYY-MM-DD_HH-MM` (UTC; `-2`, `-3`… when that name is taken) goes next to it, or into a folder picked for a document never saved, written as File › Save a Copy writes it, so the document keeps its path, title and modified state. Off on the web, which has no clock. A VectorCraft addition: headline numbers unchanged.
- 2026-11-01: Divide into Dashes joins the Path Precision Tools add-on (Object › Plug-ins › Path Precision Tools): the selected paths, or only their direct-selected segments, are cut into separate open paths of a dash length measured along the curve, with an optional gap (left empty, or kept as pieces), each keeping the path's appearance; Fit to Corners and Ends stretches or squeezes the lengths so every run starts and ends on a dash. A dialog with Preview, one undo step, at most 100000 pieces. A VectorCraft addition: headline numbers unchanged.
@@ -210,6 +211,7 @@ The full earlier record of what landed is kept in [docs/roadmap.md](docs/roadmap
| Date | Change | Summary |
|---|---|---|
| 2026-11-01 | minor | Progress log row for the Arrange Tools add-on's Randomize Opacity (#4925); headline numbers unchanged. |
| 2026-11-01 | minor | Progress log row for the Arrange Tools add-on's Sort Objects (#4917); headline numbers unchanged. |
| 2026-11-01 | minor | Progress log row for the Document Tools add-on's New Symbols from Layers (#4927); headline numbers unchanged. |
| 2026-11-01 | minor | Progress log row for the Document Tools add-on's Save a Timestamped Copy (#4926); headline numbers unchanged. |
| 2026-11-01 | minor | Progress log row for Divide into Dashes (Path Precision Tools add-on, #4950); headline numbers unchanged. |
+144 -4
View File
@@ -3,13 +3,15 @@
//! Replace with Top Object puts a copy of the topmost selected object in place of each of the
//! others; and Set Size of Each gives every selected object an exact width and height. Places and
//! sizes are measured and set as `object.setBounds` does. Group Overlapping Objects groups the
//! objects whose visual bounds overlap, and Randomize Opacity gives each selected object a random
//! opacity. Its submenu (Object › Plug-ins › Arrange Tools), its dialogs and Zoom to Selection (a
//! view command) are the UI's.
//! objects whose visual bounds overlap, Randomize Opacity gives each selected object a random
//! opacity, and Sort Objects sorts the selected objects by one attribute and gives them another in
//! that order. Its submenu (Object › Plug-ins › Arrange Tools), its dialogs and Zoom to Selection
//! (a view command) are the UI's.
#[cfg(test)]
mod tests;
use std::collections::HashMap;
use std::sync::Arc;
use serde_json::{Value, json};
@@ -31,8 +33,10 @@ const LIMIT: f64 = 1e6;
const LEAST: f64 = 0.001;
/// The most columns Arrange in Grid takes.
const MAX_COLUMNS: u64 = 10_000;
/// The most objects Group Overlapping Objects looks at.
/// The most objects Group Overlapping Objects and Sort Objects look at.
const MAX_OBJECTS: usize = 20_000;
/// The attributes Sort Objects sorts by and gives (`addon.arrange.sort`).
pub const SORT_KEYS: [&str; 7] = ["width", "height", "area", "x", "y", "opacity", "stacking"];
pub fn specs() -> Vec<CommandSpec> {
vec![
@@ -90,6 +94,15 @@ pub fn specs() -> Vec<CommandSpec> {
has_selection,
randomize_opacity
),
cmd!(
"addon.arrange.sort",
"Sort Objects",
["Object", "Plug-ins", "Arrange Tools"],
None,
"{by?: \"width\" | \"height\" | \"area\" (width × height) | \"x\" (left edge) | \"y\" (top edge) | \"opacity\" (%) | \"stacking\" (stacking order, from the back) (\"area\"), apply?: one of the same (\"stacking\"), order?: \"ascending\" (small to large) | \"descending\" (large to small) | \"random\" (\"ascending\"), distribute?: false (true: the values step evenly from the smallest to the largest; false: the objects trade the values they have), seed?: whole number (0; the order \"random\" picks), scaleStrokes?: the Scale Strokes & Effects preference (width, height and area), ids?: [id…] (default: the selection)} sort the selected objects (the top-level ones, at most 20000) by the attribute `by` and give them the values of `apply` in that order, as one undo step: the first in order gets the smallest value. Sizes and places are measured and set as object.setBounds measures them (Use Preview Bounds), a width, height or area about each object's centre (an area in proportion); stacking order always trades places, and only among the objects in the same layer or group. A run that changes nothing makes no step → {count}",
has_multi,
sort
),
]
}
@@ -393,3 +406,130 @@ fn randomize_opacity(s: &mut Session, p: &Value) -> Result<Value> {
})?;
Ok(json!({ "count": ids.len() }))
}
/// An object Sort Objects sorts: its layer or group, its place in it, its box as
/// `object.setBounds` measures it and its opacity (%).
struct Sorted {
id: NodeId,
parent: Option<NodeId>,
slot: usize,
b: OrientedBox,
opacity: f64,
}
/// Attribute `key` (one of [`SORT_KEYS`]) of `o`, the `i`th of the objects in stacking order.
fn attribute(key: &str, o: &Sorted, i: usize) -> f64 {
let r = o.b.rect;
// The top-left corner where `object.setBounds` places it (reference point 0).
let corner = o.b.to_doc() * Point::new(r.x0, r.y0);
match key {
"width" => r.width(),
"height" => r.height(),
"area" => r.width() * r.height(),
"x" => corner.x,
"y" => corner.y,
"opacity" => o.opacity,
_ => i as f64,
}
}
fn sort(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "addon.arrange.sort";
let by = choice(p, C, "by", &SORT_KEYS)?.unwrap_or("area");
let apply = choice(p, C, "apply", &SORT_KEYS)?.unwrap_or("stacking");
let order = choice(p, C, "order", &["ascending", "descending", "random"])?.unwrap_or("ascending");
let distribute = switch_or(p, "distribute", false, C)?;
let mut rng = Rng::new(whole_or(p, "seed", 0, C)?);
let strokes = switch_or(p, "scaleStrokes", s.prefs.scale_strokes, C)?;
let ids = checked_ids_or_selection(s, p, C)?;
if ids.len() > MAX_OBJECTS {
return Err(bad(C, "too many objects selected (at most 20000)"));
}
let d = &s.doc()?.doc;
// The objects in stacking order, from the back (their index paths, found in one walk).
let paths = d.index_paths(ids.iter().copied());
let mut placed: Vec<(&Vec<usize>, NodeId)> = ids.iter().filter_map(|id| Some((paths.get(id)?, *id))).collect();
placed.sort_by(|a, b| a.0.cmp(b.0));
let items: Vec<Sorted> = placed
.into_iter()
.filter_map(|(path, id)| {
let (&slot, up) = path.split_last()?;
let parent = if up.is_empty() { None } else { Some(d.node_at(up)?.id) };
Some(Sorted { id, parent, slot, b: s.transform_box(&[id])?, opacity: f64::from(d.node(id)?.opacity) * 100.0 })
})
.collect();
let keys: Vec<f64> = items.iter().enumerate().map(|(i, o)| attribute(by, o, i)).collect();
// The objects in sorted order; equal keys keep their stacking order.
let mut rank: Vec<usize> = (0..items.len()).collect();
match order {
"random" => rng.shuffle(&mut rank),
"descending" => rank.sort_by(|a, b| keys[*b].total_cmp(&keys[*a])),
_ => rank.sort_by(|a, b| keys[*a].total_cmp(&keys[*b])),
}
let count = json!({ "count": items.len() });
if apply == "stacking" {
// Each layer or group's objects take its objects' places, from the back, in sorted order.
let mut groups: HashMap<Option<NodeId>, Vec<usize>> = HashMap::new();
for o in rank.iter().filter_map(|i| items.get(*i)) {
groups.entry(o.parent).or_default().push(o.slot);
}
if groups.values().all(|slots| slots.is_sorted()) {
return Ok(count);
}
s.edit("Sort Objects", |d, _| {
for (parent, from) in groups {
let ch = d.children_mut(parent)?;
let nodes: Vec<Arc<Node>> = from.iter().filter_map(|i| ch.get(*i).cloned()).collect();
let mut to = from;
to.sort_unstable();
for (slot, n) in to.into_iter().zip(nodes) {
if let Some(c) = ch.get_mut(slot) {
*c = n;
}
}
}
Ok(())
})?;
return Ok(count);
}
// The values the objects get, smallest first: theirs, or evenly from the smallest to the largest.
let mut values: Vec<f64> = items.iter().enumerate().map(|(i, o)| attribute(apply, o, i)).collect();
values.sort_by(f64::total_cmp);
if let (true, Some(&lo), Some(&hi)) = (distribute && values.len() > 1, values.first(), values.last()) {
let steps = (values.len() - 1) as f64;
values = (0..values.len()).map(|j| lo + (hi - lo) * j as f64 / steps).collect();
}
let wanted: Vec<(&Sorted, f64)> = rank.iter().filter_map(|i| items.get(*i)).zip(values).collect();
if apply == "opacity" {
if wanted.iter().all(|(o, v)| (o.opacity - v).abs() < 1e-6) {
return Ok(count);
}
s.edit("Sort Objects", |d, _| {
for (o, v) in &wanted {
if let Some(n) = d.node_mut(o.id) {
n.opacity = (v / 100.0) as f32;
}
}
Ok(())
})?;
return Ok(count);
}
let mut xfs = Vec::with_capacity(wanted.len());
for (o, v) in wanted {
let q = match apply {
"x" => json!({ "x": v, "reference": 0 }),
"y" => json!({ "y": v, "reference": 0 }),
"width" => json!({ "width": v, "strokes": strokes }),
"height" => json!({ "height": v, "strokes": strokes }),
// An area: both sides in proportion; an object without one keeps its size.
_ => {
let (w, h) = (o.b.rect.width(), o.b.rect.height());
let k = if w * h > 1e-12 { (v / (w * h)).sqrt() } else { 1.0 };
json!({ "width": w * k, "height": h * k, "strokes": strokes })
}
};
xfs.push((o.id, object_bounds_transform(s, &q, o.id)?));
}
transform_each(s, "Sort Objects", strokes, &xfs)?;
Ok(count)
}
@@ -263,6 +263,7 @@ fn off_without_a_selection() {
"addon.arrange.size",
"addon.arrange.groupOverlapping",
"addon.arrange.randomizeOpacity",
"addon.arrange.sort",
] {
assert!(!enabled(&s, c), "{c}");
}
@@ -416,3 +417,148 @@ fn randomize_opacity_refuses_bad_params() {
let e = s.execute("addon.arrange.randomizeOpacity", &json!({"min": 80, "max": 20})).unwrap_err().to_string();
assert!(e.contains("`min` is greater than `max`"), "{e}");
}
fn layer_order(s: &Session) -> Vec<NodeId> {
s.doc().unwrap().doc.layers[0].children().unwrap().iter().map(|n| n.id).collect()
}
/// Three squares: `a` 30 wide at x 0, `b` 10 wide at x 100, `c` 20 wide at x 200, all selected.
fn squares(s: &mut Session) -> (NodeId, NodeId, NodeId) {
let a = rect(s, 0.0, 0.0, 30.0, 30.0);
let b = rect(s, 100.0, 0.0, 10.0, 10.0);
let c = rect(s, 200.0, 0.0, 20.0, 20.0);
select(s, &[a, b, c]);
(a, b, c)
}
#[test]
fn sort_by_area_lines_objects_up_along_x() {
let mut s = session();
let (a, b, c) = squares(&mut s);
let n = steps(&s);
let r = s.execute("addon.arrange.sort", &json!({"by": "area", "apply": "x"})).unwrap();
assert_eq!(r["count"], json!(3));
assert_eq!(steps(&s), n + 1, "one undo step");
// Smallest first: b, c, a take the left edges 0, 100, 200; y stays.
assert!(near(bounds(&s, b), (0.0, 0.0, 10.0, 10.0)), "{:?}", bounds(&s, b));
assert!(near(bounds(&s, c), (100.0, 0.0, 20.0, 20.0)), "{:?}", bounds(&s, c));
assert!(near(bounds(&s, a), (200.0, 0.0, 30.0, 30.0)), "{:?}", bounds(&s, a));
// Large to small: a, c, b.
s.execute("edit.undo", &json!({})).unwrap();
s.execute("addon.arrange.sort", &json!({"by": "area", "apply": "x", "order": "descending"})).unwrap();
assert_eq!((bounds(&s, a).x0, bounds(&s, c).x0, bounds(&s, b).x0), (0.0, 100.0, 200.0));
// Already in order: no step.
let n = steps(&s);
s.execute("addon.arrange.sort", &json!({"by": "area", "apply": "x", "order": "descending"})).unwrap();
assert_eq!(steps(&s), n);
}
#[test]
fn sort_distributes_values_evenly() {
let mut s = session();
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
let b = rect(&mut s, 10.0, 50.0, 10.0, 50.0);
let c = rect(&mut s, 200.0, 0.0, 10.0, 20.0);
select(&mut s, &[a, b, c]);
// Left edges 0, 10, 200 step evenly: 0, 100, 200.
s.execute("addon.arrange.sort", &json!({"by": "x", "apply": "x", "distribute": true})).unwrap();
assert_eq!((bounds(&s, a).x0, bounds(&s, b).x0, bounds(&s, c).x0), (0.0, 100.0, 200.0));
// Heights that step evenly with x (10 to 50), each about its centre.
s.execute("addon.arrange.sort", &json!({"by": "x", "apply": "height", "distribute": true})).unwrap();
assert!(near(bounds(&s, a), (0.0, 0.0, 10.0, 10.0)), "{:?}", bounds(&s, a));
assert!(near(bounds(&s, b), (100.0, 60.0, 10.0, 30.0)), "{:?}", bounds(&s, b));
assert!(near(bounds(&s, c), (200.0, -15.0, 10.0, 50.0)), "{:?}", bounds(&s, c));
}
#[test]
fn sort_restacks_within_each_layer() {
let mut s = session();
let (a, b, c) = squares(&mut s);
// Largest at the back.
s.execute("addon.arrange.sort", &json!({"by": "area", "apply": "stacking", "order": "descending"})).unwrap();
assert_eq!(layer_order(&s), [a, c, b]);
s.execute("addon.arrange.sort", &json!({})).unwrap();
assert_eq!(layer_order(&s), [b, c, a], "by area, small to large, into the stacking order by default");
// An object in another layer keeps its layer; the others trade places among themselves.
s.execute("layer.new", &json!({})).unwrap();
let big = rect(&mut s, 0.0, 100.0, 50.0, 50.0);
select(&mut s, &[a, b, c, big]);
s.execute("addon.arrange.sort", &json!({"order": "descending"})).unwrap();
assert_eq!(layer_order(&s), [a, c, b]);
let doc = &s.doc().unwrap().doc;
assert_ne!(doc.layer_of(big), doc.layer_of(a), "left in its own layer");
}
#[test]
fn sort_by_stacking_and_opacity() {
let mut s = session();
let (a, b, c) = squares(&mut s);
// Back to front a, b, c; reversed: c, b, a take the left edges 0, 100, 200.
s.execute("addon.arrange.sort", &json!({"by": "stacking", "apply": "x", "order": "descending"})).unwrap();
assert_eq!((bounds(&s, c).x0, bounds(&s, b).x0, bounds(&s, a).x0), (0.0, 100.0, 200.0));
for (id, o) in [(a, 20), (b, 80), (c, 50)] {
s.execute("object.setProps", &json!({"ids": [id.0], "opacity": o})).unwrap();
}
select(&mut s, &[a, b, c]);
let opacity = |s: &Session, id| s.doc().unwrap().doc.node(id).unwrap().opacity;
// By x, large to small: a (x 200) gets the least opacity, then b, then c.
s.execute("addon.arrange.sort", &json!({"by": "x", "apply": "opacity", "order": "descending"})).unwrap();
assert_eq!((opacity(&s, a), opacity(&s, b), opacity(&s, c)), (0.2, 0.5, 0.8));
// By opacity into the stacking order, large to small: the most opaque at the back.
s.execute("addon.arrange.sort", &json!({"by": "opacity", "order": "descending"})).unwrap();
assert_eq!(layer_order(&s), [c, b, a]);
}
#[test]
fn sort_by_area_into_areas_scales_in_proportion() {
let mut s = session();
let (a, b, c) = squares(&mut s);
// Areas 900, 100, 400 by x: a (x 0) 100, b 500, c 900.
s.execute("addon.arrange.sort", &json!({"by": "x", "apply": "area", "distribute": true})).unwrap();
let side = |s: &Session, id| bounds(s, id).width();
assert!((side(&s, a) - 10.0).abs() < 1e-6 && (side(&s, b) - 500f64.sqrt()).abs() < 1e-6 && (side(&s, c) - 30.0).abs() < 1e-6);
assert!((bounds(&s, b).height() - 500f64.sqrt()).abs() < 1e-6, "in proportion");
}
#[test]
fn sort_in_random_order_repeats_with_a_seed() {
let mut s = session();
let ids: Vec<NodeId> = (0..12).map(|i| rect(&mut s, i as f64 * 10.0, 0.0, 5.0, 5.0)).collect();
select(&mut s, &ids);
s.execute("addon.arrange.sort", &json!({"order": "random", "seed": 5})).unwrap();
let first = layer_order(&s);
assert_ne!(first, ids, "shuffled");
let mut sorted = first.clone();
sorted.sort_by_key(|i| i.0);
assert_eq!(sorted, ids, "the same objects");
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(layer_order(&s), ids);
s.execute("addon.arrange.sort", &json!({"order": "random", "seed": 5})).unwrap();
assert_eq!(layer_order(&s), first, "the same seed, the same order");
}
#[test]
fn sort_takes_ids_and_refuses_bad_params() {
let mut s = session();
let (a, b, c) = squares(&mut s);
select(&mut s, &[a]);
assert!(!enabled(&s, "addon.arrange.sort"));
s.execute("addon.arrange.sort", &json!({"ids": [b.0, a.0], "apply": "x"})).unwrap();
assert_eq!((bounds(&s, b).x0, bounds(&s, a).x0, bounds(&s, c).x0), (0.0, 100.0, 200.0), "c is left alone");
select(&mut s, &[a, b, c]);
refuses(
&mut s,
"addon.arrange.sort",
&[
json!({"by": "color"}),
json!({"apply": 3}),
json!({"order": "up"}),
json!({"distribute": "yes"}),
json!({"seed": -1}),
json!({"seed": 0.5}),
json!({"scaleStrokes": 1}),
json!({"ids": "x"}),
json!({"ids": [999_999]}),
],
);
}
+5
View File
@@ -592,6 +592,11 @@ fn structured_junk() {
("addon.arrange.randomizeOpacity", json!({"min": 0, "max": 100, "seed": u64::MAX})),
("addon.arrange.randomizeOpacity", json!({"min": f64::MAX, "max": -1e308, "seed": -1, "ids": [u64::MAX]})),
("addon.arrange.randomizeOpacity", json!({"min": 100, "max": 0, "seed": 1e308, "ids": []})),
("addon.arrange.sort", json!({"by": "area", "apply": "area", "distribute": true, "scaleStrokes": true})),
("addon.arrange.sort", json!({"by": "stacking", "apply": "stacking", "order": "random", "seed": u64::MAX})),
("addon.arrange.sort", json!({"by": "opacity", "apply": "y", "order": "descending", "ids": [u64::MAX]})),
("addon.arrange.sort", json!({"by": "x", "apply": "opacity", "distribute": true, "seed": 1e308, "ids": []})),
("addon.arrange.sort", json!({"by": "nope", "apply": null, "order": 3, "distribute": "x"})),
("addon.pathtools.divideDashes", json!({"dash": 1e-308, "gap": -1e308, "fit": "x"})),
("addon.pathtools.divideDashes", json!({"dash": 0.01, "gap": 1e6, "fit": true, "removeGaps": false})),
("addon.pathtools.divideDashes", json!({"dash": f64::MAX, "gap": [0], "removeGaps": null})),
+33 -4
View File
@@ -1,7 +1,7 @@
//! Arrange Tools in the interface: its submenu (Object › Plug-ins › Arrange Tools); the dialogs of
//! Arrange in Grid, Replace with Top Object, Set Size of Each and Randomize Opacity, which preview
//! on the canvas and run their `addon.arrange.*` command on OK; Group Overlapping Objects; and Zoom
//! to Selection ([`ZOOM`]), which fits the active view to the selection.
//! Arrange in Grid, Replace with Top Object, Set Size of Each, Randomize Opacity and Sort Objects,
//! which preview on the canvas and run their `addon.arrange.*` command on OK; Group Overlapping
//! Objects; and Zoom to Selection ([`ZOOM`]), which fits the active view to the selection.
use egui::Ui;
use serde_json::{Value, json};
@@ -19,12 +19,14 @@ pub const GRID: &str = "arrangeGrid";
pub const REPLACE: &str = "arrangeReplace";
pub const SIZE: &str = "arrangeSize";
pub const OPACITY: &str = "arrangeOpacity";
pub const SORT: &str = "arrangeSort";
/// Each dialog's kind, command and heading.
const DIALOGS: [(&str, &str, &str); 4] = [
const DIALOGS: [(&str, &str, &str); 5] = [
(GRID, "addon.arrange.grid", "Arrange in Grid"),
(REPLACE, "addon.arrange.replace", "Replace with Top Object"),
(SIZE, "addon.arrange.size", "Set Size of Each"),
(OPACITY, "addon.arrange.randomizeOpacity", "Randomize Opacity"),
(SORT, "addon.arrange.sort", "Sort Objects"),
];
/// The widths and heights Set Size of Each takes (pt), and the gaps Arrange in Grid takes.
const SIZES: std::ops::RangeInclusive<f64> = 0.001..=1e6;
@@ -44,6 +46,7 @@ pub fn menu() -> Vec<Item> {
Item::Cmd("Set Size of Each…", "addon.arrange.size", Value::Null),
Item::Cmd("Group Overlapping Objects", "addon.arrange.groupOverlapping", json!({})),
Item::Cmd("Randomize Opacity…", "addon.arrange.randomizeOpacity", Value::Null),
Item::Cmd("Sort Objects…", "addon.arrange.sort", Value::Null),
Item::Sep,
Item::Cmd("Zoom to Selection", ZOOM, json!({})),
]
@@ -64,6 +67,11 @@ pub(crate) fn dialog(app: &VectorcraftApp, id: &str) -> Option<(&'static str, Va
REPLACE => json!({ "fit": "keep", "keep": false, "scaleStrokes": strokes }),
// A new seed each time the document has changed, so running it again varies the result.
OPACITY => json!({ "min": 30, "max": 100, "seed": st.revision % SEEDS }),
// The random order's seed (not shown): new whenever the document has changed, so each run
// shuffles anew while Preview and OK agree.
SORT => json!({
"by": "area", "apply": "stacking", "order": "ascending", "distribute": false, "seed": st.revision % SEEDS, "scaleStrokes": strokes,
}),
// The first selected object's size, as the command measures it (a side it lacks: the least).
_ => {
let b = st.selection.objects.first().and_then(|id| app.session.transform_box(&[*id])).map_or((100.0, 100.0), |b| {
@@ -151,6 +159,27 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool {
}
});
}
SORT => {
let attributes = [
("width", "Width"),
("height", "Height"),
("area", "Area"),
("x", "X Position"),
("y", "Y Position"),
("opacity", "Opacity"),
("stacking", "Stacking Order"),
];
form::choice(ui, d, "by", tl!("Sort By:"), widths, &attributes);
form::choice(ui, d, "apply", tl!("Apply To:"), widths, &attributes);
let orders = [("ascending", "Small to Large"), ("descending", "Large to Small"), ("random", "Random")];
form::choice(ui, d, "order", tl!("Order:"), widths, &orders);
// Stacking order only trades places; only sizes scale strokes.
let apply = d.str("apply");
ui.add_enabled_ui(apply != "stacking", |ui| form::check(ui, d, "distribute", tl!("Distribute Evenly")));
ui.add_enabled_ui(matches!(apply.as_str(), "width" | "height" | "area"), |ui| {
form::check(ui, d, "scaleStrokes", tl!("Scale Strokes & Effects"));
});
}
_ => {
egui::Grid::new("arrange-size").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| {
for (side, tick, label) in [("width", "__width", tl!("Width:")), ("height", "__height", tl!("Height:"))] {
+23 -1
View File
@@ -2,7 +2,7 @@ use serde_json::json;
use vectorcraft_doc::NodeId;
use vectorcraft_engine::Session;
use super::{GRID, OPACITY, REPLACE, SIZE, ZOOM};
use super::{GRID, OPACITY, REPLACE, SIZE, SORT, ZOOM};
use crate::tests_labels::painted_text;
use crate::{VectorcraftApp, menus};
@@ -42,6 +42,7 @@ fn the_plugins_submenu_lists_the_tools() {
("Set Size of Each…", "addon.arrange.size"),
("Group Overlapping Objects", "addon.arrange.groupOverlapping"),
("Randomize Opacity…", "addon.arrange.randomizeOpacity"),
("Sort Objects…", "addon.arrange.sort"),
("Zoom to Selection", ZOOM),
] {
assert_eq!(under(label).and_then(|e| e.command.as_deref()), Some(id), "{label}");
@@ -168,3 +169,24 @@ fn randomize_opacity_opens_its_dialog_and_runs_on_ok() {
assert!((0.4..=0.6).contains(&doc.node(id).unwrap().opacity), "{id:?}");
}
}
#[test]
fn sort_objects_opens_its_dialog_and_runs_on_ok() {
let (mut app, a, b) = app();
click(&mut app, "Sort Objects…");
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(SORT));
let shown = painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx()));
for s in ["Sort Objects", "Sort By:", "Apply To:", "Order:", "Area", "Stacking Order", "Small to Large", "Distribute Evenly", "Preview"] {
assert!(shown.contains(s), "{s} in {shown}");
}
let d = app.ui.dialog.as_mut().unwrap();
assert_eq!((&d.fields["by"], &d.fields["apply"], &d.fields["order"]), (&json!("area"), &json!("stacking"), &json!("ascending")));
// Large to small along x: b (the larger) takes the left edge 0, a the left edge 100.
d.fields.insert("apply".into(), json!("x"));
d.fields.insert("order".into(), json!("descending"));
let n = app.session.doc().unwrap().history.undo.len();
crate::dialogs::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
assert_eq!(app.session.doc().unwrap().history.undo.len(), n + 1);
assert_eq!((bounds(&app, b).x0, bounds(&app, a).x0), (0.0, 100.0));
}
+1 -1
View File
@@ -400,7 +400,7 @@ registry! {
RearrangeArtboards: [rearrange_artboards::KIND] => rearrange_artboards::SPEC,
PathToolOptions: [crate::addons::pathtools::KIND] => crate::addons::pathtools::OPTIONS_SPEC,
DivideDashes: [crate::addons::pathtools::dashes::KIND] => crate::addons::pathtools::dashes::SPEC,
ArrangeTools: [crate::addons::arrange::GRID, crate::addons::arrange::REPLACE, crate::addons::arrange::SIZE, crate::addons::arrange::OPACITY] => crate::addons::arrange::SPEC,
ArrangeTools: [crate::addons::arrange::GRID, crate::addons::arrange::REPLACE, crate::addons::arrange::SIZE, crate::addons::arrange::OPACITY, crate::addons::arrange::SORT] => crate::addons::arrange::SPEC,
ShortcutImport: [shortcut_import::KIND] => DialogSpec::window(shortcut_import::show, shortcut_import::confirm),
}
+12
View File
@@ -6006,3 +6006,15 @@ breakText Glyphs Глифи
Maximum: Максимум:
Seed: Начално число:
@msg `min` is greater than `max` `min` е по-голямо от `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… Сортиране на обекти…
Sort Objects Сортиране на обекти
Sort By: Сортиране по:
Apply To: Прилагане към:
Area Площ
X Position Позиция по X
Y Position Позиция по Y
Stacking Order Ред на подреждане
Small to Large От малко към голямо
Large to Small От голямо към малко
Distribute Evenly Равномерно разпределение
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6039,3 +6039,15 @@ shape Open Otvoren
Maximum: Maksimum:
Seed: Početna vrijednost:
@msg `min` is greater than `max` `min` je veće od `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… Sortiraj objekte…
Sort Objects Sortiraj objekte
Sort By: Sortiraj po:
Apply To: Primijeni na:
Area Površina
X Position Položaj X
Y Position Položaj Y
Stacking Order Redoslijed slaganja
Small to Large Od manjeg prema većem
Large to Small Od većeg prema manjem
Distribute Evenly Rasporedi ravnomjerno
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6112,3 +6112,15 @@ breakText Glyphs Glifs
Maximum: Màxim:
Seed: Llavor:
@msg `min` is greater than `max` `min` és més gran que `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… Ordena els objectes…
Sort Objects Ordena els objectes
Sort By: Ordena per:
Apply To: Aplica a:
Area Àrea
X Position Posició X
Y Position Posició Y
Stacking Order Ordre d’apilament
Small to Large De petit a gran
Large to Small De gran a petit
Distribute Evenly Distribueix uniformement
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6122,3 +6122,15 @@ breakText Glyphs Glyfy
Maximum: Maximum:
Seed: Počáteční hodnota:
@msg `min` is greater than `max` `min` je větší než `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… Seřadit objekty…
Sort Objects Seřadit objekty
Sort By: Seřadit podle:
Apply To: Použít na:
Area Plocha
X Position Poloha X
Y Position Poloha Y
Stacking Order Pořadí vrstvení
Small to Large Od nejmenšího po největší
Large to Small Od největšího po nejmenší
Distribute Evenly Rozmístit rovnoměrně
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6021,3 +6021,15 @@ breakText Glyphs Glyffer
Maximum: Maksimum:
Seed: Startværdi:
@msg `min` is greater than `max` `min` er større end `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… Sortér objekter…
Sort Objects Sortér objekter
Sort By: Sortér efter:
Apply To: Anvend på:
Area Areal
X Position X-position
Y Position Y-position
Stacking Order Stablingsrækkefølge
Small to Large Fra lille til stor
Large to Small Fra stor til lille
Distribute Evenly Fordel jævnt
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -5707,3 +5707,15 @@ breakText Glyphs Glyphen
Maximum: Maximum:
Seed: Startwert:
@msg `min` is greater than `max` `min` ist größer als `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… Objekte sortieren…
Sort Objects Objekte sortieren
Sort By: Sortieren nach:
Apply To: Anwenden auf:
Area Fläche
X Position X-Position
Y Position Y-Position
Stacking Order Stapelreihenfolge
Small to Large Von klein nach groß
Large to Small Von groß nach klein
Distribute Evenly Gleichmäßig verteilen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6074,3 +6074,15 @@ breakText Glyphs Γλύφοι
Maximum: Μέγιστο:
Seed: Αρχική τιμή:
@msg `min` is greater than `max` Το `min` είναι μεγαλύτερο από το `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… Ταξινόμηση αντικειμένων…
Sort Objects Ταξινόμηση αντικειμένων
Sort By: Ταξινόμηση κατά:
Apply To: Εφαρμογή σε:
Area Εμβαδόν
X Position Θέση X
Y Position Θέση Y
Stacking Order Σειρά στοίβαξης
Small to Large Από μικρό σε μεγάλο
Large to Small Από μεγάλο σε μικρό
Distribute Evenly Ομοιόμορφη κατανομή
Can't render this file because it contains an unexpected character in line 164 and column 5.
+12
View File
@@ -6064,3 +6064,15 @@ breakText Glyphs Glifos
Maximum: Máximo:
Seed: Semilla:
@msg `min` is greater than `max` `min` es mayor que `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… Ordenar objetos…
Sort Objects Ordenar objetos
Sort By: Ordenar por:
Apply To: Aplicar a:
Area Área
X Position Posición X
Y Position Posición Y
Stacking Order Orden de apilamiento
Small to Large De menor a mayor
Large to Small De mayor a menor
Distribute Evenly Distribuir uniformemente
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6152,3 +6152,15 @@ breakText Glyphs Merkit
Maximum: Suurin:
Seed: Siemen:
@msg `min` is greater than `max` `min` on suurempi kuin `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… Lajittele objektit…
Sort Objects Lajittele objektit
Sort By: Lajitteluperuste:
Apply To: Käytä kohteeseen:
Area Pinta-ala
X Position X-sijainti
Y Position Y-sijainti
Stacking Order Pinoamisjärjestys
Small to Large Pienestä suureen
Large to Small Suuresta pieneen
Distribute Evenly Jaa tasaisesti
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6119,3 +6119,15 @@ breakText Glyphs Glyphes
Maximum: Maximum :
Seed: Graine :
@msg `min` is greater than `max` `min` est supérieur à `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… Trier les objets…
Sort Objects Trier les objets
Sort By: Trier par :
Apply To: Appliquer à :
Area Surface
X Position Position X
Y Position Position Y
Stacking Order Ordre d’empilement
Small to Large Du plus petit au plus grand
Large to Small Du plus grand au plus petit
Distribute Evenly Répartir uniformément
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6108,3 +6108,15 @@ breakText Glyphs Glifák
Maximum: Maximum:
Seed: Kezdőérték:
@msg `min` is greater than `max` `min` nagyobb, mint `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… Objektumok rendezése…
Sort Objects Objektumok rendezése
Sort By: Rendezés alapja:
Apply To: Alkalmazás erre:
Area Terület
X Position X pozíció
Y Position Y pozíció
Stacking Order Halmozási sorrend
Small to Large Kicsitől a nagyig
Large to Small Nagytól a kicsiig
Distribute Evenly Egyenletes elosztás
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6021,3 +6021,15 @@ breakText Glyphs Glif
Maximum: Maksimum:
Seed: Benih:
@msg `min` is greater than `max` `min` lebih besar dari `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… Urutkan objek…
Sort Objects Urutkan objek
Sort By: Urutkan menurut:
Apply To: Terapkan ke:
Area Luas
X Position Posisi X
Y Position Posisi Y
Stacking Order Urutan tumpukan
Small to Large Kecil ke besar
Large to Small Besar ke kecil
Distribute Evenly Distribusikan merata
Can't render this file because it contains an unexpected character in line 2190 and column 5.
+12
View File
@@ -5741,3 +5741,15 @@ breakText Glyphs Glifi
Maximum: Massimo:
Seed: Seme:
@msg `min` is greater than `max` `min` è maggiore di `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… Ordina oggetti…
Sort Objects Ordina oggetti
Sort By: Ordina per:
Apply To: Applica a:
Area Area
X Position Posizione X
Y Position Posizione Y
Stacking Order Ordine di sovrapposizione
Small to Large Dal più piccolo al più grande
Large to Small Dal più grande al più piccolo
Distribute Evenly Distribuisci uniformemente
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -5808,3 +5808,15 @@ breakText Glyphs 字形
Maximum: 最大:
Seed: シード:
@msg `min` is greater than `max` `min` が `max` より大きくなっています
# Arrange Tools add-on: Sort Objects.
Sort Objects… オブジェクトを並べ替え…
Sort Objects オブジェクトを並べ替え
Sort By: 並べ替えの基準:
Apply To: 適用先:
Area 面積
X Position X 位置
Y Position Y 位置
Stacking Order 重ね順
Small to Large 小さい順
Large to Small 大きい順
Distribute Evenly 均等に分布
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6111,3 +6111,15 @@ breakText Glyphs გლიფები
Maximum: მაქსიმუმი:
Seed: საწყისი რიცხვი:
@msg `min` is greater than `max` `min` მეტია, ვიდრე `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… ობიექტების დალაგება…
Sort Objects ობიექტების დალაგება
Sort By: დალაგება:
Apply To: გამოყენება:
Area ფართობი
X Position X პოზიცია
Y Position Y პოზიცია
Stacking Order დაწყობის რიგი
Small to Large პატარიდან დიდისკენ
Large to Small დიდიდან პატარისკენ
Distribute Evenly თანაბრად განაწილება
Can't render this file because it is too large.
+12
View File
@@ -5704,3 +5704,15 @@ breakText Glyphs 글리프
Maximum: 최대:
Seed: 시드:
@msg `min` is greater than `max` `min`이(가) `max`보다 큽니다
# Arrange Tools add-on: Sort Objects.
Sort Objects… 개체 정렬…
Sort Objects 개체 정렬
Sort By: 정렬 기준:
Apply To: 적용 대상:
Area 면적
X Position X 위치
Y Position Y 위치
Stacking Order 쌓는 순서
Small to Large 작은 것부터
Large to Small 큰 것부터
Distribute Evenly 균등 분포
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6065,3 +6065,15 @@ breakText Glyphs Glyphs
Maximum: Maximum:
Seed: Startwaarde:
@msg `min` is greater than `max` `min` is groter dan `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… Objecten sorteren…
Sort Objects Objecten sorteren
Sort By: Sorteren op:
Apply To: Toepassen op:
Area Oppervlakte
X Position X-positie
Y Position Y-positie
Stacking Order Stapelvolgorde
Small to Large Van klein naar groot
Large to Small Van groot naar klein
Distribute Evenly Gelijkmatig verdelen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -5977,3 +5977,15 @@ breakText Glyphs Glify
Maximum: Maksimum:
Seed: Ziarno:
@msg `min` is greater than `max` `min` jest większe niż `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… Sortuj obiekty…
Sort Objects Sortuj obiekty
Sort By: Sortuj według:
Apply To: Zastosuj do:
Area Pole
X Position Położenie X
Y Position Położenie Y
Stacking Order Kolejność ułożenia
Small to Large Od najmniejszego do największego
Large to Small Od największego do najmniejszego
Distribute Evenly Rozmieść równomiernie
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6196,3 +6196,15 @@ swatch Yellow Green Verde-amarelado
Maximum: Máximo:
Seed: Semente:
@msg `min` is greater than `max` `min` é maior que `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… Classificar objetos…
Sort Objects Classificar objetos
Sort By: Classificar por:
Apply To: Aplicar a:
Area Área
X Position Posição X
Y Position Posição Y
Stacking Order Ordem de empilhamento
Small to Large Do menor para o maior
Large to Small Do maior para o menor
Distribute Evenly Distribuir uniformemente
Can't render this file because it contains an unexpected character in line 6 and column 47.
+12
View File
@@ -6099,3 +6099,15 @@ breakText Glyphs Глифы
Maximum: Максимум:
Seed: Начальное число:
@msg `min` is greater than `max` `min` больше, чем `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… Сортировать объекты…
Sort Objects Сортировать объекты
Sort By: Сортировать по:
Apply To: Применить к:
Area Площадь
X Position Положение по X
Y Position Положение по Y
Stacking Order Порядок наложения
Small to Large От меньшего к большему
Large to Small От большего к меньшему
Distribute Evenly Распределить равномерно
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -6174,3 +6174,15 @@ breakText Glyphs Glyfer
Maximum: Maximum:
Seed: Startvärde:
@msg `min` is greater than `max` `min` är större än `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… Sortera objekt…
Sort Objects Sortera objekt
Sort By: Sortera efter:
Apply To: Tillämpa på:
Area Area
X Position X-position
Y Position Y-position
Stacking Order Staplingsordning
Small to Large Från liten till stor
Large to Small Från stor till liten
Distribute Evenly Fördela jämnt
Can't render this file because it contains an unexpected character in line 5 and column 25.
+12
View File
@@ -6079,3 +6079,15 @@ breakText Glyphs Glifler
Maximum: En Çok:
Seed: Başlangıç Değeri:
@msg `min` is greater than `max` `min`, `max` değerinden büyük
# Arrange Tools add-on: Sort Objects.
Sort Objects… Nesneleri Sırala…
Sort Objects Nesneleri Sırala
Sort By: Sıralama Ölçütü:
Apply To: Uygulanacak Değer:
Area Alan
X Position X Konumu
Y Position Y Konumu
Stacking Order İstifleme Sırası
Small to Large Küçükten Büyüğe
Large to Small Büyükten Küçüğe
Distribute Evenly Eşit Dağıt
Can't render this file because it contains an unexpected character in line 5 and column 25.
+12
View File
@@ -5721,3 +5721,15 @@ breakText Glyphs Гліфи
Maximum: Максимум:
Seed: Початкове число:
@msg `min` is greater than `max` `min` більше за `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… Сортувати об’єкти…
Sort Objects Сортувати об’єкти
Sort By: Сортувати за:
Apply To: Застосувати до:
Area Площа
X Position Положення за X
Y Position Положення за Y
Stacking Order Порядок накладання
Small to Large Від меншого до більшого
Large to Small Від більшого до меншого
Distribute Evenly Розподілити рівномірно
Can't render this file because it contains an unexpected character in line 178 and column 5.
+12
View File
@@ -5724,3 +5724,15 @@ breakText Glyphs Glyph
Maximum: Tối đa:
Seed: Giá trị khởi đầu:
@msg `min` is greater than `max` `min` lớn hơn `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… Sắp xếp đối tượng…
Sort Objects Sắp xếp đối tượng
Sort By: Sắp xếp theo:
Apply To: Áp dụng cho:
Area Diện tích
X Position Vị trí X
Y Position Vị trí Y
Stacking Order Thứ tự xếp chồng
Small to Large Từ nhỏ đến lớn
Large to Small Từ lớn đến nhỏ
Distribute Evenly Phân bố đều
Can't render this file because it contains an unexpected character in line 200 and column 5.
+12
View File
@@ -5964,3 +5964,15 @@ breakText Glyphs 字形
Maximum: 最大值:
Seed: 种子:
@msg `min` is greater than `max` `min` 大于 `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… 排序对象…
Sort Objects 排序对象
Sort By: 排序依据:
Apply To: 应用于:
Area 面积
X Position X 位置
Y Position Y 位置
Stacking Order 堆叠顺序
Small to Large 从小到大
Large to Small 从大到小
Distribute Evenly 均匀分布
Can't render this file because it contains an unexpected character in line 2 and column 73.
+12
View File
@@ -5761,3 +5761,15 @@ breakText Glyphs 字符
Maximum: 最大值:
Seed: 種子:
@msg `min` is greater than `max` `min` 大於 `max`
# Arrange Tools add-on: Sort Objects.
Sort Objects… 排序物件…
Sort Objects 排序物件
Sort By: 排序依據:
Apply To: 套用至:
Area 面積
X Position X 位置
Y Position Y 位置
Stacking Order 堆疊順序
Small to Large 從小到大
Large to Small 從大到小
Distribute Evenly 均勻分佈
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4 -3
View File
@@ -6342,12 +6342,13 @@ as `object.setBounds` measures them (Use Preview Bounds), except Arrange in Grid
| `addon.arrange.size` | `width`, `height` pt, 0.001…1000000 (at least one); `proportional` (false: a side not given stays; true: it follows the given one in proportion); `reference` 0…8 (4, the centre); `scaleStrokes` | Gives every selected object that width and height, keeping its reference point in place (the 9-point grid row by row from the top-left). |
| `addon.arrange.groupOverlapping` | none | Groups the selected objects (at most 20000) wherever their visual bounds overlap or touch, chains included: when A overlaps B and B overlaps C, all three go in one group. Only objects in the same layer or group are grouped together; objects that overlap nothing stay as they are. Each new group goes where its frontmost member was in the stacking order, and the groups and the objects left alone stay selected; with nothing overlapping it makes no step. Needs two objects or more. → `{groups: [id…], grouped}` (how many objects went into groups) |
| `addon.arrange.randomizeOpacity` | `min`, `max` % 0…100 (30, 100; `min` not more than `max`); `seed` whole number (0); `ids` (the selection) | Gives each selected object its own opacity (the object opacity the Transparency panel sets), picked at random and evenly from `min` to `max`; the same seed, objects and range give the same opacities. Groups take one opacity of their own; the objects inside keep theirs. → `{count}` |
| `addon.arrange.sort` | `by`, `apply`: `width`, `height`, `area` (width × height), `x` (left edge), `y` (top edge), `opacity` (%), `stacking` (stacking order from the back) (`area`, `stacking`); `order`: `ascending` (small to large), `descending` (large to small), `random` (`ascending`); `distribute` (false); `seed` whole number (0, for `random`); `scaleStrokes`; `ids` (the selection) | Sorts the selected objects (at most 20000) by `by` and gives them the values of `apply` in that order: the first gets the smallest value. Without `distribute` the objects trade the values they have; with it the values step evenly from the smallest to the largest. A width, height or area changes about each object's centre (an area in proportion), a place moves the top-left corner. Stacking order always trades places, and only among objects in the same layer or group. Needs two objects or more. → `{count}` |
Zoom to Selection is the UI command `addon.arrange.zoomSelection {}` (remote mode only): it zooms and centres the
active view so the selection's visual bounds fill the window with a margin, as `view.fitArtboard` fits the artboard,
and fails with nothing selected. Arrange in Grid…, Replace with Top Object…, Set Size of Each… and Randomize Opacity… open dialogs
(`ui.menuDialog {command}`, dialog kinds `arrangeGrid`, `arrangeReplace`, `arrangeSize` and `arrangeOpacity`) whose
fields are the command's parameters, with Preview (Randomize Opacity's seed starts new whenever the document has changed); Set Size of Each's `__width` and `__height` say which sizes OK applies.
and fails with nothing selected. Arrange in Grid…, Replace with Top Object…, Set Size of Each…, Randomize Opacity… and Sort Objects… open dialogs
(`ui.menuDialog {command}`, dialog kinds `arrangeGrid`, `arrangeReplace`, `arrangeSize`, `arrangeOpacity` and `arrangeSort`) whose
fields are the command's parameters, with Preview (Randomize Opacity's seed, and Sort Objects' `seed`, which is not shown, start new whenever the document has changed); Set Size of Each's `__width` and `__height` say which sizes OK applies.
```json
{"name":"run_command","arguments":{"command":"addon.arrange.grid","params":{"columns":4,"gapX":12,"gapY":12}}}
+6 -1
View File
@@ -1,6 +1,6 @@
# Parity with Adobe Illustrator
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Arrange Tools add-on: Randomize Opacity, #4925; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Arrange Tools add-on: Sort Objects, #4917; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
## Feature submission evidence
@@ -1417,6 +1417,10 @@ A VectorCraft addition, not a feature of the reference app, so no area score mov
A VectorCraft addition, not a feature of the reference app, so no area score moves. Object › Plug-ins › Arrange Tools › Randomize Opacity… gives each top-level selected object (or each of `ids`) its own object opacity, picked at random and evenly between a minimum and a maximum percent (30 to 100 by default); a seed (0 by default; the dialog starts with a new one whenever the document has changed) repeats a result exactly. A group takes one opacity of its own and the objects inside keep theirs. One undo step; a minimum above the maximum, a percent outside 0 to 100 or a seed that is not a whole number is refused without a change. Tests: `randomize_opacity_*` in `cmd::addons::arrange::tests` in `vectorcraft-engine` (range, one undo step, same and different seeds, defaults, `ids`, groups, bad params), `randomize_opacity_opens_its_dialog_and_runs_on_ok` in `vectorcraft-ui-egui` and `command_sweep`. Commands: `docs/mcp.md` › Arrange Tools add-on.
## Arrange Tools add-on: Sort Objects (2026-11-01)
A VectorCraft addition, not a feature of the reference app, so no area score moves. Object › Plug-ins › Arrange Tools › Sort Objects… sorts the top-level selected objects (or `ids`, at most 20000) by one attribute (width, height, area, x or y position, opacity or stacking order; small to large, large to small, or a random order from a seed) and gives them another in that order, the first the smallest value: the objects trade the values they have, or with Distribute Evenly the values step evenly from the smallest to the largest. Lining objects up left to right by size, restacking them so the largest is at the back, or giving them heights that step with their x position are each one run. Sizes and places are measured and set as `object.setBounds` measures them (Use Preview Bounds); a width, height or area changes about each object's centre (an area in proportion), and stacking order trades places only among objects of the same layer or group. One undo step; no step when nothing changes. Tests: `sort_*` in `cmd::addons::arrange::tests` in `vectorcraft-engine` (each attribute sorted by and applied, both directions, distribute, layers kept apart, the seeded random order, `ids`, no step when in order, bad params), `sort_objects_opens_its_dialog_and_runs_on_ok` in `vectorcraft-ui-egui` and `command_sweep`. Commands: `docs/mcp.md` › Arrange Tools add-on.
## Path Precision Tools add-on: Divide into Dashes (2026-11-01)
A VectorCraft addition, not a feature of the reference app, so no area score moves. Object › Plug-ins › Path Precision Tools › Divide into Dashes… cuts the selected paths into separate open paths along the curve (arc length, not straight distance): each `dash` long (0.01 to 1000000 pt, 10 by default) with `gap` between them (0 by default: the pieces touch); the gap pieces are left out unless Remove Gaps is off. With direct-selected segments (or anchors) only those runs are divided, the pattern running on across segments that follow one another, and the rest of such a path stays as pieces of its own. Fit to Corners and Ends gives every run between corners and ends a whole number of dashes, all stretched or squeezed alike so it starts and ends on a dash (a closed path without corners holds whole periods); otherwise the lengths are exact and each run's last piece is what is left. Every piece keeps the path's appearance and is selected; one undo step; at most 100000 pieces (more is refused). The dialog takes lengths in any unit and previews on the canvas. Tests: `dashes::tests` in `vectorcraft-pathops` (exact and fitted lengths, curves measured along them, picked segments on open and closed paths, the piece cap and bad patterns), `divide_into_dashes_*` in `cmd::addons::pathtools::tests` in `vectorcraft-engine` (gaps removed or kept, fitted lengths, the stroke kept, one undo step, selected segments only, bad params and too many pieces refused), `divide_into_dashes_opens_its_dialog_previews_and_runs_on_ok` in `vectorcraft-ui-egui` and `command_sweep`. Commands: `docs/mcp.md` › Path Precision Tools add-on.
@@ -1438,6 +1442,7 @@ Symbols area (#4938), from the public symbol documentation ([create and place sy
| Date | Change | Summary |
|---|---|---|
| 2026-11-01 | minor | Arrange Tools add-on (a VectorCraft addition): Randomize Opacity (#4925); evidence added; scores unchanged. |
| 2026-11-01 | minor | Arrange Tools add-on (a VectorCraft addition): Sort Objects (#4917); evidence added; scores unchanged. |
| 2026-11-01 | minor | Symbols panel: Select All Unused (`symbol.unused`) shows the symbols nothing places selected, and Delete Symbol deletes them all, asked once, in one undo step (`symbol.delete {names}`, #4918); scores and hours unchanged |
| 2026-11-01 | minor | Type core: Area Type Options › Text Flow (#4978): area type with several rows and columns fills them by columns (the default) or by rows, from the dialog's radio buttons or `text.areaOptions {flow}`; evidence updated; scores unchanged. |
| 2026-11-01 | minor | Symbols: dynamic symbols (#4938): Symbol Options › Symbol Type, per-instance appearance for objects of a dynamic symbol's art that stays linked through redefinition, Break Link baking, Reset and Static dropping it; evidence added. Score unchanged at 87% (estimated), hours 5–9 → 4–8. |
+3 -1
View File
@@ -1,6 +1,6 @@
# UI and interaction parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Arrange Tools add-on: Randomize Opacity, #4925) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Arrange Tools add-on: Sort Objects, #4917) · **Target:** Adobe Illustrator 2026 (30.x)
## Feature submission evidence
@@ -335,6 +335,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po
| Arrange Tools add-on (VectorCraft addition): Object › Plug-ins › Arrange Tools runs Arrange in Grid… (grid by row, one row or one column; columns, H and V spacing, Center on Artboard; Preview), Swap Objects, Swap Objects and Sizes, Replace with Top Object… (Actual Size, Bounds or Bounds (Fit Proportionally); Keep Original; Scale Strokes & Effects; Preview), Set Size of Each… (Width and Height each ticked on or off, Constrain Width and Height Proportions with one side, the reference point, Scale Strokes & Effects; Preview) and Zoom to Selection, each one undo step | done | `addon.arrange.*`, `crates/engine/src/cmd/addons/arrange.rs`, `crates/ui-egui/src/addons/arrange.rs` and their tests; not a feature of the reference app, so it is not scored |
| Arrange Tools add-on (VectorCraft addition): Object › Plug-ins › Arrange Tools › Group Overlapping Objects groups the selected objects whose visual bounds overlap or touch (chains included, within one layer or group), each group at its frontmost member's stacking place, one undo step | done | #4928; `addon.arrange.groupOverlapping`, `crates/engine/src/cmd/addons/arrange.rs` and its tests; not a feature of the reference app, so it is not scored |
| Arrange Tools add-on (VectorCraft addition): Object › Plug-ins › Arrange Tools › Randomize Opacity… gives each selected object its own opacity at random between Minimum and Maximum (%), with a Seed that repeats it and Preview, one undo step | done | #4925; `addon.arrange.randomizeOpacity`, `crates/engine/src/cmd/addons/arrange.rs`, `crates/ui-egui/src/addons/arrange.rs` and their tests; not a feature of the reference app, so it is not scored |
| Arrange Tools add-on (VectorCraft addition): Object › Plug-ins › Arrange Tools › Sort Objects… sorts the selected objects by Sort By (width, height, area, X or Y position, opacity, stacking order) in an Order (Small to Large, Large to Small, Random) and gives them Apply To in that order, trading the values or with Distribute Evenly; Scale Strokes & Effects; Preview; one undo step | done | #4917; `addon.arrange.sort`, `crates/engine/src/cmd/addons/arrange.rs`, `crates/ui-egui/src/addons/arrange.rs` and their tests; not a feature of the reference app, so it is not scored |
| Document Tools add-on (VectorCraft addition): Object › Plug-ins › Document Tools › Save a Timestamped Copy writes `<name>_YYYY-MM-DD_HH-MM.<ext>` next to the document (a folder picked for one never saved), which keeps its path, title and modified state; off on the web | done | #4926; `addon.document.saveTimestampedCopy`, `crates/engine/src/cmd/addons/document.rs`, `crates/ui-egui/src/addons/document.rs` and their tests; not a feature of the reference app, so it is not scored |
| Document Tools add-on (VectorCraft addition): Object › Plug-ins › Document Tools › New Symbols from Layers makes a symbol of each layer's art named after the layer (the highlighted layers when two or more are, else all); Convert Layers to Symbols also replaces each layer's art with an instance in place; empty layers skipped, one undo step | done | #4927; `addon.document.symbolsFromLayers`, `crates/engine/src/cmd/addons/document.rs`, `crates/ui-egui/src/addons/document.rs` and their tests; not a feature of the reference app, so it is not scored |
| Smart Pen add-on (VectorCraft addition): Object › Plug-ins › Smart Pen selects the Smart Pen Tool, which the All Tools drawer lists under its own heading but neither toolbar shows as shipped (no shortcut), and opens the Smart Pen panel (next segment straight or curved; Remove Points Keep Shape, Make Smooth, Make Corner, Retract Handles, Close or Join). The tool draws (click: straight or curved segment, a curve after a straight one tangent; drag: smooth anchor, Alt breaks the handle, Shift 45°, Space moves it; Tab switches a segment; rubber band; close, join, continue) and edits without switching tools (click selects a point, drag moves it, Alt-click removes it keeping the shape, Shift+Alt-click deletes it, double-click toggles smooth/corner; handle drags with Shift and Alt; segment drags reshape, Alt-click adds a point) | done | `addon.pen.*`, `crates/tools/src/addons/smart_pen.rs`, `crates/ui-egui/src/addons/pen.rs` and their tests; not a feature of the reference app, so it is not scored |
@@ -391,6 +392,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
| Date | Change | Summary |
|---|---|---|
| 2026-11-01 | minor | Arrange Tools add-on (a VectorCraft addition): Randomize Opacity (#4925); scores and estimates unchanged |
| 2026-11-01 | minor | Arrange Tools add-on (a VectorCraft addition): Sort Objects (#4917); scores and estimates unchanged |
| 2026-11-01 | minor | Blob Brush and Eraser: the tip's outline (size, angle, roundness, at the zoom) follows the pointer before and while painting, around a small centre dot, updating at once with `[` / `]` and Tool Options; precise cursors (Caps Lock) show only the crosshair (#4977); scores and estimates unchanged (estimated) |
| 2026-11-01 | minor | Layers panel: an open blend lists its spine as a `<Path>` row below its key objects, and a click selects the blend so Direct Selection edits the spine (#4820); scores and estimates unchanged |
| 2026-11-01 | minor | Pen with Cmd/Ctrl held: the lent Selection tool no longer scales the path being drawn by bounding-box handles the canvas hides while its end anchor is selected (a flat path did nothing under a resize pointer); a Cmd drag moves the path instead, and with Direct Selection moves anchors and segments, one undo step each (#4980); scores and estimates unchanged |