From c755909d67092074ef10916f045f2aac35cc0a27 Mon Sep 17 00:00:00 2001 From: Louise Fox <2503492+KaboomFox@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:01:33 +0000 Subject: [PATCH] Plug-ins: labels, units, groups, help and conditions for parameters (G17) A plug-in's dialog labelled each field with its parameter's name spaced out ("Line_spacing_mm:"), showed numbers without their unit and listed at most 64 parameters in one column. A plug-in with many settings, such as a generator's or an exporter's, could neither explain nor sort them, and its dialog showed settings that don't apply to the choices made. Each parameter may now say how dialogs show it, in keys that hosts from before them ignore (plugins::dialog): - label; unit, after the value ("0.25 mm"); help, on the label; and helpUrl, an https:// page opened from a button after the field. - group: the parameters outside any group come first, then each group as a section that folds, in the order the groups first appear, the fields lined up down the dialog. - visibleWhen: {"pattern": ["lines"]} shows a parameter only while each checkbox or choice it names, declared before it, has one of the values listed and is shown itself. Manifest::visible decides every parameter in one pass, in declaration order. A hidden parameter still goes to the plug-in with its value, so a run never depends on what the dialog showed. Two types join number, int, bool and choice: string, a text field cut to maxLength, and numberList, numbers typed separated by commas or spaces, each clamped, minItems to maxItems of them. A plug-in declares up to 256 parameters, at most 64 outside groups. The filter, live-effect and exporter dialogs use every key. The Appearance panel's editor of a plug-in effect uses the labels, units, help and conditions, and reads a typed number list as the dialog does. dialog.rs tests parsing, the round trip of a manifest written out, sections, visibility (also of 256 overlapping conditions, which a recursive check would take forever on) and the keys refused; manifest.rs the new types and limits. tests_plugin_fields.rs draws a dialog with kittest: labels and units, sections in order with their fields lined up, visibleWhen hiding and showing rows, and the help button opening its page. appearance.rs checks the editor's labels, units, conditions and a typed number list, and form.rs units after numbers (a unit starting with a digit is spaced from it) and typed lists. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un # Conflicts: # docs/gaps.md # docs/target-app-parity.md --- crates/plugins/src/dialog.rs | 307 ++++++++++++++++++ crates/plugins/src/lib.rs | 7 +- crates/plugins/src/manifest.rs | 196 +++++++++-- crates/ui-egui/src/dialogs/effect.rs | 4 +- crates/ui-egui/src/dialogs/form.rs | 243 +++++++++++--- crates/ui-egui/src/dialogs/mod.rs | 4 +- crates/ui-egui/src/dialogs/plugin.rs | 6 +- crates/ui-egui/src/dialogs/tests.rs | 19 +- crates/ui-egui/src/dialogs/tests_modal.rs | 7 +- .../src/dialogs/tests_plugin_fields.rs | 171 ++++++++++ crates/ui-egui/src/i18n/bg.tsv | 1 + crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/da.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/el.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fi.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/hu.tsv | 1 + crates/ui-egui/src/i18n/id.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/ka.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 1 + crates/ui-egui/src/i18n/nl.tsv | 1 + crates/ui-egui/src/i18n/pl.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/sv.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/vi.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + crates/ui-egui/src/panels/appearance.rs | 71 +++- docs/gaps.md | 9 +- docs/plugins.md | 37 ++- docs/target-app-parity.md | 5 +- 37 files changed, 997 insertions(+), 112 deletions(-) create mode 100644 crates/plugins/src/dialog.rs create mode 100644 crates/ui-egui/src/dialogs/tests_plugin_fields.rs diff --git a/crates/plugins/src/dialog.rs b/crates/plugins/src/dialog.rs new file mode 100644 index 000000000..3777dab2e --- /dev/null +++ b/crates/plugins/src/dialog.rs @@ -0,0 +1,307 @@ +//! How the host's generated dialogs show a plug-in's parameters: the optional `label`, `unit`, +//! `group`, `help`, `helpUrl` and `visibleWhen` keys of each one (docs/plugins.md › Parameters). +//! +//! These keys are presentation only. A host from before them ignores them, so a plug-in that +//! declares them still installs there, and a plug-in is given every parameter whether its dialog +//! showed it or not: a run never depends on what was on screen. The dialogs of filters, live +//! effects and exporters draw the parameters outside any group first, then each group as a section, +//! in the order the groups first appear, and leave out a parameter while its `visibleWhen` isn't met. +//! +//! `visibleWhen` names parameters declared before the one it belongs to, so the parameter that +//! decides comes first in the dialog and conditions can't go round in a circle. A parameter is shown +//! when each one it names is shown and has one of the values listed for it. + +use std::collections::{BTreeMap, BTreeSet}; + +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +use crate::manifest::{Manifest, Param, ParamSpec}; +use crate::{Error, Result}; + +/// Longest `label` or `group`, in characters. +pub const MAX_LABEL: usize = 64; +/// Longest `unit`, in characters. +pub const MAX_UNIT: usize = 16; +/// Longest `help`, in characters. +pub const MAX_HELP: usize = 1_000; +/// Longest `helpUrl`, in bytes. +pub const MAX_HELP_URL: usize = 512; +/// Most parameters one `visibleWhen` names. +pub const MAX_CONDITIONS: usize = 8; +/// Most values a `visibleWhen` lists for one parameter. +pub const MAX_CONDITION_VALUES: usize = 64; + +/// One section of a dialog ([`Manifest::sections`]): its group (`None`: the parameters outside any +/// group) and its parameters, in declaration order. +pub type Section<'a> = (Option<&'a str>, Vec<&'a (String, Param)>); + +/// How dialogs show one parameter (every key optional). +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ParamUi { + /// The field's label; without one, the name spaced out (`rowSpacing` → "Row Spacing"). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub label: Option, + /// The unit the value is in, shown after the field (the plug-in's own: nothing is converted). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub unit: Option, + /// The section of the dialog the parameter is in. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub group: Option, + /// What the parameter does, shown when the pointer rests on its label. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub help: Option, + /// An `https://` page about the parameter, opened by the help button after its field. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub help_url: Option, + /// Shown only while each parameter named here has one of the values listed for it. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub visible_when: BTreeMap>, +} + +/// Is `s` 1 to `max` characters, not all spaces, without control characters (`\n` too, unless +/// `lines`)? +fn printable(s: &str, max: usize, lines: bool) -> bool { + !s.trim().is_empty() && s.chars().count() <= max && s.chars().all(|c| !c.is_control() || (lines && c == '\n')) +} + +impl ParamUi { + /// Checks the keys of parameter `name`, the `index`th of manifest `m`'s parameters. + pub(crate) fn validate(&self, m: &Manifest, index: usize, name: &str) -> Result<()> { + let bad = |what: &str| Err(Error::Manifest(format!("parameter `{name}`: {what}"))); + let text = |v: &Option, max: usize, lines: bool| v.as_deref().is_none_or(|s| printable(s, max, lines)); + if !text(&self.label, MAX_LABEL, false) || !text(&self.group, MAX_LABEL, false) { + return bad(&format!("`label` and `group` must be 1-{MAX_LABEL} printable characters")); + } + if !text(&self.unit, MAX_UNIT, false) { + return bad(&format!("`unit` must be 1-{MAX_UNIT} printable characters")); + } + if !text(&self.help, MAX_HELP, true) { + return bad(&format!("`help` must be 1-{MAX_HELP} printable characters")); + } + if let Some(url) = &self.help_url { + let rest = url.strip_prefix("https://").unwrap_or_default(); + if rest.is_empty() || url.len() > MAX_HELP_URL || url.chars().any(|c| c.is_whitespace() || c.is_control()) { + return bad(&format!("`helpUrl` must be an https:// address of at most {MAX_HELP_URL} bytes")); + } + } + if self.visible_when.len() > MAX_CONDITIONS { + return bad(&format!("`visibleWhen` names at most {MAX_CONDITIONS} parameters")); + } + let before = m.params.get(..index).unwrap_or_default(); + for (other, values) in &self.visible_when { + let Some((_, p)) = before.iter().find(|(k, _)| k == other) else { + return bad(&format!("`visibleWhen` names `{other}`, which isn't a parameter declared before this one")); + }; + if values.is_empty() || values.len() > MAX_CONDITION_VALUES { + return bad(&format!("`visibleWhen` lists 1 to {MAX_CONDITION_VALUES} values for `{other}`")); + } + let fits = |v: &Value| match &p.spec { + ParamSpec::Bool { .. } => v.is_boolean(), + ParamSpec::Choice { options, .. } => v.as_str().is_some_and(|s| options.iter().any(|o| o == s)), + _ => false, + }; + if !values.iter().all(fits) { + return bad(&format!("`visibleWhen` takes `{other}`'s values, of a checkbox or a choice")); + } + } + Ok(()) + } +} + +impl Manifest { + /// The names of the parameters shown while the dialog's values are `values` (missing ones at + /// their defaults): those whose `visibleWhen` names only shown parameters, each with one of the + /// values listed for it. + /// + /// One pass in declaration order decides each parameter once, from the parameters before it. + /// Following the conditions back instead would revisit a parameter once per path to it, and + /// overlapping conditions have exponentially many. A condition on a parameter declared later, + /// which only a manifest built in code can have, hides the parameter, so conditions that go + /// round in a circle hide every parameter on it. + pub fn visible(&self, values: &Map) -> BTreeSet<&str> { + let mut shown = BTreeSet::new(); + for (name, p) in &self.params { + let met = p.ui.visible_when.iter().all(|(other, allowed)| { + shown.contains(other.as_str()) + && match values.get(other) { + Some(v) => allowed.contains(v), + None => self.param(other).is_some_and(|o| allowed.contains(&o.spec.default_value())), + } + }); + if met { + shown.insert(name.as_str()); + } + } + shown + } + + /// Is parameter `name` shown while the dialog's values are `values`? See [`Manifest::visible`], + /// which a dialog calls once for all its parameters. + pub fn shown(&self, name: &str, values: &Map) -> bool { + self.visible(values).contains(name) + } + + /// The parameter named `name`. + pub fn param(&self, name: &str) -> Option<&Param> { + self.params.iter().find(|(k, _)| k == name).map(|(_, p)| p) + } + + /// The dialog's sections in order: the parameters outside any group (`None`), then each group + /// in the order it first appears, each with its parameters in declaration order. + pub fn sections(&self) -> Vec> { + let mut out: Vec
= Vec::new(); + let ungrouped: Vec<_> = self.params.iter().filter(|(_, p)| p.ui.group.is_none()).collect(); + if !ungrouped.is_empty() { + out.push((None, ungrouped)); + } + for entry in &self.params { + let Some(group) = entry.1.ui.group.as_deref() else { continue }; + match out.iter_mut().find(|(g, _)| *g == Some(group)) { + Some((_, members)) => members.push(entry), + None => out.push((Some(group), vec![entry])), + } + } + out + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + const RICH: &str = r#"{"id":"org.example.hatch","name":"Hatch","kind":"effect","params":{ + "pattern":{"type":"choice","options":["lines","crosshatch","dots"],"default":"lines", + "label":"Pattern","group":"Fill","help":"How the shape is filled."}, + "spacing":{"type":"number","min":0.1,"max":10,"default":0.25,"label":"Spacing","unit":"mm","group":"Fill", + "help":"Distance between the lines.\nSmaller is denser.","helpUrl":"https://example.org/params/spacing", + "visibleWhen":{"pattern":["lines","crosshatch"]}}, + "outline":{"type":"bool","default":true,"label":"Outline"}, + "outline_width":{"type":"number","min":0,"max":10,"default":0.5,"unit":"pt","group":"Outline","visibleWhen":{"outline":[true]}}, + "outline_passes":{"type":"int","min":1,"max":4,"default":1,"group":"Outline", + "visibleWhen":{"outline":[true],"pattern":["lines"]}}, + "seed":{"type":"int","min":0,"max":9}}}"#; + + fn rich() -> Manifest { + Manifest::parse(RICH.as_bytes()).unwrap() + } + + #[test] + fn keys_parse_and_round_trip_with_the_parameters() { + let m = rich(); + let spacing = m.param("spacing").unwrap(); + assert_eq!( + (spacing.ui.label.as_deref(), spacing.ui.unit.as_deref(), spacing.ui.group.as_deref()), + (Some("Spacing"), Some("mm"), Some("Fill")) + ); + assert_eq!(spacing.ui.help_url.as_deref(), Some("https://example.org/params/spacing")); + assert_eq!(spacing.spec, ParamSpec::Number { min: 0.1, max: 10.0, default: 0.25 }); + assert_eq!(m.param("seed").unwrap().ui, ParamUi::default()); + // Written out, the manifest reads back as it was, parameters in their order. + assert_eq!(Manifest::parse(&serde_json::to_vec(&m).unwrap()).unwrap(), m); + assert_eq!(serde_json::to_value(&m).unwrap()["params"]["seed"], json!({"type": "int", "min": 0, "max": 9, "default": 0})); + } + + #[test] + fn sections_put_ungrouped_parameters_first_then_groups_as_they_appear() { + let m = rich(); + let names: Vec<(Option<&str>, Vec<&str>)> = + m.sections().into_iter().map(|(g, ps)| (g, ps.into_iter().map(|(k, _)| k.as_str()).collect())).collect(); + assert_eq!( + names, + [(None, vec!["outline", "seed"]), (Some("Fill"), vec!["pattern", "spacing"]), (Some("Outline"), vec!["outline_width", "outline_passes"])] + ); + } + + #[test] + fn visible_when_follows_the_values_and_the_parameters_it_names() { + let m = rich(); + let shown = |values: Value| -> Vec<&str> { + let values = values.as_object().cloned().unwrap_or_default(); + m.params.iter().filter(|(k, _)| m.shown(k, &values)).map(|(k, _)| k.as_str()).collect() + }; + // At the defaults: lines, with an outline. + assert_eq!(shown(json!({})), ["pattern", "spacing", "outline", "outline_width", "outline_passes", "seed"]); + assert_eq!(shown(json!({"pattern": "dots"})), ["pattern", "outline", "outline_width", "seed"]); + assert_eq!(shown(json!({"outline": false})), ["pattern", "spacing", "outline", "seed"]); + assert!(!m.shown("no_such_parameter", &Map::new())); + } + + /// A manifest built in code rather than parsed can name parameters in a circle: they are + /// hidden instead of recursing forever. + #[test] + fn conditions_in_a_circle_hide_their_parameters() { + let mut m = rich(); + let outline = m.params.iter().position(|(k, _)| k == "outline").unwrap(); + // `outline` names `outline_width`, which names `outline`. + m.params[outline].1.ui.visible_when.insert("outline_width".into(), vec![json!(0.5)]); + assert!(!m.shown("outline", &Map::new()) && !m.shown("outline_width", &Map::new())); + assert!(m.shown("seed", &Map::new()) && m.shown("pattern", &Map::new())); + } + + /// Conditions that each name several earlier parameters are checked once per parameter, not + /// once per path through them: 256 checkboxes, each shown while the 8 before it are checked, + /// would otherwise take longer than the age of the universe on every frame of the dialog. + #[test] + fn overlapping_conditions_are_checked_once_per_parameter() { + let params: Vec = (0..256) + .map(|i: usize| { + let when: Vec = (i.saturating_sub(MAX_CONDITIONS)..i).map(|j| format!(r#""p{j}":[true]"#)).collect(); + format!(r#""p{i}":{{"type":"bool","default":true,"group":"G","visibleWhen":{{{}}}}}"#, when.join(",")) + }) + .collect(); + let m = Manifest::parse(format!(r#"{{"id":"a","name":"x","kind":"filter","params":{{{}}}}}"#, params.join(",")).as_bytes()).unwrap(); + let (done, finished) = std::sync::mpsc::channel(); + std::thread::spawn(move || done.send(m.visible(&Map::new()).len())); + let shown = finished.recv_timeout(std::time::Duration::from_secs(10)).expect("the check took more than 10 s"); + assert_eq!(shown, 256, "every checkbox is checked, so every parameter is shown"); + // Unchecking the first hides the rest: each names it or one it hides. + let m = Manifest::parse(format!(r#"{{"id":"a","name":"x","kind":"filter","params":{{{}}}}}"#, params.join(",")).as_bytes()).unwrap(); + let values = json!({"p0": false}).as_object().cloned().unwrap(); + assert_eq!(m.visible(&values).into_iter().collect::>(), ["p0"]); + } + + #[test] + fn bad_keys_are_refused() { + let with = |key: &str, value: &str| { + format!( + r#"{{"id":"a","name":"x","kind":"filter","params":{{"mode":{{"type":"choice","options":["a","b"]}}, + "on":{{"type":"bool"}},"n":{{"type":"number","min":0,"max":1,"default":0,"{key}":{value}}}}}}}"# + ) + }; + let long = format!("\"{}\"", "x".repeat(MAX_LABEL + 1)); + for (key, value) in [ + ("label", r#""""#), + ("label", r#"" ""#), + ("label", r#""a\u0007b""#), + ("label", long.as_str()), + ("group", r#""line\nbreak""#), + ("unit", r#""millimetres and more""#), + ("help", r#""""#), + ("helpUrl", r#""http://example.org""#), + ("helpUrl", r#""https://""#), + ("helpUrl", r#""https://exa mple.org""#), + ("helpUrl", r#""javascript:alert(1)""#), + ("visibleWhen", r#"{"missing":[true]}"#), + ("visibleWhen", r#"{"n":[0]}"#), + ("visibleWhen", r#"{"on":["yes"]}"#), + ("visibleWhen", r#"{"mode":["c"]}"#), + ("visibleWhen", r#"{"mode":[]}"#), + ] { + let manifest = with(key, value); + assert!(Manifest::parse(manifest.as_bytes()).is_err(), "{key}: {value}"); + } + // A parameter declared after the one that names it, and one naming itself. + let later = r#"{"id":"a","name":"x","kind":"filter","params":{"n":{"type":"number","min":0,"max":1,"default":0, + "visibleWhen":{"on":[true]}},"on":{"type":"bool"}}}"#; + let itself = r#"{"id":"a","name":"x","kind":"filter","params":{"on":{"type":"bool","visibleWhen":{"on":[true]}}}}"#; + for bad in [later, itself] { + assert!(Manifest::parse(bad.as_bytes()).is_err(), "{bad}"); + } + let fine = with("help", r#""Two\nlines.""#); + assert!(Manifest::parse(fine.as_bytes()).is_ok()); + } +} diff --git a/crates/plugins/src/lib.rs b/crates/plugins/src/lib.rs index 3149c803f..ecd189364 100644 --- a/crates/plugins/src/lib.rs +++ b/crates/plugins/src/lib.rs @@ -19,9 +19,11 @@ //! ([`objects::apply_output`]); a **live effect** ([`effect`]) rewrites one object's geometry each //! time it is drawn, from an appearance stack; an **exporter** ([`export`]) writes the whole //! document in a file format of its own. The process-wide [`registry`] holds the installed -//! plug-ins. +//! plug-ins. A plug-in's parameters ([`manifest`]) come with what the host's dialogs need to show +//! them well: labels, units, groups, help and conditions ([`dialog`]). #![forbid(unsafe_code)] +pub mod dialog; pub mod effect; pub mod export; pub mod manifest; @@ -30,7 +32,8 @@ pub mod registry; mod runtime; pub mod wat; -pub use manifest::{Kind, Manifest, ParamSpec}; +pub use dialog::ParamUi; +pub use manifest::{Kind, Manifest, Param, ParamSpec}; pub use runtime::{ABI_VERSION, Limits, Plugin}; /// What the host puts before a plug-in's id to name what it adds: its live effect diff --git a/crates/plugins/src/manifest.rs b/crates/plugins/src/manifest.rs index 04c892ec6..609e20a84 100644 --- a/crates/plugins/src/manifest.rs +++ b/crates/plugins/src/manifest.rs @@ -1,14 +1,22 @@ -//! The plug-in manifest (`vc_manifest`) and parameter schema. +//! The plug-in manifest (`vc_manifest`) and parameter schema: what each parameter holds +//! ([`ParamSpec`]) and how dialogs show it ([`ParamUi`], in [`crate::dialog`]). use serde::{Deserialize, Serialize}; use serde_json::{Map, Value, json}; +use crate::dialog::ParamUi; use crate::{Error, Result}; /// Longest manifest the host reads. pub const MAX_MANIFEST_BYTES: usize = 64 * 1024; /// Most parameters a plug-in may declare. -pub const MAX_PARAMS: usize = 64; +pub const MAX_PARAMS: usize = 256; +/// Most parameters outside groups (`group`): a dialog of more is sorted into sections. +pub const MAX_UNGROUPED: usize = 64; +/// Longest text a `string` parameter may hold, in characters. +pub const MAX_TEXT: usize = 4_096; +/// Most numbers a `numberList` parameter may hold. +pub const MAX_LIST: usize = 256; /// Most file extensions an exporter may declare. pub const MAX_EXTENSIONS: usize = 8; @@ -63,6 +71,27 @@ pub enum ParamSpec { #[serde(default)] default: Option, }, + /// Text of at most `max_length` characters (a text field). + #[serde(rename = "string")] + Text { + #[serde(default)] + default: String, + #[serde(default = "text_max", rename = "maxLength")] + max_length: usize, + }, + /// `min_items` to `max_items` numbers, each in `min..=max` (a field of numbers separated by + /// commas or spaces). + #[serde(rename_all = "camelCase")] + NumberList { + min: f64, + max: f64, + #[serde(default)] + default: Vec, + #[serde(default)] + min_items: usize, + #[serde(default = "list_max")] + max_items: usize, + }, } fn int_min() -> i64 { @@ -71,6 +100,28 @@ fn int_min() -> i64 { fn int_max() -> i64 { 1_000_000 } +fn text_max() -> usize { + 256 +} +fn list_max() -> usize { + 16 +} + +/// One declared parameter: what it holds, and how dialogs show it. In the manifest both are keys of +/// the same object: `{"type": "number", "min": 0, "max": 10, "default": 1, "unit": "mm"}`. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Param { + #[serde(flatten)] + pub spec: ParamSpec, + #[serde(flatten)] + pub ui: ParamUi, +} + +impl From for Param { + fn from(spec: ParamSpec) -> Self { + Param { spec, ui: ParamUi::default() } + } +} impl ParamSpec { /// The value a parameter takes when it isn't given (numeric defaults clamped into range). @@ -80,6 +131,40 @@ impl ParamSpec { ParamSpec::Int { min, max, default } => json!((*default).clamp(*min, *max)), ParamSpec::Bool { default } => json!(default), ParamSpec::Choice { options, default } => json!(default.clone().or_else(|| options.first().cloned()).unwrap_or_default()), + ParamSpec::Text { default, max_length } => json!(default.chars().take(*max_length).collect::()), + ParamSpec::NumberList { min, max, default, .. } => json!(default.iter().map(|x| x.clamp(*min, *max)).collect::>()), + } + } + + /// Checks the spec of parameter `k`. + fn validate(&self, k: &str) -> Result<()> { + let bad = |what: &str| Err(Error::Manifest(format!("parameter `{k}`: {what}"))); + match self { + ParamSpec::Number { min, max, default } if !(min.is_finite() && max.is_finite() && default.is_finite()) || min > max => bad("bad range"), + ParamSpec::Int { min, max, .. } if min > max => bad("bad range"), + ParamSpec::Choice { options, default } => { + if options.is_empty() || options.len() > 64 || options.iter().any(|o| o.is_empty() || o.len() > 64 || o.chars().any(char::is_control)) + { + return bad("choices must be 1-64 non-empty, printable strings"); + } + if default.as_ref().is_some_and(|d| !options.contains(d)) { + return bad("default is not one of the options"); + } + Ok(()) + } + ParamSpec::Text { default, max_length } if !(1..=MAX_TEXT).contains(max_length) || default.chars().count() > *max_length => { + bad(&format!("`maxLength` must be 1-{MAX_TEXT}, and the default no longer")) + } + ParamSpec::NumberList { min, max, default, min_items, max_items } => { + if !(min.is_finite() && max.is_finite()) || min > max || default.iter().any(|x| !x.is_finite()) { + return bad("bad range"); + } + if !(1..=MAX_LIST).contains(max_items) || min_items > max_items || !(*min_items..=*max_items).contains(&default.len()) { + return bad(&format!("`minItems` and `maxItems` must be 0-{MAX_LIST}, `maxItems` at least 1, and the default that many")); + } + Ok(()) + } + _ => Ok(()), } } } @@ -101,7 +186,7 @@ pub struct Manifest { pub author: String, /// Parameters in declaration (dialog) order; a JSON object `{"name": spec, …}` in the manifest. #[serde(default, deserialize_with = "ordered_params", serialize_with = "params_as_object")] - pub params: Vec<(String, ParamSpec)>, + pub params: Vec<(String, Param)>, /// An exporter's file extensions, lower case and without the dot; the first is the one its /// files get. Only exporters declare them. #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -146,28 +231,15 @@ impl Manifest { return Err(Error::Manifest("`version`, `description` or `author` is too long".into())); } self.validate_extensions()?; - for (k, spec) in &self.params { + for (i, (k, p)) in self.params.iter().enumerate() { if !valid_key(k) { return Err(Error::Manifest(format!("invalid parameter name {k:?}"))); } - match spec { - ParamSpec::Number { min, max, default } if !(min.is_finite() && max.is_finite() && default.is_finite()) || min > max => { - return Err(Error::Manifest(format!("parameter `{k}`: bad range"))); - } - ParamSpec::Int { min, max, .. } if min > max => return Err(Error::Manifest(format!("parameter `{k}`: bad range"))), - ParamSpec::Choice { options, default } => { - if options.is_empty() - || options.len() > 64 - || options.iter().any(|o| o.is_empty() || o.len() > 64 || o.chars().any(char::is_control)) - { - return Err(Error::Manifest(format!("parameter `{k}`: choices must be 1-64 non-empty, printable strings"))); - } - if default.as_ref().is_some_and(|d| !options.contains(d)) { - return Err(Error::Manifest(format!("parameter `{k}`: default is not one of the options"))); - } - } - _ => {} - } + p.spec.validate(k)?; + p.ui.validate(self, i, k)?; + } + if self.params.iter().filter(|(_, p)| p.ui.group.is_none()).count() > MAX_UNGROUPED { + return Err(Error::Manifest(format!("at most {MAX_UNGROUPED} parameters outside groups: give the others a `group`"))); } Ok(()) } @@ -201,13 +273,17 @@ impl Manifest { let parts: Vec = self .params .iter() - .map(|(k, spec)| match spec { + .map(|(k, p)| match &p.spec { ParamSpec::Number { min, max, default } => format!("{k}: {min}..{max} ({})", default.clamp(*min, *max)), ParamSpec::Int { min, max, default } => format!("{k}: int {min}..{max} ({})", (*default).clamp(*min, *max)), ParamSpec::Bool { default } => format!("{k}: bool ({default})"), ParamSpec::Choice { options, .. } => { let opts: Vec = options.iter().map(|o| format!("{o:?}")).collect(); - format!("{k}: {} ({})", opts.join("|"), spec.default_value().as_str().unwrap_or_default()) + format!("{k}: {} ({})", opts.join("|"), p.spec.default_value().as_str().unwrap_or_default()) + } + ParamSpec::Text { max_length, .. } => format!("{k}: string of at most {max_length} ({})", p.spec.default_value()), + ParamSpec::NumberList { min, max, min_items, max_items, .. } => { + format!("{k}: {min_items} to {max_items} numbers {min}..{max} ({})", p.spec.default_value()) } }) .collect(); @@ -216,7 +292,7 @@ impl Manifest { /// Every parameter at its default value. pub fn defaults(&self) -> Map { - self.params.iter().map(|(k, spec)| (k.clone(), spec.default_value())).collect() + self.params.iter().map(|(k, p)| (k.clone(), p.spec.default_value())).collect() } /// Validates user `params` against the schema: missing keys take their defaults, numbers are @@ -229,7 +305,8 @@ impl Manifest { _ => return Err(Error::Params("plug-in parameters must be a JSON object".into())), }; let mut out = Map::new(); - for (k, spec) in &self.params { + for (k, p) in &self.params { + let spec = &p.spec; let Some(v) = given.get(k).filter(|v| !v.is_null()) else { out.insert(k.clone(), spec.default_value()); continue; @@ -258,6 +335,17 @@ impl Manifest { } json!(s) } + ParamSpec::Text { max_length, .. } => { + json!(v.as_str().ok_or_else(|| wrong("a string"))?.chars().take(*max_length).collect::()) + } + ParamSpec::NumberList { min, max, min_items, max_items, .. } => { + let items = v.as_array().ok_or_else(|| wrong("a list of numbers"))?; + if !(*min_items..=*max_items).contains(&items.len()) { + return Err(wrong(&format!("a list of {min_items} to {max_items} numbers"))); + } + let numbers: Option> = items.iter().map(|x| x.as_f64().filter(|f| f.is_finite()).map(|f| f.clamp(*min, *max))).collect(); + json!(numbers.ok_or_else(|| wrong("a list of numbers"))?) + } }; out.insert(k.clone(), resolved); } @@ -266,10 +354,10 @@ impl Manifest { } /// Reads the `params` object keeping its key order (serde_json's `Map` would sort it). -fn ordered_params<'de, D: serde::Deserializer<'de>>(d: D) -> std::result::Result, D::Error> { +fn ordered_params<'de, D: serde::Deserializer<'de>>(d: D) -> std::result::Result, D::Error> { struct V; impl<'de> serde::de::Visitor<'de> for V { - type Value = Vec<(String, ParamSpec)>; + type Value = Vec<(String, Param)>; fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.write_str("an object of parameter specs") } @@ -277,8 +365,8 @@ fn ordered_params<'de, D: serde::Deserializer<'de>>(d: D) -> std::result::Result Ok(Vec::new()) } fn visit_map>(self, mut map: A) -> std::result::Result { - let mut out: Vec<(String, ParamSpec)> = Vec::new(); - while let Some((k, v)) = map.next_entry::()? { + let mut out: Vec<(String, Param)> = Vec::new(); + while let Some((k, v)) = map.next_entry::()? { if out.len() >= MAX_PARAMS { return Err(serde::de::Error::custom(format!("too many parameters (limit {MAX_PARAMS})"))); } @@ -293,7 +381,7 @@ fn ordered_params<'de, D: serde::Deserializer<'de>>(d: D) -> std::result::Result d.deserialize_any(V) } -fn params_as_object(p: &[(String, ParamSpec)], s: S) -> std::result::Result { +fn params_as_object(p: &[(String, Param)], s: S) -> std::result::Result { use serde::ser::SerializeMap; let mut m = s.serialize_map(Some(p.len()))?; for (k, v) in p { @@ -413,6 +501,45 @@ mod tests { } } + #[test] + fn strings_and_number_lists_resolve_like_the_other_types() { + let m = Manifest::parse( + br#"{"id":"org.example.text","name":"Text","kind":"filter","params":{ + "label":{"type":"string","default":"Rose","maxLength":8}, + "angles":{"type":"numberList","min":0,"max":360,"default":[45,135],"minItems":1,"maxItems":3}, + "free":{"type":"string"}}}"#, + ) + .unwrap(); + assert_eq!(Value::Object(m.defaults()), json!({"label": "Rose", "angles": [45.0, 135.0], "free": ""})); + let p = m.resolve_params(&json!({"label": "Sunflowers", "angles": [-10, 90, 400]})).unwrap(); + assert_eq!((&p["label"], &p["angles"]), (&json!("Sunflowe"), &json!([0.0, 90.0, 360.0])), "cut and clamped"); + for bad in [json!({"label": 3}), json!({"angles": []}), json!({"angles": [1, 2, 3, 4]}), json!({"angles": [1, "x"]}), json!({"angles": 5})] { + assert!(m.resolve_params(&bad).is_err(), "{bad}"); + } + assert_eq!( + m.params_doc(), + r#"{label: string of at most 8 ("Rose"), angles: 1 to 3 numbers 0..360 ([45.0,135.0]), free: string of at most 256 ("")}"# + ); + } + + #[test] + fn up_to_256_parameters_with_at_most_64_outside_groups() { + let manifest = |n: usize, grouped: usize| { + let params: Vec = (0..n) + .map(|i| match i < grouped { + true => format!(r#""p{i}":{{"type":"bool","group":"G{}"}}"#, i % 4), + false => format!(r#""p{i}":{{"type":"bool"}}"#), + }) + .collect(); + format!(r#"{{"id":"a","name":"x","kind":"filter","params":{{{}}}}}"#, params.join(",")) + }; + assert!(Manifest::parse(manifest(MAX_UNGROUPED, 0).as_bytes()).is_ok()); + assert!(Manifest::parse(manifest(MAX_UNGROUPED + 1, 0).as_bytes()).is_err(), "too many outside groups"); + let all = Manifest::parse(manifest(MAX_PARAMS, MAX_PARAMS - MAX_UNGROUPED).as_bytes()).unwrap(); + assert_eq!(all.params.len(), MAX_PARAMS); + assert!(Manifest::parse(manifest(MAX_PARAMS + 1, MAX_PARAMS).as_bytes()).is_err(), "too many in all"); + } + #[test] fn rejects_bad_manifests() { for bad in [ @@ -430,6 +557,13 @@ mod tests { r#"{"id":"a","name":"x","kind":"filter","params":{"preview":{"type":"bool"}}}"#, r#"{"id":"a","name":"x","kind":"filter","params":{"a":{"type":"bool"},"a":{"type":"bool"}}}"#, r#"{"id":"a","name":"x","kind":"filter","params":[1]}"#, + r#"{"id":"a","name":"x","kind":"filter","params":{"t":{"type":"string","maxLength":0}}}"#, + r#"{"id":"a","name":"x","kind":"filter","params":{"t":{"type":"string","default":"abc","maxLength":2}}}"#, + r#"{"id":"a","name":"x","kind":"filter","params":{"l":{"type":"numberList","min":1,"max":0}}}"#, + r#"{"id":"a","name":"x","kind":"filter","params":{"l":{"type":"numberList","min":0,"max":1,"maxItems":0}}}"#, + r#"{"id":"a","name":"x","kind":"filter","params":{"l":{"type":"numberList","min":0,"max":1,"maxItems":257}}}"#, + r#"{"id":"a","name":"x","kind":"filter","params":{"l":{"type":"numberList","min":0,"max":1,"minItems":2,"maxItems":1}}}"#, + r#"{"id":"a","name":"x","kind":"filter","params":{"l":{"type":"numberList","min":0,"max":1,"minItems":2,"default":[1]}}}"#, ] { assert!(Manifest::parse(bad.as_bytes()).is_err(), "{bad}"); } diff --git a/crates/ui-egui/src/dialogs/effect.rs b/crates/ui-egui/src/dialogs/effect.rs index 4a84853a6..32b11744d 100644 --- a/crates/ui-egui/src/dialogs/effect.rs +++ b/crates/ui-egui/src/dialogs/effect.rs @@ -154,7 +154,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { } // Plug-in effects get fields from their parameter schema; colour adjustments their sliders. let changed = match vectorcraft_plugins::effect::installed(&id) { - Some(plugin) => form::schema_fields(ui, d, &plugin.manifest().params), + Some(plugin) => form::schema_fields(app, ui, d, plugin.manifest()), None if vectorcraft_effects::is_adjustment(&id) => adjust_fields(ui, d), None if id == GLASS => glass_fields(app, ui, d), None if id == TRANSFORM => transform_fields(ui, d, app.session.general_unit()), @@ -336,7 +336,7 @@ pub(super) fn own_choices(effect: &str, key: &str, cmyk: bool) -> Option, cmyk: bool) -> Option> { if let Some(plugin) = vectorcraft_plugins::effect::installed(effect) { - return plugin.manifest().params.iter().find(|(k, _)| k == key).and_then(|(_, spec)| match spec { + return plugin.manifest().param(key).and_then(|p| match &p.spec { vectorcraft_plugins::ParamSpec::Choice { options, .. } => Some(options.iter().map(|o| (o.clone(), o.clone())).collect()), _ => None, }); diff --git a/crates/ui-egui/src/dialogs/form.rs b/crates/ui-egui/src/dialogs/form.rs index d5a1b9654..a61fbdbb0 100644 --- a/crates/ui-egui/src/dialogs/form.rs +++ b/crates/ui-egui/src/dialogs/form.rs @@ -3,6 +3,7 @@ use serde_json::{Value, json}; use vectorcraft_doc::Unit; +use vectorcraft_plugins::{Manifest, Param, ParamSpec}; use crate::state::Dialog; use crate::theme::Tokens; @@ -369,64 +370,175 @@ fn typed_number(x: f64) -> Value { if x.fract() == 0.0 && x.abs() < 1e15 { json!(x as i64) } else { json!(x) } } -/// Editor for a plug-in's parameters from its schema (plug-in filter and effect dialogs): numbers -/// within their range, whole numbers, checkboxes and dropdowns, in declaration order, a row each. -/// Returns true when a value changed. The labels (from the parameter keys) and choices are the -/// plug-in's text, shown as they are. A plug-in may declare more of them +/// Editor for a plug-in's parameters from its manifest (the filter, live effect and exporter +/// dialogs): numbers within their range, whole numbers, checkboxes, dropdowns, text and lists of +/// numbers, a row each. A row is labelled as the manifest says (else from the parameter's name), +/// with its unit after the value and its help on the label and on a help button that opens its +/// page. Parameters outside any group come first, then each group as a section that folds +/// ([`Manifest::sections`]); a parameter whose `visibleWhen` the values don't meet is left out +/// ([`Manifest::visible`]). Returns true when a value changed. Labels, choices, groups and help are +/// the plug-in's text, shown as they are. A plug-in may declare more parameters /// ([`MAX_PARAMS`](vectorcraft_plugins::manifest::MAX_PARAMS)) than the window has room for, and /// the rows then scroll ([`super::modal::scrolled_rows`]). -pub(super) fn schema_fields(ui: &mut egui::Ui, d: &mut Dialog, specs: &[(String, vectorcraft_plugins::ParamSpec)]) -> bool { - use vectorcraft_plugins::ParamSpec; - let t = Tokens::get(ui.ctx()); - let mut changed = false; - super::modal::scrolled_rows(ui, "plugin-fields", |ui| { - egui::Grid::new("plugin-grid").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| { - for (k, spec) in specs { - ui.label(egui::RichText::new(humanize(k)).color(t.text)); - let cur = d.fields.get(k).cloned().unwrap_or_else(|| spec.default_value()); - let new = match spec { - ParamSpec::Number { min, max, .. } => { - let x = cur.as_f64().unwrap_or(*min); - crate::widgets::range_field(ui, ("plugin-num", k), x, *min..=*max, "", 2, 100.0).map(|x| json!(x)) - } - ParamSpec::Int { min, max, .. } => { - let x = cur.as_i64().unwrap_or(*min).clamp(*min, *max); - // An f64 holds every whole number up to 2^53, so the numeric field (arrows, wheel, arithmetic) is exact there. - // Past it, the number typed is read as an integer. - const EXACT: i64 = 1 << 53; - if -EXACT <= *min && *max <= EXACT { - crate::widgets::range_field(ui, ("plugin-int", k), x as f64, *min as f64..=*max as f64, "", 0, 100.0) - .map(|x| json!(x as i64)) - } else { - let shown = x.to_string(); - crate::widgets::take_dialog_focus(ui, ui.id().with(("plugin-int", k)), &shown); - crate::widgets::text_field(ui, ("plugin-int", k), Some(&shown), 100.0, 1) - .and_then(|text| text.parse::().ok()) - .map(|x| json!(x.clamp(*min, *max))) - } - } - ParamSpec::Bool { .. } => { - let b = cur.as_bool().unwrap_or(false); - crate::widgets::check_box(ui, &humanize(k), b, true).then(|| json!(!b)) - } - ParamSpec::Choice { options, .. } => { - let labels: Vec<&str> = options.iter().map(String::as_str).collect(); - crate::widgets::dropdown_names(ui, ("plugin-choice", k), cur.as_str().unwrap_or_default(), &labels, 140.0) - .and_then(|i| options.get(i)) - .map(|o| json!(o)) - } - }; - if let Some(v) = new { - d.fields.insert(k.clone(), v); - changed = true; - } - ui.end_row(); - } - }); - }); +pub(super) fn schema_fields(app: &mut crate::VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, m: &Manifest) -> bool { + let (changed, help) = schema_rows(ui, d, m); + if let Some(url) = help { + app.open_url(&url); + } changed } +/// [`schema_fields`] → whether a value changed, and the help page asked for, left to the caller to +/// open. +pub(super) fn schema_rows(ui: &mut egui::Ui, d: &mut Dialog, m: &Manifest) -> (bool, Option) { + let mut changed = false; + let mut help = None; + let visible = m.visible(&d.fields); + let sections = m.sections(); + // Every section's label column is as wide as the longest label shown, so the fields line up + // down the dialog; the rows outside groups make up for the indent of a group's rows. + let font = egui::TextStyle::Body.resolve(ui.style()); + let label_width = (m.params.iter().filter(|(k, _)| visible.contains(k.as_str()))) + .map(|(k, p)| ui.painter().layout_no_wrap(param_label(k, p), font.clone(), egui::Color32::WHITE).size().x) + .fold(0.0, f32::max); + let indent = if sections.iter().any(|(group, _)| group.is_some()) { ui.spacing().indent } else { 0.0 }; + super::modal::scrolled_rows(ui, "plugin-fields", |ui| { + for (section, (group, params)) in sections.into_iter().enumerate() { + let shown: Vec<&(String, Param)> = params.into_iter().filter(|(k, _)| visible.contains(k.as_str())).collect(); + if shown.is_empty() { + continue; + } + let rows = |ui: &mut egui::Ui, label_width: f32| { + let grid = egui::Grid::new(("plugin-grid", section)).num_columns(2).spacing([12.0, 8.0]).min_col_width(label_width); + grid.show(ui, |ui| { + for (k, p) in shown { + let (row_changed, opened) = param_row(ui, d, k, p); + changed |= row_changed; + help = help.take().or(opened); + ui.end_row(); + } + }); + }; + match group { + None => rows(ui, label_width + indent), + Some(g) => { + egui::CollapsingHeader::new(g).id_salt(("plugin-group", g)).default_open(true).show(ui, |ui| rows(ui, label_width)); + } + } + } + }); + (changed, help) +} + +/// The label of parameter `k`'s row: its manifest `label`, else its name spaced out. +fn param_label(k: &str, p: &Param) -> String { + p.ui.label.as_ref().map_or_else(|| humanize(k), |l| format!("{l}:")) +} + +/// One row of [`schema_fields`]: parameter `k`'s label and value field → whether the value changed, +/// and the help page asked for. +fn param_row(ui: &mut egui::Ui, d: &mut Dialog, k: &str, p: &Param) -> (bool, Option) { + let t = Tokens::get(ui.ctx()); + let label = param_label(k, p); + let shown_label = ui.label(egui::RichText::new(&label).color(t.text)); + if let Some(help) = &p.ui.help { + shown_label.on_hover_text(help); + } + let cur = d.fields.get(k).cloned().unwrap_or_else(|| p.spec.default_value()); + let unit = p.ui.unit.as_deref().unwrap_or_default(); + let suffix = unit_suffix(unit); + let mut opened = None; + let new = ui + .horizontal(|ui| { + let (new, in_field) = value_field(ui, k, &label, &p.spec, &cur, &suffix); + if !in_field && !unit.is_empty() { + ui.label(egui::RichText::new(unit).color(t.text_dim)); + } + if let Some(url) = &p.ui.help_url + && crate::widgets::icon_button(ui, "external-link", tl!("Open this setting's help page"), false, 20.0).clicked() + { + opened = Some(url.clone()); + } + new + }) + .inner; + let changed = new.map(|v| d.fields.insert(k.to_string(), v)).is_some(); + (changed, opened) +} + +/// The field of parameter `k`'s value `cur` (a checkbox is called by the row's `label`) → the new +/// value when it changed, and whether the field shows `suffix` (numbers do; the other fields leave +/// the unit to a label after them). +fn value_field(ui: &mut egui::Ui, k: &str, label: &str, spec: &ParamSpec, cur: &Value, suffix: &str) -> (Option, bool) { + match spec { + ParamSpec::Number { min, max, .. } => { + let x = cur.as_f64().unwrap_or(*min); + (crate::widgets::range_field(ui, ("plugin-num", k), x, *min..=*max, suffix, 2, 100.0).map(|x| json!(x)), true) + } + ParamSpec::Int { min, max, .. } => { + let x = cur.as_i64().unwrap_or(*min).clamp(*min, *max); + // An f64 holds every whole number up to 2^53, so the numeric field (arrows, wheel, arithmetic) is exact there. + // Past it, the number typed is read as an integer. + const EXACT: i64 = 1 << 53; + if -EXACT <= *min && *max <= EXACT { + let field = crate::widgets::range_field(ui, ("plugin-int", k), x as f64, *min as f64..=*max as f64, suffix, 0, 100.0); + (field.map(|x| json!(x as i64)), true) + } else { + let shown = x.to_string(); + crate::widgets::take_dialog_focus(ui, ui.id().with(("plugin-int", k)), &shown); + let typed = crate::widgets::text_field(ui, ("plugin-int", k), Some(&shown), 100.0, 1); + (typed.and_then(|text| text.parse::().ok()).map(|x| json!(x.clamp(*min, *max))), false) + } + } + ParamSpec::Bool { .. } => { + let b = cur.as_bool().unwrap_or(false); + (crate::widgets::check_box(ui, label, b, true).then(|| json!(!b)), false) + } + ParamSpec::Choice { options, .. } => { + let labels: Vec<&str> = options.iter().map(String::as_str).collect(); + let chosen = crate::widgets::dropdown_names(ui, ("plugin-choice", k), cur.as_str().unwrap_or_default(), &labels, 140.0); + (chosen.and_then(|i| options.get(i)).map(|o| json!(o)), false) + } + ParamSpec::Text { max_length, .. } => { + let typed = crate::widgets::text_field(ui, ("plugin-text", k), cur.as_str(), 160.0, 1); + (typed.map(|text| json!(text.chars().take(*max_length).collect::())), false) + } + ParamSpec::NumberList { .. } => { + let typed = crate::widgets::text_field(ui, ("plugin-list", k), Some(&number_list_text(cur)), 160.0, 1); + (typed.and_then(|text| read_number_list(&text, spec)), false) + } + } +} + +/// A `numberList` value as its field shows it: "45, 135". +pub(crate) fn number_list_text(cur: &Value) -> String { + let numbers: Vec = cur.as_array().into_iter().flatten().filter_map(Value::as_f64).map(number_text).collect(); + numbers.join(", ") +} + +/// The list typed in a `numberList` parameter's field: numbers separated by commas, semicolons or +/// spaces, each clamped into the parameter's range. `None` (the value stays as it was) when a +/// number doesn't read, there are too few or too many, or `spec` isn't a number list. +pub(crate) fn read_number_list(text: &str, spec: &ParamSpec) -> Option { + let ParamSpec::NumberList { min, max, min_items, max_items, .. } = spec else { return None }; + let parts = text.split([',', ';', ' ']).filter(|p| !p.trim().is_empty()); + let list: Vec = parts.map(|p| vectorcraft_doc::parse_number(p).filter(|x| x.is_finite())).collect::>()?; + (*min_items..=*max_items).contains(&list.len()).then(|| json!(list.into_iter().map(|x| x.clamp(*min, *max)).collect::>())) +} + +/// What a number field shows after its value for a plug-in parameter's `unit` (none: nothing): a +/// sign such as ° or % joins the number ("45°"), and any other unit is spaced from it ("0.4 mm", +/// "5 1/min"), so a unit starting with a digit isn't read as more of the number. +pub(crate) fn unit_suffix(unit: &str) -> String { + if unit.is_empty() || unit.starts_with(['°', '%', '′', '″']) { unit.to_string() } else { format!(" {unit}") } +} + +/// A number of a list as [`value_field`] shows it: at most 4 decimals, without trailing zeros. +fn number_text(x: f64) -> String { + let s = format!("{x:.4}"); + if s.contains('.') { s.trim_end_matches('0').trim_end_matches('.').to_string() } else { s } +} + /// A field label from its camelCase key (`miterLimit` → "Miter Limit:"). pub(crate) fn humanize(k: &str) -> String { let mut s = String::new(); @@ -715,6 +827,31 @@ pub(super) fn caption(ui: &mut egui::Ui, text: &str) { ui.label(egui::RichText::new(tl!(text)).size(11.0).color(t.text_dim)); } +#[cfg(test)] +mod plugin_field_tests { + use super::*; + + /// A unit reads after its number as it is written: a sign joins it ("45°", "50%"), anything + /// else is spaced, so a unit starting with a digit isn't read as more of the number. + #[test] + fn units_follow_the_number() { + let shown = |unit| format!("5{}", unit_suffix(unit)); + assert_eq!([shown("mm"), shown("°"), shown("%"), shown("1/min"), shown("µm")], ["5 mm", "5°", "5%", "5 1/min", "5 µm"]); + } + + #[test] + fn number_lists_read_as_typed_within_their_limits() { + let spec = ParamSpec::NumberList { min: 0.0, max: 360.0, default: vec![45.0], min_items: 1, max_items: 3 }; + assert_eq!(number_list_text(&json!([45.0, 135.5, 0.125])), "45, 135.5, 0.125"); + assert_eq!(read_number_list("10; 400 20", &spec), Some(json!([10.0, 360.0, 20.0])), "separators, and clamped"); + assert_eq!(read_number_list(" 90 , 2*45 ", &spec), Some(json!([90.0, 90.0]))); + for bad in ["", "1, 2, 3, 4", "1, x", "1, inf"] { + assert_eq!(read_number_list(bad, &spec), None, "{bad:?}"); + } + assert_eq!(read_number_list("1", &ParamSpec::Bool { default: false }), None, "not a list"); + } +} + #[cfg(test)] mod typed_number_tests { use super::*; diff --git a/crates/ui-egui/src/dialogs/mod.rs b/crates/ui-egui/src/dialogs/mod.rs index 08dde3e60..8ca1a2b84 100644 --- a/crates/ui-egui/src/dialogs/mod.rs +++ b/crates/ui-egui/src/dialogs/mod.rs @@ -120,7 +120,7 @@ pub(crate) use export_for_screens::{ KIND as EXPORT_FOR_SCREENS, formats as screen_formats, open_assets as open_export_for_screens_assets, saved_options as screen_saved_options, saved_rows as screen_saved_rows, }; -pub(crate) use form::humanize; +pub(crate) use form::{humanize, number_list_text, read_number_list, unit_suffix}; pub(crate) use new_document::preset_name; pub use new_document::{open as open_new_document, preset_card}; pub use png_options::open as open_raster_options; @@ -580,6 +580,8 @@ mod tests_package; #[cfg(test)] mod tests_plugin_export; #[cfg(test)] +mod tests_plugin_fields; +#[cfg(test)] mod tests_raster_formats; #[cfg(test)] mod tests_svg_options_range; diff --git a/crates/ui-egui/src/dialogs/plugin.rs b/crates/ui-egui/src/dialogs/plugin.rs index b03215ff4..152e8f025 100644 --- a/crates/ui-egui/src/dialogs/plugin.rs +++ b/crates/ui-egui/src/dialogs/plugin.rs @@ -115,7 +115,7 @@ fn params(d: &Dialog) -> Value { fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let Some(plugin) = described(ui, d) else { return true }; - form::schema_fields(ui, d, &plugin.manifest().params); + form::schema_fields(app, ui, d, plugin.manifest()); let (label, p) = (d.str("__label"), params(d)); form::preview(app, ui, d, &label, CMD, p); false @@ -146,9 +146,9 @@ pub fn open_export(app: &mut VectorcraftApp, plugin: &Plugin, export: Value) { app.ui.dialog = Some(Dialog { kind: EXPORT_KIND.into(), fields }); } -fn export_body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { +fn export_body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let Some(plugin) = described(ui, d) else { return true }; - form::schema_fields(ui, d, &plugin.manifest().params); + form::schema_fields(app, ui, d, plugin.manifest()); false } diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index 3d4d6ceba..be41199ba 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -522,7 +522,7 @@ fn given_headings_are_shown_as_they_are() { struct PluginIntField { ctx: egui::Context, d: Dialog, - specs: Vec<(String, vectorcraft_plugins::ParamSpec)>, + m: vectorcraft_plugins::Manifest, pos: egui::Pos2, } @@ -531,8 +531,17 @@ impl PluginIntField { let ctx = egui::Context::default(); theme::install_fonts(&ctx); theme::apply(&ctx, Default::default()); - let specs = vec![("seed".into(), vectorcraft_plugins::ParamSpec::Int { min, max, default: 0 })]; - let mut f = PluginIntField { ctx, d: Dialog::new(plugin::KIND, json!({ "seed": start })), specs, pos: egui::Pos2::ZERO }; + let m = vectorcraft_plugins::Manifest { + id: "test.seed".into(), + name: "Seed".into(), + version: String::new(), + kind: vectorcraft_plugins::Kind::Filter, + description: String::new(), + author: String::new(), + params: vec![("seed".into(), vectorcraft_plugins::ParamSpec::Int { min, max, default: 0 }.into())], + extensions: Vec::new(), + }; + let mut f = PluginIntField { ctx, d: Dialog::new(plugin::KIND, json!({ "seed": start })), m, pos: egui::Pos2::ZERO }; fn number_rect(shape: &egui::Shape, text: &str) -> Option { match shape { egui::Shape::Text(t) if t.galley.text() == text => Some(t.visual_bounding_rect()), @@ -546,9 +555,9 @@ impl PluginIntField { } fn run(&mut self, events: Vec) -> egui::FullOutput { - let (d, specs) = (&mut self.d, &self.specs); + let (d, m) = (&mut self.d, &self.m); let mut out = self.ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| { - form::schema_fields(ui, d, specs); + form::schema_rows(ui, d, m); }); out.textures_delta.clear(); out diff --git a/crates/ui-egui/src/dialogs/tests_modal.rs b/crates/ui-egui/src/dialogs/tests_modal.rs index e89dda7e6..883194916 100644 --- a/crates/ui-egui/src/dialogs/tests_modal.rs +++ b/crates/ui-egui/src/dialogs/tests_modal.rs @@ -144,12 +144,13 @@ fn check_plugin_dialog(kind: &str, count: usize, check: impl FnOnce(&egui::Conte /// settings is as tall as they are. #[test] fn a_plug_in_dialog_of_many_settings_fits_the_window() { - use vectorcraft_plugins::manifest::MAX_PARAMS; + // As many as a plug-in may declare outside groups. + use vectorcraft_plugins::manifest::MAX_UNGROUPED; let screen = Rect::from_min_size(Pos2::ZERO, SCREEN); let shown = |texts: &[(String, Rect)], text: &str| texts.iter().find(|(t, r)| t == text && screen.contains_rect(*r)).map(|(_, r)| *r); for kind in ["effect", "filter"] { - check_plugin_dialog(kind, MAX_PARAMS, |ctx, app, window, heading| { - let last = format!("Setting{}:", MAX_PARAMS - 1); + check_plugin_dialog(kind, MAX_UNGROUPED, |ctx, app, window, heading| { + let last = format!("Setting{}:", MAX_UNGROUPED - 1); let texts = frame(ctx, app, vec![]); let rect = rect_of(ctx, window); assert!(screen.contains_rect(rect) && rect.height() > 0.75 * SCREEN.y, "{kind}: the dialog is at {rect:?} in the {SCREEN:?} window"); diff --git a/crates/ui-egui/src/dialogs/tests_plugin_fields.rs b/crates/ui-egui/src/dialogs/tests_plugin_fields.rs new file mode 100644 index 000000000..8a66bc8a9 --- /dev/null +++ b/crates/ui-egui/src/dialogs/tests_plugin_fields.rs @@ -0,0 +1,171 @@ +//! A plug-in's parameters in its dialog as its manifest presents them (docs/plugins.md › +//! Parameters): labels, units after the values, help on the labels and a button to its page, +//! sections by group, text and lists of numbers, and parameters shown only while their +//! `visibleWhen` holds. The plug-in is removed after each test: plug-ins are process-wide. + +use std::sync::{Arc, Mutex}; + +use egui::vec2; +use egui_kittest::kittest::Queryable; +use serde_json::{Value, json}; +use vectorcraft_engine::Session; +use vectorcraft_plugins::wat as tpl; + +use super::*; + +/// A filter whose parameters use every presentation key. +fn manifest(id: &str) -> String { + format!( + r#"{{"id":"{id}","name":"Hatch Test","version":"1","kind":"filter","params":{{ + "outline":{{"type":"bool","default":true,"label":"Outline"}}, + "title":{{"type":"string","default":"Poster","maxLength":8,"label":"Title"}}, + "pattern":{{"type":"choice","options":["lines","crosshatch","dots"],"default":"lines","label":"Pattern", + "group":"Fill","help":"How the shape is filled."}}, + "spacing":{{"type":"number","min":0.1,"max":10,"default":0.25,"label":"Spacing","unit":"mm","group":"Fill", + "helpUrl":"https://example.org/params/spacing","visibleWhen":{{"pattern":["lines","crosshatch"]}}}}, + "angles":{{"type":"numberList","min":0,"max":360,"default":[45,135],"minItems":1,"maxItems":4,"label":"Angles", + "unit":"°","group":"Fill","visibleWhen":{{"pattern":["lines","crosshatch"]}}}}, + "width":{{"type":"number","min":0,"max":10,"default":0.5,"label":"Width","unit":"pt","group":"Outline", + "visibleWhen":{{"outline":[true]}}}}}}}}"# + ) +} + +/// The dialog of plug-in `id` in a kittest harness; the web pages it opens go to `opened`. +fn harness(id: &str, opened: Arc>>) -> Harness { + let services = crate::Services { open_url: Some(Box::new(move |u: &str| opened.lock().unwrap().push(u.to_string()))), ..Default::default() }; + let mut app = VectorcraftApp::new(Session::new(), services); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + let wasm = wat::parse_str(tpl::module(&manifest(id), tpl::ECHO, "", 1)).unwrap(); + app.run("plugin.install", json!({"dataBase64": vectorcraft_format::base64_encode(&wasm)})).unwrap(); + app.run("plugin.dialog", json!({"id": id})).unwrap(); + let mut fonts = false; + let mut h = egui_kittest::Harness::builder().with_size(vec2(900.0, 800.0)).build_ui_state( + move |ui, app: &mut VectorcraftApp| { + if !std::mem::replace(&mut fonts, true) { + crate::theme::install_fonts(ui.ctx()); + return; + } + show(app, ui.ctx()); + }, + app, + ); + h.run_steps(4); + h +} + +type Harness = egui_kittest::Harness<'static, VectorcraftApp>; + +/// The text label `text` (a checkbox is called by its row's label too). +fn label<'h>(h: &'h Harness, text: &'h str) -> Option> { + h.query_by_role_and_label(egui::accesskit::Role::Label, text) +} + +fn set(h: &mut Harness, key: &str, value: Value) { + h.state_mut().ui.dialog.as_mut().expect("the dialog is open").fields.insert(key.into(), value); + h.run_steps(2); +} + +fn remove(h: &mut Harness, id: &str) { + h.state_mut().run("plugin.remove", json!({"id": id})).unwrap(); +} + +#[test] +fn labels_units_sections_text_and_lists_come_from_the_manifest() { + let id = "test.ui.fields.shown"; + let mut h = harness(id, Arc::default()); + for text in ["Outline:", "Title:", "Pattern:", "Spacing:", "Angles:", "Width:", "°"] { + assert!(label(&h, text).is_some(), "{text} isn't shown"); + } + for section in ["Fill", "Outline"] { + assert!(h.query_by_label(section).is_some(), "the {section} section isn't shown"); + } + for value in ["Poster", "0.25 mm", "45, 135", "0.5 pt"] { + // A text field and the text inside it both carry the value. + assert!(h.query_all_by_value(value).next().is_some(), "the value {value} isn't shown"); + } + // The parameters outside groups come first, then each group's section. + let top = |text: &str| label(&h, text).map(|n| n.rect().top()).unwrap_or_default(); + let (fill, outline) = (h.get_by_label("Fill").rect().top(), h.get_by_label("Outline").rect().top()); + assert!(top("Title:") < fill && fill < top("Spacing:") && top("Angles:") < outline && outline < top("Width:")); + // The fields line up down the dialog, in sections and out of them. + let left = |value: &str| h.query_all_by_value(value).next().map(|n| n.rect().left()).unwrap_or_default(); + let (title, spacing, width) = (left("Poster"), left("0.25 mm"), left("0.5 pt")); + assert!((title - spacing).abs() < 0.5 && (spacing - width).abs() < 0.5, "fields at {title}, {spacing} and {width}"); + remove(&mut h, id); +} + +#[test] +fn visible_when_shows_a_parameter_only_while_its_condition_holds() { + let id = "test.ui.fields.visible"; + let mut h = harness(id, Arc::default()); + set(&mut h, "pattern", json!("dots")); + assert!(label(&h, "Spacing:").is_none() && label(&h, "Angles:").is_none(), "dots have no line spacing"); + set(&mut h, "outline", json!(false)); + assert!(label(&h, "Width:").is_none() && h.query_by_label("Outline").is_none(), "without an outline, nor its section"); + set(&mut h, "pattern", json!("crosshatch")); + assert!(label(&h, "Spacing:").is_some()); + // A hidden parameter still goes to the plug-in: the preview runs with every value. + let fields = &h.state().ui.dialog.as_ref().unwrap().fields; + assert_eq!(fields.get("width"), Some(&json!(0.5))); + remove(&mut h, id); +} + +#[test] +fn the_help_button_opens_the_parameter_s_page() { + let id = "test.ui.fields.help"; + let opened: Arc>> = Arc::default(); + let mut h = harness(id, opened.clone()); + let buttons = h.query_all_by_label("Open this setting's help page").count(); + assert_eq!(buttons, 1, "only Spacing has a page"); + h.get_by_label("Open this setting's help page").click(); + h.run_steps(2); + assert_eq!(*opened.lock().unwrap(), ["https://example.org/params/spacing"]); + remove(&mut h, id); +} + +/// `CRAFT_UI_PLUGIN_FIELDS_SHOT=` saves a picture of a dialog whose parameters use every +/// presentation key there, and the test plug-in beside it (`hatch-test-filter.wasm`) to install in +/// the app. Its parameter names are written as many plug-ins write them, in snake case, and it uses +/// only the types every host reads, so a host from before the keys installs it too and shows it as +/// it did. +#[test] +fn plugin_fields_picture() { + let Some(dir) = std::env::var_os("CRAFT_UI_PLUGIN_FIELDS_SHOT") else { return }; + let dir = std::path::PathBuf::from(dir); + let manifest = r#"{"id":"org.example.hatch","name":"Hatch Fill (test plug-in)","version":"1","kind":"filter", + "description":"Fills the selected shapes with lines.","params":{ + "pattern":{"type":"choice","options":["lines","crosshatch","dots"],"default":"lines", + "label":"Pattern","help":"How the shapes are filled."}, + "line_angle":{"type":"number","min":0,"max":360,"default":45,"label":"Angle","unit":"°","group":"Lines", + "help":"The direction of the lines.","visibleWhen":{"pattern":["lines","crosshatch"]}}, + "line_spacing_mm":{"type":"number","min":0.1,"max":10,"default":1.5,"label":"Spacing","unit":"mm","group":"Lines", + "help":"Distance between the lines. Smaller is denser.","helpUrl":"https://example.org/params/line_spacing_mm", + "visibleWhen":{"pattern":["lines","crosshatch"]}}, + "pen_width_mm":{"type":"number","min":0.1,"max":5,"default":0.3,"label":"Pen width","unit":"mm","group":"Lines"}, + "outline":{"type":"bool","default":true,"label":"Outline","group":"Outline"}, + "outline_inset_mm":{"type":"number","min":-10,"max":10,"default":0,"label":"Inset","unit":"mm","group":"Outline", + "visibleWhen":{"outline":[true]}}, + "outline_passes":{"type":"int","min":1,"max":4,"default":1,"label":"Passes","group":"Outline", + "visibleWhen":{"outline":[true]}}}}"#; + let src = tpl::module(manifest, tpl::ECHO, "", 1); + std::fs::write(dir.join("hatch-test-filter.wasm"), wat::parse_str(&src).unwrap()).unwrap(); + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + app.run("plugin.install", json!({"dataBase64": vectorcraft_format::base64_encode(&wat::parse_str(&src).unwrap())})).unwrap(); + app.run("plugin.dialog", json!({"id": "org.example.hatch"})).unwrap(); + let mut ready = false; + let mut h = egui_kittest::Harness::builder().with_size(vec2(720.0, 620.0)).wgpu().build_ui_state( + move |ui, app: &mut VectorcraftApp| { + if !std::mem::replace(&mut ready, true) { + crate::theme::install_fonts(ui.ctx()); + crate::theme::apply(ui.ctx(), crate::theme::Brightness::MediumDark); + return; + } + show(app, ui.ctx()); + }, + app, + ); + h.run_steps(6); + h.render().unwrap().save(dir.join("plugin-fields.png")).unwrap(); + h.state_mut().run("plugin.remove", json!({"id": "org.example.hatch"})).unwrap(); +} diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 95b8e6973..5538d303a 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -1494,6 +1494,7 @@ Inspect Object Инспектиране на обекта Install Plug-in Инсталиране на приставка Install Plug-in… Инсталиране на приставка… + Open this setting's help page Отваряне на страницата с помощ за тази настройка Installed plug-in {} Инсталирана приставка {} Intensity: Интензивност: Intent: Цел: diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 4ecb67ed2..8d1284260 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -305,6 +305,7 @@ Plug-ins Zásuvné moduly No filter plug-ins installed Nejsou nainstalovány žádné filtry Install Plug-in… Nainstalovat zásuvný modul… + Open this setting's help page Otevřít stránku nápovědy k tomuto nastavení Reload Plug-ins Znovu načíst zásuvné moduly Type Text Font Písmo diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 11656a2ef..b5faef124 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -269,6 +269,7 @@ Plug-ins Plug-ins No filter plug-ins installed Ingen filter-plug-ins installeret Install Plug-in… Installér plug-in… + Open this setting's help page Åbn hjælpesiden for denne indstilling Reload Plug-ins Genindlæs plug-ins Type Tekst Font Skrifttype diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index a14c11213..c34f268b9 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -1526,6 +1526,7 @@ file-filter Templates Vorlagen Inspect Object Objekt untersuchen Install Plug-in Plug-in installieren Install Plug-in… Plug-in installieren… + Open this setting's help page Hilfeseite zu dieser Einstellung öffnen Installed plug-in {} Plug-in {} installiert Intensity: Intensität: Intent: Renderpriorität: diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 8914e70aa..3a67cb938 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -1456,6 +1456,7 @@ Inspect Object Επιθεώρηση αντικειμένου Install Plug-in Εγκατάσταση προσθέτου Install Plug-in… Εγκατάσταση προσθέτου… + Open this setting's help page Άνοιγμα της σελίδας βοήθειας αυτής της ρύθμισης Installed plug-in {} Εγκαταστάθηκε επέκταση {} Intensity: Ένταση: Intent: Σκοπός μετατροπής: diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 41a51e136..72b8178b0 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1520,6 +1520,7 @@ Inspect Object Inspeccionar objeto Install Plug-in Instalar plugin Install Plug-in… Instalar plugin… + Open this setting's help page Abrir la página de ayuda de este ajuste Installed plug-in {} Se instaló el plugin {} Intensity: Intensidad: Intent: Propósito: diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 1206dda68..d74cc4255 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -302,6 +302,7 @@ Plug-ins Laajennukset No filter plug-ins installed Suodinlaajennuksia ei ole asennettu Install Plug-in… Asenna laajennus… + Open this setting's help page Avaa tämän asetuksen ohjesivu Reload Plug-ins Lataa laajennukset uudelleen Type Teksti Font Fontti diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index ecb85edb6..9547cc652 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -1519,6 +1519,7 @@ Inspect Object Inspecter l’objet Install Plug-in Installer un module externe Install Plug-in… Installer un module externe… + Open this setting's help page Ouvrir la page d’aide de ce réglage Installed plug-in {} Module externe {} installé Intensity: Intensité : Intent: Mode de rendu : diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index c5e9f39e9..9068696ab 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -1911,6 +1911,7 @@ file-filter Templates Sablonok Inspect Object Objektum vizsgálata Install Plug-in Bővítmény telepítése Install Plug-in… Bővítmény telepítése… + Open this setting's help page A beállítás súgóoldalának megnyitása Installed plug-in {} Telepített bővítmény: {} Intensity Erősség Intensity: Erősség: diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 5ea570e72..1c60860f2 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -269,6 +269,7 @@ Plug-ins Plug-in No filter plug-ins installed Tidak ada plug-in filter yang terinstal Install Plug-in… Instal plug-in… + Open this setting's help page Buka halaman bantuan pengaturan ini Reload Plug-ins Muat ulang plug-in Type Teks Font Font diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 1ceeda635..5f8860e47 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -1544,6 +1544,7 @@ file-filter Templates Modelli Inspect Object Ispeziona oggetto Install Plug-in Installa plug-in Install Plug-in… Installa plug-in… + Open this setting's help page Apri la pagina della guida di questa impostazione Installed plug-in {} Plug-in {} installato Intensity: Intensità: Intent: Intento: diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 437d3682a..b10d12f63 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -289,6 +289,7 @@ Plug-ins プラグイン No filter plug-ins installed フィルタープラグインはインストールされていません Install Plug-in… プラグインをインストール… + Open this setting's help page この設定のヘルプページを開く Reload Plug-ins プラグインを再読み込み Type 書式 Font フォント diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 911982187..dc034c503 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -228,6 +228,7 @@ Plug-ins პლაგინები No filter plug-ins installed ფილტრის პლაგინები არ არის დაყენებული Install Plug-in… პლაგინის დაყენება… + Open this setting's help page ამ პარამეტრის დახმარების გვერდის გახსნა Reload Plug-ins პლაგინების ხელახლა ჩატვირთვა Type ტექსტი Font შრიფტი diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 4563d9fac..8d474190c 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -285,6 +285,7 @@ Plug-ins 플러그인 No filter plug-ins installed 설치된 필터 플러그인 없음 Install Plug-in… 플러그인 설치… + Open this setting's help page 이 설정의 도움말 페이지 열기 Reload Plug-ins 플러그인 다시 불러오기 Type 문자 Font 글꼴 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index c51ff5b21..083371409 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -283,6 +283,7 @@ Plug-ins Plug-ins No filter plug-ins installed Geen filterplug-ins geïnstalleerd Install Plug-in… Plug-in installeren… + Open this setting's help page Helppagina van deze instelling openen Reload Plug-ins Plug-ins opnieuw laden Type Type Font Lettertype diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index dd674d98d..0702b15e4 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -266,6 +266,7 @@ Plug-ins Wtyczki No filter plug-ins installed Brak zainstalowanych wtyczek filtrów Install Plug-in… Zainstaluj wtyczkę… + Open this setting's help page Otwórz stronę pomocy dla tego ustawienia Reload Plug-ins Wczytaj ponownie wtyczki Type Tekst Font Czcionka diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 16be9509c..b3fcdd0d7 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -1315,6 +1315,7 @@ Inspect Object Inspecionar Objeto Install Plug-in Instalar Plug-in Install Plug-in… Instalar Plug-in… + Open this setting's help page Abrir a página de ajuda desta configuração Intensity: Intensidade: Intent: Intenção: Interface Language Idioma da Interface diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 94ee52505..b41e26e3a 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -1542,6 +1542,7 @@ file-filter Templates Шаблоны Inspect Object Проверить объект Install Plug-in Установить модуль Install Plug-in… Установить модуль… + Open this setting's help page Открыть страницу справки по этому параметру Installed plug-in {} Установлен модуль {} Intensity: Интенсивность: Intent: Назначение: diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 6cc23bb0b..a22d86327 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -1496,6 +1496,7 @@ Inspect Object Inspektera objekt Install Plug-in Installera plugin Install Plug-in… Installera plugin… + Open this setting's help page Öppna hjälpsidan för den här inställningen Installed plug-in {} Installerade plugin {} Intensity: Intensitet: Intent: Återgivningsmetod: diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 13cd0851f..8289143aa 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1538,6 +1538,7 @@ file-filter Templates Шаблони Inspect Object Перевірити об’єкт Install Plug-in Встановити модуль Install Plug-in… Встановити модуль… + Open this setting's help page Відкрити сторінку довідки для цього параметра Installed plug-in {} Встановлено модуль {} Intensity: Інтенсивність: Intent: Призначення: diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index c5ede5168..ed8fc283b 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -1787,6 +1787,7 @@ file-filter Templates Mẫu Inspect Object Kiểm tra đối tượng Install Plug-in Cài đặt plug-in Install Plug-in… Cài đặt plug-in… + Open this setting's help page Mở trang trợ giúp của thiết lập này Installed plug-in {} Đã cài plug-in {} Intensity Cường độ Intensity: Cường độ: diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 6cdc22e62..4a0e373d4 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1479,6 +1479,7 @@ file-filter Templates 模板 Inspect Object 检查对象 Install Plug-in 安装插件 Install Plug-in… 安装增效工具… + Open this setting's help page 打开此设置的帮助页面 Installed plug-in {} 已安装增效工具 {} Intensity: 强度: Intent: 意图: diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index ed9539514..15528a259 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1515,6 +1515,7 @@ file-filter Templates 範本 Inspect Object 檢查物件 Install Plug-in 安裝增效模組 Install Plug-in… 安裝增效模組… + Open this setting's help page 開啟此設定的說明頁面 Installed plug-in {} 已安裝增效模組 {} Intensity: 強度: Intent: 目的: diff --git a/crates/ui-egui/src/panels/appearance.rs b/crates/ui-egui/src/panels/appearance.rs index f1e5ce5d8..d02460957 100644 --- a/crates/ui-egui/src/panels/appearance.rs +++ b/crates/ui-egui/src/panels/appearance.rs @@ -739,6 +739,10 @@ fn effect_editor(app: &mut VectorcraftApp, ui: &mut Ui, item: Option, k: let mut change: Option<(String, Value)> = None; let mut edit = false; let built_in = built_in_effect(&e.id); + // A plug-in effect's parameters are labelled, measured and shown as its manifest says. + let plugin = vectorcraft_plugins::effect::installed(&e.id); + let manifest = plugin.as_ref().map(|p| p.manifest()); + let visible = manifest.map(|m| m.visible(¶ms)); let (unit, relative) = (app.session.general_unit(), params.get("relative").and_then(Value::as_bool).unwrap_or(false)); let cmyk = app.session.active().is_some_and(|s| s.doc.color_mode == vectorcraft_doc::ColorMode::Cmyk); egui::Frame::NONE.fill(t.panel_darker).inner_margin(egui::Margin { left: (EYE_W + 12.0) as i8, right: 6, top: 4, bottom: 4 }).show(ui, |ui| { @@ -747,9 +751,16 @@ fn effect_editor(app: &mut VectorcraftApp, ui: &mut Ui, item: Option, k: widgets::dim_label(ui, tl!("No options")); } for (key, v) in ¶ms { + if visible.as_ref().is_some_and(|v| !v.contains(key.as_str())) { + continue; + } + let ui_keys = manifest.and_then(|m| m.param(key)).map(|p| &p.ui); ui.horizontal(|ui| { - let label = param_label(key, built_in); - ui.add_sized(vec2(84.0, 22.0), egui::Label::new(egui::RichText::new(label.as_str()).size(11.5).color(t.text)).truncate()); + let label = ui_keys.and_then(|u| u.label.clone()).unwrap_or_else(|| param_label(key, built_in)); + let shown = ui.add_sized(vec2(84.0, 22.0), egui::Label::new(egui::RichText::new(label.as_str()).size(11.5).color(t.text)).truncate()); + if let Some(help) = ui_keys.and_then(|u| u.help.as_deref()) { + shown.on_hover_text(help); + } if e.id == "stylize.dropShadow" && key == "shade" { let cur = v.as_str().unwrap_or_default(); for (label, value) in crate::dialogs::SHADES { @@ -771,6 +782,19 @@ fn effect_editor(app: &mut VectorcraftApp, ui: &mut Ui, item: Option, k: } return; } + // A plug-in's number list is typed as its dialog's is: "45, 135". + if let Some(spec) = manifest.and_then(|m| m.param(key)).map(|p| &p.spec) + && matches!(spec, vectorcraft_plugins::manifest::ParamSpec::NumberList { .. }) + { + let typed = widgets::exact_text_field(ui, ("fx", item, k, key.as_str()), &crate::dialogs::number_list_text(v), 90.0); + if let Some(list) = typed.and_then(|text| crate::dialogs::read_number_list(&text, spec)) { + change = Some((key.clone(), list)); + } + if let Some(unit) = ui_keys.and_then(|u| u.unit.as_deref()) { + widgets::dim_label(ui, unit); + } + return; + } // Only our own effects show a `mode` parameter as the blend mode dropdown; a plug-in's `mode` stays a plain field. if built_in && let Some(cur) = widgets::blend_param(key, v) { if let Some(m) = widgets::blend_param_dropdown(ui, ("fx-blend", item, k, key.as_str()), cur) { @@ -786,10 +810,11 @@ fn effect_editor(app: &mut VectorcraftApp, ui: &mut Ui, item: Option, k: } Value::Number(n) => { let (x, id) = (n.as_f64().unwrap_or(0.0), ("fx", item, k, key.as_str())); + let suffix = ui_keys.and_then(|u| u.unit.as_deref()).map(crate::dialogs::unit_suffix).unwrap_or_default(); let nx = if vectorcraft_effects::is_length(&e.id, key, relative) { widgets::num_field(ui, id, Some(x), unit, 70.0) } else { - widgets::plain_field(ui, id, x, "", 2, 70.0) + widgets::plain_field(ui, id, x, &suffix, 2, 70.0) }; if let Some(nx) = nx { change = Some((key.clone(), json!(nx))); @@ -1164,6 +1189,46 @@ mod tests { assert!(texts.iter().any(|s| s == "screen") && !texts.iter().any(|s| s == "Screen"), "{texts:?}"); } + /// A plug-in effect's editor labels its parameters, shows their units and leaves out those whose + /// `visibleWhen` isn't met, as the effect's dialog does, and reads a typed number list the same + /// way. + #[test] + fn a_plug_in_effect_s_editor_follows_its_manifest() { + use vectorcraft_plugins::wat as tpl; + let params = r#"{"outline":{"type":"bool","default":true,"label":"Outline"}, + "spacing":{"type":"number","min":0.1,"max":10,"default":1.5,"label":"Spacing","unit":"mm","help":"Distance between the lines."}, + "angles":{"type":"numberList","min":0,"max":360,"default":[45,135],"minItems":1,"maxItems":4,"unit":"°"}, + "width":{"type":"number","min":0,"max":10,"default":0.5,"label":"Width","unit":"pt","visibleWhen":{"outline":[true]}}}"#; + let src = tpl::module(&tpl::manifest("test.ui.inlinekeys", "effect", params), tpl::ECHO, "", 1); + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.run("plugin.install", json!({"dataBase64": vectorcraft_format::base64_encode(&wat::parse_str(src).unwrap())})).unwrap(); + app.run("file.new", json!({"width": 100, "height": 100})).unwrap(); + let id = app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 20, "height": 20})).unwrap()["id"].as_u64().unwrap(); + app.run("effect.apply", json!({"effect": "plugin.test.ui.inlinekeys"})).unwrap(); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + set_pstate(&ctx, "ap-fx-open-None-0", true); + let texts = shown_texts(&mut app, &ctx); + for shown in ["Spacing", "1.5 mm", "Angles", "45, 135", "°", "Width", "0.5 pt"] { + assert!(texts.iter().any(|s| s == shown), "no {shown:?} in {texts:?}"); + } + assert!(!texts.iter().any(|s| s == "spacing" || s == "width"), "the labels replace the names: {texts:?}"); + // Without the outline, its width isn't shown. + app.run("effect.setParams", json!({"item": null, "index": 0, "params": {"outline": false}})).unwrap(); + let texts = shown_texts(&mut app, &ctx); + assert!(!texts.iter().any(|s| s == "Width") && texts.iter().any(|s| s == "Spacing"), "{texts:?}"); + // A list typed in the field: separated by a semicolon and a space, and clamped. + click_text(&mut app, &ctx, "45, 135"); + let key = |key, modifiers| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers }; + panel_frame(&mut app, &ctx, vec![key(egui::Key::A, egui::Modifiers::COMMAND)]); + panel_frame(&mut app, &ctx, vec![egui::Event::Text("10; 400 20".into())]); + panel_frame(&mut app, &ctx, vec![key(egui::Key::Enter, egui::Modifiers::NONE)]); + panel_frame(&mut app, &ctx, vec![]); + let angles = app.session.doc().unwrap().doc.node(NodeId(id)).unwrap().appearance.effects[0].params["angles"].clone(); + assert_eq!(angles, json!([10.0, 360.0, 20.0])); + app.run("plugin.remove", json!({"id": "test.ui.inlinekeys"})).unwrap(); + } + /// Glass with a loaded texture lists Loaded Texture in its Texture menu, as its dialog does. #[test] fn the_effect_editor_lists_a_loaded_glass_texture() { diff --git a/docs/gaps.md b/docs/gaps.md index 02d9239a0..28196b8b7 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-20 · **Change:** minor (exporter plug-ins: plug-in file formats in Export As and document.export, G17; G2: Effect › Artistic › Smudge Stick, Sponge and Watercolor; Sketch › Bas Relief, Charcoal, Photocopy and Stamp, #2533; Photoshop filters 45/57, 12 remaining (8–14 h); on-canvas Mirror Repeat axis, #2877; G12: Background Export progress dialog with Cancel, #2564; status-readout sample widths cached by language, document unit and pixels per point; status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; cached canvas reprojection preserves scale during resize, rotation and DPI changes; on-canvas Mirror Repeat axis, #2877; G12: Background Export progress dialog with Cancel, #2564; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged; transparency grid visibility saved in native documents, #1968; G6: MCP cancellation, progress notifications and busy answers during long exports, #2165, #2171, #2172; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; correction: quick Image Trace presets leave the panel as it was, the Image Trace button opens it; batch exporting, #1935; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 45/57, 12 remaining (8–14 h); G5: Symbol Options Export Type (Graphic / Movie Clip); G1: Smart Guides line extension, #2120; G16: Brazilian Portuguese catalog complete under the tests, #1914; Localization 71% → 74%, hours 55–90 → 50–85; G6: image variables landed, estimate 8–14 → 6–12 h; Warp Options controls verified by focused checks and original-image review; estimates unchanged; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; G13: Object › Intertwine landed, #1763; no entry closed, estimates unchanged; MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged; Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged.; G1: Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; G9: legacy `.ai` of version 7 saved with type as text and hidden objects kept, legacy type read, #868; estimate unchanged; Revolve: automatic smooth shading and default-on Cap with continuous rims; later 2026-10-14: G8 untrusted files, #1852, #1862–#1873; G11 anchor X/Y/W/H, #1824, Snap to Grid over Snap to Pixel, #1816, redo during a drag, #1719, Dim Images, #1683, failed-save dialog, #1685; gaps noted for reporters: G1 Warp Options for Effect › Warp, #1760, Color Picker Tab order, #1692, key anchor, #1636; G7 custom toolbars, #1759, workspace panel widths, #1753; G11 Shape Builder gaps between closed shapes, #1691, interior anchor delete; G13 Intertwine, #1763, gradient mesh from an image, #1676, Image Trace radial gradients, #1849; G12 duplicated line removed; no entry closed, estimates unchanged; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 fixes: G11 anchor Align and Distribute, #1636, Delete and selection, #1674, #1666, #1743, #1686, Roughen and Scribble, #1643, #1648; G8 file and input hardening, #1776–#1783, #1786, #1807; G10 Wayland cursors and the Windows title bar, unconfirmed, #1645, #1758; no entry closed, estimates unchanged; later 2026-10-13 fixes, #1570, #1578, #1590, #1613, #1615, #1616, #1620 in part, #1602 in part; no entry closed, estimates unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; G12: Identify Objects By acts, #394; ~16 → ~15 prefs still inert; G12: Appearance of Black › Printing / Exporting acts, #394; ~16 → ~15 prefs still inert; control transport deadlines, #1726; G1: Average and Join in one step with a corner or smooth choice added as missing (it was listed as done in ui-parity.md); Join after Average, #1533; guide objects, #1534; Keyboard Shortcuts, #1539; G9 and G11 fixes, #1528, #1530, #1544, #1553, #1512, #1537; estimates unchanged; Direct Selection anchor precedence, #1508; dialog slider rails, #1483; template layers in Outline view, #1486; Tabs panel stop drags, #1458; type that does not fit its frame is reported, #1471; 3D on outlined type, #1481, Twist range, #1524, twisted walls, #1525; print-size images, #1517; French catalog, #1511; estimates unchanged; G1: Split Into Grid options, #1462, Transform Objects and Reflect's angle, #1448 and #1460, Pathfinder on a group, #1450, value fields, #1457 and #1465 in part; G9: grayscale TIFFs with alpha, #1466 in part; G6: unknown parameter keys refused, #1442; G10: window surfaces past 8192 px, #1459 in part; G15: many-object edits, #1429 in part; estimates unchanged; G16: Vietnamese catalog added, complete under the tests; three key languages remain, 60–100 → 55–90 h; G3: Extrude & Bevel, Inflate and Rotate added, 45–75 → 25–45 h; G9: `.ai` compound path paint, #1410, overprint kept on import, #1388, text-only PDF parts, #1387; G1: Transform dialogs preview, #1412, transforms of direct-selected anchors, #1408, corner drags, #1229; G11: strokes wider than their shape, #1396; estimates unchanged; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; G9: `.ai` gradient matrices and meshes from the editing data; estimate unchanged; G9: `.ai` art styles from the editing data; estimate unchanged; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; G9: `.ai` gradient matrices and meshes from the editing data; estimate unchanged; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; SVG knockout groups of opaque art export without masks; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; G9: `.ai` keeps its RGB numbers, PDF import keeps strokes apart from fills faded or masked by their group; estimates unchanged; interactive Mirror Repeat axis; G12: Background Export progress dialog with Cancel, #2564; no entry closed, estimates unchanged; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged; transparency grid visibility saved in native documents, #1968; G6: MCP cancellation, progress notifications and busy answers during long exports, #2165, #2171, #2172; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; correction: quick Image Trace presets leave the panel as it was, the Image Trace button opens it; batch exporting, #1935; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 45/57, 12 remaining (8–14 h); G5: Symbol Options Export Type (Graphic / Movie Clip); G1: Smart Guides line extension, #2120; G16: Brazilian Portuguese catalog complete under the tests, #1914; Localization 71% → 74%, hours 55–90 → 50–85; G6: image variables landed, estimate 8–14 → 6–12 h; Warp Options controls verified by focused checks and original-image review; estimates unchanged; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; G13: Object › Intertwine landed, #1763; no entry closed, estimates unchanged; MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged; Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged.; G1: Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; G9: legacy `.ai` of version 7 saved with type as text and hidden objects kept, legacy type read, #868; estimate unchanged; Revolve: automatic smooth shading and default-on Cap with continuous rims; later 2026-10-14: G8 untrusted files, #1852, #1862–#1873; G11 anchor X/Y/W/H, #1824, Snap to Grid over Snap to Pixel, #1816, redo during a drag, #1719, Dim Images, #1683, failed-save dialog, #1685; gaps noted for reporters: G1 Warp Options for Effect › Warp, #1760, Color Picker Tab order, #1692, key anchor, #1636; G7 custom toolbars, #1759, workspace panel widths, #1753; G11 Shape Builder gaps between closed shapes, #1691, interior anchor delete; G13 Intertwine, #1763, gradient mesh from an image, #1676, Image Trace radial gradients, #1849; G12 duplicated line removed; no entry closed, estimates unchanged; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 fixes: G11 anchor Align and Distribute, #1636, Delete and selection, #1674, #1666, #1743, #1686, Roughen and Scribble, #1643, #1648; G8 file and input hardening, #1776–#1783, #1786, #1807; G10 Wayland cursors and the Windows title bar, unconfirmed, #1645, #1758; no entry closed, estimates unchanged; later 2026-10-13 fixes, #1570, #1578, #1590, #1613, #1615, #1616, #1620 in part, #1602 in part; no entry closed, estimates unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; G12: Identify Objects By acts, #394; ~16 → ~15 prefs still inert; G12: Appearance of Black › Printing / Exporting acts, #394; ~16 → ~15 prefs still inert; control transport deadlines, #1726; G1: Average and Join in one step with a corner or smooth choice added as missing (it was listed as done in ui-parity.md); Join after Average, #1533; guide objects, #1534; Keyboard Shortcuts, #1539; G9 and G11 fixes, #1528, #1530, #1544, #1553, #1512, #1537; estimates unchanged; Direct Selection anchor precedence, #1508; dialog slider rails, #1483; template layers in Outline view, #1486; Tabs panel stop drags, #1458; type that does not fit its frame is reported, #1471; 3D on outlined type, #1481, Twist range, #1524, twisted walls, #1525; print-size images, #1517; French catalog, #1511; estimates unchanged; G1: Split Into Grid options, #1462, Transform Objects and Reflect's angle, #1448 and #1460, Pathfinder on a group, #1450, value fields, #1457 and #1465 in part; G9: grayscale TIFFs with alpha, #1466 in part; G6: unknown parameter keys refused, #1442; G10: window surfaces past 8192 px, #1459 in part; G15: many-object edits, #1429 in part; estimates unchanged; G16: Vietnamese catalog added, complete under the tests; three key languages remain, 60–100 → 55–90 h; G3: Extrude & Bevel, Inflate and Rotate added, 45–75 → 25–45 h; G9: `.ai` compound path paint, #1410, overprint kept on import, #1388, text-only PDF parts, #1387; G1: Transform dialogs preview, #1412, transforms of direct-selected anchors, #1408, corner drags, #1229; G11: strokes wider than their shape, #1396; estimates unchanged; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; G9: `.ai` gradient matrices and meshes from the editing data; estimate unchanged; G9: `.ai` art styles from the editing data; estimate unchanged; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; G9: `.ai` gradient matrices and meshes from the editing data; estimate unchanged; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; SVG knockout groups of opaque art export without masks; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; G9: `.ai` keeps its RGB numbers, PDF import keeps strokes apart from fills faded or masked by their group; estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-20 · **Change:** minor (plug-in parameters: labels, units, groups, help and conditions in their dialogs, text and number-list parameters, up to 256 of them, G17; exporter plug-ins: plug-in file formats in Export As and document.export, G17; G2: Effect › Artistic › Smudge Stick, Sponge and Watercolor; Sketch › Bas Relief, Charcoal, Photocopy and Stamp, #2533; Photoshop filters 45/57, 12 remaining (8–14 h); on-canvas Mirror Repeat axis, #2877; G12: Background Export progress dialog with Cancel, #2564; status-readout sample widths cached by language, document unit and pixels per point; status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; cached canvas reprojection preserves scale during resize, rotation and DPI changes; on-canvas Mirror Repeat axis, #2877; G12: Background Export progress dialog with Cancel, #2564; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged; transparency grid visibility saved in native documents, #1968; G6: MCP cancellation, progress notifications and busy answers during long exports, #2165, #2171, #2172; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; correction: quick Image Trace presets leave the panel as it was, the Image Trace button opens it; batch exporting, #1935; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 45/57, 12 remaining (8–14 h); G5: Symbol Options Export Type (Graphic / Movie Clip); G1: Smart Guides line extension, #2120; G16: Brazilian Portuguese catalog complete under the tests, #1914; Localization 71% → 74%, hours 55–90 → 50–85; G6: image variables landed, estimate 8–14 → 6–12 h; Warp Options controls verified by focused checks and original-image review; estimates unchanged; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; G13: Object › Intertwine landed, #1763; no entry closed, estimates unchanged; MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged; Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged.; G1: Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; G9: legacy `.ai` of version 7 saved with type as text and hidden objects kept, legacy type read, #868; estimate unchanged; Revolve: automatic smooth shading and default-on Cap with continuous rims; later 2026-10-14: G8 untrusted files, #1852, #1862–#1873; G11 anchor X/Y/W/H, #1824, Snap to Grid over Snap to Pixel, #1816, redo during a drag, #1719, Dim Images, #1683, failed-save dialog, #1685; gaps noted for reporters: G1 Warp Options for Effect › Warp, #1760, Color Picker Tab order, #1692, key anchor, #1636; G7 custom toolbars, #1759, workspace panel widths, #1753; G11 Shape Builder gaps between closed shapes, #1691, interior anchor delete; G13 Intertwine, #1763, gradient mesh from an image, #1676, Image Trace radial gradients, #1849; G12 duplicated line removed; no entry closed, estimates unchanged; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 fixes: G11 anchor Align and Distribute, #1636, Delete and selection, #1674, #1666, #1743, #1686, Roughen and Scribble, #1643, #1648; G8 file and input hardening, #1776–#1783, #1786, #1807; G10 Wayland cursors and the Windows title bar, unconfirmed, #1645, #1758; no entry closed, estimates unchanged; later 2026-10-13 fixes, #1570, #1578, #1590, #1613, #1615, #1616, #1620 in part, #1602 in part; no entry closed, estimates unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; G12: Identify Objects By acts, #394; ~16 → ~15 prefs still inert; G12: Appearance of Black › Printing / Exporting acts, #394; ~16 → ~15 prefs still inert; control transport deadlines, #1726; G1: Average and Join in one step with a corner or smooth choice added as missing (it was listed as done in ui-parity.md); Join after Average, #1533; guide objects, #1534; Keyboard Shortcuts, #1539; G9 and G11 fixes, #1528, #1530, #1544, #1553, #1512, #1537; estimates unchanged; Direct Selection anchor precedence, #1508; dialog slider rails, #1483; template layers in Outline view, #1486; Tabs panel stop drags, #1458; type that does not fit its frame is reported, #1471; 3D on outlined type, #1481, Twist range, #1524, twisted walls, #1525; print-size images, #1517; French catalog, #1511; estimates unchanged; G1: Split Into Grid options, #1462, Transform Objects and Reflect's angle, #1448 and #1460, Pathfinder on a group, #1450, value fields, #1457 and #1465 in part; G9: grayscale TIFFs with alpha, #1466 in part; G6: unknown parameter keys refused, #1442; G10: window surfaces past 8192 px, #1459 in part; G15: many-object edits, #1429 in part; estimates unchanged; G16: Vietnamese catalog added, complete under the tests; three key languages remain, 60–100 → 55–90 h; G3: Extrude & Bevel, Inflate and Rotate added, 45–75 → 25–45 h; G9: `.ai` compound path paint, #1410, overprint kept on import, #1388, text-only PDF parts, #1387; G1: Transform dialogs preview, #1412, transforms of direct-selected anchors, #1408, corner drags, #1229; G11: strokes wider than their shape, #1396; estimates unchanged; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; G9: `.ai` gradient matrices and meshes from the editing data; estimate unchanged; G9: `.ai` art styles from the editing data; estimate unchanged; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; G9: `.ai` gradient matrices and meshes from the editing data; estimate unchanged; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; SVG knockout groups of opaque art export without masks; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; G9: `.ai` keeps its RGB numbers, PDF import keeps strokes apart from fills faded or masked by their group; estimates unchanged; interactive Mirror Repeat axis; G12: Background Export progress dialog with Cancel, #2564; no entry closed, estimates unchanged; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged; transparency grid visibility saved in native documents, #1968; G6: MCP cancellation, progress notifications and busy answers during long exports, #2165, #2171, #2172; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; correction: quick Image Trace presets leave the panel as it was, the Image Trace button opens it; batch exporting, #1935; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 45/57, 12 remaining (8–14 h); G5: Symbol Options Export Type (Graphic / Movie Clip); G1: Smart Guides line extension, #2120; G16: Brazilian Portuguese catalog complete under the tests, #1914; Localization 71% → 74%, hours 55–90 → 50–85; G6: image variables landed, estimate 8–14 → 6–12 h; Warp Options controls verified by focused checks and original-image review; estimates unchanged; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; G13: Object › Intertwine landed, #1763; no entry closed, estimates unchanged; MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged; Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged.; G1: Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; G9: legacy `.ai` of version 7 saved with type as text and hidden objects kept, legacy type read, #868; estimate unchanged; Revolve: automatic smooth shading and default-on Cap with continuous rims; later 2026-10-14: G8 untrusted files, #1852, #1862–#1873; G11 anchor X/Y/W/H, #1824, Snap to Grid over Snap to Pixel, #1816, redo during a drag, #1719, Dim Images, #1683, failed-save dialog, #1685; gaps noted for reporters: G1 Warp Options for Effect › Warp, #1760, Color Picker Tab order, #1692, key anchor, #1636; G7 custom toolbars, #1759, workspace panel widths, #1753; G11 Shape Builder gaps between closed shapes, #1691, interior anchor delete; G13 Intertwine, #1763, gradient mesh from an image, #1676, Image Trace radial gradients, #1849; G12 duplicated line removed; no entry closed, estimates unchanged; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 fixes: G11 anchor Align and Distribute, #1636, Delete and selection, #1674, #1666, #1743, #1686, Roughen and Scribble, #1643, #1648; G8 file and input hardening, #1776–#1783, #1786, #1807; G10 Wayland cursors and the Windows title bar, unconfirmed, #1645, #1758; no entry closed, estimates unchanged; later 2026-10-13 fixes, #1570, #1578, #1590, #1613, #1615, #1616, #1620 in part, #1602 in part; no entry closed, estimates unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; G12: Identify Objects By acts, #394; ~16 → ~15 prefs still inert; G12: Appearance of Black › Printing / Exporting acts, #394; ~16 → ~15 prefs still inert; control transport deadlines, #1726; G1: Average and Join in one step with a corner or smooth choice added as missing (it was listed as done in ui-parity.md); Join after Average, #1533; guide objects, #1534; Keyboard Shortcuts, #1539; G9 and G11 fixes, #1528, #1530, #1544, #1553, #1512, #1537; estimates unchanged; Direct Selection anchor precedence, #1508; dialog slider rails, #1483; template layers in Outline view, #1486; Tabs panel stop drags, #1458; type that does not fit its frame is reported, #1471; 3D on outlined type, #1481, Twist range, #1524, twisted walls, #1525; print-size images, #1517; French catalog, #1511; estimates unchanged; G1: Split Into Grid options, #1462, Transform Objects and Reflect's angle, #1448 and #1460, Pathfinder on a group, #1450, value fields, #1457 and #1465 in part; G9: grayscale TIFFs with alpha, #1466 in part; G6: unknown parameter keys refused, #1442; G10: window surfaces past 8192 px, #1459 in part; G15: many-object edits, #1429 in part; estimates unchanged; G16: Vietnamese catalog added, complete under the tests; three key languages remain, 60–100 → 55–90 h; G3: Extrude & Bevel, Inflate and Rotate added, 45–75 → 25–45 h; G9: `.ai` compound path paint, #1410, overprint kept on import, #1388, text-only PDF parts, #1387; G1: Transform dialogs preview, #1412, transforms of direct-selected anchors, #1408, corner drags, #1229; G11: strokes wider than their shape, #1396; estimates unchanged; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; G9: `.ai` gradient matrices and meshes from the editing data; estimate unchanged; G9: `.ai` art styles from the editing data; estimate unchanged; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; G9: `.ai` gradient matrices and meshes from the editing data; estimate unchanged; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; SVG knockout groups of opaque art export without masks; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; G9: `.ai` keeps its RGB numbers, PDF import keeps strokes apart from fills faded or masked by their group; estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) Every known shortfall against Illustrator, one entry each, **ranked by how much it stops a professional from switching**. This is the work list: unless you were given a task, pick the highest-ranked gap you can make @@ -331,6 +331,12 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall extensions). It is given the document as the native file's JSON and its parameters, with a larger budget than a filter's, which Background Export's Cancel stops; the host writes the file it returns, and nothing when it declines or returns bad data. Engine, ABI and headless Export As regressions. +- **Done:** a plug-in's parameters say how its dialogs show them: `label`, `unit`, `help`, `helpUrl`, `group` + (sections that fold) and `visibleWhen` (shown only while the checkboxes and choices it names have one of the + listed values). `string` and `numberList` parameters join the others, and a plug-in declares up to 256, at most 64 + outside groups. The filter, live-effect and exporter dialogs use them, and the Appearance panel's effect editor the + labels, units, help and conditions; hosts from before the keys ignore them. Manifest unit tests and kittest dialog + regressions. - **Estimate:** 20–45 h for the APIs; the ecosystem itself isn't agent work. ## G18 Generative AI features @@ -351,6 +357,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall | Date | Change | Summary | |---|---|---| +| 2026-10-19 | minor | G17: plug-in parameters with labels, units, help, groups and conditions (`visibleWhen`), `string` and `numberList` types, up to 256 per plug-in; manifest and kittest dialog regressions. No entry closed, estimates unchanged. | | 2026-10-19 | minor | G17: exporter plug-ins, plug-in file formats that Export As and `document.export` write; engine, ABI and headless Export As regressions. No entry closed, estimates unchanged. | | 2026-10-19 | minor | Cache status-readout sample widths per UI, language, document unit and pixels per point; regressions check reuse across live frames and refresh after display-setting changes. Scores and estimates unchanged. | | 2026-10-19 | minor | Status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates. Contributor original-file acceptance confirmed; scores and estimates unchanged. | diff --git a/docs/plugins.md b/docs/plugins.md index c290c63cc..823ee1013 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -39,9 +39,10 @@ its extensions that no built-in format has; `selectedOnly` hands it the selectio In the app, installed filters appear under **Object › Plug-ins**, next to **Install Plug-in…** and **Reload Plug-ins**; live effects under **Effect › Plug-ins** and in the Appearance panel's effect menu. A plug-in with -parameters gets a dialog generated from its schema (numbers, whole numbers, checkboxes, dropdowns) with a live -preview; without parameters it runs at once. Parameters too many for the window's height scroll inside the dialog, -which keeps its heading and buttons in view. Exporters are formats of **File › Export › Export As**, where Use +parameters gets a dialog generated from its schema (numbers, whole numbers, checkboxes, dropdowns, text and lists +of numbers, labelled, explained and sorted into sections as its manifest says) with a live preview; without +parameters it runs at once. Parameters too many for the window's height scroll inside the dialog, which keeps its +heading and buttons in view. Exporters are formats of **File › Export › Export As**, where Use Artboards is greyed out (the plug-in writes the whole document); one with parameters asks for them in a dialog of the same kind once the file is picked, and its Export button writes the file. **File › Open** of a `.wasm` file installs a plug-in too (on the web this is how plug-ins get in). @@ -111,10 +112,11 @@ pointers are offsets into the module's memory. "author": "Example", "description": "What it does.", "params": { - "amount": {"type": "number", "min": 0, "max": 100, "default": 50}, - "steps": {"type": "int", "min": 1, "max": 16, "default": 4}, + "amount": {"type": "number", "min": 0, "max": 100, "default": 50, "unit": "%"}, + "steps": {"type": "int", "min": 1, "max": 16, "default": 4, "label": "Steps per curve"}, "closed": {"type": "bool", "default": false}, - "mode": {"type": "choice", "options": ["soft", "hard"], "default": "soft"} + "mode": {"type": "choice", "options": ["soft", "hard"], "default": "soft", + "help": "Soft keeps the corners round; hard keeps them sharp."} } } ``` @@ -130,7 +132,28 @@ pointers are offsets into the module's memory. - `params`: in dialog order. Names are `[A-Za-z0-9_]`, not starting with `_`, and not `id`, `ids`, `params` or `preview`. The host validates the user's values before calling: missing values take the default, numbers are clamped to `min..max` (ints rounded), a wrong type or an unknown choice is an error. Numeric defaults - are also clamped to their range, including the implicit zero default of an `int` parameter. + are also clamped to their range, including the implicit zero default of an `int` parameter. Besides `number`, + `int`, `bool` and `choice` there are `string` (`{"type": "string", "default": "", "maxLength": 256}`: a text field; + longer text is cut to `maxLength`, at most 4,096 characters) and `numberList` (`{"type": "numberList", "min": 0, + "max": 360, "default": [45, 135], "minItems": 1, "maxItems": 16}`: numbers typed separated by commas or spaces, + each clamped to `min..max`; a list of too few or too many is an error, and `maxItems` is at most 256). A plug-in + declares up to 256 parameters, at most 64 of them outside groups. + +Each parameter may also say how dialogs show it. Hosts from before these keys ignore them, so a plug-in that +declares them still installs there: + +| Key | What the dialog does with it | +|---|---| +| `label` | Labels the field (1–64 characters). Without one, the label is the name spaced out (`rowSpacing` → Row Spacing). | +| `unit` | Shows the unit the value is in after it (1–16 characters, such as `mm` or `°`). Nothing is converted: the plug-in gets the number as typed. | +| `group` | Puts the parameter in a section that folds (1–64 characters). Parameters outside any group come first, then each group in the order it first appears. | +| `help` | Shows what the parameter does when the pointer rests on its label (1–1,000 characters; line breaks allowed). | +| `helpUrl` | Opens this `https://` page (at most 512 bytes) from a button after the field. | +| `visibleWhen` | `{"pattern": ["lines", "crosshatch"], "outline": [true]}` shows the parameter only while each parameter it names (a checkbox or a choice declared before it) has one of the values listed and is shown itself. | + +A hidden parameter still goes to the plug-in with its value: what the dialog showed never changes a run. The +filter, live-effect and exporter dialogs use all of these keys. The Appearance panel's editor of a plug-in effect's +parameters uses `label`, `unit`, `help` and `visibleWhen`, and lists the parameters without sections or help buttons. ### Input diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index cafe4c0a8..7161851f2 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-10-20 · **Change:** minor (exporter plug-ins: plug-in file formats in Export As and document.export, G17; G2: Effect › Artistic › Smudge Stick, Sponge and Watercolor; Sketch › Bas Relief, Charcoal, Photocopy and Stamp, #2533; Photoshop filters 45/57, 12 remaining (8–14 h); interactive Mirror Repeat axis; bounding-box handles take priority over the axis body while the center and end handles retain their move and rotation actions; Libraries panel: the list scrolls, items and groups reorder by dragging, items drag into and out of groups (`library.moveItem` before/after, `library.moveGroup`); scores unchanged; PDF invisible text opens as type with no fill or stroke and is written back invisible, #2675; status-readout sample widths cached by language, document unit and pixels per point; status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; cached canvas reprojection preserves scale during resize, rotation and DPI changes; interactive Mirror Repeat axis; bounding-box handles take priority over the axis body while the center and end handles retain their move and rotation actions; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged; Artboard Options' X and Y with a reference point, #1876; Image Trace: Auto Color keeps rare but distinct colours; Image Trace: large images trace with a warning; anchor limit raised to 8,000,000; Image Trace: Raster::new returns an error for a buffer of the wrong length; M12 maximal-disc graph pruning and junction merging measured; M12 crop EDT and Guo–Hall thinning measured; M12 centre-lines baseline measured; scores and hours unchanged; installed plug-ins stay installed after a restart, #2541; Group Selection picks and moves one path of a compound path, #2441 in part; the Move dialog opens on the last move and its last checkboxes; Image Trace: a higher Paths never gives fewer segments on collapsed loops; Image Trace: the notch rule reads only its own piece; Image Trace: an edge's sub-pixel shift is the same from either side; Image Trace Flat Logo traces line art on white paper instead of a blank result; score and hours unchanged; Image Trace: a simpler contour walk with no precomputed direction table; transparency grid visibility saved in native documents, #1968; MCP cancellation, progress and busy answers during long exports, #2165, #2171, #2172; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; native family-name fallback from English-only typographic names, #2208; scores unchanged; batch exporting, #1935; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 45/57, 12 remaining (8–14 h); G5: Symbol Options Export Type (Graphic / Movie Clip); Localization: Brazilian Portuguese complete under the tests, #1914, 71% → 74%; G6 image variables: Automation ~60% → ~62% / detail ~65% → ~68%, hours lowered; Warp Options controls verified by focused checks and original-image review; estimates unchanged; G11: Asset Export Art Optimized seam (#983 / #1037) and supersampled thumbs; Export for Screens 85% → 90%; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; later 2026-10-14 merges: anchor X/Y/W/H, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar and All Tools, #1751; Actions panel selection, #1629 in part; redo during a drag, #1719; preferences written on OK, #1747; type in symbols after reopening, #1738; macOS save sheets, #1855, the Format menu withdrawn; stroke outlines shared, #1667; untrusted files, #1852, #1862–#1873; failed-save dialog, #1685; effects, #1864, Dim Images, #1683; duplicated rows of the 2026-10-05 table noted for the owner; scores unchanged; Image Trace: Shapes rework, stroke caps, enhanced presets, gradients over noise, measured in `image-trace-quality.md`, score unchanged; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 merges: anchor Align and Distribute, #1636; Direct Selection and Delete fixes, #1674, #1666, #1743; Escape while typing, #1686; Pen clicks, #1663; Expand on repeats, #1756; Roughen and Scribble, #1643, #1648; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace levelling and Flat Logo on scans, #1647, #1657; scores unchanged; later 2026-10-13 merges: SVG import, #1565, #1596, #1568, #1598, #1601; type on a path from PDF and `.ai`, #1585, #1586; ICC tag alignment, #1584; saves, #1620 in part; Warp Arc, #1578; Zig Zag, #1558; Pathfinder strokes, #1570; OpenType features, #1607; intersect Smart Guides, #1602 in part; a guide's position in the Transform panel, #1597 in part, a convenience with no parity claim; scores unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; Units › Identify Objects By (`identifyObjectsBy`) acts on Layers, Variables and Layer Options, #394, scores unchanged; Appearance of Black › Printing / Exporting (`blackOutput`) acts on RGB raster export and Print as Bitmap, #394, scores unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; Color panel: the mode picked last comes back after a restart, #1675; Image Trace: shared edges, memory, thin lines, strokes, corners, and the Shapes, Auto Grouping and Gradients options, measured in `image-trace-quality.md`, score unchanged; 2026-10-13 merges: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes for `.ai`, PDF, EPS and SVG, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; correction: Average and Join with a corner or smooth choice is missing; scores unchanged; late 2026-10-12 merges: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; 3D on outlined type, #1481; French catalog, #1511; scores unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; Localization: Vietnamese catalog added, 63% → 71%, hours 60–100 → 55–90, full readiness unchanged at ~62%; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; Image Trace: colour presets no longer shatter noisy flat art and photos trace into smooth tones, measured in `image-trace-quality.md`, score unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged; Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged.; Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; File interop: legacy `.ai` of version 7 saved with type as text, hidden objects kept and an artboard picker, #868, scores unchanged; Revolve: automatic smooth shading and default-on Cap with continuous rims; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; later 2026-10-14 merges: anchor X/Y/W/H, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar and All Tools, #1751; Actions panel selection, #1629 in part; redo during a drag, #1719; preferences written on OK, #1747; type in symbols after reopening, #1738; macOS save sheets, #1855, the Format menu withdrawn; stroke outlines shared, #1667; untrusted files, #1852, #1862–#1873; failed-save dialog, #1685; effects, #1864, Dim Images, #1683; duplicated rows of the 2026-10-05 table noted for the owner; scores unchanged; Image Trace: Shapes rework, stroke caps, enhanced presets, gradients over noise, measured in `image-trace-quality.md`, score unchanged; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 merges: anchor Align and Distribute, #1636; Direct Selection and Delete fixes, #1674, #1666, #1743; Escape while typing, #1686; Pen clicks, #1663; Expand on repeats, #1756; Roughen and Scribble, #1643, #1648; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace levelling and Flat Logo on scans, #1647, #1657; scores unchanged; later 2026-10-13 merges: SVG import, #1565, #1596, #1568, #1598, #1601; type on a path from PDF and `.ai`, #1585, #1586; ICC tag alignment, #1584; saves, #1620 in part; Warp Arc, #1578; Zig Zag, #1558; Pathfinder strokes, #1570; OpenType features, #1607; intersect Smart Guides, #1602 in part; a guide's position in the Transform panel, #1597 in part, a convenience with no parity claim; scores unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; Units › Identify Objects By (`identifyObjectsBy`) acts on Layers, Variables and Layer Options, #394, scores unchanged; Appearance of Black › Printing / Exporting (`blackOutput`) acts on RGB raster export and Print as Bitmap, #394, scores unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; Color panel: the mode picked last comes back after a restart, #1675; Image Trace: shared edges, memory, thin lines, strokes, corners, and the Shapes, Auto Grouping and Gradients options, measured in `image-trace-quality.md`, score unchanged; 2026-10-13 merges: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes for `.ai`, PDF, EPS and SVG, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; correction: Average and Join with a corner or smooth choice is missing; scores unchanged; late 2026-10-12 merges: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; 3D on outlined type, #1481; French catalog, #1511; scores unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; Localization: Vietnamese catalog added, 63% → 71%, hours 60–100 → 55–90, full readiness unchanged at ~62%; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; Image Trace: colour presets no longer shatter noisy flat art and photos trace into smooth tones, measured in `image-trace-quality.md`, score unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-20 · **Change:** minor (plug-in parameters: labels, units, groups, help and conditions in their dialogs, text and number-list parameters, up to 256 of them, G17; exporter plug-ins: plug-in file formats in Export As and document.export, G17; G2: Effect › Artistic › Smudge Stick, Sponge and Watercolor; Sketch › Bas Relief, Charcoal, Photocopy and Stamp, #2533; Photoshop filters 45/57, 12 remaining (8–14 h); interactive Mirror Repeat axis; bounding-box handles take priority over the axis body while the center and end handles retain their move and rotation actions; Libraries panel: the list scrolls, items and groups reorder by dragging, items drag into and out of groups (`library.moveItem` before/after, `library.moveGroup`); scores unchanged; PDF invisible text opens as type with no fill or stroke and is written back invisible, #2675; status-readout sample widths cached by language, document unit and pixels per point; status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; cached canvas reprojection preserves scale during resize, rotation and DPI changes; interactive Mirror Repeat axis; bounding-box handles take priority over the axis body while the center and end handles retain their move and rotation actions; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged; Artboard Options' X and Y with a reference point, #1876; Image Trace: Auto Color keeps rare but distinct colours; Image Trace: large images trace with a warning; anchor limit raised to 8,000,000; Image Trace: Raster::new returns an error for a buffer of the wrong length; M12 maximal-disc graph pruning and junction merging measured; M12 crop EDT and Guo–Hall thinning measured; M12 centre-lines baseline measured; scores and hours unchanged; installed plug-ins stay installed after a restart, #2541; Group Selection picks and moves one path of a compound path, #2441 in part; the Move dialog opens on the last move and its last checkboxes; Image Trace: a higher Paths never gives fewer segments on collapsed loops; Image Trace: the notch rule reads only its own piece; Image Trace: an edge's sub-pixel shift is the same from either side; Image Trace Flat Logo traces line art on white paper instead of a blank result; score and hours unchanged; Image Trace: a simpler contour walk with no precomputed direction table; transparency grid visibility saved in native documents, #1968; MCP cancellation, progress and busy answers during long exports, #2165, #2171, #2172; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; native family-name fallback from English-only typographic names, #2208; scores unchanged; batch exporting, #1935; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 45/57, 12 remaining (8–14 h); G5: Symbol Options Export Type (Graphic / Movie Clip); Localization: Brazilian Portuguese complete under the tests, #1914, 71% → 74%; G6 image variables: Automation ~60% → ~62% / detail ~65% → ~68%, hours lowered; Warp Options controls verified by focused checks and original-image review; estimates unchanged; G11: Asset Export Art Optimized seam (#983 / #1037) and supersampled thumbs; Export for Screens 85% → 90%; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; later 2026-10-14 merges: anchor X/Y/W/H, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar and All Tools, #1751; Actions panel selection, #1629 in part; redo during a drag, #1719; preferences written on OK, #1747; type in symbols after reopening, #1738; macOS save sheets, #1855, the Format menu withdrawn; stroke outlines shared, #1667; untrusted files, #1852, #1862–#1873; failed-save dialog, #1685; effects, #1864, Dim Images, #1683; duplicated rows of the 2026-10-05 table noted for the owner; scores unchanged; Image Trace: Shapes rework, stroke caps, enhanced presets, gradients over noise, measured in `image-trace-quality.md`, score unchanged; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 merges: anchor Align and Distribute, #1636; Direct Selection and Delete fixes, #1674, #1666, #1743; Escape while typing, #1686; Pen clicks, #1663; Expand on repeats, #1756; Roughen and Scribble, #1643, #1648; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace levelling and Flat Logo on scans, #1647, #1657; scores unchanged; later 2026-10-13 merges: SVG import, #1565, #1596, #1568, #1598, #1601; type on a path from PDF and `.ai`, #1585, #1586; ICC tag alignment, #1584; saves, #1620 in part; Warp Arc, #1578; Zig Zag, #1558; Pathfinder strokes, #1570; OpenType features, #1607; intersect Smart Guides, #1602 in part; a guide's position in the Transform panel, #1597 in part, a convenience with no parity claim; scores unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; Units › Identify Objects By (`identifyObjectsBy`) acts on Layers, Variables and Layer Options, #394, scores unchanged; Appearance of Black › Printing / Exporting (`blackOutput`) acts on RGB raster export and Print as Bitmap, #394, scores unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; Color panel: the mode picked last comes back after a restart, #1675; Image Trace: shared edges, memory, thin lines, strokes, corners, and the Shapes, Auto Grouping and Gradients options, measured in `image-trace-quality.md`, score unchanged; 2026-10-13 merges: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes for `.ai`, PDF, EPS and SVG, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; correction: Average and Join with a corner or smooth choice is missing; scores unchanged; late 2026-10-12 merges: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; 3D on outlined type, #1481; French catalog, #1511; scores unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; Localization: Vietnamese catalog added, 63% → 71%, hours 60–100 → 55–90, full readiness unchanged at ~62%; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; Image Trace: colour presets no longer shatter noisy flat art and photos trace into smooth tones, measured in `image-trace-quality.md`, score unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged; Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged.; Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; File interop: legacy `.ai` of version 7 saved with type as text, hidden objects kept and an artboard picker, #868, scores unchanged; Revolve: automatic smooth shading and default-on Cap with continuous rims; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; later 2026-10-14 merges: anchor X/Y/W/H, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar and All Tools, #1751; Actions panel selection, #1629 in part; redo during a drag, #1719; preferences written on OK, #1747; type in symbols after reopening, #1738; macOS save sheets, #1855, the Format menu withdrawn; stroke outlines shared, #1667; untrusted files, #1852, #1862–#1873; failed-save dialog, #1685; effects, #1864, Dim Images, #1683; duplicated rows of the 2026-10-05 table noted for the owner; scores unchanged; Image Trace: Shapes rework, stroke caps, enhanced presets, gradients over noise, measured in `image-trace-quality.md`, score unchanged; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 merges: anchor Align and Distribute, #1636; Direct Selection and Delete fixes, #1674, #1666, #1743; Escape while typing, #1686; Pen clicks, #1663; Expand on repeats, #1756; Roughen and Scribble, #1643, #1648; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace levelling and Flat Logo on scans, #1647, #1657; scores unchanged; later 2026-10-13 merges: SVG import, #1565, #1596, #1568, #1598, #1601; type on a path from PDF and `.ai`, #1585, #1586; ICC tag alignment, #1584; saves, #1620 in part; Warp Arc, #1578; Zig Zag, #1558; Pathfinder strokes, #1570; OpenType features, #1607; intersect Smart Guides, #1602 in part; a guide's position in the Transform panel, #1597 in part, a convenience with no parity claim; scores unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; Units › Identify Objects By (`identifyObjectsBy`) acts on Layers, Variables and Layer Options, #394, scores unchanged; Appearance of Black › Printing / Exporting (`blackOutput`) acts on RGB raster export and Print as Bitmap, #394, scores unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; Color panel: the mode picked last comes back after a restart, #1675; Image Trace: shared edges, memory, thin lines, strokes, corners, and the Shapes, Auto Grouping and Gradients options, measured in `image-trace-quality.md`, score unchanged; 2026-10-13 merges: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes for `.ai`, PDF, EPS and SVG, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; correction: Average and Join with a corner or smooth choice is missing; scores unchanged; late 2026-10-12 merges: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; 3D on outlined type, #1481; French catalog, #1511; scores unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; Localization: Vietnamese catalog added, 63% → 71%, hours 60–100 → 55–90, full readiness unchanged at ~62%; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; Image Trace: colour presets no longer shatter noisy flat art and photos trace into smooth tones, measured in `image-trace-quality.md`, score unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 45/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) The authoritative parity assessment. [`ROADMAP.md`](../ROADMAP.md) summarizes it; [`gaps.md`](gaps.md) is the work list made from it. Deep areas have their own checklists: [UI and interaction](ui-parity.md), @@ -833,6 +833,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Exporter plug-ins (G17):** a WebAssembly plug-in of kind `exporter` adds a write-only file format. File › Export › Export As lists it after the built-in formats, with Use Artboards greyed out (the plug-in writes the whole document) and its parameters in a generated dialog once the file is picked; `document.export` writes it by its format id, `plugin.`, or by one of its extensions, and built-in formats keep theirs. The plug-in is given the native file's JSON (large images without their data) and `_context` (`mode`, `extension`, `name`), with 10 G instructions plus 4,000 per byte of input and 300 s, and Background Export's Cancel stops it between two slices of that budget. A decline or invalid output writes nothing; its warnings come back with the export's. Regressions: `crates/engine/tests/plugin_export.rs`, `crates/plugins/tests/abi.rs`, `crates/ui-egui/src/dialogs/tests_plugin_export.rs`. Scores and hours unchanged. +- **Plug-in parameters as their manifest presents them (G17):** each parameter may have a `label`, a `unit` shown after its value, `help` on its label and a `helpUrl` page opened from a button, a `group` (sections that fold, after the parameters outside any group) and a `visibleWhen` that shows it only while the checkboxes and choices it names have one of the listed values (a hidden parameter still goes to the plug-in). `string` and `numberList` parameters join number, int, bool and choice, and a plug-in declares up to 256 parameters, at most 64 outside groups. The filter, live-effect and exporter dialogs use these keys, and the Appearance panel's effect editor the labels, units, help and conditions; hosts from before them ignore them. Regressions: `crates/plugins/src/dialog.rs`, `crates/plugins/src/manifest.rs`, `crates/ui-egui/src/dialogs/tests_plugin_fields.rs`, `crates/ui-egui/src/panels/appearance.rs`. Scores and hours unchanged. + - **Plug-in effects after a plug-in changes:** when a plug-in is installed for the first time, installed again or removed (`plugin.install`, `plugin.reload`, `plugin.remove`, File › Open of a `.wasm` file, the Additional Plug-ins Folder preference), the next frame the canvas draws uses the plug-ins then installed for objects with a plug-in effect and their raster effects, for the blends, envelopes and repeats that show such objects, and for pattern tiles and symbol instances whose art holds one. - **Effect art after font and color settings changes:** when a font becomes available (fonts added, Refresh Font List, `text.rescanFonts`), the next frame the canvas draws outlines type in it where a geometry effect reshapes the type, a group that holds it or a symbol instance whose art holds it. When the color settings change (Edit › Color Settings, `edit.colorSettings`), the next frame shades objects with Revolve, Extrude & Bevel, Inflate or Rotate, and recolors objects and groups with a color adjustment, through the new settings. @@ -848,6 +850,7 @@ Evidence for the shared light Color: geometry tests in `tests_lighting`, effect | Date | Change | Summary | |---|---|---| +| 2026-10-19 | minor | Plug-ins: parameters with labels, units, help, groups and conditions in their dialogs, `string` and `numberList` types, up to 256 per plug-in (G17); manifest and kittest dialog regressions. Scores and hours unchanged. | | 2026-10-19 | minor | Plug-ins: exporter plug-ins add write-only file formats to Export As and `document.export` (G17); engine, ABI and headless Export As regressions. Scores and hours unchanged. | | 2026-10-19 | minor | Libraries panel: the list fills the panel and scrolls (also while a drag nears its edges); items drag before or after others of their kind and into or out of groups, and groups drag by their headers (`library.moveItem` `before`/`after`, `library.moveGroup`). Scores and estimates unchanged. | | 2026-10-19 | minor | Cache status-readout sample widths per UI, language, document unit and pixels per point; regressions check reuse across live frames and refresh after display-setting changes. Scores and estimates unchanged. |