M7: plugin panels and settings (#124)

Plugin ABI v2: a plugin can show a panel (text, number, checkbox, select, colour, list, notes, buttons) rendered with FigCraft's widgets, react to events with its state kept between calls (each editing callback one undo step, full sandbox limits on every call), and store per-plugin settings (64 KB). plugin.ui / plugin.event / plugin.close drive panels from the CLI and MCP; v1 plugins run unchanged. The Swatch grid example gains a Palettes panel that remembers the last palette.
This commit is contained in:
BFlat
2026-10-11 14:45:19 +07:00
committed by GitHub
parent 09a3b74466
commit a003e3de4f
27 changed files with 1639 additions and 233 deletions
+12 -8
View File
@@ -1,6 +1,6 @@
# Where FigCraft falls short of Figma
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail) · **Target:** Figma Design (desktop app 126.10.7)
Every known shortfall against Figma, one entry each, ranked by how much it stops a Figma user from
doing real work in FigCraft. Agents: pick work from the top unless the owner says otherwise. The
@@ -19,7 +19,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
| 3 | Nothing checked against real Figma files | Fidelity | 15–30 | [target-app-parity](target-app-parity.md) |
| 4 | Interaction fidelity not compared tool by tool | UI/UX | 40–60 | [ui-parity](ui-parity.md) |
| 5 | No localization layer; English only | Localization | 60–100 | [localization-parity](localization-parity.md) |
| 6 | Plugins: no run-time UI, distribution or widgets; Figma's plugins don't run | Ecosystem | 25–55 | [target-app-parity](target-app-parity.md) |
| 6 | Plugins: no distribution or widgets; Figma's plugins don't run | Ecosystem | 15–40 | [target-app-parity](target-app-parity.md) |
| 7 | CPU canvas where Figma uses the GPU | Hardware/performance | 30–60 | [hardware-parity](hardware-parity.md) |
| 8 | Platform QA is mostly macOS | Stability/platforms | 10–20 | [hardware-parity](hardware-parity.md) |
| 9 | Figma Draw: multi-edit, pen pressure on macOS/Linux tablets (brushes, pressure, text on a path and pattern fills landed) | Feature | 2–5 | [target-app-parity](target-app-parity.md) |
@@ -105,18 +105,21 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
- **Estimate:** 15–25 h for the layer and string extraction, then 3–5 h per language plus native
review; 60–100 h for the 11 key languages. Detail in [`localization-parity.md`](localization-parity.md).
### 6. Plugins: no run-time UI, distribution or widgets; Figma's plugins don't run
### 6. Plugins: no distribution or widgets; Figma's plugins don't run
- **Landed (#101):** sandboxed WebAssembly plugins (wasmi, metered, memory cap, no WASI, also in
the browser) acting through the command registry, one undo step per run, manifest permissions,
Plugins menu, Run last plugin, Manage plugins, `plugin.*` commands, a Rust SDK and two examples
([`plugins.md`](plugins.md)).
- **Missing:** plugin-defined UI at run time (forms or panels a plugin returns), settings storage,
distribution (a plugin directory), widgets, Community files. Figma's own plugins are JavaScript
against Figma's plugin API and don't run.
([`plugins.md`](plugins.md)). Plugin panels and settings (ABI v2, 2026-10-11): a plugin shows a
panel of fields and buttons drawn with FigCraft's widgets and is called back for each button or
changed value (one undo step per callback, same sandbox limits), driven headless with
`plugin.ui` / `plugin.event` / `plugin.close`; per-plugin settings kept with the preferences.
- **Missing:** distribution (a plugin directory), widgets, Community files; panels are one at a
time with no colour picker or images. Figma's own plugins are JavaScript against Figma's plugin
API and don't run.
- **Impact:** teams that depend on specific Figma plugins (icons, content, accessibility checks)
can't bring them. FigCraft's CLI, control channel and MCP server cover agent automation better
than Figma does.
- **Estimate:** 25–55 h for the missing pieces of FigCraft's own API. Running Figma's plugins
- **Estimate:** 15–40 h for the missing pieces of FigCraft's own API. Running Figma's plugins
needs a JS engine and an API shim: **owner decision needed**.
### 7. CPU canvas where Figma uses the GPU
@@ -275,6 +278,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | minor | Plugin panels and settings landed (gap 6, ABI v2; v1 plugins unchanged): a plugin shows a panel of notes, fields (text, number, checkbox, menu, list, colour) and buttons drawn with FigCraft's widgets; a button or a changed value calls it back with every value (same limits per callback, one undo step per callback that edits, rolled back on failure), its module staying loaded while the panel is open; `plugin.ui` / `plugin.event` / `plugin.close` drive panels headless; per-plugin settings (64 KB) kept with the preferences; the Swatch grid example gained a palette panel that remembers the last palette. Plugins & widgets breadth 35% → 45%, ready 20% → 28%, 25–55 h → 15–40 h; ecosystem 20% → 28%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~373–712 h → ~363–697 h |
| 2026-10-11 | minor | Canvas fixes (gap 13 closed): boolean groups are clicked where their result is (a click in a subtracted hole reaches what's behind; with the group or an operand selected its operands are hit, so each can still be picked), a layer dragged inside a group stays in the group (frames and auto layout still take other drops), View ▸ Frame outlines and View ▸ Slices draw on the canvas, and the idle tests wait for the render thread instead of counting frames. Shapes & vector 1–3 h → 1–2 h (vector QA with real files left); percentages, feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding |
| 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~378–722 h → ~375–717 h |
| 2026-10-11 | minor | Stretch and scatter brushes and pen pressure landed (gap 9): eight brushes drawn in code (four stretched along the path, four scattered with spacing, size, rotation and scatter jitter from a seed) in the stroke settings, drawn everywhere strokes are and flattened by outline stroke; the Pencil records pen pressure as the stroke's width profile (Windows pens, browser styluses; macOS and Linux tablets report none) and can draw with a brush; Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–9 h; Strokes 2–4 h → 1–3 h; Hardware 50% → 55%, 15–30 h → 13–27 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~378–722 h → ~373–712 h |
+13 -1
View File
@@ -699,12 +699,24 @@ manifest, the ABI and the Rust SDK). Driven with `run_command`: `plugin.import {
`figcraft-plugin.json` or its folder) or `plugin.import {manifest, wasm}` (the manifest as an
object or JSON text and the module as base64, for this session only), `plugin.list`,
`plugin.remove {id}`, and `plugin.run {id, command?, params?, fuel?}`, which returns
`{plugin, command, output, logs, calls, fuel}`. A run is one undo step and is rolled back when it
`{plugin, command, output, logs, calls, fuel, panel}`. A run is one undo step and is rolled back when it
fails; a plugin only reads the document unless its manifest asks for `write`, may be limited to
some commands, and can never touch files, other documents, the undo history, dialogs or other
plugins. A headless MCP session starts with no plugins: import them first. `plugins.runLast` runs
the last plugin command again.
A plugin that shows a panel (API 2) can be used headless too: `plugin.ui {id?}` returns the open
panel (`{plugin, command, title, controls, values}`, or null), `plugin.event {id, control, value?}`
presses a button or sets a field as a click would (the plugin is called back; one undo step;
returns like `plugin.run`, with the updated `panel`), and `plugin.close {id}` ends the session.
Plugin settings are kept per plugin with the preferences (in a headless session, for the session).
```json
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"run_command","arguments":{"command":"plugin.run","params":{"id":"org.figcraft.examples.swatch-grid","command":"panel"}}}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"run_command","arguments":{"command":"plugin.event","params":{"id":"org.figcraft.examples.swatch-grid","control":"palette","value":"Forest"}}}}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"run_command","arguments":{"command":"plugin.event","params":{"id":"org.figcraft.examples.swatch-grid","control":"generate"}}}}
```
### Dev Mode
- **Component code:** `inspect.code {id, language: react|swiftui|compose}` on a component, a
+94 -22
View File
@@ -9,9 +9,13 @@ permissions its manifest asks for, and nothing more.
WebAssembly interpreter (no JIT), in the desktop app and in the browser build alike. There is no
WASI: no file system, network, clock or randomness. A run has an instruction budget (fuel) and a
memory cap, so an endless loop or a runaway allocation fails that run, not the app.
- **One undo step.** Everything a run does is one step in the undo history, named after the plugin.
When a run fails (the plugin returns an error, traps, panics or runs out of fuel or memory), the
document, selection and page are put back as they were.
- **One undo step.** Everything a run does is one step in the undo history, named after the plugin;
so is everything one callback from its panel does. When a run or a callback fails (the plugin
returns an error, traps, panics or runs out of fuel or memory), the document, selection and page
are put back as they were.
- **Panels and settings (API 2).** A plugin can show a small panel of its own while it runs: FigCraft
draws its fields and buttons with its own widgets and calls the plugin back when one is used. A
plugin can also keep settings from one run to the next.
- **Permissions.** The manifest says whether the plugin only reads the document or may edit it, and
can narrow the commands it may call. FigCraft enforces both.
@@ -30,12 +34,30 @@ permissions its manifest asks for, and nothing more.
A message the plugin shows (`notify`) appears as a toast; so does a text result.
From scripts, the CLI and MCP the same commands apply:
**Plugin panels.** A plugin written for API 2 can show a panel while it runs: a floating window,
first placed beside the properties panel (drag its title bar to move it), with the plugin's notes,
fields (text, number, checkbox, menu, list, colour) and buttons, drawn with FigCraft's own widgets
and named for screen readers. Pressing a button or changing a field calls the plugin back with every
field's value; what that callback does is one undo step (none when it changes nothing), and the
plugin can update the panel or close it. The module stays loaded while its panel is open, so the
plugin keeps its state between callbacks. Closing the window ends the plugin's session. One plugin
panel is open at a time: running another plugin that shows one replaces it. An open panel costs
nothing while nobody uses it (no frames are drawn).
If a callback fails, it is rolled back and its message shown; a plugin that returned an error
keeps its panel, one that crashed or ran out of fuel or memory loses it.
From scripts, the CLI and MCP the same commands apply, panels included:
```jsonl
{"command": "plugin.import", "params": {"path": "plugins/tidy-names"}}
{"command": "select.all", "params": {}}
{"command": "plugin.run", "params": {"id": "org.figcraft.examples.tidy-names", "command": "tidy", "params": {"case": "Title Case"}}}
{"command": "plugin.import", "params": {"path": "target/plugins/dist/swatch-grid"}}
{"command": "plugin.run", "params": {"id": "org.figcraft.examples.swatch-grid", "command": "panel"}}
{"command": "plugin.event", "params": {"id": "org.figcraft.examples.swatch-grid", "control": "palette", "value": "Ocean"}}
{"command": "plugin.event", "params": {"id": "org.figcraft.examples.swatch-grid", "control": "generate"}}
{"command": "plugin.close", "params": {"id": "org.figcraft.examples.swatch-grid"}}
```
`figcraft-cli run script.jsonl --in design.figcraft --out tidy.figcraft` runs that headless; over
@@ -47,7 +69,10 @@ plugins: import them in the script.
| `plugin.list` | `{}` | `{plugins: [{id, name, version, description, main, api, permissions, commands, path, loaded}], last: {id, command}}` |
| `plugin.import` | `{path}` or `{manifest, wasm}` | `path` is a `figcraft-plugin.json` or its folder. Without files (the browser, tests), pass the manifest (an object or JSON text) and the module as base64 `wasm`; such a plugin lasts for the session only. A plugin with the same id is replaced. The module is checked first. |
| `plugin.remove` | `{id}` | Uninstalls it. |
| `plugin.run` | `{id, command?, params?, fuel?}` | Runs a command (default: the first) with its parameters checked against the manifest and the missing ones filled with their defaults; `fuel` lowers the instruction budget. Returns `{plugin, command, output, logs, calls, fuel}`; `output` is the plugin's result, parsed as JSON when it is JSON. |
| `plugin.run` | `{id, command?, params?, fuel?}` | Runs a command (default: the first) with its parameters checked against the manifest and the missing ones filled with their defaults; `fuel` lowers the instruction budget (for the panel's callbacks too). Returns `{plugin, command, output, logs, calls, fuel, panel}`; `output` is the plugin's result, parsed as JSON when it is JSON; `panel` is the plugin's open panel (as `plugin.ui` returns it) or null. |
| `plugin.ui` | `{id?}` | The open plugin panel: `{plugin, command, title, controls, values}` (`values`: every field's current value by name), or null. With `id`, only that plugin's. |
| `plugin.event` | `{id, control, value?}` | Uses a control of plugin `id`'s open panel, as a click would: a button (no `value`), or a field set to `value` (checked like a parameter: a number is clamped, a list or menu value must be one of its options, a colour must be hex). The plugin is called back; returns like `plugin.run`. |
| `plugin.close` | `{id}` | Closes plugin `id`'s panel and ends its session. `{closed}` says whether one was open. |
| `plugins.runLast` | `{}` | Runs the last plugin command again. |
| `plugins.manage` | `{}` | Opens the Plugins window; returns the list like `plugin.list`. |
@@ -85,11 +110,11 @@ plugins: import them in the script.
| `name` | Required. Shown in the Plugins menu (64 characters at most). |
| `version`, `description` | Optional text. |
| `main` | Required. The module, a `.wasm` path relative to the manifest, inside its folder. |
| `api` | The plugin API version, `1` (the default). A manifest asking for a newer one is refused. |
| `api` | The plugin API version the plugin was written for: `1` (the default) or `2` (panels and settings). A manifest asking for a newer one than FigCraft runs is refused, so a plugin that needs panels fails clearly on an older FigCraft. |
| `permissions.access` | `read` (the default): commands that leave the document as it is; such plugins also run from Dev Mode's Inspect tab. `write`: edits too. |
| `permissions.commands` | Optional allowlist: exact command ids or prefixes ending in `*` (`node.*`). Empty: any command the access allows. |
| `commands` | The menu commands, each `{id, label, params?}`. Without any, the plugin has one, `run`. |
| `params[]` | `{name, label?, type, default?, options?, min?, max?}`; `type` is `text`, `number` (clamped to `min`/`max`), `boolean` or `select` (one of `options`). |
| `params[]` | `{name, label?, type, default?, options?, min?, max?}`; `type` is `text`, `number` (clamped to `min`/`max`), `boolean`, `select` (one of `options`, a menu), `list` (one of `options`, shown as a list) or `color` (hex, `#RRGGBB` or `#RRGGBBAA`). A default that doesn't fit is dropped. |
## What a plugin can't do
@@ -107,13 +132,17 @@ it can handle):
- for a `read` plugin, any command that changes the document (checked before the call for commands
that make an undo step, and after it for the rest: a change is undone and refused).
Limits per run: 1,000,000,000 fuel (about a second or two of plain WebAssembly in a debug build,
far less in a release build), 64 MB of memory, 100,000 host calls (each also costs 10,000 fuel),
16 MB per string either way, 1,000 log lines, and modules up to 16 MB.
Limits per run, and again per panel callback: 1,000,000,000 fuel (about a second or two of plain
WebAssembly in a debug build, far less in a release build), 64 MB of memory, 100,000 host calls
(each also costs 10,000 fuel), 16 MB per string either way, 1,000 log lines, and modules up to
16 MB. A panel spec is at most 64 KB with 64 controls; settings are at most 64 KB per plugin (keys
and JSON values), keys 1–128 characters.
## ABI (version 1)
## ABI (version 2)
Strings are UTF-8 `(ptr, len)` pairs in the plugin's linear memory; all values are `i32`.
Version 2 adds panels and settings to version 1; a version 1 module runs unchanged and gets the
input it always got. Strings are UTF-8 `(ptr, len)` pairs in the plugin's linear memory; all values
are `i32`.
The module **exports** `memory` and `figcraft_run(input_len) -> status`. When `figcraft_run` starts,
the reply buffer holds the input JSON, `input_len` bytes:
@@ -122,6 +151,13 @@ the reply buffer holds the input JSON, `input_len` bytes:
{"api": 1, "command": "grid", "params": {"palette": "#FF5A5F", "columns": 3, "labels": true}, "selection": [12, 15]}
```
`api` is the manifest's. When the plugin's panel calls it back, `figcraft_run` runs again in the
same instance (its memory as the last call left it) with the event and every field's value added:
```json
{"api": 2, "command": "panel", "params": {}, "selection": [], "event": {"control": "generate", "value": null}, "values": {"palette": "Ocean", "columns": 3, "labels": true}}
```
It returns `0` for success; anything else fails the run (and rolls it back), with the text passed
to `set_output` as the error message.
@@ -137,8 +173,37 @@ included, is refused when the plugin is imported):
| `log` | `(ptr, len)` | A line of the run's log (returned in `logs`, and written to FigCraft's log). |
| `notify` | `(ptr, len)` | A short message for the user (a toast in the app). |
| `set_output` | `(ptr, len)` | The run's result (JSON or text), or its error message when it fails. |
| `ui_show` | `(ptr, len) -> status` | API 2. Shows the plugin's panel, or replaces the one showing (the panel spec below, as JSON). `1`: the spec is invalid, the reply says why. |
| `ui_close` | `()` | API 2. Closes the panel when the call returns. |
| `setting_get` | `(key_ptr, key_len) -> status` | API 2. `0`: the reply holds the setting's JSON. `1`: there is none. |
| `setting_set` | `(key_ptr, key_len, value_ptr, value_len) -> status` | API 2. Stores JSON under the key, for this plugin only, kept with the preferences. `1`: refused (not JSON, a bad key, over 64 KB), the reply says why. |
| `setting_delete` | `(key_ptr, key_len)` | API 2. Forgets a setting. |
A pointer or length outside the plugin's memory traps, which fails the run.
A pointer or length outside the plugin's memory traps, which fails the run. Host functions can only
be called while `figcraft_run` runs (not from a start function). Settings are forgotten when the
plugin is removed.
### Panel spec
```json
{"title": "Swatch grid", "controls": [
{"type": "list", "name": "palette", "label": "Palette", "options": ["Sunset", "Ocean"], "value": "Ocean"},
{"type": "note", "label": "#0A2463, #1E5AA8, #3E92CC"},
{"type": "number", "name": "columns", "label": "Columns", "min": 1, "max": 24, "value": 3},
{"type": "boolean", "name": "labels", "label": "Label each swatch", "value": true},
{"type": "color", "name": "accent", "label": "Accent", "value": "#FF5A5F"},
{"type": "button", "name": "generate", "label": "Generate"},
{"type": "button", "name": "close", "label": "Close"}
]}
```
`title` defaults to the plugin's name. Controls are the manifest's parameter types (`text`,
`number`, `boolean`, `select`, `list`, `color`) plus `button` and `note` (text in `label`, up to
1,000 characters, no `name` needed), in order, each `name` unique. A field shows its `value`; a
field without one keeps what the user entered before the panel was updated, so a plugin can update
a note without resetting the other fields. Buttons next to each other share a row; the first button
is the primary one. FigCraft checks the whole spec (sizes, kinds, options, bounds) and drops values
that don't fit their field.
## Writing a plugin in Rust
@@ -149,8 +214,11 @@ A pointer or length outside the plugin's memory traps, which fails the run.
- **Tidy names** (`tidy-names`): cleans up the selected layers' names (extra spaces, the "copy" left
by duplicating) and optionally their letter case, inside the selection too. Two commands: one
with a form (case, include layers inside), and a quick one without.
- **Swatch grid** (`swatch-grid`): a grid of colour swatches from a palette of hex colours, centred in
the view, named after their colours and optionally labelled.
- **Swatch grid** (`swatch-grid`, API 2): a grid of colour swatches, centred in the view, named after
their colours and optionally labelled. **Palettes…** opens a panel with a list of built-in
palettes (picking one shows its colours), columns, labels and a Generate button; the palette
picked last is remembered as a setting for next time. **From hex colours…** takes a palette of
hex colours in the parameters form.
`cargo xtask plugins` (part of `cargo xtask wasm` and `cargo xtask ci`; it needs the
`wasm32-unknown-unknown` target: `rustup target add wasm32-unknown-unknown`) checks, tests and
@@ -202,8 +270,10 @@ entry!(run);
Build it with `cargo build --release --target wasm32-unknown-unknown`, put
`target/wasm32-unknown-unknown/release/my_plugin.wasm` next to a `figcraft-plugin.json` whose `main`
names it, and import the manifest. The SDK gives `call`, `selection`, `log`, `notify` and the
`Input` (`command`, `params` with `str`/`f64`/`bool` helpers, `selection`); returning `Err` fails
the run and rolls it back. Outside WebAssembly every host call fails, so the plugin's own logic can
`Input` (`command`, `params` with `str`/`f64`/`bool` helpers, `selection`, and `event` when the
panel calls back: `control`, `value` and every field's `values`); returning `Err` fails the run and
rolls it back. With `"api": 2` in the manifest: `show_ui(&json!({...}))` and `close_ui()` for the
panel, `setting(key)`, `set_setting(key, &value)` and `delete_setting(key)` for settings. Outside WebAssembly every host call fails, so the plugin's own logic can
be unit-tested with `cargo test`.
Any language that compiles to WebAssembly works the same way: export `memory` and `figcraft_run`,
@@ -211,9 +281,11 @@ import what you need from `figcraft`.
## Not yet
- Plugin-defined UI: a plugin can't yet return a form or panel of its own at run time; parameters
come from the manifest.
- Storage of the plugin's own: plugins can keep data on layers with `node.set {props: {pluginData:
{key: value}}}`, but have no settings of their own between runs.
- Distribution: no plugin browser, signing or automatic updates; plugins are imported from a
- Distribution: no plugin directory, signing or automatic updates; plugins are imported from a
manifest.
- Widgets (objects on the canvas drawn by a plugin) and plugins written for other apps (JavaScript
plugins): neither runs.
- Panels: one at a time; a colour field is a swatch with a hex field (no colour picker); no images,
sliders or free layout inside a panel.
- Data in the document: plugins keep data on layers (pages included) with `node.set {props:
{pluginData: {key: value}}}`; there is no separate per-document store.
+3 -2
View File
@@ -1,6 +1,6 @@
# FigCraft roadmap: milestones and what's next
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%) · **Target:** Figma Design (desktop app 126.10.7)
Forward-looking plan. Where FigCraft stands today: [`ROADMAP.md`](../ROADMAP.md) and
[`target-app-parity.md`](target-app-parity.md); the full ranked list: [`gaps.md`](gaps.md). Hours
@@ -57,7 +57,7 @@ blockers.
| M8 Hardening & performance (bench, retained canvas, accessibility, idle) | ~60% | real-file corpus, platform QA, GPU canvas, screen readers | 60–110 |
| M9 Real-time collaboration | core done (#103: ids, change sets, server ordering, convergence); live sessions (WebSocket transport with limits and reconnect, hosting from the app, invite links, document sync between apps, cursors, selections, avatars, follow, `collab.*` commands); per-user undo, rejection handling, offline queue and resuming after a reconnect; styles, variables, collections, modes and comments created in a session get ids from their creator's space; others' text carets and selections on the canvas; the browser client (joins desktop-hosted sessions); `figcraft://` links open the app and join (Windows registers per user; macOS and Linux packages declare the scheme); the standalone `figcraft-server` (persistence with compaction, restarts that lose nothing confirmed, publishing from the app, share links to edit or view, revocable); team libraries kept on the server and enabled by a share link, with usage counts, and swapping a library | accounts, teams, TLS in the desktop app, packaging the server | 45–90 |
| M10 Localization (new) | not started | layer + 11 languages | 60–100 |
| M11 Plugins | runtime, API, menu, manager and SDK done (#101) | run-time plugin UI, settings storage, distribution, widgets; Figma plugin compatibility is an owner decision | 25–55 |
| M11 Plugins | runtime, API, menu, manager and SDK done (#101); plugin panels and settings (ABI v2) | distribution, widgets; Figma plugin compatibility is an owner decision | 15–40 |
| M12 AI features (new, owner decision) | not started | local models | 30–60 |
## Next, after the current focus
@@ -72,6 +72,7 @@ blockers.
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | minor | Plugin panels and settings landed (gap 6, ABI v2; v1 plugins unchanged): a plugin shows a panel of notes, fields (text, number, checkbox, menu, list, colour) and buttons drawn with FigCraft's widgets; a button or a changed value calls it back with every value (same limits per callback, one undo step per callback that edits, rolled back on failure), its module staying loaded while the panel is open; `plugin.ui` / `plugin.event` / `plugin.close` drive panels headless; per-plugin settings (64 KB) kept with the preferences; the Swatch grid example gained a palette panel that remembers the last palette. Plugins & widgets breadth 35% → 45%, ready 20% → 28%, 25–55 h → 15–40 h; ecosystem 20% → 28%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~373–712 h → ~363–697 h |
| 2026-10-11 | minor | Canvas fixes (gap 13 closed): boolean groups are clicked where their result is (a click in a subtracted hole reaches what's behind; with the group or an operand selected its operands are hit, so each can still be picked), a layer dragged inside a group stays in the group (frames and auto layout still take other drops), View ▸ Frame outlines and View ▸ Slices draw on the canvas, and the idle tests wait for the render thread instead of counting frames. Shapes & vector 1–3 h → 1–2 h (vector QA with real files left); percentages, feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding |
| 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~378–722 h → ~375–717 h |
| 2026-10-11 | minor | Stretch and scatter brushes and pen pressure landed (gap 9): eight brushes drawn in code (four stretched along the path, four scattered with spacing, size, rotation and scatter jitter from a seed) in the stroke settings, drawn everywhere strokes are and flattened by outline stroke; the Pencil records pen pressure as the stroke's width profile (Windows pens, browser styluses; macOS and Linux tablets report none) and can draw with a brush; Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–9 h; Strokes 2–4 h → 1–3 h; Hardware 50% → 55%, 15–30 h → 13–27 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~378–722 h → ~373–712 h |
+13 -10
View File
@@ -1,6 +1,6 @@
# FigCraft parity with Figma
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%; ready ~47%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%; ready ~47%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS)
The authoritative assessment of how close FigCraft is to Figma Design. [`ROADMAP.md`](../ROADMAP.md)
summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu checklist is
@@ -21,7 +21,7 @@ summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu c
| Essentials user | **~53%** | estimated ([below](#essentials-user): core features 77.8% × discounts 0.92 × 0.92 × 0.8) |
| Mainstream practitioner | **~43%** | estimated ([below](#mainstream-practitioner): weekly areas 66.8% × discounts 0.9 × 0.9 × 0.8) |
| Remaining to beta | **~200–365 h** one Opus 5.5 agent (~40–80 h wall-clock with 5 parallel agents) | estimated |
| Remaining to full parity | **~373–712 h** one agent (~95–165 h wall-clock with 5 parallel agents) | estimated |
| Remaining to full parity | **~363–697 h** one agent (~95–165 h wall-clock with 5 parallel agents) | estimated |
| Stage | **alpha**: the alpha gate passes ([roadmap](roadmap.md#alpha-gate)); `.fig` import and multiplayer are beta gaps (see [ROADMAP](../ROADMAP.md#stage)) | |
The previous figures (ROADMAP of 2026-10-10, "feature depth ~79%", "full parity 115–195 h") were
@@ -35,7 +35,7 @@ tool; presence was being graded as behaviour in places.
| Audience | Ready % | Opus 5.5 agent wall-clock hours to ~95% | Work that dominates |
|---|---|---|---|
| Full target (ready for real work) | ~47% | ~355–675 h | multiplayer (45–90), localization (60–100), plugins (25–55), the interaction pass (40–60), `.fig` import (30–60), GPU canvas (30–60), AI (30–60) |
| Full target (ready for real work) | ~47% | ~345–660 h | multiplayer (45–90), localization (60–100), plugins (15–40), the interaction pass (40–60), `.fig` import (30–60), GPU canvas (30–60), AI (30–60) |
| Mainstream practitioner | ~43% | ~165–310 h | core editor depth incl. `.fig` import (83–163), the interaction pass (40–60), real-file corpus and platform QA (25–50), `.fig` export for handing work back (15–30) |
| Essentials user | ~53% | ~85–160 h | `.fig` import so files people send open (30–60), core tool polish (25–45), interaction pass on the core tools (15–25), stability QA (10–20), onboarding and starter templates (5–10) |
@@ -111,11 +111,11 @@ Breadth: does it exist. Ready: depth, correctness and fidelity for real projects
| 15 | Dev Mode / inspect | 2 | 90% | 64% | Dev resources links, import lines for connected instances inside generated layer trees; an editor extension is left to the MCP server (code connect and plugins in Dev Mode landed) ([dev-mode-parity](dev-mode-parity.md)) | 1–3 |
| 16 | Comments, versions, find, misc | 3 | 85% | 60% | Comments are local only; a branch's own version history (branches with review and merge landed) | 2–5 |
| 17 | Multiplayer & sharing | 8 | 15% | 5% | Core landed (`crates/collab`: ids, change sets, server ordering, convergence tested); live sessions (preview): WebSocket transport, hosting from the desktop app, joining from the app or the browser, invite links that open the app, cursors, selections and follow, per-person undo, offline resume; a self-hosted `figcraft-server` with persistence and share links to edit or view. Left: accounts and teams, cursor chat, TLS in the desktop app ([collaboration-parity](collaboration-parity.md)) | 45–90 |
| 18 | Plugins & widgets | 4 | 35% | 20% | Own sandboxed WASM plugins landed (#101: manifest, permissions, menu, Run last, Manage, SDK, 2 examples). Left: plugin UI at run time, settings storage, distribution, widgets; Figma's plugins are JavaScript and don't run (a JS engine would be an owner decision) ([plugins.md](plugins.md)) | 25–55 |
| 18 | Plugins & widgets | 4 | 45% | 28% | Own sandboxed WASM plugins landed (#101: manifest, permissions, menu, Run last, Manage, SDK, 2 examples), then plugin panels called back per button or changed value and per-plugin settings (ABI v2). Left: distribution, widgets; panels one at a time without a colour picker; Figma's plugins are JavaScript and don't run (a JS engine would be an owner decision) ([plugins.md](plugins.md)) | 15–40 |
| 19 | Team libraries & organization | 2 | 70% | 40% | Libraries kept on `figcraft-server` under their file and enabled by a share link (per-asset versions, background update checks, overrides kept), usage counts per component and file, swapping a library by names. Left: team-wide library lists, accounts and permissions, analytics over time, pushed update notices | 4–8 |
| 20 | Figma Draw extras | 2 | 80% | 55% | Have: variable width, lasso, shape builder, noise/texture, text on a path, pattern fills, stretch and scatter brushes, pen pressure (Windows pens, browser styluses). Missing: multi-edit, pressure from macOS/Linux tablets | 2–5 |
| 21 | AI features | 3 | 0% | 0% | First draft, rename layers, image edit/remove background, visual search, Make: needs local models (owner decision) | 30–60 |
| | **Weighted (÷113)** | 113 | **~79%** | **~57%** | | **200–401** |
| | **Weighted (÷113)** | 113 | **~79%** | **~57%** | | **190–386** |
| | Core editor only (1–16, ÷94) | 94 | ~90% | ~67% | | 85–167 |
## By dimension (ready for real work)
@@ -131,9 +131,9 @@ Breadth: does it exist. Ready: depth, correctness and fidelity for real projects
| Hardware | 3 | 55% | 13–27 | Trackpad pinch and HiDPI yes; pen pressure on Windows and from browser styluses (none from macOS/Linux tablets); no GPU canvas, no P3 on screen, one window | [hardware-parity](hardware-parity.md) |
| Localization | 3 | 10% | 60–100 | English only; Figma ships 7 more UI languages | [localization-parity](localization-parity.md) |
| Platforms | 3 | 75% | 10–20 | macOS, Windows (x64/x86/arm64), Linux, FreeBSD, web all packaged (v0.2.0); Figma also has iOS/Android apps; non-macOS QA thin | [hardware-parity](hardware-parity.md) |
| Ecosystem/plugins | 3 | 20% | 25–55 | Own WASM plugins with a Rust SDK and two examples (#101); no Figma plugin compatibility, no widgets, no distribution or community files; the CLI, control channel and MCP server are ahead of Figma for agents | [gaps](gaps.md) |
| Ecosystem/plugins | 3 | 28% | 15–40 | Own WASM plugins with a Rust SDK and two examples (#101), plugin panels and settings (ABI v2); no Figma plugin compatibility, no widgets, no distribution or community files; the CLI, control channel and MCP server are ahead of Figma for agents | [gaps](gaps.md) |
| AI features | 2 | 0% | 30–60 | None | [gaps](gaps.md) |
| **Ready for real work** | 100 | **~47%** | **~373–712** | | |
| **Ready for real work** | 100 | **~47%** | **~363–697** | | |
Dimension hours add up to the full-parity total without double counting: the feature-area rows
for import/export (14), multiplayer and team libraries (17, 19), plugins (18) and AI (21) are
@@ -463,9 +463,11 @@ Hours in this narrative are the builders' estimates; the tables above supersede
manifest… (kept in the preferences, the module re-read on every run), the installed plugins in
the Plugins menu with a parameters form drawn from the manifest, Run last plugin (⌥⌘P), Manage
plugins, and `plugin.*` commands for scripts, the CLI and MCP; a Rust SDK and two example
plugins (Tidy names, Swatch grid) in `plugins/`, built by `cargo xtask plugins`. *Left:*
plugin-defined UI at run time (forms or panels a plugin returns), plugin settings storage,
distribution; for collaboration, the browser client and a standalone server. 18–45 h.
plugins (Tidy names, Swatch grid) in `plugins/`, built by `cargo xtask plugins`. Then plugin
panels (ABI v2: fields and buttons drawn with FigCraft's widgets, the plugin called back for
each button or changed value as one undo step, `plugin.ui` / `plugin.event` / `plugin.close`
for scripts and MCP) and per-plugin settings kept with the preferences. *Left:* plugin
distribution; for collaboration, the browser client and a standalone server. 15–40 h.
9. **Hardening and performance**: *landed:* `cargo xtask bench` (synthetic 1k/10k/50k-layer
pages: engine, render and headless UI-frame timings). The canvas keeps its last image and
re-renders only what a pan uncovers or an edit touches: moving one layer at 10k layers re-renders
@@ -578,6 +580,7 @@ Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count th
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | minor | Plugin panels and settings landed (gap 6, ABI v2; v1 plugins unchanged): a plugin shows a panel of notes, fields (text, number, checkbox, menu, list, colour) and buttons drawn with FigCraft's widgets; a button or a changed value calls it back with every value (same limits per callback, one undo step per callback that edits, rolled back on failure), its module staying loaded while the panel is open; `plugin.ui` / `plugin.event` / `plugin.close` drive panels headless; per-plugin settings (64 KB) kept with the preferences; the Swatch grid example gained a palette panel that remembers the last palette. Plugins & widgets breadth 35% → 45%, ready 20% → 28%, 25–55 h → 15–40 h; ecosystem 20% → 28%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~373–712 h → ~363–697 h |
| 2026-10-11 | minor | Canvas fixes (gap 13 closed): boolean groups are clicked where their result is (a click in a subtracted hole reaches what's behind; with the group or an operand selected its operands are hit, so each can still be picked), a layer dragged inside a group stays in the group (frames and auto layout still take other drops), View ▸ Frame outlines and View ▸ Slices draw on the canvas, and the idle tests wait for the render thread instead of counting frames. Shapes & vector 1–3 h → 1–2 h (vector QA with real files left); percentages, feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding |
| 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~378–722 h → ~375–717 h |
| 2026-10-11 | minor | Stretch and scatter brushes and pen pressure landed (gap 9): eight brushes drawn in code (four stretched along the path, four scattered with spacing, size, rotation and scatter jitter from a seed) in the stroke settings, drawn everywhere strokes are and flattened by outline stroke; the Pencil records pen pressure as the stroke's width profile (Windows pens, browser styluses; macOS and Linux tablets report none) and can draw with a brush; Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–9 h; Strokes 2–4 h → 1–3 h; Hardware 50% → 55%, 15–30 h → 13–27 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~378–722 h → ~373–712 h |