mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 06:35:03 +00:00
Plug-ins: a light filter that reports progress runs in the background with Cancel
A light filter of plug-in API level 2 with the `progress` permission ran on the UI thread, so its progress reports and `vc_cancelled` never reached a progress window. It now runs in the background under its light budget, as a heavy filter does, when it runs at once from Object › Plug-ins or its shortcut, or when OK runs it with Preview off. The progress window shows its share and Cancel stops it. With Preview on, OK keeps the previewed run. A filter that lists a `commandAllowlist` still runs on the UI thread. Docs: docs/plugins.md, docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -1493,7 +1493,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"plugin.dialog",
|
||||
"Plug-in…",
|
||||
"",
|
||||
"{id: an object filter plug-in (plugin.list), params?: {…}} Object › Plug-ins: run the filter at once when it takes no parameters or params are given (plugin.run), else open its dialog (dialog `plugin`: a field per parameter, `__plugin` the id, live preview; OK runs plugin.run as one undo step)",
|
||||
"{id: an object filter plug-in (plugin.list), params?: {…}} Object › Plug-ins: run the filter at once when it takes no parameters or params are given (plugin.run), else open its dialog (dialog `plugin`: a field per parameter, `__plugin` the id, live preview; OK runs plugin.run as one undo step). A heavy filter, and a filter that lists `progress` and no commandAllowlist, run in the background when they run at once or OK runs them without a kept preview → {background: true, status} (see plugin.cancelRuns)",
|
||||
),
|
||||
(
|
||||
"ui.installPlugin",
|
||||
@@ -1505,13 +1505,13 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"plugin.cancelRuns",
|
||||
"Cancel Plug-in Run",
|
||||
"",
|
||||
"{} stop every heavy filter running in the background (its progress window's Cancel): it stops at the next slice of its budget and changes nothing → {cancelled: n}",
|
||||
"{} stop every filter running in the background (its progress window's Cancel): it stops at the next slice of its budget and changes nothing → {cancelled: n}",
|
||||
),
|
||||
(
|
||||
"plugin.hideRuns",
|
||||
"Continue in Background",
|
||||
"",
|
||||
"{} close the progress window of the heavy filters running in the background; they go on, shown in the status bar → {hidden: n}",
|
||||
"{} close the progress window of the filters running in the background; they go on, shown in the status bar → {hidden: n}",
|
||||
),
|
||||
(
|
||||
"imageTrace.cancelRuns",
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
//! 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. A heavy filter (its manifest's `budget`) has no Preview, since a run may take minutes:
|
||||
//! it runs in the background ([`crate::plugin_runs`]). A filter or live effect with commands of
|
||||
//! it runs in the background ([`crate::plugin_runs`]). A light filter that reports its progress
|
||||
//! (plug-in API level 2 with the `progress` permission) and calls no commands runs in the
|
||||
//! background too, with its progress and Cancel, when it runs at once or when OK runs it without a
|
||||
//! kept preview ([`in_background`]). A filter or live effect with commands of
|
||||
//! its own is a submenu named after it, holding its item and then its commands; a command with
|
||||
//! parameters opens the same dialog for them ([`open_command`]). Exporters are formats of File › Export ›
|
||||
//! Export As; one with parameters opens dialog `pluginExport` once the file is picked, and its
|
||||
@@ -200,12 +203,19 @@ fn params(d: &Dialog) -> Value {
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs filter `plugin` with `plugin.run`'s params `p`: at once, or in the background when it is
|
||||
/// heavy.
|
||||
/// Does filter `m` run in the background ([`crate::plugin_runs`]) rather than on the UI thread? A
|
||||
/// heavy one does, and so does a light one that reports its progress (the `progress` permission)
|
||||
/// unless it calls commands, which run only in the session.
|
||||
fn in_background(m: &Manifest) -> bool {
|
||||
m.budget == Budget::Heavy || (m.permissions().contains(&permissions::Permission::Progress) && m.command_allowlist.is_empty())
|
||||
}
|
||||
|
||||
/// Runs filter `plugin` with `plugin.run`'s params `p`: at once, or in the background
|
||||
/// ([`in_background`]).
|
||||
fn run(app: &mut VectorcraftApp, plugin: &Plugin, p: Value) -> Result<Value, String> {
|
||||
match plugin.manifest().budget {
|
||||
Budget::Light => app.run(CMD, p),
|
||||
Budget::Heavy => crate::plugin_runs::start(app, p),
|
||||
match in_background(plugin.manifest()) {
|
||||
false => app.run(CMD, p),
|
||||
true => crate::plugin_runs::start(app, p),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,11 +244,18 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// OK: a filter keeps its previewed run as one undo step; a heavy one, which has no preview, starts
|
||||
/// in the background ([`run`]) and the dialog closes.
|
||||
/// OK: a filter keeps its previewed run as one undo step; a heavy one, which has no preview, and a
|
||||
/// light one that runs in the background ([`in_background`]) with Preview off, start in the
|
||||
/// background ([`run`]) and the dialog closes. A preview still shown when Preview was just turned
|
||||
/// off is rolled back first.
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let own_run = d.str("__command").is_empty();
|
||||
let kept = |plugin: &Plugin| plugin.manifest().budget == Budget::Light && d.bool("preview") && app.session.in_interaction();
|
||||
match registry::get(&d.str("__plugin")) {
|
||||
Some(plugin) if plugin.manifest().budget == Budget::Heavy => {
|
||||
Some(plugin) if own_run && in_background(plugin.manifest()) && !kept(&plugin) => {
|
||||
if d.fields.contains_key(form::PREVIEWED) && app.session.in_interaction() {
|
||||
let _ = app.session.cancel_interaction();
|
||||
}
|
||||
let r = run(app, &plugin, params(d));
|
||||
if r.is_ok() {
|
||||
app.ui.dialog = None;
|
||||
|
||||
@@ -1494,7 +1494,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"plugin.dialog",
|
||||
"Plug-in…",
|
||||
"",
|
||||
"{id: an object filter plug-in (plugin.list), params?: {…}} Object › Plug-ins: run the filter at once when it takes no parameters or params are given (plugin.run), else open its dialog (dialog `plugin`: a field per parameter, `__plugin` the id, live preview; OK runs plugin.run as one undo step)",
|
||||
"{id: an object filter plug-in (plugin.list), params?: {…}} Object › Plug-ins: run the filter at once when it takes no parameters or params are given (plugin.run), else open its dialog (dialog `plugin`: a field per parameter, `__plugin` the id, live preview; OK runs plugin.run as one undo step). A heavy filter, and a filter that lists `progress` and no commandAllowlist, run in the background when they run at once or OK runs them without a kept preview → {background: true, status} (see plugin.cancelRuns)",
|
||||
),
|
||||
(
|
||||
"ui.installPlugin",
|
||||
@@ -1506,13 +1506,13 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"plugin.cancelRuns",
|
||||
"Cancel Plug-in Run",
|
||||
"",
|
||||
"{} stop every heavy filter running in the background (its progress window's Cancel): it stops at the next slice of its budget and changes nothing → {cancelled: n}",
|
||||
"{} stop every filter running in the background (its progress window's Cancel): it stops at the next slice of its budget and changes nothing → {cancelled: n}",
|
||||
),
|
||||
(
|
||||
"plugin.hideRuns",
|
||||
"Continue in Background",
|
||||
"",
|
||||
"{} close the progress window of the heavy filters running in the background; they go on, shown in the status bar → {hidden: n}",
|
||||
"{} close the progress window of the filters running in the background; they go on, shown in the status bar → {hidden: n}",
|
||||
),
|
||||
(
|
||||
"imageTrace.cancelRuns",
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
//! A filter of plug-in API level 2 may report how far it has got (`vc_progress`): the progress
|
||||
//! window then shows a bar with the share done. Its Cancel also makes the filter's `vc_cancelled`
|
||||
//! return 1, so a filter that checks it stops at once; one that doesn't stops at the next slice of
|
||||
//! its budget.
|
||||
//! its budget. A light filter that may report its progress (the `progress` permission) and calls
|
||||
//! no commands runs here too, under its light budget, so its progress and Cancel show while it
|
||||
//! computes.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::{Receiver, TryRecvError, channel};
|
||||
@@ -152,7 +154,7 @@ fn finish(app: &mut VectorcraftApp, r: Running, done: Option<Done>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The progress window's Cancel (also Esc): every heavy filter running stops (at once when it checks
|
||||
/// The progress window's Cancel (also Esc): every filter running in the background stops (at once when it checks
|
||||
/// `vc_cancelled`, else at the next slice of its budget) and changes nothing → `{cancelled: n}`.
|
||||
pub fn cancel_all(app: &mut VectorcraftApp) -> Value {
|
||||
let mut n = 0;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Heavy plug-in filters in the app ([`crate::plugin_runs`]): installing one asks first, its dialog
|
||||
//! has no Preview, and it runs off the UI thread, its result applied as one undo step once it is
|
||||
//! done, unless the document changed meanwhile. Cancel stops it; its progress window shows it.
|
||||
//! done, unless the document changed meanwhile. Cancel stops it; its progress window shows it. A
|
||||
//! light filter that reports its progress runs there too.
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
@@ -214,6 +215,130 @@ fn a_filter_that_reports_progress_shows_a_bar_and_sees_cancel() {
|
||||
assert_eq!(app.ui.status, vectorcraft_engine::EngineError::Cancelled.to_string());
|
||||
}
|
||||
|
||||
/// The triangle the light test filters return.
|
||||
const TRIANGLE: &str = r#"{"objects":[{"path":{"subpaths":[{"anchors":[{"p":[0,0]},{"p":[30,0]},{"p":[15,20]}]}]}}]}"#;
|
||||
|
||||
/// Installs a light filter `id` of plug-in API level 2 with `params` that returns [`TRIANGLE`],
|
||||
/// granted as the user would grant it. With `progress` it lists the `progress` permission, and its
|
||||
/// `vc_run` first runs `first`, which may report its progress.
|
||||
fn install_light(app: &mut VectorcraftApp, id: &str, progress: bool, params: &str, first: &str) {
|
||||
let may: &[&str] = if progress { &["doc.read", "doc.write", "progress"] } else { &["doc.read", "doc.write"] };
|
||||
let manifest = format!(r#"{{"id":"{id}","name":"Test {id}","kind":"filter","api":2,"permissions":{},"params":{params}}}"#, json!(may));
|
||||
// Without `progress` the module may not import the host functions.
|
||||
let src = if progress { tpl::reporting(&manifest, first, TRIANGLE) } else { tpl::returning(&manifest, TRIANGLE) };
|
||||
let data = vectorcraft_format::base64_encode(&wat::parse_str(src).unwrap());
|
||||
app.run("plugin.install", json!({"dataBase64": data, "grant": may})).unwrap();
|
||||
}
|
||||
|
||||
/// Applies the runs' results as the frames would, until none is left (each run ends: it returns,
|
||||
/// or its budget stops it).
|
||||
fn finish_runs(app: &mut VectorcraftApp) {
|
||||
while !app.plugin_runs.running.is_empty() {
|
||||
plugin_runs::poll(app);
|
||||
std::thread::yield_now();
|
||||
}
|
||||
}
|
||||
|
||||
/// A light filter that may report its progress runs off the UI thread when it runs at once, as a
|
||||
/// heavy one does: the status bar says so, and its result is one undo step named after it.
|
||||
#[test]
|
||||
fn a_light_filter_that_reports_progress_runs_off_the_ui_thread() {
|
||||
let mut app = app();
|
||||
let id = "test.ui.light.progress.run";
|
||||
install_light(&mut app, id, true, "{}", "(call $vc_progress (i64.const 1) (i64.const 2))");
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
let r = app.run("plugin.dialog", json!({"id": id})).unwrap();
|
||||
assert_eq!(r["background"], true, "{r}");
|
||||
assert_eq!(app.ui.status, format!("Running “Test {id}”…"));
|
||||
finish_runs(&mut app);
|
||||
let st = app.session.doc().unwrap();
|
||||
assert_eq!(st.history.undo.len(), undo + 1, "one undo step");
|
||||
assert_eq!(st.history.undo.last().unwrap().label, format!("Test {id}"));
|
||||
assert_eq!(st.selection.objects.len(), 1, "the triangle it added, selected");
|
||||
}
|
||||
|
||||
/// A light filter that reports its progress shows it in the progress window, and Cancel there
|
||||
/// makes its `vc_cancelled` return 1: it stops, changing nothing.
|
||||
#[test]
|
||||
fn a_light_filter_s_progress_window_shows_its_share_and_cancel_stops_it() {
|
||||
let mut app = app();
|
||||
let id = "test.ui.light.progress.cancel";
|
||||
let first = "(call $vc_progress (i64.const 250) (i64.const 1000)) (block $stop (loop $l (br_if $stop (call $vc_cancelled)) (br $l)))";
|
||||
install_light(&mut app, id, true, "{}", first);
|
||||
let doc = app.session.doc().unwrap().doc.clone();
|
||||
assert_eq!(app.run("plugin.dialog", json!({"id": id})).unwrap()["background"], true);
|
||||
// Waits for the run's thread to report the share (a handshake: the thread never stops before
|
||||
// it is cancelled or its budget runs out, and a run that ended leaves the loop).
|
||||
loop {
|
||||
plugin_runs::poll(&mut app);
|
||||
match app.plugin_runs.running.first() {
|
||||
Some(r) if r.progress.counts() != (250, 1000) => std::thread::yield_now(),
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
let ctx = egui::Context::default();
|
||||
theme::install_fonts(&ctx);
|
||||
progress(&mut app, &ctx, 0.0);
|
||||
progress(&mut app, &ctx, 1.0);
|
||||
let shown = progress(&mut app, &ctx, 1.1);
|
||||
assert!(shown.contains(&format!("Running “Test {id}”…")) && shown.contains("25 %") && shown.contains("Cancel"), "{shown}");
|
||||
assert_eq!(app.run("plugin.cancelRuns", json!({})).unwrap()["cancelled"], 1);
|
||||
finish_runs(&mut app);
|
||||
assert_eq!(app.session.doc().unwrap().doc, doc);
|
||||
assert_eq!(app.ui.status, vectorcraft_engine::EngineError::Cancelled.to_string());
|
||||
}
|
||||
|
||||
/// In the dialog of a light filter that reports its progress, OK with Preview off runs it in the
|
||||
/// background, also right after Preview was turned off; with Preview on, OK keeps the previewed
|
||||
/// run, as for any light filter.
|
||||
#[test]
|
||||
fn ok_runs_a_light_progress_filter_in_the_background_without_a_preview() {
|
||||
let mut app = app();
|
||||
let id = "test.ui.light.progress.dialog";
|
||||
install_light(&mut app, id, true, r#"{"size":{"type":"number","min":1,"max":9,"default":3}}"#, "");
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
app.run("plugin.dialog", json!({"id": id})).unwrap();
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("preview".into(), json!(false));
|
||||
frame(&mut app);
|
||||
assert!(!app.session.in_interaction(), "nothing previewed");
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
assert!(app.ui.dialog.is_none());
|
||||
assert_eq!(app.plugin_runs.running.len(), 1, "running in the background");
|
||||
finish_runs(&mut app);
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1);
|
||||
// Preview on: OK keeps the previewed run, and nothing runs in the background.
|
||||
app.run("plugin.dialog", json!({"id": id})).unwrap();
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().fields.get("preview"), Some(&json!(true)));
|
||||
frame(&mut app);
|
||||
assert!(app.session.in_interaction(), "previewed");
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
assert!(app.plugin_runs.running.is_empty());
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 2);
|
||||
// Preview turned off after a preview and OK before the next frame: the preview is rolled back
|
||||
// and the run goes to the background.
|
||||
app.run("plugin.dialog", json!({"id": id})).unwrap();
|
||||
frame(&mut app);
|
||||
assert!(app.session.in_interaction(), "previewed");
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("preview".into(), json!(false));
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
assert!(!app.session.in_interaction(), "the preview was rolled back");
|
||||
assert_eq!(app.plugin_runs.running.len(), 1, "running in the background");
|
||||
finish_runs(&mut app);
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 3);
|
||||
}
|
||||
|
||||
/// A light filter without the `progress` permission still runs at once, on the UI thread.
|
||||
#[test]
|
||||
fn a_light_filter_that_does_not_report_progress_runs_at_once() {
|
||||
let mut app = app();
|
||||
let id = "test.ui.light.plain";
|
||||
install_light(&mut app, id, false, "{}", "");
|
||||
let r = app.run("plugin.dialog", json!({"id": id})).unwrap();
|
||||
assert!(r.get("background").is_none(), "{r}");
|
||||
assert!(app.plugin_runs.running.is_empty());
|
||||
assert_eq!(app.session.doc().unwrap().selection.objects.len(), 1, "the triangle, added at once");
|
||||
}
|
||||
|
||||
/// `CRAFT_UI_HEAVY_FILTER_WASM=<folder>` writes a heavy test filter to install in the app
|
||||
/// (`order-paths-test-filter.wasm`): it computes until it is cancelled or its budget runs out, so
|
||||
/// its progress window stays up for a picture.
|
||||
|
||||
+3
-1
@@ -7464,7 +7464,9 @@ grant in the store. A stored module that is disabled, or not read in Safe Mode,
|
||||
filter without `doc.write` or `state` that returns output needing them gets an error and the document is unchanged.
|
||||
A plug-in of API level 2 (`api: 2` in its entry) that lists `progress` may report its progress, see Cancel and write
|
||||
log lines: `notifications/cancelled` makes its `vc_cancelled` return 1, and its log lines go to the server's log
|
||||
under its id. One that lists `settings` or `cache` keeps settings or a cache of its own in the `Plug-ins` folder
|
||||
under its id. With a desktop app connected, `plugin.dialog` runs such a filter (one without a `commandAllowlist`) in the
|
||||
background when it runs at once or OK runs it with Preview off, and returns `{background: true, status}`;
|
||||
`plugin.cancelRuns` stops it. One that lists `settings` or `cache` keeps settings or a cache of its own in the `Plug-ins` folder
|
||||
(VectorCraft's own caps: 256 settings and 1 MiB, 512 MiB of cache): `plugin.list` and `plugin.info` give the cache's
|
||||
size as `cacheBytes`, `plugin.clearCache {id?}` empties the cache of one plug-in or of all of them and returns
|
||||
`{cleared: [id], bytesFreed}`, replacing a plug-in keeps both, and `plugin.remove` deletes both (also for an id that
|
||||
|
||||
+5
-1
@@ -336,7 +336,11 @@ still returns `1`. `vc_progress`, `vc_cancelled` and `vc_log` need `progress`; t
|
||||
and one the manifest doesn't allow (an `api` below 2, or without the import's permission) refuse the module, and the
|
||||
error names the import and the permission.
|
||||
- A filter's or an exporter's progress is shown as a share: a heavy filter's progress window and the export's progress
|
||||
window draw a bar with a percentage, and Cancel there makes `vc_cancelled` return `1`. A plug-in that checks it can
|
||||
window draw a bar with a percentage, and Cancel there makes `vc_cancelled` return `1`. In the desktop app a light
|
||||
filter that lists `progress` runs in the background as a heavy one does, under its light budget, when it runs at once
|
||||
from Object › Plug-ins or its shortcut, or when OK runs it with Preview off; with Preview on, its dialog keeps the
|
||||
previewed run. A filter that also lists a `commandAllowlist` runs where it is called, since its command calls need
|
||||
the document. A plug-in that checks `vc_cancelled` can
|
||||
stop early, and the host discards what it returns after the Cancel or after its time budget ran out; one that
|
||||
ignores it is still stopped by the instruction and time budgets. With a known total, a report is passed on only when
|
||||
it reaches another tenth of a percent; with an unknown total, each change of `done` is passed on. After 2,000 reports
|
||||
|
||||
@@ -1655,7 +1655,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **A plug-in with no permissions ends its id's grant (G17):** installing a module that lists no permissions under a granted id forgets the grant, in the process and in `grants.json`, so the formerly approved bytes need a new approval. Attaching a `Plug-ins` folder whose module for that id lists no permissions forgets it too, and plug-in folders forget the process's grant. A refused install leaves the grant. Regressions: `crates/engine/src/tests/plugins.rs`, `crates/engine/tests/plugin_safe_mode.rs`. Scores and hours unchanged.
|
||||
- **Plug-in commands (G17):** a filter or live effect may declare up to 32 `commands` (`{id, label, params?}`); each is the command `plugin.<id>.<command>` while the plug-in is installed, in a submenu named after the plug-in under Object › Plug-ins (Effect › Plug-ins for a live effect) whatever menu the manifest names, with its label escaped. Commands are recorded in actions, listed in Keyboard Shortcuts for the user to give a shortcut, and run through `run` and MCP with their parameters checked first (a command with parameters opens a dialog from its menu item or shortcut). A run uses the plug-in's permissions (a live effect with commands may also hold `doc.write` and `state`, and gets them without a `permissions` list), is one undo step named after the command, and on failure (a trap or an exhausted instruction budget) leaves the document and the undo history as they were with an error naming the plug-in and the command. For a command and for `plugin.run`, an `ids` that is not a list of object ids is an error rather than the selection. A malformed entry is refused by name when the module is installed. Regressions: `crates/plugins/src/manifest.rs`, `crates/plugins/src/permissions.rs`, `crates/engine/src/tests/plugins.rs`, `crates/engine/tests/plugin_fuzz.rs`, `crates/ui-egui/src/tests/plugins.rs`. Scores and hours unchanged.
|
||||
- **Filter plug-ins call commands (G17, #2186):** a filter at plug-in API level 2 with the `commands` permission names the commands it may run in its manifest's `commandAllowlist` and runs them with `env.vc_call`, reading each result with `env.vc_call_result`. Each call sees what the calls before it changed; the calls and the filter's output are one undo step, and a run that fails, is canceled or makes a call that fails leaves every document, its selection and its history as they were. Commands that read or write files, use the clipboard, change preferences or the view, run scripts, actions or plug-ins, switch documents or undo are never called, and a manifest that lists one is refused when the module is installed. The install dialog lists the allowed commands. A run makes at most 1,000 calls, and each call costs 10,000 instructions of its budget. Regressions: `crates/plugins/tests/call.rs`, `crates/engine/tests/plugin_call.rs`, `crates/engine/src/cmd/plugin_call.rs`. Scores and hours unchanged.
|
||||
- **Plug-in progress, Cancel and log lines (G17):** at plug-in API level 2 a plug-in with the `progress` permission imports `env.vc_progress`, `env.vc_cancelled` and `env.vc_log`. A heavy filter's and an export's progress windows show its progress as a percentage bar, their Cancel reaches it through `vc_cancelled`, and its log lines go to the app's log with its id, capped per line and per run. Installing checks every import and names the one it refuses. Regressions: `crates/plugins/src/host.rs`, `crates/plugins/tests/abi.rs`, `crates/engine/src/tests/plugins.rs`, `crates/engine/tests/plugin_fuzz.rs`, `crates/ui-egui/src/tests/plugin_runs.rs`. Scores and hours unchanged.
|
||||
- **Plug-in progress, Cancel and log lines (G17):** at plug-in API level 2 a plug-in with the `progress` permission imports `env.vc_progress`, `env.vc_cancelled` and `env.vc_log`. A heavy filter's and an export's progress windows show its progress as a percentage bar, their Cancel reaches it through `vc_cancelled`, a light filter with `progress` and no `commandAllowlist` runs in the background with the same progress window when it runs at once or OK runs it with Preview off, and its log lines go to the app's log with its id, capped per line and per run. Installing checks every import and names the one it refuses. Regressions: `crates/plugins/src/host.rs`, `crates/plugins/tests/abi.rs`, `crates/engine/src/tests/plugins.rs`, `crates/engine/tests/plugin_fuzz.rs`, `crates/ui-egui/src/tests/plugin_runs.rs`. Scores and hours unchanged.
|
||||
- **Live-effect plug-ins paint their output (G17, #121):** a live effect whose manifest has `"api": 2` and `"paintOutput": true` returns pieces, each drawn by itself in order with fills and strokes of its own in the filter's paint forms (a piece without them keeps the object's; an empty list paints none; an effect on a fill returns only fills, one on a stroke only strokes). The whole result is checked first: limits, numbers in range with nothing clamped, known keys only, and the swatches and patterns it names in the document; one fault draws that step of the stack unchanged, keeps the effect record and puts the plug-in's name and the field in `plugin.info`'s `lastError`. The pieces are drawn, measured and exported to PDF and SVG from the appearance stack without changing the document, and Expand Appearance writes them as ordinary paths in one undo step. Effects without the key behave as before. Regressions: `crates/plugins/src/painted/tests.rs`, `crates/plugins/src/manifest.rs`, `crates/engine/src/tests/plugins.rs`, `crates/engine/tests/plugin_fuzz.rs`. Scores and hours unchanged.
|
||||
- **Live-effect plug-ins while a document opens (G17):** the live-effect plug-in runs of a document's first view on screen share a 2,000 ms budget (VectorCraft's own value). A stage left out for time, or one that times out, fails, has no plug-in or meets Safe Mode, draws its input unchanged and is deferred; the status bar says how many, `effect.deferredStatus` lists them with their object, effect and reason, and Effect › Refresh Deferred Effects (`effect.refreshDeferred`) runs them again with a fresh budget. Each document has its own budget, an edit runs the stages it changes, and exports, Expand Appearance and commands run every stage under the 5 s limit of each run. The web build times the budget and each run's limit with the page's `performance.now()`. Regressions: `crates/plugins/src/deferred.rs`, `crates/plugins/src/effect.rs`, `crates/plugins/src/host.rs`, `crates/engine/src/tests/plugins.rs`, `crates/ui-egui/src/canvas/tests.rs`. Scores and hours unchanged.
|
||||
- **Plug-in settings and cache (G17, #2191):** at plug-in API level 2 a plug-in with the `settings` permission keeps settings of its own (`env.vc_settings_get`, `_set`, `_delete`) and one with `cache` a cache the user can clear (`env.vc_cache_read`, `_write`, `_clear`), under its id in the `Plug-ins` folder. Keys are plain names, never paths; VectorCraft's own caps refuse a write past 256 settings, 1 MiB of settings or 512 MiB of cache; a damaged settings file is moved aside and the settings start empty. `plugin.clearCache` empties a cache, `plugin.list` and `plugin.info` give `cacheBytes`, replacing a plug-in keeps its storage and `plugin.remove` deletes it. Another module under an id that is installed or has storage installs only with `replace: true`, which the app asks the user for; each storage call counts the whole settings file or cache entry it reads and writes toward the run's 1 GiB. Regressions: `crates/plugins/src/storage.rs`, `crates/plugins/tests/abi.rs`, `crates/engine/src/tests/plugins.rs`, `crates/engine/tests/plugin_clear_cache.rs`, `crates/engine/tests/plugin_fuzz.rs`. Scores and hours unchanged.
|
||||
@@ -1935,7 +1935,7 @@ Edit › Keyboard Shortcuts lists every item of the Effect menu under its menu p
|
||||
|
||||
A live-effect plug-in installed while the app runs gets its Effect › Plug-ins row in Edit › Keyboard Shortcuts, in `shortcuts.list` and in a Keyboard Shortcuts dialog that is already open, and its key runs it. A removed plug-in's row goes away and its key runs nothing; the shortcut stays saved and applies again when a plug-in with the same id is installed, opening the effect's dialog if the new one has options and applying the effect if it has none. Tests: `shortcuts::tests::a_live_effect_plugin_installed_later_takes_a_shortcut_of_its_own`, `shortcut_editor::tests::an_open_dialog_follows_live_effect_plugins`.
|
||||
|
||||
Each installed filter plug-in's Object › Plug-ins item has a row of its own (stored as `plugin:<id>`, since every filter runs `plugin.dialog {id}`), with no default shortcut, also when two filters share a name. The menu shows its shortcut, and pressing it does what a click on the item does: it opens the filter's dialog, or runs a filter without parameters at once as one undo step (a heavy filter starts in the background). A removed filter's row goes away and its key runs nothing; the shortcut stays saved and applies again when a plug-in with the same id is installed. With no document open the key does nothing, as the dimmed item does. The command palette has one result per filter, and Import Shortcut Set offers each filter as a target without matching a row to it by name. Plug-in names stay as the plug-in gives them; the menu path is translated. Tests: `shortcuts::tests::each_filter_plugin_takes_a_shortcut_of_its_own`, `shortcuts::tests::a_filter_plugin_keeps_its_shortcut_when_replaced_or_reloaded`, `shortcuts::tests::a_heavy_filter_plugin_runs_from_its_shortcut_as_from_its_menu_item`, `shortcuts::tests::a_filter_plugin_row_translates_its_path_and_not_its_name`, `palette::tests::each_filter_plugin_is_a_result_of_its_own`, `dialogs::tests_shortcut_import::an_imported_shortcut_reaches_a_filter_plugin_picked_as_its_target`.
|
||||
Each installed filter plug-in's Object › Plug-ins item has a row of its own (stored as `plugin:<id>`, since every filter runs `plugin.dialog {id}`), with no default shortcut, also when two filters share a name. The menu shows its shortcut, and pressing it does what a click on the item does: it opens the filter's dialog, or runs a filter without parameters at once as one undo step (a heavy filter, and a light one that lists `progress` and no `commandAllowlist`, starts in the background). A removed filter's row goes away and its key runs nothing; the shortcut stays saved and applies again when a plug-in with the same id is installed. With no document open the key does nothing, as the dimmed item does. The command palette has one result per filter, and Import Shortcut Set offers each filter as a target without matching a row to it by name. Plug-in names stay as the plug-in gives them; the menu path is translated. Tests: `shortcuts::tests::each_filter_plugin_takes_a_shortcut_of_its_own`, `shortcuts::tests::a_filter_plugin_keeps_its_shortcut_when_replaced_or_reloaded`, `shortcuts::tests::a_heavy_filter_plugin_runs_from_its_shortcut_as_from_its_menu_item`, `shortcuts::tests::a_filter_plugin_row_translates_its_path_and_not_its_name`, `palette::tests::each_filter_plugin_is_a_result_of_its_own`, `dialogs::tests_shortcut_import::an_imported_shortcut_reaches_a_filter_plugin_picked_as_its_target`.
|
||||
|
||||
## Live Corners: arrow keys change the corner kind while dragging (2026-11-01)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user