From 24936cd006fabba3ffb9319c21188c6684df688a Mon Sep 17 00:00:00 2001 From: Louise Fox <2503492+KaboomFox@users.noreply.github.com> Date: Mon, 19 Oct 2026 18:34:39 +0000 Subject: [PATCH 1/2] Plug-ins: exporters write file formats of their own (G17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A plug-in could filter objects or reshape them as a live effect, but nothing it made could leave the app: pen plotters, cutting machines and laser cutters read formats VectorCraft doesn't write. An exporter is a third kind of plug-in ("kind": "exporter", with the file "extensions" it writes), in the same sandbox and with the same ABI v1 exports. - It adds a write-only format, plugin.. document.formats lists it with its plug-in id, and document.export writes it by that id, or by a path ending in one of its extensions that no built-in format has; "params" carries its parameters, checked against its manifest as a filter's are. - File › Export › Export As lists it after the built-in formats, with Use Artboards greyed out, since the plug-in writes the whole document. One with parameters asks for them in a dialog generated from its schema (pluginExport) once the file is picked. - The plug-in is given the native file's JSON (format::json_part: images of 8 KiB or more without their data) and _context {mode: "export", extension, name}. It returns {"dataBase64", "warnings"?} or {"error"}. The host writes the file, and nothing when the plug-in declines or returns bad data. - An export gets 10 G instructions plus 4,000 per byte and 300 s. Background Export's Cancel stops it between two slices of that budget (Error::Cancelled), and a cancelled live-effect run is not cached as a failure. - plugin.run and Object › Plug-ins say where an exporter is instead of calling it a live effect. engine/tests/plugin_export.rs reads back what an exporter is handed with wat::echo_exporter, an exporter whose file is its input or its parameters (its base64 encoder is written in WAT). The document it is given is document.save's JSON, without the large image's data, and its parameters arrive defaulted, clamped and with the file's name and extension. It also covers listing and removal, built-in extensions winning, selectedOnly, declines, bad base64, missing data, warnings, and Cancel stopping a plug-in that never returns. It is a test binary of its own: installed plug-ins are process-wide, and the format-list unit tests check exact lists. dialogs/tests_plugin_export.rs drives Export As headlessly; plugins/tests/abi.rs covers the budget, the parameters and the echo exporter; format/tests/native_v4.rs covers json_part. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un --- crates/engine/src/cmd/fileio/encode.rs | 29 +++ crates/engine/src/cmd/fileio/mod.rs | 61 ++++- crates/engine/src/cmd/plugin.rs | 26 ++- crates/engine/tests/plugin_export.rs | 217 ++++++++++++++++++ crates/format/src/lib.rs | 7 + crates/format/tests/native_v4.rs | 13 ++ crates/plugins/src/effect.rs | 7 +- crates/plugins/src/export.rs | 141 ++++++++++++ crates/plugins/src/lib.rs | 12 +- crates/plugins/src/manifest.rs | 58 +++++ crates/plugins/src/objects.rs | 19 +- crates/plugins/src/runtime.rs | 4 +- crates/plugins/src/wat.rs | 79 +++++++ crates/plugins/tests/abi.rs | 61 +++++ crates/ui-egui/src/dialogs/export_as.rs | 49 ++-- crates/ui-egui/src/dialogs/mod.rs | 3 + crates/ui-egui/src/dialogs/plugin.rs | 85 +++++-- .../src/dialogs/tests_plugin_export.rs | 116 ++++++++++ crates/ui-egui/src/i18n/bg.tsv | 1 + crates/ui-egui/src/i18n/cs.tsv | 2 + crates/ui-egui/src/i18n/da.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 2 + crates/ui-egui/src/i18n/el.tsv | 2 + crates/ui-egui/src/i18n/es.tsv | 2 + crates/ui-egui/src/i18n/fi.tsv | 2 + crates/ui-egui/src/i18n/fr.tsv | 2 + crates/ui-egui/src/i18n/hu.tsv | 2 + crates/ui-egui/src/i18n/id.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 2 + crates/ui-egui/src/i18n/ja.tsv | 2 + crates/ui-egui/src/i18n/ka.tsv | 2 + crates/ui-egui/src/i18n/ko.tsv | 2 + crates/ui-egui/src/i18n/nl.tsv | 1 + crates/ui-egui/src/i18n/pl.tsv | 2 + crates/ui-egui/src/i18n/pt-br.tsv | 2 + crates/ui-egui/src/i18n/ru.tsv | 2 + crates/ui-egui/src/i18n/sv.tsv | 2 + crates/ui-egui/src/i18n/uk.tsv | 2 + crates/ui-egui/src/i18n/vi.tsv | 2 + crates/ui-egui/src/i18n/zh-hans.tsv | 2 + crates/ui-egui/src/i18n/zh-hant.tsv | 2 + crates/ui-egui/src/io.rs | 8 +- docs/control-protocol.md | 4 +- docs/gaps.md | 13 +- docs/mcp.md | 5 +- docs/plugins.md | 55 ++++- docs/target-app-parity.md | 5 +- 47 files changed, 1051 insertions(+), 68 deletions(-) create mode 100644 crates/engine/tests/plugin_export.rs create mode 100644 crates/plugins/src/export.rs create mode 100644 crates/ui-egui/src/dialogs/tests_plugin_export.rs diff --git a/crates/engine/src/cmd/fileio/encode.rs b/crates/engine/src/cmd/fileio/encode.rs index 8dda63ee4..ae705ca48 100644 --- a/crates/engine/src/cmd/fileio/encode.rs +++ b/crates/engine/src/cmd/fileio/encode.rs @@ -411,6 +411,9 @@ pub fn encode_all(doc: &Document, format: &str, p: &Value) -> Result { /// [`encode_all`] of `doc` as it is. fn encode_all_of(doc: &Document, format: &str, p: &Value) -> Result { let f = super::writable(C, Some(format), None)?; + if let Some(plugin) = vectorcraft_plugins::export::by_format(f.id) { + return plugin_file(doc, &plugin, f, p); + } let failed = vectorcraft_render::decode_failures(); let mut enc = encode_files(doc, f, p)?; // Baking, flattening and drawing stop early when cancelled and leave a truncated result. @@ -432,6 +435,32 @@ fn encode_all_of(doc: &Document, format: &str, p: &Value) -> Result { Ok(enc) } +/// The file exporter plug-in `plugin` writes of `doc` in its format `f` +/// ([`vectorcraft_plugins::export`]). The plug-in is given the native file's JSON +/// ([`vectorcraft_format::json_part`]) and `p`'s `params`, for the file `p`'s `path` names (else +/// one named after the document) with that path's extension when the plug-in lists it, else its +/// first. Its warnings come back with the file. +fn plugin_file(doc: &Document, plugin: &vectorcraft_plugins::Plugin, f: &Format, p: &Value) -> Result { + use vectorcraft_plugins::{Error, export}; + let doc = &*doc.without_edit_modes(); + let file = vectorcraft_format::save_with(doc, &vectorcraft_format::SaveOptions::default()).map_err(|e| EngineError::Other(e.to_string()))?; + let path = p.get("path").and_then(Value::as_str); + let listed = path.map(super::extension).and_then(|e| f.extensions.iter().copied().find(|x| *x == e)); + let extension = listed.or(f.extensions.first().copied()).unwrap_or_default(); + let name = super::file_stem(path.unwrap_or(&super::default_name(doc, extension))); + let params = p.get("params").cloned().unwrap_or(Value::Null); + let target = export::Target { extension, name: &name }; + let out = export::run(plugin, vectorcraft_format::json_part(&file), ¶ms, target).map_err(|e| match e { + Error::Cancelled => EngineError::Cancelled, + Error::Params(why) => bad(C, why), + e => EngineError::Other(e.to_string()), + })?; + // The plug-in's output, so reported as its other output errors are. + let invalid = || EngineError::Other(Error::Output("`dataBase64` is not valid base64".into()).to_string()); + let bytes = vectorcraft_format::base64_decode(&out.data_base64).ok_or_else(invalid)?; + Ok(Encoded { warnings: out.warnings, ..Encoded::one(bytes) }) +} + /// [`encode_all`] without the font substitution warning. fn encode_files(doc: &Document, f: &Format, p: &Value) -> Result { let doc = &*doc.without_edit_modes(); diff --git a/crates/engine/src/cmd/fileio/mod.rs b/crates/engine/src/cmd/fileio/mod.rs index 7aba9471e..e5acf01a1 100644 --- a/crates/engine/src/cmd/fileio/mod.rs +++ b/crates/engine/src/cmd/fileio/mod.rs @@ -126,7 +126,7 @@ pub fn specs() -> Vec { "Export Document", [], None, - "{path?, format?: svg|svgz|pdf|ai|png|jpg|webp|gif|png8|txt|dxf|eps|emf|wmf|tiff|bmp|tga|psd|vectorcraft|template (default: from the path's extension, else png; png8 writes an indexed .png; ait, a .ait template, is written by file.saveAsTemplate), selectedOnly?: false (the selected objects alone, in their layers), artboard?: 0, artboards?: [i…], rect?: [x, y, width, height] (raster formats: that area of the page in points, y down, instead of an artboard; not with artboard, artboards, range or useArtboards), range?: \"1-3, 5\" | \"all\" (1-based; PDF and .ai write one page per artboard, default all; SVG writes one file per artboard, {stem}-{artboard}.svg; raster formats write one artboard), useArtboards?: true (raster: one file per chosen artboard, default all, {stem}-{artboard}.{ext}; pdf/ai: every page) | false (pdf/ai/raster: the bounds of the visible art; SVG has it as an SVG option), ppi?: 72 (raster formats: pixels per inch, stored in the file; wins over scale), scale?: 1 (pixels per point), background?: transparent|white|black|\"#rrggbb\" (jpg: white when transparent), antiAlias?: none|art (default)|type (text snapped to pixels), interlaced?: false (png, Adam7), embedIcc?: true (png: an sRGB chunk marks the colours as sRGB), quality?: 90 (jpg: 0–100), colorModel?: rgb|cmyk|gray (jpg), method?: baseline|optimized|progressive, scans?: 3 (3–5, progressive), embedIcc?: true, imageMap?: none|client|server (an HTML or NCSA map of the objects with a URL, written as .html / .map), colors?: 256 (gif/png8: 2–256), reduction?: perceptual|selective (default)|adaptive|web|blackWhite|gray, dither?: none|diffusion (default)|pattern|noise, ditherAmount?: 100, transparency?: true, matte?: white|\"#rrggbb\"|none, interlaced?, lossless?: true (webp: lossy WebP isn't available yet: written lossless, with a warning), encoding?: utf8|utf16 (txt: the stories in stacking order, back to front, a thread once; utf16 with a byte order mark), lineEndings?: lf|crlf, selectionOnly?: false, svg?: {styling, outlineText, images, objectIds, decimals, minify, responsive, useArtboards, preserveEditing, metadata, fewerTspans, hiddenLayers, encoding, profile, embedFonts} (see document.formats), …the SVG options given flat instead, …the PDF options of document.exportPdf, …the DXF options of document.exportDxf (useArtboards: one drawing per artboard), …the EPS options of document.exportEps (useArtboards: one file per artboard, {stem}_{artboard}.eps; else the visible art), …emf and wmf write one picture of the artboard (useArtboards: true one file per chosen artboard, false the bounds of the visible art; EMF keeps curves, clipping, transparent images and gradients as images clipped to their shape; WMF flattens curves into polygons behind a placeable header; what a format leaves out comes back in warnings), colorModel?: rgb|cmyk|gray (tiff: rgb keeps transparency as an alpha channel), lzw?: true, byteOrder?: little|big, embedIcc?: true, colorModel?: rgb|gray (bmp), depth?: 24 (bmp: 1 black and white, 4|8 a palette by reduction and dither, 16, 24, 32 with alpha), fileFormat?: windows|os2 (1, 4, 8 or 24 bits), rle?: false (4 and 8 bits, windows), flipRows?: false (top-down rows), depth?: 24 (tga: 16 one-bit alpha, 24, 32 with alpha), colorModel?: rgb|cmyk|gray (psd), layers?: true (each top-level layer a pixel layer with its opacity and blend mode; a background colour is a Background layer; false: one flat image, on white), maxEditability?: false (layers and sublayers become groups, each object a layer named as in the Layers panel, text by its text; clipping, masked or knockout layers stay one layer), hiddenLayers?: false (hidden layers and objects written hidden instead of left out), embedIcc?: true (at most 30000 pixels a side; formats without alpha are flattened on white)} → {path, format, bytes, warnings, files?: [path…] (several), linked?: [path…] (linked images, image maps)}; no path → {dataBase64, format, bytes, warnings, files?: [{name, dataBase64}], linked?: [{name, dataBase64}]}. Never changes the document's path", + "{path?, format?: svg|svgz|pdf|ai|png|jpg|webp|gif|png8|txt|dxf|eps|emf|wmf|tiff|bmp|tga|psd|vectorcraft|template (default: from the path's extension, else png; png8 writes an indexed .png; ait, a .ait template, is written by file.saveAsTemplate; an exporter plug-in's format is plugin., which an extension only it lists picks too), params?: {…} (an exporter plug-in's parameters; missing ones take their defaults, see plugin.info), selectedOnly?: false (the selected objects alone, in their layers), artboard?: 0, artboards?: [i…], rect?: [x, y, width, height] (raster formats: that area of the page in points, y down, instead of an artboard; not with artboard, artboards, range or useArtboards), range?: \"1-3, 5\" | \"all\" (1-based; PDF and .ai write one page per artboard, default all; SVG writes one file per artboard, {stem}-{artboard}.svg; raster formats write one artboard), useArtboards?: true (raster: one file per chosen artboard, default all, {stem}-{artboard}.{ext}; pdf/ai: every page) | false (pdf/ai/raster: the bounds of the visible art; SVG has it as an SVG option), ppi?: 72 (raster formats: pixels per inch, stored in the file; wins over scale), scale?: 1 (pixels per point), background?: transparent|white|black|\"#rrggbb\" (jpg: white when transparent), antiAlias?: none|art (default)|type (text snapped to pixels), interlaced?: false (png, Adam7), embedIcc?: true (png: an sRGB chunk marks the colours as sRGB), quality?: 90 (jpg: 0–100), colorModel?: rgb|cmyk|gray (jpg), method?: baseline|optimized|progressive, scans?: 3 (3–5, progressive), embedIcc?: true, imageMap?: none|client|server (an HTML or NCSA map of the objects with a URL, written as .html / .map), colors?: 256 (gif/png8: 2–256), reduction?: perceptual|selective (default)|adaptive|web|blackWhite|gray, dither?: none|diffusion (default)|pattern|noise, ditherAmount?: 100, transparency?: true, matte?: white|\"#rrggbb\"|none, interlaced?, lossless?: true (webp: lossy WebP isn't available yet: written lossless, with a warning), encoding?: utf8|utf16 (txt: the stories in stacking order, back to front, a thread once; utf16 with a byte order mark), lineEndings?: lf|crlf, selectionOnly?: false, svg?: {styling, outlineText, images, objectIds, decimals, minify, responsive, useArtboards, preserveEditing, metadata, fewerTspans, hiddenLayers, encoding, profile, embedFonts} (see document.formats), …the SVG options given flat instead, …the PDF options of document.exportPdf, …the DXF options of document.exportDxf (useArtboards: one drawing per artboard), …the EPS options of document.exportEps (useArtboards: one file per artboard, {stem}_{artboard}.eps; else the visible art), …emf and wmf write one picture of the artboard (useArtboards: true one file per chosen artboard, false the bounds of the visible art; EMF keeps curves, clipping, transparent images and gradients as images clipped to their shape; WMF flattens curves into polygons behind a placeable header; what a format leaves out comes back in warnings), colorModel?: rgb|cmyk|gray (tiff: rgb keeps transparency as an alpha channel), lzw?: true, byteOrder?: little|big, embedIcc?: true, colorModel?: rgb|gray (bmp), depth?: 24 (bmp: 1 black and white, 4|8 a palette by reduction and dither, 16, 24, 32 with alpha), fileFormat?: windows|os2 (1, 4, 8 or 24 bits), rle?: false (4 and 8 bits, windows), flipRows?: false (top-down rows), depth?: 24 (tga: 16 one-bit alpha, 24, 32 with alpha), colorModel?: rgb|cmyk|gray (psd), layers?: true (each top-level layer a pixel layer with its opacity and blend mode; a background colour is a Background layer; false: one flat image, on white), maxEditability?: false (layers and sublayers become groups, each object a layer named as in the Layers panel, text by its text; clipping, masked or knockout layers stay one layer), hiddenLayers?: false (hidden layers and objects written hidden instead of left out), embedIcc?: true (at most 30000 pixels a side; formats without alpha are flattened on white)} → {path, format, bytes, warnings, files?: [path…] (several), linked?: [path…] (linked images, image maps)}; no path → {dataBase64, format, bytes, warnings, files?: [{name, dataBase64}], linked?: [{name, dataBase64}]}. Never changes the document's path", has_doc, export::export ), @@ -198,7 +198,7 @@ pub fn specs() -> Vec { "File Formats", [], None, - "{} → {formats: [{id, label, extensions, mime, read, write, raster, options: {name: {type, default, description}}}], readable: [id…], writable: [id…], openExtensions: [ext…], unsupported: [{id, label, extensions, hint}] (formats asked for that can be neither opened nor written, such as DWG and PICT, with what to use instead)}", + "{} → {formats: [{id, label, extensions, mime, read, write, raster, options: {name: {type, default, description}}, plugin?: the id of the exporter plug-in that adds the format (its id is plugin.; see plugin.list)}], readable: [id…], writable: [id…], openExtensions: [ext…], unsupported: [{id, label, extensions, hint}] (formats asked for that can be neither opened nor written, such as DWG and PICT, with what to use instead)}", always, formats ), @@ -713,7 +713,8 @@ pub const UNASSOCIATED_EXTS: &[&str] = &["psd", "psb", "epsf", "ps", "tga", "vda /// `.png` with PNG, so `.png` comes once), in [`FORMATS`] order. pub fn export_extensions() -> Vec<&'static str> { let mut v: Vec<&'static str> = Vec::new(); - for e in FORMATS.iter().filter(|f| f.write).filter_map(|f| f.extensions.first()) { + let added = plugin_formats(); + for e in FORMATS.iter().chain(added.iter().copied()).filter(|f| f.write).filter_map(|f| f.extensions.first()) { if !v.contains(e) { v.push(e); } @@ -800,10 +801,47 @@ pub fn place_filters() -> impl Iterator Option<&'static Format> { let k = id_or_ext.trim_start_matches('.').to_ascii_lowercase(); - FORMATS.iter().find(|f| f.id == k).or_else(|| FORMATS.iter().find(|f| f.extensions.contains(&k.as_str()))) + FORMATS.iter().find(|f| f.id == k).or_else(|| FORMATS.iter().find(|f| f.extensions.contains(&k.as_str()))).or_else(|| { + let added = plugin_formats(); + added.iter().find(|f| f.id.eq_ignore_ascii_case(&k)).or_else(|| added.iter().find(|f| f.extensions.contains(&k.as_str()))).copied() + }) +} + +/// The formats the installed exporter plug-ins add ([`vectorcraft_plugins::export`]), by plug-in +/// id: write only, with the plug-in's name as the label and its extensions. Each is made once for +/// each installation of its plug-in, its strings interned, so formats live as long as the catalogues +/// that hold them. +pub fn plugin_formats() -> Vec<&'static Format> { + use vectorcraft_plugins::{export, registry}; + static MADE: std::sync::Mutex> = std::sync::Mutex::new(Vec::new()); + let mut made = MADE.lock().unwrap_or_else(|e| e.into_inner()); + export::installed() + .iter() + .map(|p| { + if let Some((_, f)) = made.iter().find(|(serial, _)| *serial == p.serial()) { + return *f; + } + let m = p.manifest(); + let extensions: Vec<&'static str> = m.extensions.iter().map(|e| registry::intern(e)).collect(); + let f: &'static Format = Box::leak(Box::new(Format { + id: registry::intern(&export::format_id(&m.id)), + label: registry::intern(&m.name), + extensions: Box::leak(extensions.into_boxed_slice()), + mime: "application/octet-stream", + read: false, + write: true, + raster: false, + options: &[], + })); + made.push((p.serial(), f)); + f + }) + .collect() } /// The lower-case extension of a file name or path (empty when it has none). @@ -858,9 +896,18 @@ fn writable(cmd: &str, format_param: Option<&str>, path: Option<&str>) -> Result } fn formats(_: &mut Session, _: &Value) -> Result { - let ids = |pick: fn(&Format) -> bool| FORMATS.iter().filter(|f| pick(f)).map(|f| f.id).collect::>(); + let added = plugin_formats(); + let all = || FORMATS.iter().chain(added.iter().copied()); + let ids = |pick: fn(&Format) -> bool| all().filter(|f| pick(f)).map(|f| f.id).collect::>(); + let listed = |f: &Format| { + let mut v = f.to_json(); + if let Some(id) = vectorcraft_plugins::export::plugin_id(f.id) { + v["plugin"] = json!(id); + } + v + }; Ok(json!({ - "formats": FORMATS.iter().map(Format::to_json).collect::>(), + "formats": all().map(listed).collect::>(), "readable": ids(|f| f.read), "writable": ids(|f| f.write), "openExtensions": OPEN_EXTS, diff --git a/crates/engine/src/cmd/plugin.rs b/crates/engine/src/cmd/plugin.rs index b38d62909..f7b62f950 100644 --- a/crates/engine/src/cmd/plugin.rs +++ b/crates/engine/src/cmd/plugin.rs @@ -5,12 +5,13 @@ //! preference, and listed with `plugin.list` / `plugin.info`. Object filters run //! with `plugin.run` on the selected paths and compound paths (one undo step, transactional: a //! plug-in that fails leaves the document untouched); live-effect plug-ins are applied like any -//! effect (`effect.apply {effect: "plugin."}`) and drawn by `vectorcraft-effects`. +//! effect (`effect.apply {effect: "plugin."}`) and drawn by `vectorcraft-effects`; exporters +//! add a file format that `document.export` writes (`fileio::plugin_formats`). use std::sync::{Arc, Mutex}; use serde_json::{Map, Value, json}; -use vectorcraft_plugins::{Kind, Plugin, effect, objects, registry}; +use vectorcraft_plugins::{Kind, Plugin, effect, export, objects, registry}; use super::*; @@ -27,8 +28,13 @@ pub fn describe(p: &Plugin) -> Value { "params": m.params_doc(), "paramsSchema": serde_json::to_value(m).ok().and_then(|v| v.get("params").cloned()), "size": p.size(), "source": p.source(), }); - if m.kind == Kind::Effect { - v["effect"] = json!(effect::effect_id(&m.id)); + match m.kind { + Kind::Effect => v["effect"] = json!(effect::effect_id(&m.id)), + Kind::Exporter => { + v["format"] = json!(export::format_id(&m.id)); + v["extensions"] = json!(m.extensions); + } + Kind::Filter => {} } v } @@ -56,8 +62,14 @@ fn run(s: &mut Session, p: &Value) -> Result { const C: &str = "plugin.run"; let plugin = plugin_by_id(C, p)?; let m = plugin.manifest(); - if m.kind != Kind::Filter { - return Err(bad(C, format!("`{}` is a live effect: apply it with effect.apply {{\"effect\": \"{}\"}}", m.id, effect::effect_id(&m.id)))); + match m.kind { + Kind::Filter => {} + Kind::Effect => { + return Err(bad(C, format!("`{}` is a live effect: apply it with effect.apply {{\"effect\": \"{}\"}}", m.id, effect::effect_id(&m.id)))); + } + Kind::Exporter => { + return Err(bad(C, format!("`{}` is an exporter: export with document.export {{\"format\": \"{}\"}}", m.id, export::format_id(&m.id)))); + } } // Bad parameters fail before anything runs. let mut params = Value::Object(m.resolve_params(&plugin_params(p)).map_err(|e| bad(C, e.to_string()))?); @@ -206,7 +218,7 @@ pub fn specs() -> Vec { "List Plug-ins", [], None, - "{} → {plugins: [{id, name, version, kind: \"filter\"|\"effect\", author, description, params (doc), paramsSchema, effect? (the effect id of a live effect: plugin.), size, source}], folder: the Additional Plug-ins Folder load report {folder, loaded, failed: [{file, error}]} | null}", + "{} → {plugins: [{id, name, version, kind: \"filter\"|\"effect\"|\"exporter\", author, description, params (doc), paramsSchema, effect? (the effect id of a live effect: plugin.), format? and extensions? (an exporter's format id, plugin., for document.export, and its file extensions), size, source}], folder: the Additional Plug-ins Folder load report {folder, loaded, failed: [{file, error}]} | null}", always, list ), diff --git a/crates/engine/tests/plugin_export.rs b/crates/engine/tests/plugin_export.rs new file mode 100644 index 000000000..868ec71e9 --- /dev/null +++ b/crates/engine/tests/plugin_export.rs @@ -0,0 +1,217 @@ +//! Exporter plug-ins (`"kind": "exporter"`, docs/plugins.md › Exporters) through the engine: the +//! formats they add to `document.formats` and `document.export`, the document and parameters they +//! are given, and what is written when they succeed, decline, misbehave or are cancelled. +//! +//! A test binary of its own: installed plug-ins are process-wide, and the unit tests that check the +//! exact list of formats would see these ones. +// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness). +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + +use std::path::PathBuf; +use std::time::{Duration, Instant}; + +use serde_json::{Value, json}; +use vectorcraft_engine::cmd::fileio; +use vectorcraft_engine::{EngineError, Session}; +use vectorcraft_plugins::wat::{self as tpl, Echo}; + +/// A document with one rectangle. +fn session() -> Session { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 400, "height": 200})).unwrap(); + s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 100, "height": 50})).unwrap(); + s +} + +/// Installs the plug-in written in WAT `src` → its `plugin.list` entry. +fn install(s: &mut Session, src: &str) -> Value { + let wasm = wat::parse_str(src).unwrap_or_else(|e| panic!("bad test WAT: {e}")); + s.execute("plugin.install", &json!({"dataBase64": vectorcraft_format::base64_encode(&wasm)})).unwrap() +} + +/// The file a `document.export` without a path gives back. +fn exported(r: &Value) -> Vec { + vectorcraft_format::base64_decode(r["dataBase64"].as_str().expect("dataBase64")).unwrap() +} + +/// A fresh folder for files this test writes. +fn tmp_dir(tag: &str) -> PathBuf { + let d = std::env::temp_dir().join(format!("vc-plugin-export-{tag}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&d); + std::fs::create_dir_all(&d).unwrap(); + d +} + +fn path_of(p: &std::path::Path) -> String { + p.to_string_lossy().into_owned() +} + +#[test] +fn an_exporter_adds_a_write_only_format_while_it_is_installed() { + let mut s = session(); + let entry = install(&mut s, &tpl::returning(&tpl::exporter("test.list", r#"["tsl","tsl2"]"#, "{}"), r#"{"dataBase64":""}"#)); + assert_eq!((&entry["kind"], &entry["format"], &entry["extensions"]), (&json!("exporter"), &json!("plugin.test.list"), &json!(["tsl", "tsl2"]))); + let formats = s.execute("document.formats", &json!({})).unwrap(); + let listed = formats["formats"].as_array().unwrap().iter().find(|f| f["id"] == "plugin.test.list").expect("listed"); + assert_eq!((&listed["label"], &listed["extensions"]), (&json!("Test test.list"), &json!(["tsl", "tsl2"]))); + assert_eq!((&listed["read"], &listed["write"], &listed["plugin"]), (&json!(false), &json!(true), &json!("test.list"))); + assert!(formats["writable"].as_array().unwrap().contains(&json!("plugin.test.list"))); + assert!(!formats["readable"].as_array().unwrap().contains(&json!("plugin.test.list"))); + assert!(fileio::export_extensions().contains(&"tsl")); + assert_eq!(fileio::format(".TSL2").map(|f| f.id), Some("plugin.test.list"), "by any of its extensions, in any case"); + assert_eq!(fileio::format("plugin.test.list").map(|f| f.label), Some("Test test.list")); + + s.execute("plugin.remove", &json!({"id": "test.list"})).unwrap(); + let formats = s.execute("document.formats", &json!({})).unwrap(); + assert!(!formats["writable"].as_array().unwrap().contains(&json!("plugin.test.list"))); + assert!(fileio::format("tsl").is_none()); + let e = s.execute("document.export", &json!({"format": "plugin.test.list"})).unwrap_err(); + assert!(e.to_string().contains("unknown format"), "{e}"); +} + +/// The exporter is given the document as a native file's JSON: what `document.save` writes before +/// the data of large images, which stays behind (compact, where a save of a small document is +/// indented). +#[test] +fn the_exporter_is_given_the_native_file_s_json() { + let mut s = session(); + install(&mut s, &tpl::echo_exporter(&tpl::exporter("test.input", r#"["tsi"]"#, "{}"), Echo::Input)); + // Noise, so the PNG is large enough to go after the JSON in a native file. + let mut seed = 1_u32; + let noise = image::RgbaImage::from_fn(64, 64, |_, _| { + seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12_345); + let [r, g, b, _] = seed.to_be_bytes(); + image::Rgba([r, g, b, 255]) + }); + let mut png = vec![]; + noise.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap(); + assert!(png.len() >= vectorcraft_format::INLINE_MAX, "{} bytes", png.len()); + s.execute("file.place", &json!({"name": "noise.png", "dataBase64": vectorcraft_format::base64_encode(&png)})).unwrap(); + + let r = s.execute("document.export", &json!({"format": "plugin.test.input"})).unwrap(); + assert_eq!(r["format"], "plugin.test.input"); + let given = exported(&r); + let saved = exported(&s.execute("document.save", &json!({})).unwrap()); + assert!(saved.len() > given.len() + png.len(), "the image data follows the JSON in the saved file"); + let doc: Value = serde_json::from_slice(&given).unwrap(); + let saved_json: Value = serde_json::from_slice(vectorcraft_format::json_part(&saved)).unwrap(); + assert_eq!(doc, saved_json, "the saved file's JSON"); + assert!(!given.contains(&b'\n'), "without indentation"); + assert_eq!((&doc["format"], &doc["version"]), (&json!("vectorcraft"), &json!(vectorcraft_format::VERSION))); + let images = doc["images"].as_object().unwrap(); + let image = images.values().next().unwrap(); + assert_eq!(images.len(), 1); + assert!(image.get("blob").is_some() && image.get("data").is_none(), "{image}"); +} + +#[test] +fn the_exporter_is_given_its_parameters_and_the_file_it_writes() { + let dir = tmp_dir("params"); + let mut s = session(); + let params = r#"{"density":{"type":"number","min":0,"max":10,"default":4},"mode":{"type":"choice","options":["fast","fine"],"default":"fast"}}"#; + install(&mut s, &tpl::echo_exporter(&tpl::exporter("test.params", r#"["tsp","tsq"]"#, params), Echo::Params)); + let sent = |r: &Value| -> Value { serde_json::from_slice(&exported(r)).unwrap() }; + + // The defaults, and without a path its first extension and the document's name. + let r = s.execute("document.export", &json!({"format": "plugin.test.params"})).unwrap(); + let got = sent(&r); + assert_eq!((&got["density"], &got["mode"]), (&json!(4.0), &json!("fast"))); + assert_eq!((&got["_context"]["mode"], &got["_context"]["extension"]), (&json!("export"), &json!("tsp"))); + let title = s.doc().unwrap().doc.title.clone(); + assert_eq!(got["_context"]["name"], json!(title)); + + // Given ones, clamped as a filter's are, and the path's extension when the exporter lists it. + let path = dir.join("Poster.tsq"); + let r = s.execute("document.export", &json!({"path": path_of(&path), "params": {"density": 12, "mode": "fine"}})).unwrap(); + assert_eq!(r["format"], "plugin.test.params"); + let written: Value = serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap(); + assert_eq!(written, json!({"density": 10.0, "mode": "fine", "_context": {"mode": "export", "extension": "tsq", "name": "Poster"}})); + + // Bad parameters fail before the plug-in runs, and nothing is written. + let bad = dir.join("Bad.tsp"); + let e = s.execute("document.export", &json!({"path": path_of(&bad), "params": {"mode": "slow"}})).unwrap_err(); + assert!(matches!(e, EngineError::BadParams { .. }), "{e}"); + assert!(!bad.exists()); + let _ = std::fs::remove_dir_all(&dir); +} + +/// Only the selected objects, with `selectedOnly`, as for the built-in formats. +#[test] +fn selected_only_hands_over_the_selection() { + let mut s = session(); + install(&mut s, &tpl::echo_exporter(&tpl::exporter("test.selection", r#"["tse"]"#, "{}"), Echo::Input)); + let ellipse = s.execute("shape.ellipse", &json!({"x": 200, "y": 10, "width": 50, "height": 50})).unwrap()["id"].clone(); + s.execute("select.set", &json!({"ids": [ellipse]})).unwrap(); + let count = |r: &Value| String::from_utf8(exported(r)).unwrap().matches("\"anchors\"").count(); + let all = count(&s.execute("document.export", &json!({"format": "plugin.test.selection"})).unwrap()); + let selected = count(&s.execute("document.export", &json!({"format": "plugin.test.selection", "selectedOnly": true})).unwrap()); + assert!(selected >= 1 && selected < all, "{selected} of {all} paths"); +} + +/// A built-in format keeps its extension: an exporter that lists `svg` too writes it only when +/// asked for by its format id. +#[test] +fn built_in_formats_keep_their_extensions() { + let dir = tmp_dir("builtin"); + let mut s = session(); + install(&mut s, &tpl::echo_exporter(&tpl::exporter("test.svgtoo", r#"["tsv","svg"]"#, "{}"), Echo::Params)); + assert_eq!(fileio::format("svg").map(|f| f.id), Some("svg")); + let svg = dir.join("a.svg"); + s.execute("document.export", &json!({"path": path_of(&svg)})).unwrap(); + assert!(String::from_utf8(std::fs::read(&svg).unwrap()).unwrap().contains("= wait && took < Duration::from_secs(10), "stopped while running, not before: {took:?}"); +} diff --git a/crates/format/src/lib.rs b/crates/format/src/lib.rs index 751d3487a..1317a3f45 100644 --- a/crates/format/src/lib.rs +++ b/crates/format/src/lib.rs @@ -163,6 +163,13 @@ fn preview_mime(bytes: &[u8]) -> &'static str { if bytes.starts_with(&[0xff, 0xd8]) { "image/jpeg" } else { vectorcraft_doc::links::PROXY_MIME } } +/// The JSON of a native file without the data after it (version 4 keeps images there): a +/// complete JSON value whose `images` give each image's MIME type and where its data was, without +/// the data. +pub fn json_part(file: &[u8]) -> &[u8] { + split_tail(file).0 +} + /// The JSON of a file and the data after it (v4), split at [`BLOB_MAGIC`] after the first JSON /// value. fn split_tail(text: &[u8]) -> (&[u8], &[u8]) { diff --git a/crates/format/tests/native_v4.rs b/crates/format/tests/native_v4.rs index 57f29c279..a8071fa43 100644 --- a/crates/format/tests/native_v4.rs +++ b/crates/format/tests/native_v4.rs @@ -58,6 +58,19 @@ fn small_files_stay_plain_json_and_older_versions_inline() { assert_eq!(load(&v3).unwrap().images["big"].bytes, big.images["big"].bytes); } +/// `json_part` (what exporter plug-ins are given): the whole JSON value, without the data after +/// it; a file without such data is its JSON already. +#[test] +fn json_part_is_the_json_without_the_data_after_it() { + let big = save_with(&doc_with_image("big", INLINE_MAX + 10), &SaveOptions::default()).unwrap(); + let json = vectorcraft_format::json_part(&big); + assert!(json.len() < big.len() - INLINE_MAX && !has_tail(json)); + let v: Value = serde_json::from_slice(json).unwrap(); + assert!(v["images"]["big"].get("blob").is_some() && v["images"]["big"].get("data").is_none(), "{}", v["images"]); + let small = save_with(&doc_with_image("small", 100), &SaveOptions::default()).unwrap(); + assert_eq!(vectorcraft_format::json_part(&small), &small[..]); +} + #[test] fn damaged_offsets_are_refused_not_a_crash() { let d = doc_with_image("big", INLINE_MAX + 10); diff --git a/crates/plugins/src/effect.rs b/crates/plugins/src/effect.rs index b95f12a0f..1ee3fc046 100644 --- a/crates/plugins/src/effect.rs +++ b/crates/plugins/src/effect.rs @@ -17,7 +17,7 @@ use crate::objects::{decode, output_geometry}; use crate::{Error, Kind, Limits, Result, registry}; /// The effect id prefix of live-effect plug-ins: `plugin.`. -pub const PREFIX: &str = "plugin."; +pub const PREFIX: &str = crate::PREFIX; /// Wall-clock budget of one live-effect run (drawing waits for it), in milliseconds. pub const WALL_TIME_MS: u64 = 5_000; @@ -104,6 +104,11 @@ pub fn apply(id: &str, params: &Value, path: &PathData, bounds: Rect, rule: Fill return out.clone(); } let r = run(&plugin, resolved, path, bounds, rule); + // A drawing that was cancelled (an export stopped half way) says nothing about the plug-in: + // the next drawing runs it again. + if matches!(r, Err(Error::Cancelled)) { + return None; + } note(id, &r); let out = r.ok(); let mut cache = CACHE.lock().unwrap_or_else(|e| e.into_inner()); diff --git a/crates/plugins/src/export.rs b/crates/plugins/src/export.rs new file mode 100644 index 000000000..9ecc76277 --- /dev/null +++ b/crates/plugins/src/export.rs @@ -0,0 +1,141 @@ +//! Exporter plug-ins: a plug-in of kind `exporter` writes the document in a file format of its +//! own, such as a pen plotter's or a cutting machine's. +//! +//! File › Export › Export As lists each installed exporter after the built-in formats, under its +//! name, and `document.export` writes it by its format id (`plugin.`) or by one of the +//! extensions its manifest lists. A built-in format keeps its extension: an exporter that lists +//! `svg` too is picked by its format id. The host hands the exporter the document as the native +//! file's JSON (`vectorcraft_format::json_part`: images of `INLINE_MAX` bytes or more without +//! their data) and writes the bytes it returns. +//! +//! An export is one run the user asked for, often of a whole document, so it gets a larger budget +//! than a filter: [`FUEL`] instructions, plus [`Limits::fuel_per_byte`] for each byte of input, +//! and [`WALL_TIME_MS`]. It runs where the export runs: with Background Export on the export's +//! worker, whose Cancel stops it between two slices of the budget ([`Error::Cancelled`]). + +use std::sync::Arc; + +use serde_json::{Value, json}; + +use crate::objects::{declined, printable}; +use crate::{Error, Kind, Limits, Plugin, Result, registry}; + +/// Instructions an export may use, besides [`Limits::fuel_per_byte`] for each byte of the +/// document: 200 times a filter's base budget. +pub const FUEL: u64 = 10_000_000_000; + +/// Wall-clock budget of one export, in milliseconds (native builds; the web build has no clock, +/// and the instruction budget alone bounds it there). +pub const WALL_TIME_MS: u64 = 300_000; + +/// Most warnings an export reports. +pub const MAX_WARNINGS: usize = 100; + +/// Longest warning, in characters. +pub const MAX_WARNING_CHARS: usize = 500; + +/// The format id of exporter `plugin_id`: `plugin.`. +pub fn format_id(plugin_id: &str) -> String { + format!("{}{plugin_id}", crate::PREFIX) +} + +/// The plug-in id in format id `format_id`. +pub fn plugin_id(format_id: &str) -> Option<&str> { + format_id.strip_prefix(crate::PREFIX) +} + +/// The installed exporters, by plug-in id. +pub fn installed() -> Vec> { + registry::list().into_iter().filter(|p| p.manifest().kind == Kind::Exporter).collect() +} + +/// The installed exporter of format id `format_id`. +pub fn by_format(format_id: &str) -> Option> { + registry::get(plugin_id(format_id)?).filter(|p| p.manifest().kind == Kind::Exporter) +} + +/// What an exporter returned. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Exported { + /// The file, base64 encoded, as the plug-in returned it (the host decodes it: this crate has + /// no base64 of its own). + pub data_base64: String, + /// What the export left out or changed, for the user (at most [`MAX_WARNINGS`]). + pub warnings: Vec, +} + +/// The file an export writes, as the exporter is told in `_context`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Target<'a> { + /// Its extension, without the dot: one the exporter lists. + pub extension: &'a str, + /// Its name without the extension (some formats keep a name in their header). + pub name: &'a str, +} + +/// The limits of an export by `plugin`: its own, with the budgets of an export. +pub fn limits(plugin: &Plugin) -> Limits { + Limits { fuel_base: FUEL, wall_time_ms: WALL_TIME_MS, ..plugin.limits().clone() } +} + +/// Runs exporter `plugin` on `document`, the native file's JSON, with `params` (checked against +/// its manifest: missing ones take their defaults) for the file `target`. +pub fn run(plugin: &Plugin, document: &[u8], params: &Value, target: Target) -> Result { + let m = plugin.manifest(); + if m.kind != Kind::Exporter { + return Err(Error::Params(format!("`{}` is a {}, not an exporter", m.id, m.kind.name()))); + } + let mut params = Value::Object(m.resolve_params(params)?); + params["_context"] = json!({"mode": "export", "extension": target.extension, "name": target.name}); + let params = serde_json::to_vec(¶ms).map_err(|e| Error::Params(e.to_string()))?; + decode(&plugin.run_limited(document, ¶ms, &limits(plugin))?) +} + +/// An exporter's output: `{"dataBase64": "…", "warnings"?: ["…"]}`, or `{"error": "…"}` to decline +/// with a message for the user. +pub fn decode(bytes: &[u8]) -> Result { + let v: Value = serde_json::from_slice(bytes).map_err(|e| Error::Output(format!("not JSON: {e}")))?; + if let Some(e) = declined(&v) { + return Err(e); + } + let data = v.get("dataBase64").and_then(Value::as_str).ok_or_else(|| Error::Output("expected {\"dataBase64\": \"…\"}".into()))?; + let warnings = match v.get("warnings") { + None | Some(Value::Null) => Vec::new(), + Some(Value::Array(w)) => w.iter().take(MAX_WARNINGS).map(|w| printable(w, MAX_WARNING_CHARS)).collect(), + Some(_) => return Err(Error::Output("`warnings` must be a list of strings".into())), + }; + Ok(Exported { data_base64: data.to_string(), warnings }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn format_ids_name_the_plug_in() { + assert_eq!(format_id("org.example.plotter"), "plugin.org.example.plotter"); + assert_eq!(plugin_id("plugin.org.example.plotter"), Some("org.example.plotter")); + assert_eq!(plugin_id("png"), None); + } + + #[test] + fn output_is_the_data_and_the_warnings_or_a_decline() { + let out = decode(br#"{"dataBase64": "SGk=", "warnings": ["a", 2]}"#).unwrap(); + assert_eq!(out, Exported { data_base64: "SGk=".into(), warnings: vec!["a".into(), "2".into()] }); + assert_eq!(decode(br#"{"dataBase64": ""}"#).unwrap().warnings, Vec::::new()); + assert_eq!(decode(br#"{"error": "Nothing to\nwrite."}"#), Err(Error::Failed("Nothing towrite.".into()))); + for bad in [&b"[]"[..], b"{}", b"not json", br#"{"dataBase64": 1}"#, br#"{"dataBase64": "", "warnings": "x"}"#] { + assert!(matches!(decode(bad), Err(Error::Output(_))), "{}", String::from_utf8_lossy(bad)); + } + } + + #[test] + fn warnings_are_capped_in_number_and_length() { + let many: Vec = (0..MAX_WARNINGS + 5).map(|i| format!("w{i}")).collect(); + let long = "x".repeat(MAX_WARNING_CHARS + 1); + let out = decode(json!({"dataBase64": "", "warnings": many}).to_string().as_bytes()).unwrap(); + assert_eq!(out.warnings.len(), MAX_WARNINGS); + let out = decode(json!({"dataBase64": "", "warnings": [long]}).to_string().as_bytes()).unwrap(); + assert_eq!(out.warnings.first().map(|w| w.chars().count()), Some(MAX_WARNING_CHARS)); + } +} diff --git a/crates/plugins/src/lib.rs b/crates/plugins/src/lib.rs index beafd045b..3149c803f 100644 --- a/crates/plugins/src/lib.rs +++ b/crates/plugins/src/lib.rs @@ -17,11 +17,13 @@ //! their geometry, fills, strokes (gradients included) and opacity. An **object filter** returns //! the objects to keep, change or add, applied to the document as one edit //! ([`objects::apply_output`]); a **live effect** ([`effect`]) rewrites one object's geometry each -//! time it is drawn, from an appearance stack. The process-wide [`registry`] holds the installed +//! 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. #![forbid(unsafe_code)] pub mod effect; +pub mod export; pub mod manifest; pub mod objects; pub mod registry; @@ -31,6 +33,10 @@ pub mod wat; pub use manifest::{Kind, Manifest, 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 +/// (`plugin.`, [`effect`]) or its file format ([`export`]). +pub const PREFIX: &str = "plugin."; + /// A plug-in error. Every way a module can misbehave ends here, never in a panic. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum Error { @@ -52,6 +58,10 @@ pub enum Error { Failed(String), #[error("no plug-in {0:?} is installed")] NotFound(String), + /// The caller asked the run to stop ([`vectorcraft_doc::cancel`]), between two slices of + /// its instruction budget. + #[error("cancelled")] + Cancelled, #[error("{0}")] Io(String), } diff --git a/crates/plugins/src/manifest.rs b/crates/plugins/src/manifest.rs index d74458ca3..04c892ec6 100644 --- a/crates/plugins/src/manifest.rs +++ b/crates/plugins/src/manifest.rs @@ -9,6 +9,8 @@ use crate::{Error, Result}; pub const MAX_MANIFEST_BYTES: usize = 64 * 1024; /// Most parameters a plug-in may declare. pub const MAX_PARAMS: usize = 64; +/// Most file extensions an exporter may declare. +pub const MAX_EXTENSIONS: usize = 8; /// What a plug-in is. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -20,6 +22,9 @@ pub enum Kind { /// A live effect: rewrites one object's geometry each time it is drawn, from its appearance /// stack (Effect › Plug-ins, `effect.apply`). Effect, + /// An exporter: writes the document in a file format of its own, whose extensions the manifest + /// lists (File › Export › Export As, `document.export`; see [`crate::export`]). + Exporter, } impl Kind { @@ -27,6 +32,7 @@ impl Kind { match self { Kind::Filter => "filter", Kind::Effect => "effect", + Kind::Exporter => "exporter", } } } @@ -96,6 +102,10 @@ pub struct Manifest { /// 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)>, + /// 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")] + pub extensions: Vec, } impl Manifest { @@ -120,6 +130,7 @@ impl Manifest { description: String::new(), author: String::new(), params: Vec::new(), + extensions: Vec::new(), } } @@ -134,6 +145,7 @@ impl Manifest { if self.version.len() > 32 || self.description.len() > 1024 || self.author.len() > 128 { return Err(Error::Manifest("`version`, `description` or `author` is too long".into())); } + self.validate_extensions()?; for (k, spec) in &self.params { if !valid_key(k) { return Err(Error::Manifest(format!("invalid parameter name {k:?}"))); @@ -160,6 +172,29 @@ impl Manifest { Ok(()) } + /// An exporter lists 1 to [`MAX_EXTENSIONS`] distinct extensions of 1 to 16 lower-case + /// letters and digits; other kinds list none. + fn validate_extensions(&self) -> Result<()> { + let exts = &self.extensions; + match self.kind { + Kind::Exporter => {} + _ if exts.is_empty() => return Ok(()), + _ => return Err(Error::Manifest("only an exporter declares `extensions`".into())), + } + if exts.is_empty() || exts.len() > MAX_EXTENSIONS { + return Err(Error::Manifest(format!("an exporter declares 1 to {MAX_EXTENSIONS} `extensions`"))); + } + for (i, e) in exts.iter().enumerate() { + if !(1..=16).contains(&e.len()) || !e.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit()) { + return Err(Error::Manifest(format!("extension {e:?}: use 1-16 lower-case letters and digits, without the dot"))); + } + if exts.get(..i).is_some_and(|before| before.contains(e)) { + return Err(Error::Manifest(format!("extension {e:?} is listed twice"))); + } + } + Ok(()) + } + /// The parameters documented the way command and effect parameters are /// (`{amount: 0..100 (50), mode: "soft"|"hard" (soft), mono: bool (false)}`). pub fn params_doc(&self) -> String { @@ -355,6 +390,29 @@ mod tests { } } + #[test] + fn an_exporter_lists_its_extensions_and_other_kinds_list_none() { + let m = Manifest::parse(br#"{"id":"org.example.plotter","name":"Plotter","kind":"exporter","extensions":["plt","plot"]}"#).unwrap(); + assert_eq!((m.kind, m.extensions.as_slice()), (Kind::Exporter, &["plt".to_string(), "plot".to_string()][..])); + assert_eq!(serde_json::to_value(&m).unwrap()["extensions"], json!(["plt", "plot"])); + let filter = Manifest::parse(br#"{"id":"a","name":"x","kind":"filter"}"#).unwrap(); + assert!(serde_json::to_value(&filter).unwrap().get("extensions").is_none()); + for bad in [ + r#"{"id":"a","name":"x","kind":"exporter"}"#, + r#"{"id":"a","name":"x","kind":"exporter","extensions":[]}"#, + r#"{"id":"a","name":"x","kind":"exporter","extensions":["PLT"]}"#, + r#"{"id":"a","name":"x","kind":"exporter","extensions":[".plt"]}"#, + r#"{"id":"a","name":"x","kind":"exporter","extensions":[""]}"#, + r#"{"id":"a","name":"x","kind":"exporter","extensions":["abcdefghijklmnopq"]}"#, + r#"{"id":"a","name":"x","kind":"exporter","extensions":["plt","plt"]}"#, + r#"{"id":"a","name":"x","kind":"exporter","extensions":["a","b","c","d","e","f","g","h","i"]}"#, + r#"{"id":"a","name":"x","kind":"filter","extensions":["plt"]}"#, + r#"{"id":"a","name":"x","kind":"effect","extensions":["plt"]}"#, + ] { + assert!(Manifest::parse(bad.as_bytes()).is_err(), "{bad}"); + } + } + #[test] fn rejects_bad_manifests() { for bad in [ diff --git a/crates/plugins/src/objects.rs b/crates/plugins/src/objects.rs index 7c0d60f02..92cf81c32 100644 --- a/crates/plugins/src/objects.rs +++ b/crates/plugins/src/objects.rs @@ -286,9 +286,8 @@ fn decode_object(v: &Value, at: &str, anchors: &mut usize) -> Result /// declined (the message reaches the user). pub fn decode(bytes: &[u8]) -> Result> { let v: Value = serde_json::from_slice(bytes).map_err(|e| Error::Output(format!("not JSON: {e}")))?; - if let Some(e) = v.get("error").filter(|e| !e.is_null()) { - let msg: String = e.as_str().map_or_else(|| e.to_string(), str::to_string).chars().filter(|c| !c.is_control()).take(300).collect(); - return Err(Error::Failed(msg)); + if let Some(e) = declined(&v) { + return Err(e); } let objects = v.get("objects").and_then(Value::as_array).ok_or_else(|| Error::Output("expected {\"objects\": [...]}".into()))?; if objects.len() > MAX_OBJECTS { @@ -298,6 +297,20 @@ pub fn decode(bytes: &[u8]) -> Result> { objects.iter().enumerate().map(|(i, o)| decode_object(o, &format!("objects[{i}]"), &mut anchors)).collect() } +/// The decline in a plug-in's output `v`, `{"error": "…"}`: its message as the user reads it, +/// without control characters and at most 300 of them. +pub(crate) fn declined(v: &Value) -> Option { + let e = v.get("error").filter(|e| !e.is_null())?; + Some(Error::Failed(printable(e, 300))) +} + +/// A plug-in's text the user reads: `v` as a string (any other JSON as its text), without control +/// characters, at most `max` characters of it. +pub(crate) fn printable(v: &Value, max: usize) -> String { + let s = v.as_str().map_or_else(|| v.to_string(), str::to_string); + s.chars().filter(|c| !c.is_control()).take(max).collect() +} + /// The paints of `ap`'s fills (`fill`) or strokes, in paint order. fn paints(ap: &Appearance, fill: bool) -> Vec<&Paint> { ap.items.iter().filter(|i| i.is_fill() == fill).map(AppearanceItem::paint).collect() diff --git a/crates/plugins/src/runtime.rs b/crates/plugins/src/runtime.rs index d33aab7ef..311d296ce 100644 --- a/crates/plugins/src/runtime.rs +++ b/crates/plugins/src/runtime.rs @@ -206,7 +206,8 @@ impl Plugin { Ok(Live { store, instance, memory }) } - /// Calls `f` with at most `fuel` instructions, in slices, checking the deadline between them. + /// Calls `f` with at most `fuel` instructions, in slices, checking the deadline and the + /// caller's cancellation ([`vectorcraft_doc::cancel`]) between them. fn call(&self, live: &mut Live, f: &TypedFunc, params: P, fuel: u64, deadline: &Deadline) -> Result { let mut left = fuel; let give = left.min(FUEL_SLICE); @@ -223,6 +224,7 @@ impl Plugin { return Err(Error::Limit("instruction budget".into())); } deadline.check()?; + vectorcraft_doc::cancel::check().map_err(|_| Error::Cancelled)?; let give = left.min(FUEL_SLICE.max(need)); left -= give; live.store.set_fuel(give).map_err(wasm_err)?; diff --git a/crates/plugins/src/wat.rs b/crates/plugins/src/wat.rs index df544a8e6..241525637 100644 --- a/crates/plugins/src/wat.rs +++ b/crates/plugins/src/wat.rs @@ -45,3 +45,82 @@ pub fn returning(manifest: &str, output: &str) -> String { pub fn manifest(id: &str, kind: &str, params: &str) -> String { format!(r#"{{"id":"{id}","name":"Test {id}","version":"1","kind":"{kind}","params":{params}}}"#) } + +/// The manifest of a test exporter `id` writing `extensions` (a JSON list) with `params`. +pub fn exporter(id: &str, extensions: &str, params: &str) -> String { + format!(r#"{{"id":"{id}","name":"Test {id}","version":"1","kind":"exporter","extensions":{extensions},"params":{params}}}"#) +} + +/// What [`echo_exporter`] writes as its file. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Echo { + /// The input: the document, as the host hands it over. + Input, + /// The parameters JSON, `_context` included. + Params, +} + +/// Where [`echo_exporter`] keeps the base64 alphabet, then its output's opening and closing text. +const ALPHABET_AT: usize = 0x8000; + +/// An exporter (manifest `manifest`) whose file is exactly what it is given (`echo`), so a test +/// reads back what the host hands a plug-in: it returns `{"dataBase64": "…"}`, encoding the bytes +/// in base64 itself, three bytes to four characters, the last group padded with `=`. +pub fn echo_exporter(manifest: &str, echo: Echo) -> String { + let (src, len) = match echo { + Echo::Input => ("$in", "$il"), + Echo::Params => ("$pp", "$pl"), + }; + let (open, close) = (r#"{"dataBase64":""#, r#""}"#); + let (open_at, close_at) = (ALPHABET_AT + 64, ALPHABET_AT + 96); + // One character of the alphabet: the six bits of `$b` from `shift` up. + let char_at = |shift: u32| { + format!("(i32.load8_u (i32.add (i32.const {ALPHABET_AT}) (i32.and (i32.shr_u (local.get $b) (i32.const {shift})) (i32.const 63))))") + }; + let byte = |k: u32| format!("(i32.load8_u (i32.add (local.get {src}) (i32.add (local.get $i) (i32.const {k}))))"); + let run = format!( + r#"(local $o i32) (local $n i32) (local $w i32) (local $i i32) (local $b i32) (local $rest i32) + (local.set $n (i32.add (i32.const {frame}) (i32.mul (i32.const 4) (i32.div_u (i32.add (local.get {len}) (i32.const 2)) (i32.const 3))))) + (local.set $o (memory.grow (i32.add (i32.shr_u (local.get $n) (i32.const 16)) (i32.const 1)))) + (if (i32.eq (local.get $o) (i32.const -1)) (then (return (i64.const -1)))) + (local.set $o (i32.mul (local.get $o) (i32.const 65536))) + (memory.copy (local.get $o) (i32.const {open_at}) (i32.const {open_len})) + (local.set $w (i32.add (local.get $o) (i32.const {open_len}))) + (block $done (loop $triple + (br_if $done (i32.gt_u (i32.add (local.get $i) (i32.const 3)) (local.get {len}))) + (local.set $b (i32.or (i32.or (i32.shl {b0} (i32.const 16)) (i32.shl {b1} (i32.const 8))) {b2})) + (i32.store8 (local.get $w) {c18}) (i32.store8 offset=1 (local.get $w) {c12}) + (i32.store8 offset=2 (local.get $w) {c6}) (i32.store8 offset=3 (local.get $w) {c0}) + (local.set $w (i32.add (local.get $w) (i32.const 4))) + (local.set $i (i32.add (local.get $i) (i32.const 3))) + (br $triple))) + (local.set $rest (i32.sub (local.get {len}) (local.get $i))) + (if (i32.ne (local.get $rest) (i32.const 0)) (then + (local.set $b (i32.shl {b0} (i32.const 16))) + (if (i32.eq (local.get $rest) (i32.const 2)) (then (local.set $b (i32.or (local.get $b) (i32.shl {b1} (i32.const 8)))))) + (i32.store8 (local.get $w) {c18}) (i32.store8 offset=1 (local.get $w) {c12}) + (i32.store8 offset=2 (local.get $w) (if (result i32) (i32.eq (local.get $rest) (i32.const 2)) (then {c6}) (else (i32.const 61)))) + (i32.store8 offset=3 (local.get $w) (i32.const 61)) + (local.set $w (i32.add (local.get $w) (i32.const 4))))) + (memory.copy (local.get $w) (i32.const {close_at}) (i32.const {close_len})) + (i64.or (i64.shl (i64.extend_i32_u (local.get $n)) (i64.const 32)) (i64.extend_i32_u (local.get $o)))"#, + frame = open.len() + close.len(), + open_len = open.len(), + close_len = close.len(), + b0 = byte(0), + b1 = byte(1), + b2 = byte(2), + c18 = char_at(18), + c12 = char_at(12), + c6 = char_at(6), + c0 = char_at(0), + ); + let extra = format!( + r#"(data (i32.const {ALPHABET_AT}) "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") + (data (i32.const {open_at}) "{}") + (data (i32.const {close_at}) "{}")"#, + string(open), + string(close) + ); + module(manifest, &run, &extra, 1) +} diff --git a/crates/plugins/tests/abi.rs b/crates/plugins/tests/abi.rs index 337254b68..5ddd8efb3 100644 --- a/crates/plugins/tests/abi.rs +++ b/crates/plugins/tests/abi.rs @@ -164,6 +164,67 @@ fn wall_clock_budget_stops_runaway_plugins() { assert!(t < Duration::from_secs(3), "{t:?}"); } +#[test] +fn cancelling_stops_a_run_between_two_slices_of_its_budget() { + let spin = ok_module("(loop $l (br $l)) (i64.const 0)"); + let p = Plugin::load(&spin, Limits { fuel_base: u64::MAX / 4, ..Limits::default() }).unwrap(); + let token = vectorcraft_doc::cancel::CancelToken::new(); + token.cancel(); + let t = Instant::now(); + let r = vectorcraft_doc::cancel::scope(&token, None, || p.run(INPUT.as_bytes(), b"{}")); + assert_eq!(r, Err(Error::Cancelled)); + assert!(t.elapsed() < Duration::from_secs(3), "{:?}", t.elapsed()); + // Outside a cancelled scope the same module still runs to its budget. + let (e, _) = run_err(&spin, Limits { fuel_base: 1_000_000, ..Limits::default() }); + assert_eq!(e, Error::Limit("instruction budget".into())); +} + +#[test] +fn an_exporter_returns_its_file_with_an_export_s_budget() { + use vectorcraft_plugins::export::{self, Target}; + let target = Target { extension: "tst", name: "Poster" }; + let manifest = tpl::exporter("test.exporter", r#"["tst"]"#, r#"{"level":{"type":"int","min":1,"max":3,"default":2}}"#); + let p = Plugin::load(&build(&tpl::returning(&manifest, r#"{"dataBase64":"SGk=","warnings":["note"]}"#)), Limits::default()).unwrap(); + assert_eq!((p.manifest().kind, p.manifest().extensions.clone()), (Kind::Exporter, vec!["tst".to_string()])); + let out = export::run(&p, br#"{"format":"vectorcraft"}"#, &json!({"level": 9}), target).unwrap(); + assert_eq!((out.data_base64.as_str(), out.warnings), ("SGk=", vec!["note".to_string()])); + let limits = export::limits(&p); + assert_eq!((limits.fuel_base, limits.wall_time_ms), (export::FUEL, export::WALL_TIME_MS)); + // Its parameters are checked, and a filter is not an exporter. + assert!(matches!(export::run(&p, b"{}", &json!({"level": "high"}), target), Err(Error::Params(_)))); + let filter = Plugin::load(&ok_module("(i64.const 0)"), Limits::default()).unwrap(); + assert!(matches!(export::run(&filter, b"{}", &Value::Null, target), Err(Error::Params(_)))); +} + +/// The echo exporter the engine and UI tests read back what they hand a plug-in with: its file is +/// its input, or its parameters, byte for byte, whatever their length modulo three. +#[test] +fn the_echo_exporter_writes_what_it_is_given() { + use vectorcraft_plugins::export::{self, Target}; + let manifest = tpl::exporter("test.echo", r#"["txt"]"#, "{}"); + let input = Plugin::load(&build(&tpl::echo_exporter(&manifest, tpl::Echo::Input)), Limits::default()).unwrap(); + let params = Plugin::load(&build(&tpl::echo_exporter(&manifest, tpl::Echo::Params)), Limits::default()).unwrap(); + let target = Target { extension: "txt", name: "a" }; + let decoded = |out: export::Exported| String::from_utf8(base64_decode(&out.data_base64)).unwrap(); + for doc in ["{}", r#"{"a":1}"#, r#"{"ab":12}"#, r#"{"format":"vectorcraft","é":"\u0000"}"#] { + assert_eq!(decoded(export::run(&input, doc.as_bytes(), &Value::Null, target).unwrap()), doc); + } + let sent: Value = serde_json::from_str(&decoded(export::run(¶ms, b"{}", &Value::Null, target).unwrap())).unwrap(); + assert_eq!(sent, json!({"_context": {"mode": "export", "extension": "txt", "name": "a"}})); +} + +/// Standard base64 (RFC 4648, padded), decoded here so the test needs no other crate. +fn base64_decode(s: &str) -> Vec { + const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + let sextets: Vec = s.bytes().filter(|b| *b != b'=').map(|b| ALPHABET.iter().position(|a| *a == b).unwrap() as u32).collect(); + let mut out = Vec::new(); + for group in sextets.chunks(4) { + let bits = group.iter().enumerate().fold(0u32, |acc, (i, v)| acc | v << (18 - 6 * i)); + out.extend(bits.to_be_bytes()[1..].iter().take(group.len() - 1)); + } + out +} + #[test] fn memory_cap_limits_input_and_growth() { let echo = build(&tpl::module(MANIFEST, tpl::ECHO, "", 1)); diff --git a/crates/ui-egui/src/dialogs/export_as.rs b/crates/ui-egui/src/dialogs/export_as.rs index 60142f962..8e55a31bd 100644 --- a/crates/ui-egui/src/dialogs/export_as.rs +++ b/crates/ui-egui/src/dialogs/export_as.rs @@ -2,14 +2,16 @@ //! artboard, or one page each in a PDF); without artboards the export covers the visible art. OK //! picks the file, then the format's options dialog follows (PNG/JPEG/WebP/PNG-8/GIF Options, SVG //! Options, Text Export Options, DXF Options, EPS Options, and Save PDF for artboards as pages), -//! else the file is written. DWG is listed, greyed out, with what to use instead. +//! else the file is written. DWG is listed, greyed out, with what to use instead. The formats of +//! installed exporter plug-ins come after the built-in ones: they write the whole document, and one +//! with parameters asks for them before writing (`plugin::open_export`). use std::sync::LazyLock; use serde_json::{Value, json}; use vectorcraft_engine::cmd::fileio::{self, ArtboardPick, Format}; -use super::{DialogSpec, dxf_options, eps_options, form, png_options, save_pdf, svg_options, text_export}; +use super::{DialogSpec, dxf_options, eps_options, form, plugin, png_options, save_pdf, svg_options, text_export}; use crate::state::Dialog; use crate::theme::Tokens; use crate::{VectorcraftApp, io, widgets}; @@ -17,12 +19,21 @@ use crate::{VectorcraftApp, io, widgets}; pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Export As").into(), body, confirm, ok: Some("Export…"), min_width: 380.0, ..DialogSpec::FORM }; -/// The formats Export As writes (the native format and `.ai` are Save As's). -static FORMATS: LazyLock> = +/// The built-in formats Export As writes (the native format and `.ai` are Save As's). +static BUILT_IN: LazyLock> = LazyLock::new(|| fileio::FORMATS.iter().filter(|f| f.write && !matches!(f.id, "vectorcraft" | "ai")).collect()); /// Formats listed after them that can't be written (`fileio::unsupported` ids), greyed out. const UNAVAILABLE: [(&str, &str); 1] = [("dwg", "DWG (use DXF)")]; -static LABELS: LazyLock> = LazyLock::new(|| FORMATS.iter().map(|f| f.label).chain(UNAVAILABLE.iter().map(|u| u.1)).collect()); + +/// The formats Export As writes: the built-in ones, then those of the installed exporter plug-ins. +pub(super) fn formats() -> Vec<&'static Format> { + BUILT_IN.iter().copied().chain(fileio::plugin_formats()).collect() +} + +/// Is `f` an exporter plug-in's format? +fn from_plugin(f: &Format) -> bool { + vectorcraft_plugins::export::by_format(f.id).is_some() +} /// What to use instead when the field names a format that can't be written (DWG). fn unavailable(d: &Dialog) -> Option<(&'static str, &'static str)> { @@ -38,7 +49,7 @@ pub fn open(app: &mut VectorcraftApp, format: Option<&str>, p: &Value) { let missing = format.and_then(|f| UNAVAILABLE.iter().find(|u| u.0.eq_ignore_ascii_case(f.trim_start_matches('.')))); let format = match missing { Some((id, _)) => id, - None => format.and_then(fileio::format).filter(|f| FORMATS.iter().any(|g| g.id == f.id)).map_or("png", |f| f.id), + None => format.and_then(fileio::format).filter(|f| formats().iter().any(|g| g.id == f.id)).map_or("png", |f| f.id), }; let range = p.get("range").and_then(Value::as_str); app.ui.dialog = Some(Dialog::new( @@ -55,7 +66,8 @@ pub fn open(app: &mut VectorcraftApp, format: Option<&str>, p: &Value) { /// The chosen format (PNG when the field names none Export As writes). fn chosen(d: &Dialog) -> Option<&'static Format> { let id = d.str("format"); - let find = |id: &str| FORMATS.iter().copied().find(|f| f.id == id); + let all = formats(); + let find = |id: &str| all.iter().copied().find(|f| f.id == id); find(&id).or_else(|| find("png")) } @@ -66,7 +78,9 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { egui::Grid::new("export-as").num_columns(2).spacing([10.0, 8.0]).show(ui, |ui| { ui.label(egui::RichText::new(tl!("Format:")).color(t.text_dim)); let shown = missing.map_or(f.label, |(label, _)| label); - if let Some(g) = widgets::dropdown_with(ui, "export-as-format", shown, &LABELS, 160.0, |i| i < FORMATS.len()).and_then(|i| FORMATS.get(i)) { + let all = formats(); + let labels: Vec<&str> = all.iter().map(|f| f.label).chain(UNAVAILABLE.iter().map(|u| u.1)).collect(); + if let Some(g) = widgets::dropdown_with(ui, "export-as-format", shown, &labels, 160.0, |i| i < all.len()).and_then(|i| all.get(i)) { d.fields.insert("format".into(), json!(g.id)); } ui.end_row(); @@ -76,8 +90,10 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { form::caption(ui, hint); } ui.add_space(10.0); - form::check(ui, d, "useArtboards", tl!("Use Artboards")); - let use_artboards = d.bool("useArtboards"); + // An exporter plug-in writes the whole document: Use Artboards is not its to apply. + let whole = from_plugin(f); + ui.add_enabled_ui(!whole, |ui| form::check(ui, d, "useArtboards", tl!("Use Artboards"))); + let use_artboards = d.bool("useArtboards") && !whole; ui.add_enabled_ui(use_artboards, |ui| { ui.horizontal(|ui| { ui.add_space(22.0); @@ -94,6 +110,7 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.add_space(8.0); let ext = f.extensions[0]; let hint = match (use_artboards, f.id) { + _ if whole => tl!("The plug-in writes the whole document.").to_string(), (true, "pdf") => tl!("One page per artboard.").to_string(), (true, _) => crate::i18n::fmt(tl!("One file per artboard: -.{ext}"), &[("ext", ext)]), (false, _) => tl!("The bounds of the visible art.").to_string(), @@ -108,7 +125,8 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { return Err(hint.into()); } let f = chosen(d).ok_or("no format to export to")?; - let use_artboards = d.bool("useArtboards"); + let plugin = vectorcraft_plugins::export::by_format(f.id); + let use_artboards = d.bool("useArtboards") && plugin.is_none(); let all = d.bool("all"); let range = d.str("range"); if use_artboards && !all { @@ -116,8 +134,9 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { let n = app.session.active().map_or(0, |s| s.doc.artboards.len()); ArtboardPick { range: Some(range.clone()), ..Default::default() }.resolve(n)?; } - // Nothing to export keeps the dialog open too (before the save dialog, not after it). - if !use_artboards && app.session.active().is_some_and(|s| vectorcraft_render::encode::art_bounds(&s.doc).is_none()) { + // Nothing to export keeps the dialog open too (before the save dialog, not after it). An + // exporter plug-in decides for itself what it can write. + if !use_artboards && plugin.is_none() && app.session.active().is_some_and(|s| vectorcraft_render::encode::art_bounds(&s.doc).is_none()) { return Err("nothing to export: the document has no visible art (or turn on Use Artboards)".into()); } app.ui.dialog = None; @@ -142,6 +161,10 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { if use_artboards { params["range"] = json!(range); } + if let Some(plugin) = plugin.filter(|p| !p.manifest().params.is_empty()) { + plugin::open_export(app, &plugin, params); + return Ok(Value::Null); + } if f.raster { png_options::open(app, f, params); return Ok(Value::Null); diff --git a/crates/ui-egui/src/dialogs/mod.rs b/crates/ui-egui/src/dialogs/mod.rs index 7f1edf31c..08dde3e60 100644 --- a/crates/ui-egui/src/dialogs/mod.rs +++ b/crates/ui-egui/src/dialogs/mod.rs @@ -324,6 +324,7 @@ registry! { PrintPreset: [print::PRESET_KIND] => print::PRESET_SPEC, PrintPresets: [print_presets::KIND] => print_presets::SPEC, Plugin: [plugin::KIND] => plugin::SPEC, + PluginExport: [plugin::EXPORT_KIND] => plugin::EXPORT_SPEC, ScriptCheck: [script_check::KIND] => script_check::SPEC, VectorHalftone: [halftone::KIND] => halftone::SPEC, SimplifySlider: [simplify_slider::KIND] => simplify_slider::SPEC, @@ -577,6 +578,8 @@ mod tests_import_pdf; #[cfg(test)] mod tests_package; #[cfg(test)] +mod tests_plugin_export; +#[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 d2e9cad22..b03215ff4 100644 --- a/crates/ui-egui/src/dialogs/plugin.rs +++ b/crates/ui-egui/src/dialogs/plugin.rs @@ -5,10 +5,13 @@ //! effect dialog (`effect.dialog`). A filter with parameters opens dialog `plugin`: one field per //! declared parameter (fields named after them; `__plugin` is the plug-in id) with a live //! Preview, and OK keeps the run as one undo step (`plugin.run`). A filter without parameters runs -//! at once. +//! at once. Exporters are formats of File › Export › Export As; one with parameters opens dialog +//! `pluginExport` once the file is picked, and its Export button writes the file. + +use std::sync::Arc; use serde_json::{Map, Value, json}; -use vectorcraft_plugins::{Kind, registry}; +use vectorcraft_plugins::{Kind, Manifest, Plugin, registry}; use super::{DialogSpec, form}; use crate::VectorcraftApp; @@ -70,34 +73,49 @@ pub fn open(app: &mut VectorcraftApp, p: &Value) -> Result { let id = p.get("id").and_then(Value::as_str).ok_or("missing `id` (see plugin.list)")?; let plugin = registry::get(id).ok_or_else(|| format!("no plug-in {id:?} is installed"))?; let m = plugin.manifest(); - if m.kind != Kind::Filter { - return Err(format!("{} is a live effect (Effect › Plug-ins)", m.name)); + match m.kind { + Kind::Filter => {} + Kind::Effect => return Err(format!("{} is a live effect (Effect › Plug-ins)", m.name)), + Kind::Exporter => return Err(format!("{} is an exporter (File › Export › Export As…)", m.name)), } let given = p.get("params").filter(|v| v.as_object().is_some_and(|o| !o.is_empty())); if m.params.is_empty() || given.is_some() { return app.run(CMD, json!({"id": id, "params": given.cloned().unwrap_or(Value::Null)})); } - let mut fields: Map = m.defaults(); - fields.insert("__plugin".into(), json!(id)); - fields.insert("__label".into(), json!(m.name.trim_end_matches('…'))); + let mut fields = fields_for(m); fields.insert("preview".into(), json!(true)); app.ui.dialog = Some(Dialog { kind: KIND.into(), fields }); Ok(Value::Null) } +/// The fields of plug-in `m`'s dialog: its parameters at their defaults, `__plugin` (its id) and +/// `__label` (its name, the heading). +fn fields_for(m: &Manifest) -> Map { + let mut fields = m.defaults(); + fields.insert("__plugin".into(), json!(m.id)); + fields.insert("__label".into(), json!(m.name.trim_end_matches('…'))); + fields +} + +/// The plug-in dialog `d` is for, its description drawn above the fields. `None` once it was +/// uninstalled, which closes the dialog. +fn described(ui: &mut egui::Ui, d: &Dialog) -> Option> { + let plugin = registry::get(&d.str("__plugin"))?; + let m = plugin.manifest(); + if !m.description.is_empty() { + ui.label(egui::RichText::new(&m.description).color(crate::theme::Tokens::get(ui.ctx()).text_dim)); + ui.add_space(8.0); + } + Some(plugin) +} + fn params(d: &Dialog) -> Value { json!({"id": d.str("__plugin"), "params": form::params(d)}) } fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { - // A plug-in uninstalled while its dialog is open closes it. - let Some(plugin) = registry::get(&d.str("__plugin")) else { return true }; - let m = plugin.manifest(); - if !m.description.is_empty() { - ui.label(egui::RichText::new(&m.description).color(crate::theme::Tokens::get(ui.ctx()).text_dim)); - ui.add_space(8.0); - } - form::schema_fields(ui, d, &m.params); + let Some(plugin) = described(ui, d) else { return true }; + form::schema_fields(ui, d, &plugin.manifest().params); let (label, p) = (d.str("__label"), params(d)); form::preview(app, ui, d, &label, CMD, p); false @@ -106,3 +124,40 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { form::commit_preview(app, CMD, params(d)) } + +/// The dialog kind of an exporter's parameters, after Export As picked the file. +pub const EXPORT_KIND: &str = "pluginExport"; + +/// Headed by the plug-in's name, as it is; Export writes the file. +pub(super) const EXPORT_SPEC: DialogSpec = DialogSpec { + heading: |d| d.str("__label"), + body: export_body, + confirm: export_confirm, + ok: Some("Export"), + max_width: Some(420.0), + ..DialogSpec::FORM +}; + +/// Export As with exporter `plugin`, which has parameters: its fields at their defaults, and +/// `export`, the export's own parameters (`format`, `path`, …), kept for Export. +pub fn open_export(app: &mut VectorcraftApp, plugin: &Plugin, export: Value) { + let mut fields = fields_for(plugin.manifest()); + fields.insert("__export".into(), export); + app.ui.dialog = Some(Dialog { kind: EXPORT_KIND.into(), fields }); +} + +fn export_body(_: &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); + false +} + +/// Writes the file with the export's parameters and the plug-in's, as `params`. +fn export_confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { + let mut p = d.fields.get("__export").cloned().filter(Value::is_object).ok_or("no format to export to")?; + p["params"] = form::params(d); + let format = p.get("format").and_then(Value::as_str).map(str::to_string); + let path = p.get("path").and_then(Value::as_str).map(str::to_string); + app.ui.dialog = None; + crate::io::export(app, format.as_deref(), path, &p) +} diff --git a/crates/ui-egui/src/dialogs/tests_plugin_export.rs b/crates/ui-egui/src/dialogs/tests_plugin_export.rs new file mode 100644 index 000000000..85156326f --- /dev/null +++ b/crates/ui-egui/src/dialogs/tests_plugin_export.rs @@ -0,0 +1,116 @@ +//! Exporter plug-ins in File › Export › Export As: listed after the built-in formats, written +//! whole (Use Artboards is not theirs to apply), and their parameters asked for once the file is +//! picked. Each test installs exporters of its own: plug-ins are process-wide. + +use serde_json::{Value, json}; +use vectorcraft_plugins::wat::{self as tpl, Echo}; + +use super::tests_export::{app, frame, kind, names, set}; +use super::*; + +fn install(app: &mut VectorcraftApp, src: &str) { + let wasm = wat::parse_str(src).unwrap(); + app.run("plugin.install", json!({"dataBase64": vectorcraft_format::base64_encode(&wasm)})).unwrap(); +} + +#[test] +fn export_as_lists_exporters_and_writes_their_file_whole() { + let (mut app, written) = app(2); + install(&mut app, &tpl::returning(&tpl::exporter("test.ui.whole", r#"["tuw"]"#, "{}"), r#"{"dataBase64":"SGk=","warnings":["a note"]}"#)); + // After the built-in formats, with the other exporters installed. + let listed: Vec<&str> = export_as::formats().iter().map(|f| f.id).collect(); + let first = listed.iter().position(|id| id.starts_with("plugin.")).expect("an exporter listed"); + let added = listed.get(first..).unwrap_or_default(); + assert!(added.iter().all(|id| id.starts_with("plugin.")) && added.contains(&"plugin.test.ui.whole"), "{listed:?}"); + // It isn't a filter: Object › Plug-ins' command says where it is. + let e = app.run("plugin.dialog", json!({"id": "test.ui.whole"})).unwrap_err(); + assert!(e.contains("is an exporter (File › Export › Export As…)"), "{e}"); + + app.run("file.exportAs", json!({"format": "plugin.test.ui.whole"})).unwrap(); + assert_eq!(kind(&app), Some("exportAs")); + assert_eq!(app.ui.dialog.as_ref().unwrap().str("format"), "plugin.test.ui.whole"); + // Use Artboards is greyed out and doesn't apply: one file of the whole document. + set(&mut app, "useArtboards", json!(true)); + frame(&mut app); + confirm(&mut app).unwrap(); + assert!(app.ui.dialog.is_none(), "an exporter without parameters writes at once"); + assert_eq!(names(&written), ["/out/Untitled-1.tuw"]); + assert_eq!(written.borrow()[0].1, b"Hi"); + assert!(app.ui.status.contains("a note"), "{}", app.ui.status); +} + +#[test] +fn an_exporter_with_parameters_asks_for_them_once_the_file_is_picked() { + let (mut app, written) = app(1); + let params = r#"{"density":{"type":"number","min":0,"max":10,"default":4}}"#; + install(&mut app, &tpl::echo_exporter(&tpl::exporter("test.ui.params", r#"["tup"]"#, params), Echo::Params)); + // Asked for by its extension, as agents may. + app.run("file.exportAs", json!({"format": "tup"})).unwrap(); + assert_eq!(app.ui.dialog.as_ref().unwrap().str("format"), "plugin.test.ui.params"); + confirm(&mut app).unwrap(); + assert_eq!(kind(&app), Some(plugin::EXPORT_KIND)); + let d = app.ui.dialog.as_ref().unwrap(); + assert_eq!((d.str("__label"), d.fields.get("density")), ("Test test.ui.params".to_string(), Some(&json!(4.0)))); + assert!(written.borrow().is_empty(), "nothing written before Export"); + frame(&mut app); + set(&mut app, "density", json!(7)); + confirm(&mut app).unwrap(); + assert!(app.ui.dialog.is_none()); + assert_eq!(names(&written), ["/out/Untitled-1.tup"]); + let sent: Value = serde_json::from_slice(&written.borrow()[0].1).unwrap(); + assert_eq!(sent, json!({"density": 7.0, "_context": {"mode": "export", "extension": "tup", "name": "Untitled-1"}})); +} + +/// `CRAFT_UI_PLUGIN_EXPORT_SHOT=` saves the pictures of the dialogs to that folder: Export As +/// with an exporter's format chosen, then the exporter's own dialog. The plug-in is a test one with +/// parameters a plotter's exporter could have, saved beside them (`plotter-test-exporter.wasm`) to +/// install in the app. +#[test] +fn export_as_and_exporter_dialog_pictures() { + let Some(dir) = std::env::var_os("CRAFT_UI_PLUGIN_EXPORT_SHOT") else { return }; + let dir = std::path::PathBuf::from(dir); + let (mut app, _) = app(1); + let params = r#"{"speed":{"type":"number","min":1,"max":100,"default":40},"passes":{"type":"int","min":1,"max":10,"default":1}, + "optimizeTravel":{"type":"bool","default":true},"paper":{"type":"choice","options":["A4","A3","Letter"],"default":"A4"}}"#; + let manifest = r#"{"id":"org.example.plotter","name":"Plotter (test exporter)","version":"1","kind":"exporter","extensions":["plt"], + "description":"Writes the document as pen plotter commands.","params":PARAMS}"# + .replace("PARAMS", params); + let src = tpl::echo_exporter(&manifest, Echo::Params); + std::fs::write(dir.join("plotter-test-exporter.wasm"), wat::parse_str(&src).unwrap()).unwrap(); + install(&mut app, &src); + app.run("file.exportAs", json!({"format": "plugin.org.example.plotter"})).unwrap(); + let shot = |app: VectorcraftApp, name: &str| -> VectorcraftApp { + let mut ready = false; + let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(720.0, 520.0)).wgpu().build_ui_state( + move |ui, app: &mut VectorcraftApp| { + if !ready { + crate::theme::install_fonts(ui.ctx()); + crate::theme::apply(ui.ctx(), crate::theme::Brightness::MediumDark); + ready = true; + return; + } + show(app, ui.ctx()); + }, + app, + ); + h.run_steps(6); + h.render().unwrap().save(dir.join(name)).unwrap(); + h.into_state() + }; + app = shot(app, "export-as-exporter.png"); + confirm(&mut app).unwrap(); + assert_eq!(kind(&app), Some(plugin::EXPORT_KIND)); + shot(app, "exporter-options.png"); +} + +/// A plug-in that declines keeps nothing written and says why. +#[test] +fn a_declining_exporter_writes_nothing() { + let (mut app, written) = app(1); + install(&mut app, &tpl::returning(&tpl::exporter("test.ui.declines", r#"["tud"]"#, "{}"), r#"{"error":"Nothing to plot."}"#)); + app.run("file.exportAs", json!({"format": "plugin.test.ui.declines"})).unwrap(); + let r = confirm(&mut app); + assert!(written.borrow().is_empty()); + let said = r.err().unwrap_or_default() + &app.ui.status; + assert!(said.contains("Nothing to plot."), "{said}"); +} diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 0de5e0a59..95b8e6973 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -3153,6 +3153,7 @@ Text Wrap Options Опции за обтичане на текст Text Wrap Options… Опции за обтичане на текст… The bounds of the visible art. Границите на видимия макет. + The plug-in writes the whole document. Приставката експортира целия документ. The cell shows the type object's text as HTML. Клетката показва текста на текстовия обект като HTML. The document has no colour swatches. Документът няма цветови образци. The edge of the paper the printer can't reach: the art goes inside it, and the preview shows it dashed. Краят на хартията, до който принтерът не може да стигне: макетът навлиза в него, а прегледът го показва с пунктир. diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 78b18a50f..4ecb67ed2 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2168,6 +2168,7 @@ lighting Light Světlo One file per artboard: -.{ext} Jeden soubor na pracovní plochu: -.{ext} One page per artboard. Jedna stránka na pracovní plochu. The bounds of the visible art. Hranice viditelné kresby. + The plug-in writes the whole document. Zásuvný modul exportuje celý dokument. Use Artboards Použít pracovní plochy Choose a folder Vyberte složku Create Sub-folders Vytvořit podsložky @@ -4543,6 +4544,7 @@ imageTrace Outline Obrys @msg {_1} files are written by file.saveAsTemplate soubory {_1} zapisuje file.saveAsTemplate @msg {_1} font families available Dostupné rodiny písem: {_1} @msg {_1} is a live effect (Effect › Plug-ins) {_1} je živý efekt (Efekt › Zásuvné moduly) +@msg {_1} is an exporter (File › Export › Export As…) {_1} je exportní zásuvný modul (Soubor › Export › Exportovat jako…) @msg {_1} is already used by {_2} {_1} už používá {_2} @msg {_1} linked file(s) can't be read here: their previews show propojené soubory ({_1}) zde nelze přečíst: zobrazují se jejich náhledy @msg {_1} linked image(s) changed on disk: Update Links shows the new versions propojené obrázky ({_1}) se na disku změnily: nové verze zobrazí Aktualizovat propojení diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 318e474cb..11656a2ef 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -1980,6 +1980,7 @@ lighting Light Lys One file per artboard: -.{ext} Én fil pr. tegnebræt: -.{ext} One page per artboard. Én side pr. tegnebræt. The bounds of the visible art. Den synlige grafiks grænser. + The plug-in writes the whole document. Plug-ins eksporterer hele dokumentet. Use Artboards Brug tegnebrætter Choose a folder Vælg en mappe Create Sub-folders Opret undermapper diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 8e358da03..a14c11213 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -3178,6 +3178,7 @@ effect Points Punkte Text Wrap Options Textumflussoptionen Text Wrap Options… Textumflussoptionen… The bounds of the visible art. Die Begrenzung der sichtbaren Grafik. + The plug-in writes the whole document. Das Plug-in exportiert das ganze Dokument. The cell shows the type object's text as HTML. Die Zelle zeigt den Text des Textobjekts als HTML. The document has no colour swatches. Das Dokument enthält keine Farbfelder. The edge of the paper the printer can't reach: the art goes inside it, and the preview shows it dashed. Der Papierrand, den der Drucker nicht erreicht: Die Grafik liegt innerhalb davon, und die Vorschau zeigt ihn gestrichelt. @@ -4180,6 +4181,7 @@ theme Appearance Erscheinungsbild @msg {_1} files are written by file.saveAsTemplate {_1}-Dateien werden von file.saveAsTemplate geschrieben @msg {_1} font families available {_1} Schriftfamilien verfügbar @msg {_1} is a live effect (Effect › Plug-ins) {_1} ist ein Live-Effekt (Effekt › Plug-ins) +@msg {_1} is an exporter (File › Export › Export As…) {_1} ist ein Export-Plug-in (Datei › Exportieren › Exportieren als…) @msg {_1} is already used by {_2} {_1} wird bereits von {_2} verwendet @msg {_1} linked file(s) can't be read here: their previews show {_1} verknüpfte Datei(en) können hier nicht gelesen werden: Ihre Vorschauen werden angezeigt @msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} verknüpfte(s) Bild(er) wurde(n) auf dem Datenträger geändert: „Verknüpfungen aktualisieren“ zeigt die neuen Versionen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 4fbb9fb58..8914e70aa 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -3541,6 +3541,7 @@ theme Appearance Εμφάνιση Text Wrap Options Επιλογές αναδίπλωσης κειμένου Text Wrap Options… Επιλογές αναδίπλωσης κειμένου… The bounds of the visible art. Τα όρια του ορατού σχεδίου. + The plug-in writes the whole document. Το πρόσθετο εξάγει ολόκληρο το έγγραφο. The cell shows the type object's text as HTML. Το κελί εμφανίζει το κείμενο του αντικειμένου ως HTML. The document has no colour swatches. Το έγγραφο δεν περιέχει δείγματα χρωμάτων. The edge of the paper the printer can't reach: the art goes inside it, and the preview shows it dashed. Το περιθώριο του χαρτιού όπου δεν μπορεί να εκτυπώσει ο εκτυπωτής: το σχέδιο βρίσκεται εντός του και η προεπισκόπηση το δείχνει διακεκομμένο. @@ -4216,6 +4217,7 @@ theme Appearance Εμφάνιση @msg {_1} files are written by file.saveAsTemplate τα αρχεία {_1} αποθηκεύονται με το file.saveAsTemplate @msg {_1} font families available {_1} διαθέσιμες οικογένειες γραμματοσειρών @msg {_1} is a live effect (Effect › Plug-ins) το {_1} είναι δυναμικό εφέ (Εφέ › Πρόσθετα) +@msg {_1} is an exporter (File › Export › Export As…) το {_1} είναι πρόσθετο εξαγωγής (Αρχείο › Εξαγωγή › Εξαγωγή ως…) @msg {_1} is already used by {_2} το {_1} χρησιμοποιείται ήδη από το {_2} @msg {_1} linked file(s) can't be read here: their previews show {_1} συνδεδεμένα αρχεία δεν μπορούν να διαβαστούν εδώ: εμφανίζονται οι προεπισκοπήσεις τους @msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} συνδεδεμένες εικόνες άλλαξαν στον δίσκο: η Ενημέρωση συνδέσμων εμφανίζει τις νέες εκδόσεις diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 49e07b02f..41a51e136 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -3212,6 +3212,7 @@ Text Wrap Options Opciones de ceñir texto Text Wrap Options… Opciones de ceñir texto… The bounds of the visible art. Los límites de la ilustración visible. + The plug-in writes the whole document. El plugin exporta el documento completo. The cell shows the type object's text as HTML. La celda muestra el texto del objeto de texto como HTML. The document has no colour swatches. El documento no tiene muestras de color. The edge of the paper the printer can't reach: the art goes inside it, and the preview shows it dashed. El borde del papel al que no llega la impresora: la ilustración queda dentro y la vista previa lo muestra discontinuo. @@ -4242,6 +4243,7 @@ axis Both Ambos @msg {_1} files are written by file.saveAsTemplate los archivos {_1} se escriben con file.saveAsTemplate @msg {_1} font families available {_1} familias de fuentes disponibles @msg {_1} is a live effect (Effect › Plug-ins) {_1} es un efecto interactivo (Efecto › Plugins) +@msg {_1} is an exporter (File › Export › Export As…) {_1} es un plugin de exportación (Archivo › Exportar › Exportar como…) @msg {_1} is already used by {_2} {_1} ya lo usa {_2} @msg {_1} linked file(s) can't be read here: their previews show aquí no se pueden leer {_1} archivo(s) vinculado(s): se muestran sus previsualizaciones @msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} imagen(es) vinculada(s) cambiaron en el disco: Actualizar vínculos muestra las versiones nuevas diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index eb390e885..1206dda68 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -2125,6 +2125,7 @@ lighting Light Valo One file per artboard: -.{ext} Yksi tiedosto piirtoalustaa kohden: -.{ext} One page per artboard. Yksi sivu piirtoalustaa kohden. The bounds of the visible art. Näkyvän kuvituksen rajat. + The plug-in writes the whole document. Laajennus vie koko asiakirjan. Use Artboards Käytä piirtoalustoja Choose a folder Valitse kansio Create Sub-folders Luo alikansiot @@ -4463,6 +4464,7 @@ imageTrace Outline Ääriviiva @msg {_1} files are written by file.saveAsTemplate {_1}-tiedostot kirjoitetaan komennolla file.saveAsTemplate @msg {_1} font families available {_1} fonttiperhettä käytettävissä @msg {_1} is a live effect (Effect › Plug-ins) {_1} on live-tehoste (Tehoste › Laajennukset) +@msg {_1} is an exporter (File › Export › Export As…) {_1} on vientilaajennus (Tiedosto › Vie › Vie muodossa…) @msg {_1} is already used by {_2} {_1} on jo käytössä kohteella {_2} @msg {_1} linked file(s) can't be read here: their previews show {_1} linkitettyä tiedostoa ei voi lukea täällä: niiden esikatselut näytetään @msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} linkitettyä kuvaa muuttui levyllä: Päivitä linkit näyttää uudet versiot diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 68c1c1230..ecb85edb6 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3209,6 +3209,7 @@ Text Wrap Options Options d’habillage de texte Text Wrap Options… Options d’habillage de texte… The bounds of the visible art. Les limites de l’illustration visible. + The plug-in writes the whole document. Le module externe exporte le document entier. The cell shows the type object's text as HTML. La cellule affiche le texte de l’objet texte au format HTML. The document has no colour swatches. Le document ne contient aucune nuance de couleur. The edge of the paper the printer can't reach: the art goes inside it, and the preview shows it dashed. Le bord du papier que l’imprimante ne peut pas atteindre : l’illustration reste à l’intérieur, et l’aperçu l’affiche en pointillés. @@ -4239,6 +4240,7 @@ axis Both Les deux @msg {_1} files are written by file.saveAsTemplate les fichiers {_1} sont écrits par file.saveAsTemplate @msg {_1} font families available {_1} familles de polices disponibles @msg {_1} is a live effect (Effect › Plug-ins) {_1} est un effet dynamique (Effet › Plug-ins) +@msg {_1} is an exporter (File › Export › Export As…) {_1} est un module externe d’exportation (Fichier › Exporter › Exporter sous…) @msg {_1} is already used by {_2} {_1} est déjà utilisé par {_2} @msg {_1} linked file(s) can't be read here: their previews show {_1} fichier(s) lié(s) illisible(s) ici : leurs aperçus sont affichés @msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} image(s) liée(s) modifiée(s) sur le disque : Mettre à jour les liens affiche les nouvelles versions diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index ccca14d44..c5e9f39e9 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -3869,6 +3869,7 @@ mark-style Japanese Japán Texturizer… Textúrázó… Thai Thai The bounds of the visible art. A látható grafika határai. + The plug-in writes the whole document. A bővítmény a teljes dokumentumot exportálja. The cell shows the type object's text as HTML. A cella a szövegobjektum szövegét HTML-ként mutatja. The dialog was closed before its file dialog answered A párbeszédablak bezárult, mielőtt a fájlválasztó válaszolt volna The document has no colour swatches. A dokumentumban nincs színminta. @@ -4978,6 +4979,7 @@ imageTrace Outline Körvonal @msg {_1} font families available Elérhető betűcsaládok: {_1} @msg {_1} has no {_2} style ennek nincs ilyen stílusa: {_1} ({_2}) @msg {_1} is a live effect (Effect › Plug-ins) élő effektus: {_1} (Effektus › Bővítmények) +@msg {_1} is an exporter (File › Export › Export As…) exportáló bővítmény: {_1} (Fájl › Exportálás › Exportálás másként…) @msg {_1} is already used by {_2} már foglalt: {_1} (használja: {_2}) @msg {_1} linked file(s) can't be read here: their previews show Itt nem olvasható csatolt fájlok: {_1}; az előnézetük látszik @msg {_1} linked image(s) changed on disk: Update Links shows the new versions A lemezen módosult csatolt képek: {_1}; a Csatolások frissítése az új változatokat mutatja diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index fade030a5..5ea570e72 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -1980,6 +1980,7 @@ lighting Light Cahaya One file per artboard: -.{ext} Satu file per artboard: -.{ext} One page per artboard. Satu halaman per artboard. The bounds of the visible art. Batas karya seni yang terlihat. + The plug-in writes the whole document. Plug-in mengekspor seluruh dokumen. Use Artboards Gunakan artboard Choose a folder Pilih folder Create Sub-folders Buat subfolder diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index c3314eca4..1ceeda635 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -3245,6 +3245,7 @@ mark-style Japanese Giapponese Text Wrap Options Opzioni testo attorno Text Wrap Options… Opzioni testo attorno… The bounds of the visible art. I limiti della grafica visibile. + The plug-in writes the whole document. Il plug-in esporta l'intero documento. The cell shows the type object's text as HTML. La cella mostra il testo dell'oggetto testo come HTML. The document has no colour swatches. Il documento non ha campioni di colore. The edge of the paper the printer can't reach: the art goes inside it, and the preview shows it dashed. Il bordo del foglio che la stampante non riesce a raggiungere: la grafica resta al suo interno e l'anteprima lo mostra tratteggiato. @@ -4281,6 +4282,7 @@ axis Both Entrambi @msg {_1} files are written by file.saveAsTemplate i file {_1} vengono scritti da file.saveAsTemplate @msg {_1} font families available {_1} famiglie di font disponibili @msg {_1} is a live effect (Effect › Plug-ins) {_1} è un effetto dinamico (Effetto › Plug-in) +@msg {_1} is an exporter (File › Export › Export As…) {_1} è un plug-in di esportazione (File › Esporta › Esporta come…) @msg {_1} is already used by {_2} {_1} è già usato da {_2} @msg {_1} linked file(s) can't be read here: their previews show file collegati non leggibili qui: {_1}; vengono mostrate le loro anteprime @msg {_1} linked image(s) changed on disk: Update Links shows the new versions immagini collegate modificate su disco: {_1}; Aggiorna collegamenti mostra le nuove versioni diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index b89f07adc..437d3682a 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2960,6 +2960,7 @@ mark-style Japanese 日本式 Text Import Options テキスト読み込みオプション The Print Tiling tool placed the pages. プリント分割ツールでページが配置されています。 The bounds of the visible art. 表示されているアートの範囲。 + The plug-in writes the whole document. プラグインがドキュメント全体を書き出します。 The cell shows the type object's text as HTML. セルには文字オブジェクトのテキストが HTML として表示されます。 The document has no colour swatches. ドキュメントにカラースウォッチがありません。 The edge of the paper the printer can't reach: the art goes inside it, and the preview shows it dashed. プリンターが印刷できない用紙の端の部分です。アートはこの内側に配置され、プレビューでは破線で表示されます。 @@ -4271,6 +4272,7 @@ swatch Bright Violet ビビッドバイオレット @msg missing `id` (see plugin.list) `id` が指定されていません (plugin.list を参照) @msg no plug-in {id} is installed プラグイン {id} はインストールされていません @msg {_1} is a live effect (Effect › Plug-ins) {_1} はライブ効果です (「効果 › プラグイン」から使用してください) +@msg {_1} is an exporter (File › Export › Export As…) {_1} は書き出しプラグインです (「ファイル › 書き出し › 形式を指定して書き出し…」から使用してください) @msg a preset named `{_1}` exists `{_1}` という名前のプリセットはすでに存在します @msg a swatch or color group named `{n}` exists `{n}` という名前のスウォッチまたはカラーグループはすでに存在します @msg no print preset named `{base}` `{base}` という名前のプリントプリセットが見つかりません diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index d4c1e0db9..911982187 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -1879,6 +1879,7 @@ lighting Light სინათლე One file per artboard: -.{ext} ერთი ფაილი თითო სამუშაო დაფაზე: -.{ext} One page per artboard. ერთი გვერდი თითო სამუშაო დაფაზე. The bounds of the visible art. ხილული ნახატის საზღვრები. + The plug-in writes the whole document. პლაგინი მთელ დოკუმენტს ჩაწერს. Use Artboards სამუშაო დაფების გამოყენება Choose a folder საქაღალდის არჩევა Create Sub-folders ქვესაქაღალდეების შექმნა @@ -4015,6 +4016,7 @@ effect Regular ჩვეულებრივი @msg {_1} files can be opened but not written (see document.formats) {_1} ფაილების გახსნა შესაძლებელია, ჩაწერა — არა (იხ. document.formats) @msg {_1} font families available ხელმისაწვდომი შრიფტების ოჯახები: {_1} @msg {_1} is a live effect (Effect › Plug-ins) {_1} ცოცხალი ეფექტია (ეფექტი › პლაგინები) +@msg {_1} is an exporter (File › Export › Export As…) {_1} ექსპორტის პლაგინია (ფაილი › ექსპორტირება › ექსპორტირება ფორმატში…) @msg {_1} is already used by {_2} {_1} უკვე გამოიყენება აქ: {_2} @msg {_1} linked file(s) can't be read here: their previews show მიბმული ფაილების წაკითხვა აქ ვერ ხერხდება ({_1}): ჩანს მათი გადახედვა @msg {_1} linked image(s) changed on disk: Update Links shows the new versions მიბმული გამოსახულებები შეიცვალა დისკზე ({_1}): ბმულების განახლება აჩვენებს ახალ ვერსიებს diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 1e7ea536e..4563d9fac 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -2954,6 +2954,7 @@ mark-style Japanese 일본식 Text Import Options 텍스트 가져오기 옵션 The Print Tiling tool placed the pages. 인쇄 타일링 도구가 페이지를 배치했습니다. The bounds of the visible art. 보이는 아트워크의 경계입니다. + The plug-in writes the whole document. 플러그인이 문서 전체를 내보냅니다. The cell shows the type object's text as HTML. 셀에 문자 개체의 텍스트가 HTML로 표시됩니다. The document has no colour swatches. 문서에 색상 견본이 없습니다. The edge of the paper the printer can't reach: the art goes inside it, and the preview shows it dashed. 프린터가 닿지 않는 용지 가장자리입니다. 아트워크는 이 안쪽에 놓이며, 미리 보기에는 점선으로 표시됩니다. @@ -4203,6 +4204,7 @@ theme Appearance 외관 @msg missing `id` (see plugin.list) `id`가 없습니다(plugin.list 참조) @msg no plug-in {id} is installed 플러그인 {id}이(가) 설치되어 있지 않습니다 @msg {_1} is a live effect (Effect › Plug-ins) {_1}은(는) 라이브 효과입니다(효과 › 플러그인) +@msg {_1} is an exporter (File › Export › Export As…) {_1}은(는) 내보내기 플러그인입니다(파일 › 내보내기 › 다른 이름으로 내보내기…) @msg a preset named `{_1}` exists `{_1}`이라는 프리셋이 이미 있습니다 @msg a swatch or color group named `{n}` exists `{n}`이라는 견본 또는 색상 그룹이 이미 있습니다 @msg no print preset named `{base}` `{base}`라는 인쇄 프리셋이 없습니다 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 52da2db1b..c51ff5b21 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -1994,6 +1994,7 @@ lighting Light Licht One file per artboard: -.{ext} Eén bestand per tekenbord: -.{ext} One page per artboard. Eén pagina per tekenbord. The bounds of the visible art. De grenzen van de zichtbare artwork. + The plug-in writes the whole document. De plug-in exporteert het hele document. Use Artboards Tekenborden gebruiken Choose a folder Kies een map Create Sub-folders Submappen maken diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 317d86201..dd674d98d 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2083,6 +2083,7 @@ lighting Light Światło One file per artboard: -.{ext} Jeden plik na obszar kompozycji: -.{ext} One page per artboard. Jedna strona na obszar kompozycji. The bounds of the visible art. Granice widocznej kompozycji. + The plug-in writes the whole document. Wtyczka eksportuje cały dokument. Use Artboards Użyj obszarów kompozycji Choose a folder Wybierz folder Create Sub-folders Utwórz podfoldery @@ -4368,6 +4369,7 @@ imageTrace Outline Kontur @msg {_1} files are written by file.saveAsTemplate pliki {_1} zapisuje file.saveAsTemplate @msg {_1} font families available dostępne rodziny czcionek: {_1} @msg {_1} is a live effect (Effect › Plug-ins) {_1} to efekt na żywo (Efekt › Wtyczki) +@msg {_1} is an exporter (File › Export › Export As…) {_1} to wtyczka eksportu (Plik › Eksportuj › Eksportuj jako…) @msg {_1} is already used by {_2} {_1} jest już używany przez {_2} @msg {_1} linked file(s) can't be read here: their previews show połączonych plików nie można tu odczytać ({_1}): wyświetlane są ich podglądy @msg {_1} linked image(s) changed on disk: Update Links shows the new versions połączone obrazy zmienione na dysku: {_1} — Aktualizuj łącza pokaże nowe wersje diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 5a14528be..16be9509c 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2625,6 +2625,7 @@ Text Wrap Options… Opções de Quebra de Texto… The Print Tiling tool placed the pages. A ferramenta Tesselação de Impressão colocou as páginas. The bounds of the visible art. Os limites da arte visível. + The plug-in writes the whole document. O plug-in exporta o documento inteiro. The cell shows the type object's text as HTML. A célula mostra o texto do objeto de tipo como HTML. The document has no colour swatches. O documento não tem amostras de cor. The edge of the paper the printer can't reach: the art goes inside it, and the preview shows it dashed. A borda do papel que a impressora não alcança: a arte fica dentro dela, e a visualização a mostra tracejada. @@ -3822,6 +3823,7 @@ axis Both Ambos @msg {_1} files are written by file.saveAsTemplate arquivos {_1} são gravados por file.saveAsTemplate @msg {_1} font families available {_1} famílias de fontes disponíveis @msg {_1} is a live effect (Effect › Plug-ins) {_1} é um efeito dinâmico (Efeito › Plug-ins) +@msg {_1} is an exporter (File › Export › Export As…) {_1} é um plug-in de exportação (Arquivo › Exportar › Exportar Como…) @msg {_1} is already used by {_2} {_1} já é usado por {_2} @msg {_1} linked file(s) can't be read here: their previews show {_1} arquivo(s) vinculado(s) não podem ser lidos aqui: suas visualizações são exibidas @msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} imagem(ns) vinculada(s) mudou(aram) no disco: Atualizar links mostra as novas versões diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index aa9c97bb9..94ee52505 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -3241,6 +3241,7 @@ mark-style Japanese Японский Text Wrap Options Параметры обтекания текстом Text Wrap Options… Параметры обтекания текстом… The bounds of the visible art. Границы видимого рисунка. + The plug-in writes the whole document. Модуль экспортирует весь документ. The cell shows the type object's text as HTML. Ячейка показывает текст текстового объекта как HTML. The document has no colour swatches. В документе нет цветовых образцов. The edge of the paper the printer can't reach: the art goes inside it, and the preview shows it dashed. Край бумаги, до которого не доходит принтер: рисунок остаётся внутри него, а просмотр показывает его пунктиром. @@ -4277,6 +4278,7 @@ axis Both Оба @msg {_1} files are written by file.saveAsTemplate файлы {_1} записывает file.saveAsTemplate @msg {_1} font families available доступно семейств шрифтов: {_1} @msg {_1} is a live effect (Effect › Plug-ins) {_1} — динамический эффект (Эффект › Модули) +@msg {_1} is an exporter (File › Export › Export As…) {_1} — модуль экспорта (Файл › Экспорт › Экспортировать как…) @msg {_1} is already used by {_2} {_1} уже используется {_2} @msg {_1} linked file(s) can't be read here: their previews show здесь невозможно прочитать связанных файлов: {_1}: отображаются их предварительные просмотры @msg {_1} linked image(s) changed on disk: Update Links shows the new versions связанных изображений ({_1}) изменились на диске: «Обновить связи» покажет новые версии diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 1ab9cdfbe..6cc23bb0b 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -3153,6 +3153,7 @@ Text Wrap Options Alternativ för textombrytning Text Wrap Options… Alternativ för textombrytning… The bounds of the visible art. Gränserna för den synliga grafiken. + The plug-in writes the whole document. Insticksprogrammet exporterar hela dokumentet. The cell shows the type object's text as HTML. Cellen visar textobjektets text som HTML. The document has no colour swatches. Dokumentet har inga färgrutor. The edge of the paper the printer can't reach: the art goes inside it, and the preview shows it dashed. Den kant av papperet som skrivaren inte når: grafiken placeras innanför den, och förhandsgranskningen visar den streckad. @@ -4164,6 +4165,7 @@ axis Both Båda @msg {_1} files are written by file.saveAsTemplate {_1}-filer skrivs av file.saveAsTemplate @msg {_1} font families available {_1} teckensnittsfamiljer tillgängliga @msg {_1} is a live effect (Effect › Plug-ins) {_1} är en live-effekt (Effekt › Insticksprogram) +@msg {_1} is an exporter (File › Export › Export As…) {_1} är ett exportinsticksprogram (Fil › Exportera › Exportera som…) @msg {_1} is already used by {_2} {_1} används redan av {_2} @msg {_1} linked file(s) can't be read here: their previews show {_1} länkade fil(er) kan inte läsas här: deras förhandsgranskningar visas @msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} länkade bild(er) har ändrats på disken: Uppdatera länkar visar de nya versionerna diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index fb3095b9b..13cd0851f 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -3238,6 +3238,7 @@ effect Points Точки Text Wrap Options Параметри обтікання текстом Text Wrap Options… Параметри обтікання текстом… The bounds of the visible art. Кордони видимого малюнка. + The plug-in writes the whole document. Модуль експортує весь документ. The cell shows the type object's text as HTML. Осередок показує текст текстового об’єкта як HTML. The document has no colour swatches. У документі немає кольорових зразків. The edge of the paper the printer can't reach: the art goes inside it, and the preview shows it dashed. Край паперу, до якого не приходить принтер: малюнок залишається всередині нього, а перегляд показує його пунктиром. @@ -4265,6 +4266,7 @@ axis Both Обидві @msg {_1} files are written by file.saveAsTemplate файли {_1} записує file.saveAsTemplate @msg {_1} font families available доступно сімейства шрифтів: {_1} @msg {_1} is a live effect (Effect › Plug-ins) {_1} - динамічний ефект (Ефект › Модулі) +@msg {_1} is an exporter (File › Export › Export As…) {_1} - модуль експорту (Файл › Експорт › Експортувати як…) @msg {_1} is already used by {_2} {_1} вже використовується {_2} @msg {_1} linked file(s) can't be read here: their previews show тут неможливо прочитати пов’язані файли: {_1}: відображаються їх попередні перегляди @msg {_1} linked image(s) changed on disk: Update Links shows the new versions пов’язаних зображень ({_1}) змінилися на диску: «Оновити зв’язки» покаже нові версії diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 1f3a4fb11..c5ede5168 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -3572,6 +3572,7 @@ mark-style Japanese Kiểu Nhật Texturizer… Texturizer… Thai Tiếng Thái The bounds of the visible art. Vùng giới hạn của đối tượng hiển thị. + The plug-in writes the whole document. Plug-in xuất toàn bộ tài liệu. The cell shows the type object's text as HTML. Ô hiển thị văn bản của đối tượng chữ dưới dạng HTML. The dialog was closed before its file dialog answered Hộp thoại đã đóng trước khi hộp thoại tệp phản hồi The document has no colour swatches. Tài liệu không có mẫu màu. @@ -4636,6 +4637,7 @@ theme Appearance Giao diện @msg {_1} font families available {_1} họ phông chữ khả dụng @msg {_1} has no {_2} style {_1} không có kiểu {_2} @msg {_1} is a live effect (Effect › Plug-ins) {_1} là hiệu ứng trực tiếp (Hiệu ứng › Plug-ins) +@msg {_1} is an exporter (File › Export › Export As…) {_1} là plug-in xuất (Tệp › Xuất › Xuất thành…) @msg {_1} is already used by {_2} {_1} đã được dùng bởi {_2} @msg {_1} linked file(s) can't be read here: their previews show {_1} tệp liên kết không thể đọc ở đây: bản xem trước được hiển thị @msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} ảnh liên kết đã thay đổi trên đĩa: Cập nhật liên kết hiển thị phiên bản mới diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index d63df5b8f..6cdc22e62 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -3096,6 +3096,7 @@ font-script Japanese 汉字 Text Wrap Options 文字绕图选项 Text Wrap Options… 文本绕排选项… The bounds of the visible art. 可见图稿的边界。 + The plug-in writes the whole document. 增效工具会导出整个文档。 The cell shows the type object's text as HTML. 该单元格以 HTML 显示文字对象的内容。 The document has no colour swatches. 文档没有颜色色板。 The edge of the paper the printer can't reach: the art goes inside it, and the preview shows it dashed. 打印机无法触及的纸张边缘:图稿会放在其内侧,预览中会以虚线显示。 @@ -4014,6 +4015,7 @@ imageTrace Outline 轮廓 @msg missing `id` (see plugin.list) 缺少 `id` (请参阅 plugin.list) @msg no plug-in {id} is installed 未安装增效工具 {id} @msg {_1} is a live effect (Effect › Plug-ins) {_1} 是实时效果 (效果 › 增效工具) +@msg {_1} is an exporter (File › Export › Export As…) {_1} 是导出增效工具 (文件 › 导出 › 导出为…) @msg a preset named `{_1}` exists 名为 `{_1}` 的预设已存在 @msg a swatch or color group named `{n}` exists 名为 `{n}` 的色板或颜色组已存在 @msg no print preset named `{base}` 没有名为 `{base}` 的打印预设 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 50ffdca20..ed9539514 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2019,6 +2019,7 @@ file-filter Templates 範本 @msg missing `id` (see plugin.list) 未提供 `id` (請參閱 plugin.list) @msg no plug-in {id} is installed 未安裝增效模組 {id} @msg {_1} is a live effect (Effect › Plug-ins) {_1} 是即時效果 (效果 › 增效模組) +@msg {_1} is an exporter (File › Export › Export As…) {_1} 是匯出增效模組 (檔案 › 匯出 › 匯出為…) @msg a preset named `{_1}` exists 名為 `{_1}` 的預設集已存在 @msg a swatch or color group named `{n}` exists 名為 `{n}` 的色票或顏色群組已存在 @msg no print preset named `{base}` 沒有名為 `{base}` 的列印預設集 @@ -3964,6 +3965,7 @@ font-script Japanese 漢字 Text Wrap Options 文字繞圖選項 Text Wrap Options… 文字繞圖選項… The bounds of the visible art. 可見圖稿的邊界。 + The plug-in writes the whole document. 增效模組會匯出整份文件。 The cell shows the type object's text as HTML. 儲存格以 HTML 顯示文字物件的文字。 The document has no colour swatches. 文件中沒有色票。 The edge of the paper the printer can't reach: the art goes inside it, and the preview shows it dashed. 印表機無法觸及的紙張邊緣:圖稿會放在其內側,預覽中會以虛線顯示。 diff --git a/crates/ui-egui/src/io.rs b/crates/ui-egui/src/io.rs index 55a39f923..9ff9a4d63 100644 --- a/crates/ui-egui/src/io.rs +++ b/crates/ui-egui/src/io.rs @@ -725,9 +725,13 @@ pub fn export(app: &mut VectorcraftApp, format: Option<&str>, path: Option z, _ => f, }; - // Background Export: encoded and written from this snapshot while editing goes on. + // Background Export: encoded and written from this snapshot while editing goes on. The + // encoder is told the file it writes (an exporter plug-in gets its name and extension). let label = format!("Exporting {}", fileio::file_name(&path)); - let (params, target) = (params.clone(), path.clone()); + let (mut params, target) = (params.clone(), path.clone()); + if let Some(o) = params.as_object_mut() { + o.insert("path".into(), json!(path)); + } // The progress dialog's counts and Cancel: a cancelled export stops drawing and never starts // writing. let progress = Progress::new(); diff --git a/docs/control-protocol.md b/docs/control-protocol.md index ad51faa86..ff6911079 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -198,7 +198,9 @@ Plug-in dialogs: `engine.execute {command: "plugin.dialog", params: {id}}` (Obje plug-in at once when it takes no parameters, else opens the `plugin` dialog: one field per declared parameter (named after it), `__plugin` (the plug-in id) and `preview`. It previews `plugin.run` on the canvas while open; `ui.dialog.confirm` keeps the result as one undo step and `ui.dialog.cancel` rolls it back. Live-effect plug-ins open -the `effect` dialog (`effect.dialog {effect: "plugin."}`). `ui.installPlugin {path?}` installs a `.wasm` (without +the `effect` dialog (`effect.dialog {effect: "plugin."}`). An exporter plug-in with parameters opens the +`pluginExport` dialog once Export As has picked the file: one field per parameter, `__plugin`, and `__export` (the +export's own parameters); `ui.dialog.confirm` writes the file. `ui.installPlugin {path?}` installs a `.wasm` (without a path it asks for one); see [plugins.md](plugins.md). Overprint Black: the Edit → Edit Colors → Overprint Black… menu item opens a `command` parameter dialog for diff --git a/docs/gaps.md b/docs/gaps.md index 62157e9ff..02d9239a0 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 (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 (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 @@ -323,8 +323,14 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall ## G17 Ecosystem: third-party plug-ins - **Missing:** Illustrator's ecosystem is a C++ plug-in SDK and a large commercial plug-in market. VectorCraft - has sandboxed WebAssembly plug-ins for object filters and live effects ([plugins.md](plugins.md)) and MCP; - missing are tool plug-ins, panel plug-ins, a plug-in directory and any third-party plug-ins (#710). + has sandboxed WebAssembly plug-ins for object filters, live effects and file formats it writes + ([plugins.md](plugins.md)) and MCP; missing are tool plug-ins, panel plug-ins, importers, a plug-in directory and + any third-party plug-ins (#710). +- **Done:** exporter plug-ins (`"kind": "exporter"`). A plug-in adds a write-only file format that File › Export › + Export As lists after the built-in ones and `document.export` writes (`plugin.`, or a path ending in one of its + 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. - **Estimate:** 20–45 h for the APIs; the ecosystem itself isn't agent work. ## G18 Generative AI features @@ -345,6 +351,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: 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. | | 2026-10-19 | minor | Cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission. Regression coverage; scores and estimates unchanged. | diff --git a/docs/mcp.md b/docs/mcp.md index 51ae5ab0c..330453dc1 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -4005,7 +4005,10 @@ them again at startup (`plugin.remove` deletes them there): schema, defaults). An object filter runs on the selected paths and compound paths with `plugin.run {id, params}` (one undo step; it may change, delete or add objects, and the result is selected). A live-effect plug-in is an effect with id `plugin.`: `effect.apply`, `effect.setParams` and `effect.expandAppearance` work as for built-in effects, and -`effect.list` lists it. A failing plug-in returns an error and leaves the document as it was. +`effect.list` lists it. An exporter adds a write-only file format `plugin.` (listed by `document.formats`): +`document.export {format: "plugin.", params?, path?}` writes it, as does a `path` ending in one of its extensions +that no built-in format has. A failing plug-in returns an error and leaves the document as it was (an export writes +no file). ```json {"name":"run_command","arguments":{"command":"plugin.install","params":{"path":"/plugins/desaturate.wasm"}}} diff --git a/docs/plugins.md b/docs/plugins.md index 8a34dd7a9..c290c63cc 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -5,13 +5,16 @@ arbitrary machine code and cannot work in the browser build. Instead, plug-ins a run in a sandbox. One `.wasm` file works on macOS, Windows, Linux and the web. The design follows PhotoCraft's plug-in API (same sandbox, same manifest and parameter schema), extended for vector documents. -There are two kinds of plug-in: +There are three kinds of plug-in: - **Object filters** read the selected paths and compound paths (geometry, fills, strokes, gradients, opacity) and return the objects to keep, change or add. A run is one undo step. Use them for recolouring, path clean-up, halftones, generative patterns and the like. - **Live effects** rewrite one object's geometry each time it is drawn, from its appearance stack, like the built-in Distort & Transform effects. The art stays editable: move or reshape it and the effect follows. +- **Exporters** write the whole document in a file format of their own, for pen plotters, cutting machines, + laser cutters and the like. File › Export › Export As lists them after the built-in formats, and the host writes the + bytes they return: a plug-in still never touches a file. This page is the contract for plug-in authors and the reference for the host (`crates/plugins`, the `plugin.*` commands in `crates/engine/src/cmd/plugin.rs`). @@ -20,7 +23,7 @@ This page is the contract for plug-in authors and the reference for the host (`c | Command | Params | What it does | |---|---|---| -| `plugin.list` | `{}` | `{"plugins": [{id, name, version, kind, author, description, params, paramsSchema, effect?, size, source}], "folder": report}` | +| `plugin.list` | `{}` | `{"plugins": [{id, name, version, kind, author, description, params, paramsSchema, effect?, format?, extensions?, size, source}], "folder": report}` | | `plugin.info` | `{"id"}` | One plug-in: the `plugin.list` entry plus `manifest`, `defaults` and `lastError` (a live effect's last failure). | | `plugin.run` | `{"id", "params"?: {…}, "ids"?: [...]}` (plug-in params may also be top-level keys) | Runs an object filter on the selection (or `ids`). One undo step; the result is selected. | | `plugin.install` | `{"path": "/x/y.wasm"}` (desktop) or `{"dataBase64": "…", "name"?}`, `"replace": bool = true` | Validates and installs a module; returns its `plugin.list` entry. The desktop app and `vectorcraft-cli` keep it in the `Plug-ins` folder next to the preferences, so it is installed again after a restart (#2541). | @@ -30,12 +33,18 @@ This page is the contract for plug-in authors and the reference for the host (`c Live effects are applied like any effect: `effect.apply {"effect": "plugin.", "params": {…}}`, edited with `effect.setParams`, listed by `effect.list` (menu `Effect › Plug-ins`) and baked by Object › Expand Appearance. +An exporter adds a write-only format, `plugin.`, which `document.formats` lists with its `plugin` id. +`document.export {"format": "plugin.", "params"?: {…}, "path"?}` writes it, and so does a `path` ending in one of +its extensions that no built-in format has; `selectedOnly` hands it the selection, as for the built-in formats. + 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. **File › Open** of a `.wasm` file installs it too (on the web this is -how plug-ins get in). +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). **Preference folder.** Edit › Preferences › Performance & Storage › *Additional Plug-ins Folder*: when set, every `*.wasm` in that folder (not recursive) is installed at startup and whenever the preference changes (desktop builds; @@ -57,11 +66,14 @@ The host is [`wasmi`](https://github.com/wasmi-labs/wasmi), a pure-Rust WebAssem | Module size | 32 MiB | rejected | | Linear memory | 512 MiB per instance (input and parameters: at most half) | `memory.grow` returns -1; oversized input or initial memory fails | | Output | 64 MiB of JSON | `Limit("output size")` | -| Instructions (fuel) | 50 M per call + 4,000 per input byte for `vc_run` | `Limit("instruction budget")` | -| Wall clock | 60 s per filter run, 5 s per live-effect run (desktop; checked every 20 M instructions) | `Limit("time budget")` | +| Instructions (fuel) | 50 M per call + 4,000 per input byte for `vc_run`; an export 10 G + 4,000 per byte | `Limit("instruction budget")` | +| Wall clock | 60 s per filter run, 5 s per live-effect run, 300 s per export (desktop; checked every 20 M instructions) | `Limit("time budget")` | | Call depth | 1,024 frames | trap | | Module shape | wasmi's strict limits (≤ 10,000 functions, 1 memory, …) | rejected | +An export runs where the export runs: with Background Export on, on the export's worker, whose Cancel stops the +plug-in between two of those 20 M-instruction slices; nothing is written then. + Every run gets a **fresh instance**, so no state survives between calls. Traps, exhausted budgets, malformed modules, bad manifests, out-of-range pointers, negative return codes and invalid output all become errors, and the document is left untouched: a filter's output is checked completely before one edit applies it. A live effect that @@ -109,7 +121,12 @@ pointers are offsets into the module's memory. - `id`: 1–64 characters from `A-Z a-z 0-9 . _ -` (reverse-DNS recommended). Installing a module with the same id replaces the old one. A live effect's effect id is `plugin.`. -- `name`: the menu label (1–64 printable characters). `kind`: `"filter"` (object filter) or `"effect"` (live effect). +- `name`: the menu label, or an exporter's format name in Export As (1–64 printable characters). `kind`: `"filter"` + (object filter), `"effect"` (live effect) or `"exporter"`. Exporters came after the first ABI v1 hosts, which refuse + the kind when the plug-in is installed. +- `extensions` (exporters only, and required): 1–8 file extensions, lower-case letters and digits without the dot + (`["plt"]`); the first is the one Export As gives the file. A built-in format keeps an extension both list (`svg`): + `document.export` then writes the exporter's format only when asked for by its format id. - `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 @@ -123,6 +140,12 @@ paths are left out, and so are other kinds of object (type, images, symbols, liv touches. With nothing selected the list is empty (a generator then adds its own objects). A live effect gets one object, holding only geometry (`type`, `path`, `fillRule`, `bounds`). +An **exporter** gets something else: the document as a native `.vectorcraft` file's JSON, as `document.save` writes it +but without indentation (the format is documented in [`crates/format/src/lib.rs`](../crates/format/src/lib.rs)). That is every layer, object, +appearance and effect record, artboard, swatch and symbol, so an exporter reads whatever the document holds; it checks +`version`, since a later host may write a newer one. Images of 8 KiB or more are listed in `images` with their MIME +type and `blob` but without their data, which follows the JSON in a saved file; smaller ones keep their base64 `data`. + ```json {"objects": [ {"id": 12, "type": "path", "name": "Leaf", @@ -161,9 +184,11 @@ object, holding only geometry (`type`, `path`, `fillRule`, `bounds`). "_context": {"mode": "filter", "bounds": [10, 0, 75, 60], "artboard": [0, 0, 612, 792]}} ``` -`mode` is `"filter"` or `"effect"`; `bounds` the selection's geometric bounds (a live effect: the reference box of the -effect, the object's bounds or those of the previous effect's result); `artboard` (filters) the artboard under the -selection's centre, else the first. +`mode` is `"filter"`, `"effect"` or `"export"`; `bounds` the selection's geometric bounds (a live effect: the reference +box of the effect, the object's bounds or those of the previous effect's result); `artboard` (filters) the artboard +under the selection's centre, else the first. An export's `_context` is `{"mode": "export", "extension": "plt", +"name": "Poster"}` instead: the extension of the file being written (one the exporter lists) and its name without the +extension (the document's name when the export is not given a path), which some formats keep in their header. ### Output @@ -189,6 +214,10 @@ keeps its swatch link; any other paint is unlinked. For a **live effect**, the geometry of every returned object's `path` (their subpaths, in order) replaces the object's; the rest is ignored (the object keeps its own appearance). An empty list removes the geometry. +An **exporter** returns `{"dataBase64": "…", "warnings"?: ["…"]}`: the file, in base64, which the host writes as it +is, and what the export left out or changed, for the user (at most 100 warnings of at most 500 characters each, shown +with the export's own). A decline (`{"error": …}`), a missing `dataBase64` or data that isn't base64 writes nothing. + ## Writing a plug-in in Rust [`crates/plugins/example`](../crates/plugins/example) is a complete object filter, **Desaturate**: it moves every @@ -204,7 +233,8 @@ The script also refreshes `crates/plugins/tests/fixtures/desaturate.wasm`, the t The crate is not a workspace member. Any language that targets core WebAssembly works (C, Zig, AssemblyScript, WAT…), as long as the module has no imports: in Rust, `std` on `wasm32-unknown-unknown` is fine as long as it never touches WASI. `vectorcraft_plugins::wat::module` is the skeleton of a plug-in in WebAssembly text, which the tests -build their plug-ins from. +build their plug-ins from; `wat::echo_exporter` is an exporter whose file is its input or its parameters (with a +base64 encoder written out in WebAssembly text), which the tests read back what the host hands a plug-in with. ## Performance @@ -215,7 +245,8 @@ per change of their input (results are cached), not per frame. ## Not in v1 - Tool plug-ins (pointer events on the canvas, a guide line or rectangle, geometry back), panel plug-ins, host - callbacks (progress, cancel), plug-in file formats. + callbacks (a plug-in reporting progress or asking whether it was cancelled), importers (plug-in formats that open + files). - Live effects that change paint (they reshape geometry; an object filter recolours), and filters on type, images or symbols. - Apply Last Effect and Last Effect work for plug-in effects; there is no "last filter" for object filters yet. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 3c0b75b91..cafe4c0a8 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 (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 (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), @@ -831,6 +831,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Blend, mesh and envelope numbers given as text:** `object.blend.make` and `object.blend.options` (`steps`, `distance`, `value`), `object.envelope.makeWithWarp`, `resetWithWarp` and `options` (`bend`, `h`, `v`, `horizontalDistortion`, `verticalDistortion`), `object.envelope.makeWithMesh` and `resetWithMesh` (`rows`, `cols`, `columns`), `object.envelope.options` (`fidelity`), `object.mesh.create` (`rows`, `cols`, `columns`, `highlight`) and `object.mesh.setPointColor` (`opacity`) refuse a value other than a number or null, such as `"8"`, and change nothing. `object.mesh.create` refuses an `at` that is not `[x, y]`. +- **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 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. @@ -846,6 +848,7 @@ Evidence for the shared light Color: geometry tests in `tests_lighting`, effect | Date | Change | Summary | |---|---|---| +| 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. | | 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. | From 30f372a220fd8ccb3893c04a710f09f19366b66e Mon Sep 17 00:00:00 2001 From: Louise Fox <2503492+KaboomFox@users.noreply.github.com> Date: Mon, 19 Oct 2026 19:15:01 +0000 Subject: [PATCH 2/2] Contributors: credit KaboomFox by their GitHub profile name Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un --- contributors/people.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contributors/people.toml b/contributors/people.toml index d2f765745..62fcd8d18 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -137,3 +137,6 @@ sync_github_name = true [people.MiQ71] real_name = "Mirko Kerezovic" sync_github_name = false + +[people.KaboomFox] +sync_github_name = true