mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 02:19:56 -06:00
Merge main into feat/plugin-filter-inputs
This commit is contained in:
+66
-24
@@ -5,7 +5,7 @@ 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 three kinds of plug-in:
|
||||
There are four 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
|
||||
@@ -15,6 +15,9 @@ There are three kinds of plug-in:
|
||||
- **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.
|
||||
- **Importers** read files in a format of their own, such as a plotter's or a cutting machine's, into a new document of
|
||||
paths and compound paths. File › Open lists their files, File › Open With lists the importers by name, and the host
|
||||
hands them the file's bytes.
|
||||
|
||||
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`).
|
||||
@@ -39,6 +42,16 @@ An exporter adds a write-only format, `plugin.<id>`, which `document.formats` li
|
||||
`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.
|
||||
|
||||
An importer adds a read-only format, `plugin.<id>`, which `document.formats` lists with its `plugin` id. `openExtensions`
|
||||
holds the extensions File › Open opens with the importer (the lists below describe which are left out). `document.open {"path"}` (or `{"name", "dataBase64"}`) opens a file with it when no built-in format
|
||||
reads the file, by its content or by its extension, and the importer lists the file's extension; when several do, the
|
||||
one whose id sorts first opens it. `document.open {"importer": "plugin.<id>", "params"?: {…}}` names the importer,
|
||||
which must list the file's extension, also one a built-in format reads (an importer that lists `svg` opens an `.svg`
|
||||
only this way). A damaged file of a built-in format reports that format's error, and the file is not handed to an
|
||||
importer. The importer's objects become a new document whose artboard is their bounds (at least 1 pt wide and high,
|
||||
and within ±4,000,000 pt); the document keeps no path, so Save asks for a name. File › Place reads only the built-in formats, and a file only an importer reads opens as a new document when it is dropped on the canvas. These rules are
|
||||
VectorCraft's own.
|
||||
|
||||
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, text, lists
|
||||
@@ -48,7 +61,21 @@ it can be given a shortcut (`plugin:<id>`); the shortcut stays saved while the p
|
||||
when it is reinstalled. Parameters too many for the window's height scroll inside the dialog, which keeps its
|
||||
heading and buttons in view. Exporters are formats of **File › Export › Export As**, where Use
|
||||
Artboards is greyed out (the plug-in writes the whole document); one with parameters asks for them in a dialog of the
|
||||
same kind once the file is picked, and its Export button writes the file. **File › Open** of a `.wasm` file installs
|
||||
same kind once the file is picked, and its Export button writes the file. Importers' files are listed under File ›
|
||||
Open's "All readable files" and under a filter of their own, named after the importer (other open dialogs, such as New
|
||||
from Template and Load Styles, list only the files the built-in formats read). These lists leave out an importer's
|
||||
extensions that a built-in format reads, and those File › Open installs, imports or loads as something else (`.wasm`,
|
||||
`.kys`, `.jsx`, presets, libraries and swatch or graphic style libraries such as `.ase`). File › Open does not give
|
||||
those files to the importer, and Open With does. **File › Open With** lists the
|
||||
installed importers by name (`file.openWith {"importer", "path"?, "params"?}`), and each opens a file picked among its
|
||||
extensions with that importer (on the web, File › Open's picker opens instead) and adds it to Open Recent. On the
|
||||
desktop the importer runs off the UI thread, and a run that takes a while shows a progress window with Cancel, which
|
||||
stops it. An importer
|
||||
with parameters, given no `params`, asks for them in a dialog of the same kind once the file is picked, and its Open
|
||||
button opens the file; File › Open runs an importer with its parameters' defaults. Open Recent opens a file the way
|
||||
File › Open does: it does not remember the importer or the parameters given to Open With. A built-in format reads the
|
||||
file if one can, and otherwise the first importer by id that lists its extension reads it with its parameters' defaults.
|
||||
**File › Open** of a `.wasm` file installs
|
||||
a plug-in too (on the web this is how plug-ins get in). A heavy filter asks before it is installed, and runs in the
|
||||
background ([below](#heavy-filters)). A plug-in that lists permissions asks for them before it is installed
|
||||
([below](#permissions)).
|
||||
@@ -99,20 +126,22 @@ 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`; a [heavy filter](#heavy-filters) 1 G + 4,000 per byte; 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 heavy filter run or export (desktop; checked every 20 M instructions) | `Limit("time budget")` |
|
||||
| Instructions (fuel) | 50 M per call + 4,000 per input byte for `vc_run`; a [heavy filter](#heavy-filters) 1 G + 4,000 per byte; an export or import 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 heavy filter run, export or import (desktop; checked every 20 M instructions) | `Limit("time budget")` |
|
||||
| Import input | 128 MiB of the file (`vectorcraft_plugins::import::MAX_INPUT_BYTES`) | the file doesn't open; a larger file named by `path` is not read past that |
|
||||
| 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.
|
||||
plug-in between two of those 20 M-instruction slices; nothing is written then. An import stops the same way when the
|
||||
open it runs in is cancelled, and nothing opens then.
|
||||
|
||||
### Heavy filters
|
||||
|
||||
Some filters do heavy but legitimate work, such as routing a plotter's pen, packing shapes onto a sheet or running a
|
||||
simulation, which a minute and 50 M instructions don't cover. Such a filter declares `"budget": "heavy"` in its
|
||||
manifest (only a filter may: a live effect runs whenever the document is drawn, and an exporter has an export's
|
||||
budget). Each of its runs gets 1 G instructions (20 times a light run's) plus the 4,000 per input byte, and 300 s
|
||||
manifest (only a filter may: a live effect runs whenever the document is drawn, an exporter has an export's budget and
|
||||
an importer an import's). Each of its runs gets 1 G instructions (20 times a light run's) plus the 4,000 per input byte, and 300 s
|
||||
(`vectorcraft_plugins::{HEAVY_FUEL, HEAVY_WALL_TIME_MS}`). Its output can't add to the budget: the host learns how
|
||||
much there is only once the run has ended.
|
||||
|
||||
@@ -138,22 +167,24 @@ capability model proposed for PhotoCraft plug-ins
|
||||
|
||||
| Permission | Kinds | What it allows |
|
||||
|---|---|---|
|
||||
| `doc.read` | all | Read the objects it is given. |
|
||||
| `doc.read` | filter, effect, exporter | Read the objects it is given. |
|
||||
| `doc.write` | filter, effect with `commands` | Change, add and delete objects in the document. |
|
||||
| `state` | filter, effect with `commands` | Keep data of its own with the document (`documentData`). |
|
||||
| `effect.render` | effect | Change how the objects its effect is applied to are drawn. |
|
||||
| `export.write` | exporter | Write the document as a file in its own format. |
|
||||
| `import.read` | importer | Read files you open in its format and make documents from them. |
|
||||
| `progress` | all | Show its progress, see Cancel and write log lines through the [host imports](#host-imports-api-level-2). Needs `api` 2. |
|
||||
| `settings` | all | Keep settings of its own between runs and sessions ([settings and cache](#settings-and-cache)). Needs `api` 2. |
|
||||
| `cache` | all | Keep a cache of its own, which the user can clear ([settings and cache](#settings-and-cache)). Needs `api` 2. |
|
||||
|
||||
- A manifest without `permissions` gets the ones its kind implies, as plug-ins did before the key existed: a filter
|
||||
`doc.read`, `doc.write` and `state`; an effect `doc.read` and `effect.render`, and also `doc.write` and `state`
|
||||
when it declares [commands](#commands); an exporter `doc.read` and `export.write`. Such a plug-in installs without
|
||||
a prompt. This rule is VectorCraft's own. A filter whose list is empty may do none of these, and also installs
|
||||
without a prompt.
|
||||
- An effect or an exporter that lists permissions lists `doc.read` and its own (`effect.render`, `export.write`). An
|
||||
effect with commands may add `doc.write` and `state`, which only its commands use. A filter may list any of its three.
|
||||
when it declares [commands](#commands); an exporter `doc.read` and `export.write`; an importer `import.read`. Such a
|
||||
plug-in installs without a prompt. This rule is VectorCraft's own. A filter whose list is empty may do none of
|
||||
these, and also installs without a prompt.
|
||||
- An effect, an exporter or an importer that lists permissions lists all of its implied ones (`doc.read` and
|
||||
`effect.render`; `doc.read` and `export.write`; `import.read`). An effect with commands may add `doc.write` and
|
||||
`state`, which only its commands use. A filter may list any of its three.
|
||||
Any kind may add `progress`, `settings` and `cache`, which no kind implies. An unknown name, a name listed twice, a
|
||||
name that doesn't apply to the kind, more than 16 names, `progress`, `settings` or `cache` with an `api` below 2, or
|
||||
one of the names kept for a later plug-in API
|
||||
@@ -329,9 +360,9 @@ pass the 1 GiB returns `-7` before it reads or writes anything.
|
||||
|
||||
- `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, 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.
|
||||
- `name`: the menu label, or an exporter's or importer's format name in Export As, Open and Open With (1–64 printable
|
||||
characters). `kind`: `"filter"` (object filter), `"effect"` (live effect), `"exporter"` or `"importer"`. Exporters
|
||||
and importers came after the first ABI v1 hosts, which refuse the kind when the plug-in is installed.
|
||||
- `budget` (filters only): `"light"` (the default) or `"heavy"`, for work that takes minutes ([heavy
|
||||
filters](#heavy-filters)). Hosts from before it ignore the key, so there the filter gets a light run's budget.
|
||||
- `api`: the plug-in API level, `1` when left out. `permissions`: what the plug-in may do, a list of names
|
||||
@@ -341,9 +372,10 @@ pass the 1 GiB returns `-7` before it reads or writes anything.
|
||||
filter gets paths and compound paths alone, as before. An unknown, repeated or wrongly typed entry, `inputs` on
|
||||
another kind, below `api` 2, or without a `permissions` list that holds `doc.read`, is refused when the plug-in is
|
||||
installed, and so is `api` 2 on hosts from before it.
|
||||
- `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.
|
||||
- `extensions` (exporters and importers 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, and
|
||||
`document.open` reads a file with the importer only when `importer` names it.
|
||||
- `commands` (filters and live effects; not heavy filters): up to 32 commands of the plug-in's own
|
||||
([Commands](#commands)), each `{"id": "grow", "label": "Grow", "params"?: {…}}`. An `id` is 1–32 characters from
|
||||
`a-z 0-9 _` that starts with a letter, and appears once. A `label` is 1–64 printable characters; menus show it with
|
||||
@@ -398,6 +430,8 @@ appearance and effect record, artboard, swatch and symbol, so an exporter reads
|
||||
`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`.
|
||||
|
||||
An **importer** gets the bytes of the file being opened, as they are (not JSON), at most 128 MiB of them.
|
||||
|
||||
```json
|
||||
{"objects": [
|
||||
{"id": 12, "type": "path", "name": "Leaf",
|
||||
@@ -501,15 +535,17 @@ toward the 64 MiB output cap.
|
||||
"_context": {"mode": "filter", "bounds": [10, 0, 75, 60], "artboard": [0, 0, 612, 792], "documentData": {"pens": 4}}}
|
||||
```
|
||||
|
||||
`mode` is `"filter"`, `"effect"`, `"export"` or `"command"` (a [command](#commands) run, whose `command` is the
|
||||
command's id); `bounds` the selection's geometric bounds (a live effect: the reference
|
||||
`mode` is `"filter"`, `"effect"`, `"export"`, `"import"` or `"command"` (a [command](#commands) run, whose
|
||||
`command` is the command's id); `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; `documentData` (filters) what the plug-in last kept with the document
|
||||
(`null` until it keeps something; see Output). A live effect's `item` says where it sits: `"object"` (one of the
|
||||
object's effects), or `{"fill": i}` / `{"stroke": i}` (in the own effects of the object's `i`th fill or stroke,
|
||||
counted from the bottom as in `fills` and `strokes`). 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.
|
||||
extension (the document's name when the export is not given a path), which some formats keep in their header. An
|
||||
import's `_context` is `{"mode": "import", "extension": "plt", "name": "Poster"}`: the extension of the file being
|
||||
opened and its name without the extension.
|
||||
|
||||
### Output
|
||||
|
||||
@@ -560,6 +596,11 @@ An **exporter** returns `{"dataBase64": "…", "warnings"?: ["…"]}`: the file,
|
||||
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.
|
||||
|
||||
An **importer** returns `{"objects": [...], "warnings"?: ["…"]}`: new paths and compound paths (no `id`) in the
|
||||
filters' object form, checked as a filter's new objects are, with warnings capped as an exporter's are and shown with
|
||||
the open's own. An output with no objects, with keys other than `objects`, `warnings` and `error`, or with an object
|
||||
that fails the checks opens nothing, and neither does a decline (`{"error": …}`).
|
||||
|
||||
## Writing a plug-in in Rust
|
||||
|
||||
[`crates/plugins/example`](../crates/plugins/example) is a complete object filter, **Desaturate**: it moves every
|
||||
@@ -588,8 +629,9 @@ per change of their input, parameters or paints (results are cached), not per fr
|
||||
|
||||
## Not in v1
|
||||
|
||||
- Tool plug-ins (pointer events on the canvas, a guide line or rectangle, geometry back), panel plug-ins, importers
|
||||
(plug-in formats that open files).
|
||||
- Tool plug-ins (pointer events on the canvas, a guide line or rectangle, geometry back), panel plug-ins, and
|
||||
importers that place files (File › Place reads only the built-in formats) or make type, images, layers or
|
||||
artboards.
|
||||
- Live effects that change paint (they reshape geometry; an object filter recolors), and filters that change type,
|
||||
images or symbol instances (a filter at `api` 2 reads them, see [`inputs`](#reading-type-images-and-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