Commit Graph
26 Commits
Author SHA1 Message Date
dotCooCoo bd5ea2a1bf Merge main into feat/plugin-effect-paint 2026-11-08 17:59:33 -08:00
dotCooCoo d6a91b6142 Merge main into feat/plugin-effect-paint 2026-11-08 17:26:35 -08:00
dotCooCoo 918ef940cb Merge main into feat/plugin-importers 2026-11-08 16:45:20 -08:00
BFlat 3e836ad856 Merge pull request #6193 from dotCooCoo/feat/plugin-menu-commands
Plug-ins: a plug-in adds menu commands of its own under Object › Plug-ins
2026-11-09 07:47:49 +08:00
dotCooCoo 64c7c2703e Merge main into feat/plugin-menu-commands 2026-11-08 14:41:13 -08:00
dotCooCoo 4b665ad13c Plug-ins: clearing every cache is tested in a binary of its own
`plugin.clearCache` without an id empties the cache of every plug-in
installed in the process. In the engine's library tests it could empty
the cache of another storage test running at the same time, and its
`bytesFreed` could count that test's cache. It now runs in
`crates/engine/tests/plugin_clear_cache.rs`, and the library test checks
that clearing one plug-in's cache leaves the other plug-in's cache.

The storage fuzz test starts each case with an empty plug-in store,
deletes the store when the case ends, and installs `fuzz.storage` with
`replace: true`, so the cases that shrink a failure install their
module.

docs/mcp.md says that files `plugin.remove` can't delete stay in the
folder renamed with `.removed` until the plug-in is removed again.
2026-11-08 13:03:31 -08:00
dotCooCoo 11703bd350 Merge main into feat/plugin-effect-paint 2026-11-08 12:56:42 -08:00
dotCooCoo 9518dc0e3a Merge main into feat/plugin-importers 2026-11-08 12:33:17 -08:00
dotCooCoo 26804fb0c4 Plug-ins: tests hold an importer to its budget and hostile extensions in the manifest fuzz
An import's instruction and time budgets are now host limits on Limits, as a heavy filter's are, so a test can give an importer a small budget. An importer past its instruction or time limit, and a file past the input limit, return their error and leave an open document and its undo history unchanged. The manifest fuzz varies extensions (missing, dotted, mixed-case, repeated, too long and too many).
2026-11-08 12:29:27 -08:00
bflatastic d4f2cf5394 Plug-ins: the fuzz tests install each case's module in place of the one before 2026-11-09 03:08:22 +07:00
dotCooCoo 218c711181 Plug-ins: a plug-in keeps settings and a cache of its own
Squashed from #6228 onto main.
2026-11-09 02:54:33 +07:00
dotCooCoo 64817c2e8c Merge main into feat/plugin-effect-paint 2026-11-08 10:46:47 -08:00
dotCooCoo 79351d7063 Merge main into feat/plugin-importers 2026-11-08 10:25:28 -08:00
dotCooCoo 332e115b68 Plug-ins: the bulk memory fuzz generators follow the paint generators
Moves the bulk memory case types, strategies and their test after arb_paint, so they no longer share the lines after output where other fuzz generators are added.
2026-11-09 00:31:14 +07:00
dotCooCoo 44432a4310 Plug-ins: the fuzz test generates bulk memory operations
plugin_fuzz now builds plug-ins whose run executes memory.fill,
memory.copy (overlapping ranges included), memory.init followed by
data.drop, and data.drop followed by memory.init. Lengths and offsets
are sampled at zero, small, page and mebibyte sizes, at an exact end,
one before and one past it, with copy sources and destinations and init
memory and segment offsets chosen separately. Fill and copy can repeat
up to about one fuel slice of copying.

Each case runs directly under the default budget, a small fixed budget
or one near its estimated cost. A run must trap exactly when a range
leaves memory or the data segment (empty once dropped), and must finish
when it is in bounds and has ample fuel; otherwise it may hit the
instruction budget. The same plug-in then runs through plugin.run: a
failed run leaves the document unchanged and the document still renders
and exports.

bulk_memory_cases_reach_their_instruction checks that each operation's
module contains its instruction and traps on a small out-of-bounds case.
2026-11-09 00:31:14 +07:00
dotCooCoo 7a38e2c990 Merge main into feat/plugin-menu-commands 2026-11-08 02:04:06 -08:00
dotCooCoo a3c37ea1f4 Merge main into feat/plugin-importers 2026-11-07 23:49:11 -08:00
dotCooCoo c2f0a5667f Merge main into feat/plugin-effect-paint
# Conflicts:
#	crates/engine/src/cmd/plugin.rs
#	crates/engine/src/store.rs
#	crates/engine/src/tests/plugins.rs
#	crates/engine/tests/plugin_fuzz.rs
#	crates/plugins/src/lib.rs
#	crates/plugins/src/manifest.rs
#	crates/plugins/src/permissions.rs
#	crates/plugins/src/runtime.rs
#	crates/ui-egui/src/i18n/bg.tsv
#	crates/ui-egui/src/i18n/bs.tsv
#	crates/ui-egui/src/i18n/ca.tsv
#	crates/ui-egui/src/i18n/cs.tsv
#	crates/ui-egui/src/i18n/da.tsv
#	crates/ui-egui/src/i18n/de.tsv
#	crates/ui-egui/src/i18n/el.tsv
#	crates/ui-egui/src/i18n/es.tsv
#	crates/ui-egui/src/i18n/fi.tsv
#	crates/ui-egui/src/i18n/fr.tsv
#	crates/ui-egui/src/i18n/hi.tsv
#	crates/ui-egui/src/i18n/hu.tsv
#	crates/ui-egui/src/i18n/id.tsv
#	crates/ui-egui/src/i18n/it.tsv
#	crates/ui-egui/src/i18n/ja.tsv
#	crates/ui-egui/src/i18n/ka.tsv
#	crates/ui-egui/src/i18n/ko.tsv
#	crates/ui-egui/src/i18n/nl.tsv
#	crates/ui-egui/src/i18n/pl.tsv
#	crates/ui-egui/src/i18n/pt-br.tsv
#	crates/ui-egui/src/i18n/ru.tsv
#	crates/ui-egui/src/i18n/sv.tsv
#	crates/ui-egui/src/i18n/tr.tsv
#	crates/ui-egui/src/i18n/uk.tsv
#	crates/ui-egui/src/i18n/vi.tsv
#	crates/ui-egui/src/i18n/zh-hans.tsv
#	crates/ui-egui/src/i18n/zh-hant.tsv
#	crates/ui-egui/src/io/open.rs
#	crates/ui-egui/src/tests/plugins.rs
#	docs/mcp.md
#	docs/plugins.md
#	docs/target-app-parity.md
2026-11-07 23:43:15 -08:00
dotCooCoo b109378ae6 Plug-ins: live-effect plug-ins at API level 2 can paint the pieces they return
A live effect whose manifest has "api": 2 and "paintOutput": true returns
{"objects": [...]} as pieces, each drawn by itself in order with fills and
strokes of its own in the forms a filter returns. A piece without fills or
strokes keeps the ones the effect was given, and an empty list paints none.
An effect on a fill returns only fills, one on a stroke only strokes.

The whole result is checked before any of it is drawn: the output limits,
finite numbers in range with nothing clamped, known keys only, and every
swatch and pattern 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. Each piece counts against the
geometry-effect budget, so the painted effects of one stack make at most
100,000 pieces.

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. The plug-in API level
rises to 2; plugin.list and plugin.info give api and paintOutput. Effects
without the key behave as before.
2026-11-07 23:22:11 -08:00
dotCooCoo fd94ebe86c Plug-ins: importer plug-ins open file types VectorCraft does not read
An importer plug-in (kind "importer", permission import.read) lists 1 to 8
file extensions and turns a file's bytes into objects, which open as a new
document whose artboard is their bounds. File › Open lists its extensions,
File › Open With (file.openWith) opens a file with a chosen importer, and
document.open takes importer and params. A built-in format that reads the
file by its content or extension always opens it; among importers listing
the same extension, the one whose id sorts first opens it. An importer reads
at most 128 MiB, and Place still reads only the built-in formats.

Docs: docs/plugins.md, docs/mcp.md, docs/gaps.md, docs/target-app-parity.md.
2026-11-07 23:05:09 -08:00
dotCooCoo b071fb109c Plug-ins: the host import fuzz test reaches vc_log's text checks
The fuzz test for hostile host import arguments drew vc_log's level from every i32, so almost
every call stopped at the level check and never read memory. Levels are now also drawn from -1
to 4. The module docs, a test doc comment and docs/plugins.md now say that with an unknown total
each change of done is passed on.
2026-11-08 13:21:16 +07:00
dotCooCoo 1c676fc372 Plug-ins: API level 2 lets a plug-in report progress, see Cancel and write log lines
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.
2026-11-08 13:21:15 +07:00
dotCooCoo 874ae85fab Plug-ins: manifests list permissions, the user grants them at install, and the host enforces the grant
A manifest may list `permissions` (doc.read, doc.write, state,
effect.render, export.write) and an `api` level. doc.read, doc.write
and state follow the capability model proposed in
storytold/photocraft#662; the other names are VectorCraft's own. An
unknown, reserved, duplicate or wrong-kind name is refused by name. A
manifest without the key gets the permissions its kind implies and
installs as before.

- Install Plug-in... and File > Open show the name, id, version, file
  SHA-256 and each permission in plain words, with control and
  bidirectional characters escaped and each string cut to 64
  characters. OK installs the module only if it still has the SHA-256
  shown; Cancel installs nothing. The heavy-budget prompt and the
  status line show the escaped name too, and OK keeps the `replace` it
  was given.
- `plugin.install` needs `grant` listing every permission, and
  `plugin.inspect` reports `needsApproval`. `hash` pins the module:
  one with another SHA-256 is refused. The grant is kept with the
  module's hash in the store's `grants.json`, and only that store's
  grants count while it is attached; the same set does not ask again,
  more permissions do, and `plugin.remove` forgets it.
- The Additional Plug-ins Folder and `plugin.reload` install only
  plug-ins the attached store has granted and report the others as
  needing approval. At startup the store skips an ungranted plug-in.
- A filter run without doc.read is given no objects; without state it
  neither reads nor writes `documentData`; without doc.write an output
  that changes the document is refused and the document is unchanged.

Docs: docs/plugins.md, docs/mcp.md, docs/target-app-parity.md.
2026-11-08 13:20:32 +07:00
dotCooCoo ed5de5edcb Plug-ins: a plug-in adds menu commands of its own under Object › Plug-ins
A filter or live effect may declare up to 32 `commands` in its manifest,
each `{id, label, params?}`. While the plug-in is installed each one is the
command `plugin.<id>.<command>`: listed by `command.list` and in the
plug-in's `plugin.list` entry, run through `run`, MCP and `command.batch`
with its parameters checked before the plug-in runs, and recorded in
actions. A run goes through `vc_run` on the selection with
`_context.mode` "command" and `_context.command`, under the plug-in's
permissions, as one undo step named after the command. A failing run
leaves the document as it was, and its error starts with the plug-in's
name and the command's label. Removing or replacing the plug-in removes
its commands.

In the app, a plug-in with commands is a submenu named after it under
Object › Plug-ins (Effect › Plug-ins for a live effect), holding its own
item and then its commands; a `menu` key in a manifest is ignored. A
command with parameters opens a dialog for them from its menu item or a
shortcut. Edit › Keyboard Shortcuts lists the commands for the user to
give a shortcut; a manifest cannot set one. Labels and names are shown
with control and bidirectional characters escaped, and a malformed entry
is refused by name when the module is installed.

Docs: docs/plugins.md, docs/mcp.md, docs/target-app-parity.md.
2026-11-07 17:32:54 -08:00
dotCooCoo 1f352bc7c1 Plug-ins: manifests list permissions, the user grants them at install, and the host enforces the grant
A manifest may list `permissions` (doc.read, doc.write, state,
effect.render, export.write) and an `api` level. doc.read, doc.write
and state follow the capability model proposed in
storytold/photocraft#662; the other names are VectorCraft's own. An
unknown, reserved, duplicate or wrong-kind name is refused by name. A
manifest without the key gets the permissions its kind implies and
installs as before.

- Install Plug-in... and File > Open show the name, id, version, file
  SHA-256 and each permission in plain words, with control and
  bidirectional characters escaped and each string cut to 64
  characters. OK installs the module only if it still has the SHA-256
  shown; Cancel installs nothing. The heavy-budget prompt and the
  status line show the escaped name too, and OK keeps the `replace` it
  was given.
- `plugin.install` needs `grant` listing every permission, and
  `plugin.inspect` reports `needsApproval`. `hash` pins the module:
  one with another SHA-256 is refused. The grant is kept with the
  module's hash in the store's `grants.json`, and only that store's
  grants count while it is attached; the same set does not ask again,
  more permissions do, and `plugin.remove` forgets it.
- The Additional Plug-ins Folder and `plugin.reload` install only
  plug-ins the attached store has granted and report the others as
  needing approval. At startup the store skips an ungranted plug-in.
- A filter run without doc.read is given no objects; without state it
  neither reads nor writes `documentData`; without doc.write an output
  that changes the document is refused and the document is unchanged.

Docs: docs/plugins.md, docs/mcp.md, docs/target-app-parity.md.
2026-11-07 15:20:07 -08:00
bflatasticandClaude Opus 5.5 811ad1efd8 Fix #121: sandboxed WebAssembly plug-ins: object filters and live effects
VectorCraft had an Additional Plug-ins Folder preference but nothing loaded from it. Port
PhotoCraft's wasmi plug-in host (same sandbox, manifest and parameter schema) as the new
vectorcraft-plugins crate (L2), extended for vector documents:

- Object filters read the selected paths and compound paths as JSON (native PathData and
  Paint forms: geometry, fills, strokes, gradients, opacity) and return the objects to keep,
  change, add or delete; the output is validated in full and applied as one undo step
  (plugin.run). A failing plug-in leaves the document untouched.
- Live-effect plug-ins are geometry effects in the appearance stack (effect id
  plugin.<id>), re-evaluated (and cached) when the object or parameters change, editable,
  exported and baked by Expand Appearance; a missing plug-in leaves the geometry as is.
- plugin.list / info / run / install / remove / reload; the preference folder installs
  every *.wasm at startup and when it changes; File > Open of a .wasm installs it (web).
- Object > Plug-ins and Effect > Plug-ins menus with dialogs generated from the parameter
  schema (live preview), the Appearance panel's fx menu, docs/plugins.md, an original
  example plug-in (Desaturate, a Rust crate outside the workspace) and fuzzing of the
  plug-in boundary (engine/tests/plugin_fuzz.rs).

Tool plug-ins and paint-changing live effects are left for later (docs/plugins.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:19:41 +07:00