Merge main into fix/plugin-stale-grant

This commit is contained in:
dotCooCoo
2026-11-08 18:59:23 -08:00
117 changed files with 5093 additions and 545 deletions
+66 -9
View File
@@ -11,7 +11,8 @@ There are four kinds of plug-in:
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.
built-in Distort & Transform effects. The art stays editable: move or reshape it and the effect follows. A live
effect may also paint what it returns ([painted live effects](#painted-live-effects)).
- **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.
@@ -26,7 +27,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?, format?, extensions?, budget?, commands?, size, source, api, permissions, hash, cacheBytes}], "folder": report}` (`api` is the manifest's [plug-in API level](#permissions); `budget: "heavy"` for a [heavy filter](#heavy-filters); `commands` lists its [commands](#commands); `permissions` lists what it [may do](#permissions), `hash` is the module's SHA-256; `cacheBytes` is the size of its [cache](#settings-and-cache)) |
| `plugin.list` | `{}` | `{"plugins": [{id, name, version, kind, author, description, params, paramsSchema, effect?, format?, extensions?, budget?, commands?, size, source, api, paintOutput?, permissions, hash, cacheBytes}], "folder": report}` (`api` is the manifest's [plug-in API level](#permissions); `budget: "heavy"` for a [heavy filter](#heavy-filters); `commands` lists its [commands](#commands); `paintOutput: true` marks a [painted live effect](#painted-live-effects); `permissions` lists what it [may do](#permissions), `hash` is the module's SHA-256; `cacheBytes` is the size of its [cache](#settings-and-cache)) |
| `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`; an `ids` that is not a list of object ids is an error). One undo step; the result is selected, except the objects it locked or made guides. |
| `plugin.install` | `{"path": "/x/y.wasm"}` (desktop) or `{"dataBase64": "…", "name"?}`, `"replace"?: bool`, `"allowHeavy": bool = false`, `"grant"?: [permission]`, `"hash"?: "sha256 hex"` | 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). A module with other bytes than the plug-in installed with its id, or, with none installed, one whose id has [settings and a cache](#settings-and-cache) in the `Plug-ins` folder, installs only with `replace: true`, once the user agreed; it takes that plug-in's place and gets its settings and cache. The same module installs again without `replace`, and `replace: false` refuses a module whose id is installed. A [heavy filter](#heavy-filters) installs only with `allowHeavy: true`, once the user agreed. A plug-in whose manifest lists [permissions](#permissions) installs only when `grant` lists every one of them, once the user agreed, unless the user already granted a module with the same SHA-256. With `hash`, a module whose SHA-256 differs is refused. |
@@ -237,7 +238,9 @@ capability model proposed for PhotoCraft plug-ins
`documentData` is `null` and an output that sets it is refused; without `doc.write` an output that changes the
document is refused. A refused output leaves the document as it was.
- `api` is the plug-in API level the manifest is written for: `1` (the default) or `2`, which adds the [host
imports](#host-imports-api-level-2). A higher one is refused. `plugin.list` and `plugin.info` give it as `api`.
imports](#host-imports-api-level-2) and a live effect's [`paintOutput`](#painted-live-effects). A higher one is
refused. `plugin.list` and `plugin.info` give it as `api`. A level 2 effect that declares `paintOutput` needs no
other permission than `doc.read` and `effect.render`.
- Parameter declarations are unchanged by permissions (`params`, below).
Every run gets a **fresh instance**, so no state survives between calls except what a plug-in writes to its
@@ -255,8 +258,10 @@ takes as many instructions the first time as the next. Charged to the run, as wa
could fail it: when the 20 M-instruction slice the run was in ran out while a function compiled, wasmi ended the run
with an error rather than a pause the host resumes, and the run stopped with most of its budget left.
Drawing evaluates effects often, so live-effect results are cached by plug-in installation, parameters and input
geometry: a plug-in runs again only when one of them changes.
Drawing evaluates effects often, so live-effect results are cached by plug-in installation, parameters, input
geometry, fill rule, reference box and the paints the effect is given: a plug-in runs again only when one of them
changes. Failures are cached too. The swatches and patterns a painted result names are checked against the document
each time it is drawn, and a paint linked to a global swatch is drawn in that swatch's current color at its own tint.
## ABI v1
@@ -378,6 +383,11 @@ pass the 1 GiB returns `-7` before it reads or writes anything.
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
([permissions](#permissions)); without it, what its kind implies.
- `paintOutput` (live effects at `api` 2 only): `true` makes the effect return [painted pieces](#painted-live-effects)
instead of geometry alone. `true` is the only value the key takes; `false`, `null`, another type, another kind or
`api` 1 is refused when the module is installed, and the error names `paintOutput`. Without the key the effect returns
geometry, at any level. Hosts at `api` 1 refuse the manifest for its `api`, so such an effect never draws unpainted
there.
- `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
@@ -427,7 +437,8 @@ guides too) and clipping paths are left out, and so are other kinds of object (t
touches. With nothing selected the list is empty (a generator then adds its own objects). A live effect gets one
object: its geometry (`type`, `path`, `fillRule`, `bounds`) and its `fills` and `strokes`, as a filter gets them (hosts
from before these give geometry only). The paints are read only: an effect may draw as wide as a stroke or use a
fill's colour to decide its shape, but it changes only the geometry.
fill's colour to decide its shape, but it changes only the geometry, unless it declares `paintOutput` ([painted live
effects](#painted-live-effects)).
An **exporter** gets something else: the native `.vectorcraft` document JSON without indentation, optional preview/PDF
renditions or save-preference metadata (the format is documented in [`crates/format/src/lib.rs`](../crates/format/src/lib.rs)). That is every layer, object,
@@ -536,7 +547,53 @@ document, so it can read what every plug-in keeps there, not only its own. A liv
is worked out from the object alone.
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.
object's; the rest is ignored (the object keeps its own appearance). An empty list removes the geometry. A live effect
that declares `paintOutput` returns painted pieces instead ([below](#painted-live-effects)).
#### Painted live effects
A live effect whose manifest has `"api": 2` and `"paintOutput": true` returns `{"objects": [...]}` too, but each entry
is a piece drawn by itself, in the order returned, with paints of its own:
```json
{"objects": [
{"path": {…}, "fills": [{"type": "solid", "color": {"model": "rgb", "r": 0, "g": 0.6, "b": 0.3}}]},
{"path": {…}, "fillRule": "evenodd", "strokes": [{"paint": null}, {"paint": {…}, "width": 4, "join": "round"}]},
{"path": {…}}
]}
```
- `path` is required; `fillRule` defaults to the object's. `type` and `bounds` may be handed back as given and are
ignored. Any other key (`opacity`, `effects`, `guide`, `locked`, `name`, a top-level `documentData`…) refuses the
result, as does a key that a paint, its color, a gradient stop, a gradient's placement or a freeform point does not
have, such as `opacity` on a solid paint.
- `fills` and `strokes` take the forms a filter returns ([Input](#input)). A piece without `fills` (or `strokes`) is
painted with the ones the effect was given; an empty list paints none. Given ones replace the object's in order, as
a filter's output sets them: an existing fill or stroke keeps its own effects, opacity and blending, and a new stroke
starts at 1 pt, miter join, butt cap, miter limit 10.
- An effect on a fill (`_context.item` `{"fill": i}`) returns only `fills`, which replace that fill on each piece; an
effect on a stroke returns only `strokes`. The object's other fills and strokes stay where they are.
- The effects after it in the stack apply to the pieces together, the reference box being the bounds of all of them.
Bounds, raster effects, Pathfinder effects, compound shapes and Envelope Options › Distort Appearance use the pieces
as they are drawn. Outside outline mode, an image is also hit where the pieces of its own fills and strokes paint; a
path or compound path is hit-tested on its source path and appearance. On an object with no geometry effect of its
own, Pathfinder effects and compound shapes use the object's path for an effect on a fill or stroke, as they do for
every other effect on a fill or stroke; on an object that has one, they also use the pieces of a painted effect on a
fill or stroke. A 3D effect later in the stack uses the
object's input path when the pieces name a swatch or pattern. Each piece counts against the budget of the
object's geometry effects as well as its anchors, so all the painted effects in a stack make at most 100,000 pieces
together; a run whose pieces would pass the budget draws its input as it was.
- The result is checked whole before any of it is drawn: the limits of a filter's output (100,000 objects, 4,000,000
anchors, 256 fills and 256 strokes per piece, 256 stops per gradient), finite numbers in range with nothing clamped
(color components, tints, offsets, opacities, midpoints 0.13 to 0.87, stops in order of `offset`, stroke
widths 0 to 1,000 pt, miter limits 1 to 500), and every swatch a solid paint, gradient or stop links to and every pattern named in
the document. One fault refuses the whole result: that step of the stack draws its input as it was, the effect
record stays, and `plugin.info`'s `lastError` names the plug-in and the field, for example
`Test Tone: plug-in output: objects[1].strokes[0].width: …`.
- The pieces are never written into the document while it is drawn, measured, previewed, saved or exported. PDF and SVG
export draw them as the canvas does, from a copy of the document. Object › Expand Appearance writes each piece as an
ordinary path or compound path with its fills and strokes, as one undo step. Object › Expand of gradient and pattern
fills expands the fills of each piece, and leaves a piece with other paints as it is drawn.
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
@@ -578,6 +635,6 @@ per change of their input, parameters or paints (results are cached), not per fr
- 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 recolours), and filters on type, images or
symbols.
- Live effects that change opacity, blending or effects (at `api` 2 one may [paint](#painted-live-effects) the pieces
it returns), 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.