The owner's engraving-style chameleon drawing, traced to three colours (ink, paper,
FigCraft green #02da96) on the shared 512-unit tile. assets/app-icon/figcraft.svg is the
master; cargo xtask icon now renders the PNGs (macOS grid), the hicolor theme and the .ico
(tight crop, 16-256 px) from it with resvg instead of drawing the old geometric mark.
Off macOS the window icon uses the tight 256 px render.
Widgets for FigCraft's own WebAssembly plugins (plugin API 3; API 1 and 2 plugins run unchanged), plus the panel tail.
- **Widgets.** A manifest with `"api": 3, "widget": true` makes a widget plugin. Plugins > Widgets (or `widget.insert {plugin, x?, y?}`) puts its widget on the page: the plugin is called with `widget: {id, state, event}` and hands back a description through the new `widget_render` import (`{state?, root}`: frames with auto layout, text, rectangles, ellipses and images it carries as base64). FigCraft validates the description (4 MB, 256 layers, 16 deep, finite numbers within ranges, hex colours, text length and control characters, a 16 KB state, images 1 MB each / 2 MB together / 4096 px a side) and builds it as a frame whose layers are locked, so the widget moves as one layer.
- **Clicks.** A plain click with the Move tool on a part the plugin named as a `target` (the cursor turns into a pointing hand there; quick repeated clicks each count) calls the plugin back in a fresh, metered sandbox (same fuel, memory and call limits as runs; nothing kept between calls, so the state and the click alone decide the result) as one undo step, and the widget is redrawn. Dragging moves it; Shift/Cmd/Alt clicks only select. Agents use `widget.click {id, target}` and `widget.list`.
- **State in the document.** The plugin id and the state live in the widget's own layer data (`figcraft.widget*`), so widgets survive save and reload, duplicate/copy and paste (the copy is a widget of its own), undo/redo, export (the built layers draw) and live sessions (synced like any layer data). No plugin can write `figcraft.widget*` data or call `widget.*`; a callback can only rebuild its own widget, and touches other layers only through the commands its manifest permits. A widget whose plugin is missing stays as drawn.
- **Example.** `plugins/examples/counter`: a counter between - and + buttons (read access, no assets). The SDK gained `Input::widget` and `render_widget`.
- **Panel tail.** Panels of several plugins are open at once, side by side (`plugin.ui` without an id returns the one opened last); a colour field's swatch opens FigCraft's colour picker beside the plugin's window (the plugin hears of the colour when a drag ends; also for parameters forms). Swatch grid's palette panel gained a label colour field.
- Docs: plugins.md (ABI v3, widget description, commands, limits), mcp.md, target-app-parity area 18, ROADMAP, gaps gap 6, roadmap M11; Spanish labels for the new commands.
* M9: real-time collaboration protocol (design)
docs/collab-protocol.md: WebSocket sessions with a server-ordered,
per-property last-writer-wins document sync, client-scoped ids,
fractional positions for children, ephemeral presence (cursors,
selections, follow), per-user undo, hosting from the app first and a
self-hosted server next, and the implementation plan. ROADMAP gains M9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M9: collaboration core: ids, change sets, server ordering, convergence
Step 1 of docs/collab-protocol.md, no networking:
- Client-scoped node ids: Document::set_client_id makes alloc_id hand out
client << 32 | counter; single-user files allocate as before; the id
counter is no longer part of document equality.
- crates/collab (layer 2): the synced object/property model (every saved
Node field, plus document-level styles, comments, variables, images,
dev snapshots, library data), Change batches, an O(changed) diff, strict
and lenient apply, fractional sibling positions, snapshots.
- Server (ordering, validation, rejects with fixes) and Client (optimistic
view, replay under remote batches, author-side repair of derived data)
as pure state machines; binary (version byte + postcard, JSON values) and
JSON message forms, fuzzed decoders.
- Engine: normalize as the recompute hook, collab.diff.
- Tests: round trips over random real commands, an N-client convergence
fuzz test over delayed in-order links, junk batches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M9: fmt after merge
* M9: live sessions: host from the app, invite links, live cursors and presence
Step 2 of docs/collab-protocol.md, networking and presence half:
- crates/collab-net (layer 6): WebSocket transport (sync tungstenite, a
reader and a writer thread per connection, no async runtime), the
session host the app embeds (runs figcraft_collab::Server as the
authority, carries its messages), invite links, presence throttling
(~30 Hz, latest wins), limits (sockets, hello/idle timeouts, message
size, rate, slow clients), keep-alive, clean close, reconnect with
backoff; a browser stand-in until the web-sys client (step 4).
- engine: live session state (status, peers, presence, follow) and the
collab.host/stop/join/leave/peers/follow/cursor commands; the network
is reached only through the LiveLink the app installs.
- ui-egui: remote cursors (pointer drawn in code, name tag, gliding),
selection outlines (once documents sync), avatars in the right panel
with follow, the Share popover (start, invite link + URL with Copy,
stop), File > Join live session.
- desktop app and MCP headless session install the link; xtask layers
keeps networking crates out of L0-L5.
- Documents don't sync between apps yet (follow-up).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M9: live sessions: document sync between apps
- The host's session runs figcraft_collab::Server seeded with the shared
document; every app (the host's too, through the hub without a socket)
is a collab Client. Guests open the host's file from the welcome as a
new tab; leaving keeps it as a local copy.
- Engine: after each local action (a gesture once it ends; nothing during
version or text previews) the session document is diffed into one
Changes batch; server batches go through Client::receive with
EngineRecompute and are installed with Session::apply_remote, which
never touches the undo history. Batches arriving mid-gesture wait.
- Transport: Changes/Reject/Welcome reach the engine as Event::Sync;
LiveLink::send carries batches in order; hellos are limited to 64 KiB,
joined clients to the core's message size.
- Tests: engine sync against a real Server (ids in the client's space,
one batch per gesture, remote edits skip undo, deferral, closing the
file leaves), three apps over loopback converging (create, move,
recolour, delete, rename) and a late joiner getting the server's state.
- Docs: collab-protocol, mcp, README, roadmap docs; the UI's
"cursors only" note is gone, replaced by the per-person undo caveat.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Progress docs per craftrules standards/progress-docs.md: target-app parity, gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, and auto layout, components, prototyping, dev mode, collaboration, format, hardware, UI and localization parity docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M9: real-time collaboration protocol (design)
docs/collab-protocol.md: WebSocket sessions with a server-ordered,
per-property last-writer-wins document sync, client-scoped ids,
fractional positions for children, ephemeral presence (cursors,
selections, follow), per-user undo, hosting from the app first and a
self-hosted server next, and the implementation plan. ROADMAP gains M9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M9: collaboration core: ids, change sets, server ordering, convergence
Step 1 of docs/collab-protocol.md, no networking:
- Client-scoped node ids: Document::set_client_id makes alloc_id hand out
client << 32 | counter; single-user files allocate as before; the id
counter is no longer part of document equality.
- crates/collab (layer 2): the synced object/property model (every saved
Node field, plus document-level styles, comments, variables, images,
dev snapshots, library data), Change batches, an O(changed) diff, strict
and lenient apply, fractional sibling positions, snapshots.
- Server (ordering, validation, rejects with fixes) and Client (optimistic
view, replay under remote batches, author-side repair of derived data)
as pure state machines; binary (version byte + postcard, JSON values) and
JSON message forms, fuzzed decoders.
- Engine: normalize as the recompute hook, collab.diff.
- Tests: round trips over random real commands, an N-client convergence
fuzz test over delayed in-order links, junk batches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M9: fmt after merge
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sandboxed WebAssembly plugins whose API is the command registry:
- figcraft-plugin (new crate, L2): the figcraft-plugin.json manifest
(menu commands with parameters, read/write access, command allowlist)
and the sandbox: wasmi (pure Rust, no JIT, also in the web build) with
fuel metering, a memory cap, no WASI and seven host functions (call,
selection, reply_len, reply_read, log, notify, set_output).
- Engine: plugin.list / plugin.import / plugin.remove / plugin.run,
plugins.runLast (Alt+Cmd+P; Ctrl+Alt+Shift+P on Windows/Linux) and
plugins.manage. A run is one undo step (Session::grouped, which
command.batch now uses too) and is rolled back when it fails. Files,
other documents, the undo history, dialogs and plugins are off limits;
read-only plugins can't change the document. Installed plugins live in
the preferences; development plugins re-read their module every run.
- UI: installed plugins in the Plugins menu, a parameters form drawn
from the manifest, Manage plugins, Plugins > Development > Import
plugin from manifest.
- plugins/: a no_std Rust SDK and two example plugins (Tidy names,
Swatch grid), built for wasm32 by `cargo xtask plugins` (part of
`cargo xtask wasm`).
- Docs: docs/plugins.md (manifest, permissions, limits, ABI, SDK),
docs/mcp.md, docs/control-protocol.md, README, ROADMAP, parity.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M3: components at scale; Alt-drag to swap instances
- Synthetic design system (engine `synthetic_ds`): ~300 components (icons; button, input,
card, nav and badge sets of size x state x type x theme variants), boolean/text/instance
swap/slot properties, instances nested four deep, text and paint styles, light/dark
variables bound across it, and 2,000 instances with overrides at every level.
- Propagation: value, size and added/removed-layer edits of mains reach their copies
directly; other edits rebuild each affected instance once, inner mains first; rebuilds keep
unchanged layers shared; bound variables no longer touch unchanged layers; auto layout
re-flows only where sizes or places changed.
- Fixes: values from variables (theme switches, bound radii) are no longer recorded as
overrides; a detached variable on an instance layer is an override that survives main
edits; exposed nested instances stay exposed; swapped nested instances keep their layer
names and the ids/overrides of layers their own main adds; renamed nested instances keep
their name when detached out.
- Instance swap properties have preferred values. `instance.swapAt` / `library.swapAt` and
Alt-drop from the Assets panel onto an instance swap it (highlighted while dragging).
- Instances pasted into other files follow their mains (library copies shared, published
components used from their library, other mains left behind as a missing main).
- Assets panel lays out only visible tiles and renders thumbnails lazily with a cache
invalidated per changed component; instance panel lists components once per revision.
- `figcraft-cli perf --instances`, UI frame bench and tests in components_scale_tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: the swap perf case drops on a glyph the hit test reaches
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge origin/main into feat/components-scale; weighted depth 79%
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Large documents stay interactive: the canvas re-renders only what a pan uncovers or an edit
touches, the layers panel lays out only the rows in view, and per-frame work no longer walks the
whole page.
- Benchmark: `cargo xtask bench [SIZES]` builds synthetic pages (`figcraft_engine::synthetic`:
screens with an auto layout header, a card group, button instances and shapes/text) at
1k/10k/50k layers and prints engine/render timings (`figcraft-cli perf --layers 1000,10000`) and
headless whole-UI frame timings (an ignored ui-egui test).
- Retained canvas rendering (`figcraft_render::Retained`, used by the render worker): views snap
to whole device pixels, so a pan shifts the previous image in place and renders only the
uncovered strips; an edit renders only where changed layers were and are
(`Document::changed_ids` diffs the two versions skipping shared structure; ancestors that paint
from their children's pixels - booleans, effects, masks - count whole; background blur reach
and noise are allowed for). Zoom, page, options, font or page-level changes, or large changes,
render the whole view as before. `ui.inspect` reports `canvas.renderWork`.
- The texture image is converted on the render thread, not the UI thread.
- Layers panel: only visible rows are laid out (stable ids while scrolling); rows are cached per
document version, page, expanded set and filter.
- Frame labels and layout grids of off-screen top-level frames are skipped; the Prototype tab's
connection sources are cached per document version.
- Text: the layout cache keys by a cheap fingerprint confirmed by `TextData` equality (no Debug
formatting per lookup, 32k entries) and hands out shared layouts; a layout generation marks
renders stale when fonts arrive. Overflowing text is culled by its glyphs, so it no longer
vanishes while its box is off screen.
Before -> after (release, Windows, 1440x900 view, ms; 10k / 50k layers):
- move one layer, re-render @100%: 7.65 / 6.49 -> 0.18 / 0.81
- move one layer, re-render zoomed out: 11.95 / 24.10 -> 0.16 / 0.77
- pan 40 px @100%: 7.53 / 6.82 -> 0.69 / 0.81
- pan 40 px zoomed out: 12.07 / 24.30 -> 1.62 / 4.17
- full render @100% / zoomed to fit: 7.78 / 44.22 -> 6.56 / 38.83 (10k @100%, 50k fit)
- UI frame, every screen expanded in layers: 6.16 / 39.07 -> 0.24 / 0.57
- UI frame, scrolling that panel: 6.11 / 40.18 -> 0.43 / 0.40
- UI frame, layers filtered by name: 1.03 / 6.92 -> 0.17 / 0.36
- UI frame, Prototype tab: 0.89 / 6.45 -> 0.18 / 0.80
- UI frame, idle: 0.31 / 1.43 -> 0.11 / 0.39
Unchanged and within budget: single-layer edits through the engine ~0.01 ms, normalize after a
one-node edit < 0.01 ms, hit test 26 / 93 us. Left: save/load at 50k (~0.7 s each), zoomed-out
renders at 50k (~40 ms).
Tests: retained renders match full renders after pans, moves, recolours, hide/remove/add, undo,
masks, overflowing text, edits under a background blur and an inner-shadow/noise layer, a drop-
shadowed group, and a blur panned in from the view's edge; zoom/page/options/page background
render full; junk views don't panic. Layer-row and prototype-source caches follow edits, undo,
page switches, expansion and the filter. `changed_ids`, the text cache, and the perf CLI.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>