mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
Plug-ins: exporters write file formats of their own (G17)
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.<id>. 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
1dda8d9e67
commit
24936cd006
+43
-12
@@ -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.<id>", "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.<id>`, which `document.formats` lists with its `plugin` id.
|
||||
`document.export {"format": "plugin.<id>", "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.<id>`.
|
||||
- `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.
|
||||
|
||||
Reference in New Issue
Block a user