diff --git a/ROADMAP.md b/ROADMAP.md index 0206168ac..4c33d5d16 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 Document Tools add-on's New Symbols from Layers; 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 Randomize Opacity; 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 @@ -129,6 +129,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: 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 `_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. @@ -208,6 +209,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 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. | diff --git a/crates/engine/src/cmd/addons/arrange.rs b/crates/engine/src/cmd/addons/arrange.rs index 986016213..0a7d168d0 100644 --- a/crates/engine/src/cmd/addons/arrange.rs +++ b/crates/engine/src/cmd/addons/arrange.rs @@ -3,8 +3,9 @@ //! 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. 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, 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. #[cfg(test)] mod tests; @@ -12,14 +13,16 @@ mod tests; use std::sync::Arc; use serde_json::{Value, json}; +use vectorcraft_color::recolor::Rng; use vectorcraft_doc::{Node, NodeId, OrientedBox}; use vectorcraft_geom::{Affine, Point, Rect}; use vectorcraft_render::effects; use super::super::edit::selected_roots; +use super::super::menucmds::whole_or; use super::super::object::{choice, object_bounds_transform, scaling, transform_node, transform_patterns}; use super::super::typecmd::refresh_bounds_of; -use super::super::{CommandSpec, bad, cmd, has_doc, has_multi, has_selection, number_or, switch_or}; +use super::super::{CommandSpec, bad, checked_ids_or_selection, cmd, has_doc, has_multi, has_selection, number_or, switch_or}; use crate::{EngineError, Result, Session}; /// The largest gap, width or height (pt) the commands take. @@ -78,6 +81,15 @@ pub fn specs() -> Vec { has_multi, group_overlapping ), + cmd!( + "addon.arrange.randomizeOpacity", + "Randomize Opacity", + ["Object", "Plug-ins", "Arrange Tools"], + None, + "{min?: % 0..100 (30), max?: % 0..100 (100; not less than min), seed?: whole number (0; the same seed, objects and range give the same opacities), ids?: [id…] (default: the selection)} give each selected object (the top-level ones) its own opacity, picked at random and evenly from min to max, as one undo step; it is the object's opacity, as the Transparency panel sets it → {count}", + has_selection, + randomize_opacity + ), ] } @@ -356,3 +368,28 @@ fn group_overlapping(s: &mut Session, _: &Value) -> Result { })?; Ok(json!({ "groups": groups.iter().map(|i| i.0).collect::>(), "grouped": grouped.len() })) } + +fn randomize_opacity(s: &mut Session, p: &Value) -> Result { + const C: &str = "addon.arrange.randomizeOpacity"; + let min = number_in(p, "min", 30.0, (0.0, 100.0), C)?; + let max = number_in(p, "max", 100.0, (0.0, 100.0), C)?; + if min > max { + return Err(bad(C, "`min` is greater than `max`")); + } + let mut rng = Rng::new(whole_or(p, "seed", 0, C)?); + let ids = checked_ids_or_selection(s, p, C)?; + if ids.is_empty() { + return Ok(json!({ "count": 0 })); + } + let (lo, hi) = ((min / 100.0) as f32, (max / 100.0) as f32); + s.edit("Randomize Opacity", |d, _| { + for id in &ids { + let o = rng.range(lo, hi); + if let Some(n) = d.node_mut(*id) { + n.opacity = o; + } + } + Ok(()) + })?; + Ok(json!({ "count": ids.len() })) +} diff --git a/crates/engine/src/cmd/addons/arrange/tests.rs b/crates/engine/src/cmd/addons/arrange/tests.rs index 48f9ae4ec..e19969f2e 100644 --- a/crates/engine/src/cmd/addons/arrange/tests.rs +++ b/crates/engine/src/cmd/addons/arrange/tests.rs @@ -256,7 +256,14 @@ fn off_without_a_selection() { let mut s = session(); rect(&mut s, 0.0, 0.0, 20.0, 10.0); s.execute("select.none", &json!({})).unwrap(); - for c in ["addon.arrange.grid", "addon.arrange.swap", "addon.arrange.replace", "addon.arrange.size", "addon.arrange.groupOverlapping"] { + for c in [ + "addon.arrange.grid", + "addon.arrange.swap", + "addon.arrange.replace", + "addon.arrange.size", + "addon.arrange.groupOverlapping", + "addon.arrange.randomizeOpacity", + ] { assert!(!enabled(&s, c), "{c}"); } assert!(!enabled(&Session::new(), "addon.arrange.swap")); @@ -336,3 +343,76 @@ fn group_overlapping_is_off_with_one_object_and_scales_to_many() { assert_eq!(sets.len(), 2000); assert!(sets.iter().all(|s| s.len() == 2 && s[1] == s[0] + 1)); } + +fn opacity(s: &Session, id: NodeId) -> f32 { + s.doc().unwrap().doc.node(id).unwrap().opacity +} + +#[test] +fn randomize_opacity_gives_each_object_its_own_opacity_within_the_range() { + let mut s = session(); + let ids: Vec = (0..20).map(|i| rect(&mut s, i as f64 * 10.0, 0.0, 5.0, 5.0)).collect(); + select(&mut s, &ids); + let n = steps(&s); + let r = s.execute("addon.arrange.randomizeOpacity", &json!({"min": 30, "max": 60, "seed": 7})).unwrap(); + assert_eq!(r["count"], json!(20)); + assert_eq!(steps(&s), n + 1, "one undo step"); + let first: Vec = ids.iter().map(|id| opacity(&s, *id)).collect(); + assert!(first.iter().all(|o| (0.3..=0.6).contains(o)), "{first:?}"); + assert!(first.windows(2).any(|w| w[0] != w[1]), "each its own: {first:?}"); + s.execute("edit.undo", &json!({})).unwrap(); + assert!(ids.iter().all(|id| opacity(&s, *id) == 1.0), "undone"); + // The same seed repeats them; another seed doesn't. + s.execute("addon.arrange.randomizeOpacity", &json!({"min": 30, "max": 60, "seed": 7})).unwrap(); + assert_eq!(ids.iter().map(|id| opacity(&s, *id)).collect::>(), first); + s.execute("addon.arrange.randomizeOpacity", &json!({"min": 30, "max": 60, "seed": 8})).unwrap(); + assert_ne!(ids.iter().map(|id| opacity(&s, *id)).collect::>(), first); + // Defaults 30 to 100; min = max gives every object that opacity. + s.execute("addon.arrange.randomizeOpacity", &json!({})).unwrap(); + assert!(ids.iter().all(|id| (0.3..=1.0).contains(&opacity(&s, *id)))); + s.execute("addon.arrange.randomizeOpacity", &json!({"min": 45, "max": 45})).unwrap(); + assert!(ids.iter().all(|id| opacity(&s, *id) == 0.45)); +} + +#[test] +fn randomize_opacity_takes_ids_and_leaves_the_rest() { + let mut s = session(); + let a = rect(&mut s, 0.0, 0.0, 5.0, 5.0); + let b = rect(&mut s, 10.0, 0.0, 5.0, 5.0); + select(&mut s, &[a, b]); + let g = NodeId(s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap()); + let c = rect(&mut s, 20.0, 0.0, 5.0, 5.0); + s.execute("select.none", &json!({})).unwrap(); + assert!(!enabled(&s, "addon.arrange.randomizeOpacity")); + s.execute("addon.arrange.randomizeOpacity", &json!({"ids": [c.0], "min": 10, "max": 10})).unwrap(); + assert_eq!((opacity(&s, a), opacity(&s, b), opacity(&s, c)), (1.0, 1.0, 0.1)); + // A group takes one opacity of its own; the objects inside keep theirs. + select(&mut s, &[g]); + s.execute("addon.arrange.randomizeOpacity", &json!({"min": 20, "max": 20})).unwrap(); + assert_eq!((opacity(&s, g), opacity(&s, a), opacity(&s, b)), (0.2, 1.0, 1.0)); +} + +#[test] +fn randomize_opacity_refuses_bad_params() { + let mut s = session(); + let a = rect(&mut s, 0.0, 0.0, 20.0, 10.0); + select(&mut s, &[a]); + refuses( + &mut s, + "addon.arrange.randomizeOpacity", + &[ + json!({"min": -1}), + json!({"max": 101}), + json!({"min": 80, "max": 20}), + json!({"min": 1e308}), + json!({"max": "50"}), + json!({"seed": -1}), + json!({"seed": 1.5}), + json!({"seed": "x"}), + json!({"ids": [999_999]}), + json!({"ids": "x"}), + ], + ); + 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}"); +} diff --git a/crates/engine/tests/command_sweep.rs b/crates/engine/tests/command_sweep.rs index fd879a301..06046338f 100644 --- a/crates/engine/tests/command_sweep.rs +++ b/crates/engine/tests/command_sweep.rs @@ -589,6 +589,9 @@ fn structured_junk() { ("addon.arrange.size", json!({"width": 1e6, "height": 0.001, "reference": 8, "scaleStrokes": true})), ("addon.arrange.size", json!({"width": 0.001, "proportional": true, "reference": u64::MAX})), ("addon.arrange.groupOverlapping", json!({"ids": "x", "junk": f64::MAX})), + ("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.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})), diff --git a/crates/ui-egui/src/addons/arrange.rs b/crates/ui-egui/src/addons/arrange.rs index f33757cee..f251aa69d 100644 --- a/crates/ui-egui/src/addons/arrange.rs +++ b/crates/ui-egui/src/addons/arrange.rs @@ -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 and Set Size of Each, 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 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. use egui::Ui; use serde_json::{Value, json}; @@ -18,15 +18,19 @@ pub const ZOOM: &str = "addon.arrange.zoomSelection"; pub const GRID: &str = "arrangeGrid"; pub const REPLACE: &str = "arrangeReplace"; pub const SIZE: &str = "arrangeSize"; +pub const OPACITY: &str = "arrangeOpacity"; /// Each dialog's kind, command and heading. -const DIALOGS: [(&str, &str, &str); 3] = [ +const DIALOGS: [(&str, &str, &str); 4] = [ (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"), ]; /// The widths and heights Set Size of Each takes (pt), and the gaps Arrange in Grid takes. const SIZES: std::ops::RangeInclusive = 0.001..=1e6; const GAPS: std::ops::RangeInclusive = -1e6..=1e6; +/// The seeds the Randomize Opacity dialog offers. +const SEEDS: u64 = 1_000_000; const LABEL_W: f32 = 110.0; const FIELD_W: f32 = 90.0; @@ -39,6 +43,7 @@ pub fn menu() -> Vec { Item::Cmd("Replace with Top Object…", "addon.arrange.replace", Value::Null), 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::Sep, Item::Cmd("Zoom to Selection", ZOOM, json!({})), ] @@ -57,6 +62,8 @@ pub(crate) fn dialog(app: &VectorcraftApp, id: &str) -> Option<(&'static str, Va "gapX": 10, "gapY": 10, "centerOnArtboard": false, }), 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 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| { @@ -76,7 +83,11 @@ pub(crate) const SPEC: DialogSpec = DialogSpec { confirm, preview: true, min_width: 320.0, - ok_enabled: Some(|_, d| d.kind != SIZE || d.bool("__width") || d.bool("__height")), + ok_enabled: Some(|_, d| match d.kind.as_str() { + SIZE => d.bool("__width") || d.bool("__height"), + OPACITY => d.f64("min", 0.0) <= d.f64("max", 100.0), + _ => true, + }), ..DialogSpec::FORM }; @@ -126,6 +137,20 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool { form::check(ui, d, "keep", tl!("Keep Original")); form::check(ui, d, "scaleStrokes", tl!("Scale Strokes & Effects")); } + OPACITY => { + for (key, label) in [("min", tl!("Minimum:")), ("max", tl!("Maximum:"))] { + widgets::label_row(ui, label, LABEL_W, |ui| { + if let Some(v) = widgets::range_field(ui, ("arrange", key), d.f64(key, 0.0), 0.0..=100.0, "%", 0, FIELD_W) { + d.fields.insert(key.into(), json!(v)); + } + }); + } + widgets::label_row(ui, tl!("Seed:"), LABEL_W, |ui| { + if let Some(v) = widgets::range_field(ui, ("arrange", "seed"), d.f64("seed", 0.0), 0.0..=(SEEDS - 1) as f64, "", 0, FIELD_W) { + d.fields.insert("seed".into(), json!(v as u64)); + } + }); + } _ => { 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:"))] { diff --git a/crates/ui-egui/src/addons/arrange/tests.rs b/crates/ui-egui/src/addons/arrange/tests.rs index 72411f388..6aa864673 100644 --- a/crates/ui-egui/src/addons/arrange/tests.rs +++ b/crates/ui-egui/src/addons/arrange/tests.rs @@ -2,7 +2,7 @@ use serde_json::json; use vectorcraft_doc::NodeId; use vectorcraft_engine::Session; -use super::{GRID, REPLACE, SIZE, ZOOM}; +use super::{GRID, OPACITY, REPLACE, SIZE, ZOOM}; use crate::tests_labels::painted_text; use crate::{VectorcraftApp, menus}; @@ -41,6 +41,7 @@ fn the_plugins_submenu_lists_the_tools() { ("Replace with Top Object…", "addon.arrange.replace"), ("Set Size of Each…", "addon.arrange.size"), ("Group Overlapping Objects", "addon.arrange.groupOverlapping"), + ("Randomize Opacity…", "addon.arrange.randomizeOpacity"), ("Zoom to Selection", ZOOM), ] { assert_eq!(under(label).and_then(|e| e.command.as_deref()), Some(id), "{label}"); @@ -139,3 +140,31 @@ fn group_overlapping_objects_runs_directly() { assert_eq!(doc.parent_of(c), Some(g), "a and c overlap: grouped"); assert_ne!(doc.parent_of(b), Some(g), "b is left alone"); } + +#[test] +fn randomize_opacity_opens_its_dialog_and_runs_on_ok() { + let (mut app, a, b) = app(); + click(&mut app, "Randomize Opacity…"); + assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(OPACITY)); + let shown = painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx())); + for s in ["Randomize Opacity", "Minimum:", "Maximum:", "Seed:", "Preview"] { + assert!(shown.contains(s), "{s} in {shown}"); + } + let d = app.ui.dialog.as_mut().unwrap(); + assert_eq!((&d.fields["min"], &d.fields["max"]), (&json!(30), &json!(100))); + // Minimum above maximum: OK is off. + d.fields.insert("min".into(), json!(80)); + d.fields.insert("max".into(), json!(20)); + assert!(!(crate::addons::arrange::SPEC.ok_enabled.unwrap())(&app, app.ui.dialog.as_ref().unwrap())); + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("min".into(), json!(40)); + d.fields.insert("max".into(), json!(60)); + 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); + let doc = &app.session.doc().unwrap().doc; + for id in [a, b] { + assert!((0.4..=0.6).contains(&doc.node(id).unwrap().opacity), "{id:?}"); + } +} diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index f9da884c4..371c02695 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -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::SPEC, + ArrangeTools: [crate::addons::arrange::GRID, crate::addons::arrange::REPLACE, crate::addons::arrange::SIZE, crate::addons::arrange::OPACITY] => crate::addons::arrange::SPEC, ShortcutImport: [shortcut_import::KIND] => DialogSpec::window(shortcut_import::show, shortcut_import::confirm), } diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index f4bbcda00..01896f2ed 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -5999,3 +5999,10 @@ breakText Glyphs Глифи Text Flow Поток на текста By Rows По редове By Columns По колони +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Произволна непрозрачност… + Randomize Opacity Произволна непрозрачност + Minimum: Минимум: + Maximum: Максимум: + Seed: Начално число: +@msg `min` is greater than `max` `min` е по-голямо от `max` diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index ab592a291..e32201e97 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -6032,3 +6032,10 @@ shape Open Otvoren Text Flow Tok teksta By Rows Po redovima By Columns Po kolonama +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Nasumična neprozirnost… + Randomize Opacity Nasumična neprozirnost + Minimum: Minimum: + Maximum: Maksimum: + Seed: Početna vrijednost: +@msg `min` is greater than `max` `min` je veće od `max` diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index fe48fb73a..e5ec3feb6 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -6105,3 +6105,10 @@ breakText Glyphs Glifs Text Flow Flux del text By Rows Per files By Columns Per columnes +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Opacitat aleatòria… + Randomize Opacity Opacitat aleatòria + Minimum: Mínim: + Maximum: Màxim: + Seed: Llavor: +@msg `min` is greater than `max` `min` és més gran que `max` diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 2f2892c00..0efefa092 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6115,3 +6115,10 @@ breakText Glyphs Glyfy Text Flow Tok textu By Rows Po řádcích By Columns Po sloupcích +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Náhodné krytí… + Randomize Opacity Náhodné krytí + Minimum: Minimum: + Maximum: Maximum: + Seed: Počáteční hodnota: +@msg `min` is greater than `max` `min` je větší než `max` diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 056f8c357..f4d16eef2 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -6014,3 +6014,10 @@ breakText Glyphs Glyffer Text Flow Tekstflow By Rows Efter rækker By Columns Efter kolonner +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Tilfældig opacitet… + Randomize Opacity Tilfældig opacitet + Minimum: Minimum: + Maximum: Maksimum: + Seed: Startværdi: +@msg `min` is greater than `max` `min` er større end `max` diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 23336fb9f..18ea74459 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5700,3 +5700,10 @@ breakText Glyphs Glyphen Text Flow Textfluss By Rows Nach Zeilen By Columns Nach Spalten +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Zufällige Deckkraft… + Randomize Opacity Zufällige Deckkraft + Minimum: Minimum: + Maximum: Maximum: + Seed: Startwert: +@msg `min` is greater than `max` `min` ist größer als `max` diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 83d50b752..15c008eb8 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -6067,3 +6067,10 @@ breakText Glyphs Γλύφοι Text Flow Ροή κειμένου By Rows Κατά γραμμές By Columns Κατά στήλες +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Τυχαία αδιαφάνεια… + Randomize Opacity Τυχαία αδιαφάνεια + Minimum: Ελάχιστο: + Maximum: Μέγιστο: + Seed: Αρχική τιμή: +@msg `min` is greater than `max` Το `min` είναι μεγαλύτερο από το `max` diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index a6824a320..f69b7bec9 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -6057,3 +6057,10 @@ breakText Glyphs Glifos Text Flow Flujo de texto By Rows Por filas By Columns Por columnas +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Opacidad aleatoria… + Randomize Opacity Opacidad aleatoria + Minimum: Mínimo: + Maximum: Máximo: + Seed: Semilla: +@msg `min` is greater than `max` `min` es mayor que `max` diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 87debec2e..fc76393f6 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6145,3 +6145,10 @@ breakText Glyphs Merkit Text Flow Tekstin kulku By Rows Riveittäin By Columns Sarakkeittain +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Satunnainen peittävyys… + Randomize Opacity Satunnainen peittävyys + Minimum: Pienin: + Maximum: Suurin: + Seed: Siemen: +@msg `min` is greater than `max` `min` on suurempi kuin `max` diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index b21056324..24a3a59ad 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6112,3 +6112,10 @@ breakText Glyphs Glyphes Text Flow Flux du texte By Rows Par lignes By Columns Par colonnes +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Opacité aléatoire… + Randomize Opacity Opacité aléatoire + Minimum: Minimum : + Maximum: Maximum : + Seed: Graine : +@msg `min` is greater than `max` `min` est supérieur à `max` diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 729597666..29305857e 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6101,3 +6101,10 @@ breakText Glyphs Glifák Text Flow Szövegfolyás By Rows Soronként By Columns Oszloponként +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Véletlenszerű átlátszatlanság… + Randomize Opacity Véletlenszerű átlátszatlanság + Minimum: Minimum: + Maximum: Maximum: + Seed: Kezdőérték: +@msg `min` is greater than `max` `min` nagyobb, mint `max` diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 82337045e..b682c757f 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -6014,3 +6014,10 @@ breakText Glyphs Glif Text Flow Alur teks By Rows Per baris By Columns Per kolom +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Opasitas acak… + Randomize Opacity Opasitas acak + Minimum: Minimum: + Maximum: Maksimum: + Seed: Benih: +@msg `min` is greater than `max` `min` lebih besar dari `max` diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 764059d5b..74fcfb60c 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5734,3 +5734,10 @@ breakText Glyphs Glifi Text Flow Flusso del testo By Rows Per righe By Columns Per colonne +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Opacità casuale… + Randomize Opacity Opacità casuale + Minimum: Minimo: + Maximum: Massimo: + Seed: Seme: +@msg `min` is greater than `max` `min` è maggiore di `max` diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 7de9bc5bd..69ee5fd86 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5801,3 +5801,10 @@ breakText Glyphs 字形 Text Flow テキストの流れ By Rows 行方向 By Columns 列方向 +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… 不透明度をランダム化… + Randomize Opacity 不透明度をランダム化 + Minimum: 最小: + Maximum: 最大: + Seed: シード: +@msg `min` is greater than `max` `min` が `max` より大きくなっています diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 27be89a18..07fc51fb9 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -6104,3 +6104,10 @@ breakText Glyphs გლიფები Text Flow ტექსტის დინება By Rows სტრიქონების მიხედვით By Columns სვეტების მიხედვით +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… შემთხვევითი გაუმჭვირვალობა… + Randomize Opacity შემთხვევითი გაუმჭვირვალობა + Minimum: მინიმუმი: + Maximum: მაქსიმუმი: + Seed: საწყისი რიცხვი: +@msg `min` is greater than `max` `min` მეტია, ვიდრე `max` diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 41ddf0a82..022d19494 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5697,3 +5697,10 @@ breakText Glyphs 글리프 Text Flow 텍스트 흐름 By Rows 행 방향 By Columns 열 방향 +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… 불투명도 무작위화… + Randomize Opacity 불투명도 무작위화 + Minimum: 최소: + Maximum: 최대: + Seed: 시드: +@msg `min` is greater than `max` `min`이(가) `max`보다 큽니다 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 1434eb8c0..a1ace5190 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -6058,3 +6058,10 @@ breakText Glyphs Glyphs Text Flow Tekstdoorloop By Rows Per rij By Columns Per kolom +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Willekeurige dekking… + Randomize Opacity Willekeurige dekking + Minimum: Minimum: + Maximum: Maximum: + Seed: Startwaarde: +@msg `min` is greater than `max` `min` is groter dan `max` diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index ae48aacfc..fc4cb5748 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -5970,3 +5970,10 @@ breakText Glyphs Glify Text Flow Przepływ tekstu By Rows Wierszami By Columns Kolumnami +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Losowe krycie… + Randomize Opacity Losowe krycie + Minimum: Minimum: + Maximum: Maksimum: + Seed: Ziarno: +@msg `min` is greater than `max` `min` jest większe niż `max` diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index d1e047590..bf70bd2fd 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -6189,3 +6189,10 @@ swatch Yellow Green Verde-amarelado Text Flow Fluxo do texto By Rows Por linhas By Columns Por colunas +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Opacidade aleatória… + Randomize Opacity Opacidade aleatória + Minimum: Mínimo: + Maximum: Máximo: + Seed: Semente: +@msg `min` is greater than `max` `min` é maior que `max` diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 4d630635b..566cd78a2 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -6092,3 +6092,10 @@ breakText Glyphs Глифы Text Flow Поток текста By Rows По строкам By Columns По столбцам +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Случайная непрозрачность… + Randomize Opacity Случайная непрозрачность + Minimum: Минимум: + Maximum: Максимум: + Seed: Начальное число: +@msg `min` is greater than `max` `min` больше, чем `max` diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 61dc8cbd6..e93c877fe 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6167,3 +6167,10 @@ breakText Glyphs Glyfer Text Flow Textflöde By Rows Radvis By Columns Kolumnvis +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Slumpmässig opacitet… + Randomize Opacity Slumpmässig opacitet + Minimum: Minimum: + Maximum: Maximum: + Seed: Startvärde: +@msg `min` is greater than `max` `min` är större än `max` diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index fc59f5676..9fdffe544 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -6072,3 +6072,10 @@ breakText Glyphs Glifler Text Flow Metin Akışı By Rows Satırlara Göre By Columns Sütunlara Göre +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Rastgele Opaklık… + Randomize Opacity Rastgele Opaklık + Minimum: En Az: + Maximum: En Çok: + Seed: Başlangıç Değeri: +@msg `min` is greater than `max` `min`, `max` değerinden büyük diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index a721beb96..f0fed9fb4 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5714,3 +5714,10 @@ breakText Glyphs Гліфи Text Flow Потік тексту By Rows За рядками By Columns За стовпцями +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Випадкова непрозорість… + Randomize Opacity Випадкова непрозорість + Minimum: Мінімум: + Maximum: Максимум: + Seed: Початкове число: +@msg `min` is greater than `max` `min` більше за `max` diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index de0b1e822..7cb29ce4f 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5717,3 +5717,10 @@ breakText Glyphs Glyph Text Flow Luồng văn bản By Rows Theo hàng By Columns Theo cột +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… Độ mờ ngẫu nhiên… + Randomize Opacity Độ mờ ngẫu nhiên + Minimum: Tối thiểu: + Maximum: Tối đa: + Seed: Giá trị khởi đầu: +@msg `min` is greater than `max` `min` lớn hơn `max` diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index f5d3e051b..b160639b0 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -5957,3 +5957,10 @@ breakText Glyphs 字形 Text Flow 文本流向 By Rows 按行 By Columns 按列 +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… 随机不透明度… + Randomize Opacity 随机不透明度 + Minimum: 最小值: + Maximum: 最大值: + Seed: 种子: +@msg `min` is greater than `max` `min` 大于 `max` diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 7c2ad2ffe..aefdbb366 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5754,3 +5754,10 @@ breakText Glyphs 字符 Text Flow 文字流向 By Rows 依列 By Columns 依欄 +# Arrange Tools add-on: Randomize Opacity. + Randomize Opacity… 隨機不透明度… + Randomize Opacity 隨機不透明度 + Minimum: 最小值: + Maximum: 最大值: + Seed: 種子: +@msg `min` is greater than `max` `min` 大於 `max` diff --git a/docs/mcp.md b/docs/mcp.md index 31f541e06..2a8210444 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -6341,12 +6341,13 @@ as `object.setBounds` measures them (Use Preview Bounds), except Arrange in Grid | `addon.arrange.replace` | `fit`: `keep`, `bounds` (the replaced object's width and height), `proportional` (fits inside them) (`keep`); `scaleStrokes`; `keep` (false) | Puts a copy of the topmost selected object centred on each of the other selected objects, in that object's place in the stacking order; the replaced objects are deleted unless `keep` (the copy then goes right above each). Needs two objects or more; the copies are selected. → `{ids}` | | `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}` | 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… and Set Size of Each… open dialogs -(`ui.menuDialog {command}`, dialog kinds `arrangeGrid`, `arrangeReplace` and `arrangeSize`) whose fields are the -command's parameters, with Preview; 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… 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. ```json {"name":"run_command","arguments":{"command":"addon.arrange.grid","params":{"columns":4,"gapX":12,"gapY":12}}} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 168c38123..55872ac36 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Symbols panel: Select All Unused, #4918) · **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: Randomize Opacity, #4925; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) ## Feature submission evidence @@ -1413,6 +1413,10 @@ dialogs, Preview and Cancel, Zoom to Selection) and `command_sweep`. Commands: ` A VectorCraft addition, not a feature of the reference app, so no area score moves. Object › Plug-ins › Arrange Tools › Group Overlapping Objects groups the top-level selected objects wherever their visual bounds overlap or touch, chains included (A overlapping B and B overlapping C makes one group of three), only among objects of the same layer or group; objects that overlap nothing stay as they are, each new group takes its frontmost member's place in the stacking order, and the groups and the loose objects stay selected. One undo step; at most 20000 objects, compared by a sweep along x with union-find. Tests: `group_overlapping_*` in `cmd::addons::arrange::tests` in `vectorcraft-engine` (chains, loners, stacking order, layers kept apart, visual bounds, no step when nothing overlaps, many objects), `group_overlapping_objects_runs_directly` in `vectorcraft-ui-egui` and `command_sweep`. Commands: `docs/mcp.md` › Arrange Tools add-on. +## Arrange Tools add-on: Randomize Opacity (2026-11-01) + +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. + ## 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. @@ -1433,6 +1437,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 | 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. | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index c1feab7e4..bc788b8f0 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Blob Brush and Eraser show their tip's outline at the pointer, #4977) · **Target:** Adobe Illustrator 2026 (30.x) +> **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) ## Feature submission evidence @@ -334,6 +334,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po | Outline view keeps template layers in Preview, dimmed | done | #1486; a picture placed to be traced stays visible under the outlines | | 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 | | Document Tools add-on (VectorCraft addition): Object › Plug-ins › Document Tools › Save a Timestamped Copy writes `_YYYY-MM-DD_HH-MM.` 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 | @@ -389,6 +390,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 | 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 `` 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 |