mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 14:49:18 +00:00
A plug-in whose manifest gives "api": 2 and lists the new `progress` permission may import three host functions from module `env`: vc_progress(done, total), vc_cancelled() -> i32 and vc_log(level, ptr, len) -> i32. Installing checks every import and refuses, by name, one the host doesn't offer, one with another type and one the manifest doesn't allow. - Progress is clamped and passed on once per tenth of a percent (past 2,000 reports, once per 1,000 changes). A heavy filter's progress window and the export's progress window show it as a percentage bar, and their Cancel makes vc_cancelled return 1. A live effect's progress is not shown. - A run cancelled before it returns ends as cancelled, and one that returns after its time budget ran out ends at that budget, so what a plug-in returns after it sees vc_cancelled changes nothing. This also applies to a plug-in of API level 1 that finishes after a Cancel or past its time budget. - Log text must lie in the module's memory and be UTF-8. Lines go to the app's log as "<id>: <text>" with hidden characters escaped, at most 1,024 characters a line, 200 lines and 32 KiB a run, and the host reads at most 1 MiB of log text a run. - plugin.list and plugin.info give the manifest's `api`; the install prompt names the new permission in plain words. Docs: docs/plugins.md, docs/mcp.md, docs/target-app-parity.md.
117 lines
4.8 KiB
Rust
117 lines
4.8 KiB
Rust
//! Progress and Cancel of an export running off the UI thread (Background Export), on the shared
|
|
//! [`vectorcraft_doc::cancel`] mechanism.
|
|
//!
|
|
//! The frontend makes a [`Progress`], runs the export inside [`scope`] on its worker and reads the
|
|
//! counts from the UI thread to draw them; its Cancel button calls [`Progress::cancel`]. Inside the
|
|
//! scope the encoders that work in steps (raster formats: one artboard at a time) report each step
|
|
//! through [`vectorcraft_doc::cancel::progress`] and stop at the next check once cancelled; the
|
|
//! renderer stops drawing and keeps no half-drawn pattern tile or effect raster. Encoders that
|
|
//! make one file in one go report nothing: the frontend shows them as busy and checks
|
|
//! [`Progress::cancelled`] itself before writing, so a cancelled export never writes a file.
|
|
//! Export for Screens ([`super::ScreenExport::write`]) reports one step per file instead, keeps
|
|
//! its encoders' reports out, and stops before the next file, so the files it wrote before the
|
|
//! cancel stay.
|
|
|
|
use std::rc::Rc;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
|
|
use vectorcraft_doc::cancel::{self, CancelToken, ProgressSink};
|
|
|
|
/// How far an export has got, shared between the worker and the UI thread.
|
|
#[derive(Debug, Default)]
|
|
pub struct Progress {
|
|
token: CancelToken,
|
|
done: AtomicU64,
|
|
total: AtomicU64,
|
|
/// The steps are files (Export for Screens), not artboards.
|
|
files: bool,
|
|
/// The last report came from a plug-in ([`vectorcraft_plugins::host::PROGRESS_STEP`]): its
|
|
/// steps are its own, shown as a share of the work.
|
|
share: AtomicBool,
|
|
}
|
|
|
|
impl Progress {
|
|
pub fn new() -> Arc<Self> {
|
|
Arc::new(Self::default())
|
|
}
|
|
|
|
/// A [`Progress`] whose steps are files: the export of [`super::ScreenExport::write`].
|
|
pub fn of_files() -> Arc<Self> {
|
|
Arc::new(Self { files: true, ..Self::default() })
|
|
}
|
|
|
|
/// Are the steps files rather than artboards?
|
|
pub fn counts_files(&self) -> bool {
|
|
self.files
|
|
}
|
|
|
|
/// Are the steps a plug-in's own, which only a share of the work done describes?
|
|
pub fn counts_share(&self) -> bool {
|
|
self.share.load(Ordering::Relaxed)
|
|
}
|
|
|
|
/// `(steps done, steps in all)`; `total` is 0 while the export can't say (one file made in one go).
|
|
pub fn counts(&self) -> (u64, u64) {
|
|
let total = self.total.load(Ordering::Relaxed);
|
|
(self.done.load(Ordering::Relaxed).min(total), total)
|
|
}
|
|
|
|
/// The share of the export done, 0 to 1; `None` while the export can't say.
|
|
pub fn fraction(&self) -> Option<f64> {
|
|
let (done, total) = self.counts();
|
|
(total > 0).then(|| done as f64 / total as f64)
|
|
}
|
|
|
|
/// Ask the export to stop: it writes nothing more, and a render under way stops drawing.
|
|
pub fn cancel(&self) {
|
|
self.token.cancel();
|
|
}
|
|
|
|
pub fn cancelled(&self) -> bool {
|
|
self.token.is_cancelled()
|
|
}
|
|
}
|
|
|
|
/// Run `f` with `progress`'s token in effect on this thread and the steps reported to it.
|
|
pub fn scope<R>(progress: &Arc<Progress>, f: impl FnOnce() -> R) -> R {
|
|
let to = Arc::clone(progress);
|
|
let sink: ProgressSink = Rc::new(move |done, total, what| {
|
|
to.share.store(what == vectorcraft_plugins::host::PROGRESS_STEP, Ordering::Relaxed);
|
|
to.total.store(total, Ordering::Relaxed);
|
|
to.done.store(done, Ordering::Relaxed);
|
|
});
|
|
cancel::scope(&progress.token, Some(sink), f)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn a_raster_export_counts_its_artboards_and_stops_once_cancelled() {
|
|
let mut doc = vectorcraft_doc::Document::new(100.0, 100.0);
|
|
let second = doc.artboards[0].clone();
|
|
doc.artboards.push(second);
|
|
let p = Progress::new();
|
|
assert_eq!(p.fraction(), None, "nothing reported yet");
|
|
let png = scope(&p, || super::super::encode_all(&doc, "png", &serde_json::json!({"useArtboards": true})));
|
|
assert!(png.is_ok(), "{:?}", png.err());
|
|
assert_eq!(p.counts(), (1, 2), "the step under way when the last artboard started");
|
|
p.cancel();
|
|
let r = scope(&p, || super::super::encode_all(&doc, "png", &serde_json::json!({"useArtboards": true})));
|
|
assert!(matches!(r, Err(crate::EngineError::Cancelled)), "{:?}", r.err());
|
|
assert!(!cancel::is_cancelled(), "the scope ended");
|
|
}
|
|
|
|
/// A plug-in's reports count as a share of the work, not as artboards or files.
|
|
#[test]
|
|
fn a_plugin_s_reports_count_as_a_share_of_the_work() {
|
|
let p = Progress::new();
|
|
scope(&p, || cancel::progress(1, 4, "artboard"));
|
|
assert_eq!((p.counts(), p.counts_share()), ((1, 4), false));
|
|
scope(&p, || cancel::progress(3, 8, vectorcraft_plugins::host::PROGRESS_STEP));
|
|
assert_eq!((p.counts(), p.counts_share()), ((3, 8), true));
|
|
}
|
|
}
|