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. |