* 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>
6.9 KiB
FigCraft architecture
Last reviewed: 2026-10-10 · Last updated: 2026-10-10 · Change: minor (added the
pluginandcollabcrates from #101 and #103) · Target: Figma Design
How FigCraft is built today. Engine first, a thin swappable UI, everything a command; a Figma-shaped data model (a scene graph of frames with auto layout, components and variables). ~108,800 lines of Rust (2026-10-10).
Crates and layers
cargo xtask layers enforces the layering (xtask/src/layers.rs): a crate may depend only on
lower layers. Nothing below L6 depends on egui, eframe, rfd or winit, so the UI crate can be
replaced.
| Layer | Crate | Lines | Role |
|---|---|---|---|
| L0 | figcraft-geom |
~0.5k | Affine transforms, shape path builders (rounded and smoothed corners, polygons, stars, arcs) on kurbo |
| L0 | figcraft-color |
~1.3k | Colours (HSB/HSL, hex/CSS, Display P3), paints (solid, gradients, image), blend modes |
| L1 | figcraft-doc |
~8.5k | Document model: pages, node tree in a persistent map (imbl, stable ids), properties, components, styles, variables, prototype data, comments, versions. Serde everywhere |
| L2 | figcraft-layout |
~1.8k | Auto layout (horizontal, vertical, wrap, grid; hug/fill/fixed; min/max), constraints, layout grid geometry |
| L2 | figcraft-text |
~3.1k | Font database (optional embedded craft-fonts + system scan), shaping (harfrust), bidi, line layout, glyph outlines (skrifa), variable fonts |
| L2 | figcraft-collab |
~3.4k | Real-time collaboration core (no networking): synced object/property model, change sets, fractional positions, server ordering and validation, client reconciliation, wire messages |
| L2 | figcraft-plugin |
~1.0k | Plugin manifest and the WebAssembly sandbox (wasmi, metered, memory cap, no file system, network or clock) |
| L2 | figcraft-pathops |
~1.1k | Booleans (linesweeper), outline stroke with alignment, arrowheads |
| L3 | figcraft-render |
~5.4k | Scene → pixels on tiny-skia (CPU): isolated layers for opacity/blend/effects, clips, masks, shadows, blurs (uniform, progressive, background), noise, texture, glass, text; banded in parallel; retained rendering of only what changed; levels of detail when zoomed out |
| L3 | figcraft-format |
~5.2k | Native .figcraft (zip of document.json + images; streamed JSON), local library files, versions, REST API file JSON import (rest/) |
| L4 | figcraft-svg |
~3.7k | SVG export and import (usvg) |
| L4 | figcraft-pdf |
~2.3k | PDF export (krilla) with print options |
| L5 | figcraft-engine |
~39.3k | Sessions, undo history (Arc snapshots), the command registry (cmd/*, spec!), pointer tools and vector edit mode, snapping and measuring, components sync, libraries, prototype runtime, Dev Mode code generation, the menu catalog (catalog.rs) and the keymap (keymap.rs), a panic guard |
| L6 | figcraft-ui-egui |
~31.9k | The UI3-style frontend: canvas (render worker), toolbar, layers, Design/Prototype/Inspect panels, colour picker, menus, dialogs, presenter, comments, accessibility (AccessKit), the control channel server |
| L6 | figcraft-mcp |
~2.4k | MCP server (JSON-RPC over stdio), headless or bridged to a running app |
| L6 | figcraft-collab-net |
~2.4k | Live sessions over the network: the WebSocket transport (tungstenite, a reader and a writer thread per connection), the session host the app embeds (runs the collaboration core's server), invite links, presence relay and limits; the only crate allowed networking dependencies |
| apps | figcraft, figcraft-cli, figcraft-web |
~1.9k | Desktop app (eframe + wgpu), command line, browser build (WASM, WebGPU/WebGL2) |
| plugins | plugins/ (own workspace) |
Rust plugin SDK and two example plugins, built for wasm32 by cargo xtask plugins (plugins.md) |
|
| tools | xtask |
ci, layers, assets, parity, version, icon, wasm, plugins, bench |
Data model
A Document holds pages, a persistent map of nodes (frames, groups, sections, rectangles,
ellipses, polygons, stars, lines, vector networks, text with styled runs, boolean groups,
components, component sets, instances, slices), styles, variable collections, comments, versions
and images by hash. Instances keep their main's id and per-layer overrides; the engine keeps their
materialised children in sync when a main changes (directly for value, size and add/remove edits,
by a rebuild otherwise). Snapshots are cheap (Arc and structural sharing), which gives unlimited
undo with one step per gesture.
Commands: one path for every surface
Every user-visible behaviour is a command in crates/engine/src/cmd/* (455 ids after #101)
with an id, label, parameter docs and undo flag. The menus (generated from the catalog), keyboard
shortcuts, Actions palette, CLI (figcraft-cli run), JSON control channel (--control <port>) and
MCP server all dispatch the same ids, so agents can do anything a person can; WebAssembly plugins call them too, within their manifest's permissions. Commands never open
dialogs themselves; they request UI, and programmatic callers drop those requests.
cargo xtask parity measures the catalog's coverage (parity-checklist.md).
Rendering
The CPU renderer is the single source of pixels for the canvas, exports and the browser. The desktop canvas renders on a worker thread at the device pixel ratio and keeps its last image, re-rendering only the region a pan uncovers or an edit touches; overlays (selection, handles, guides, labels) are drawn by egui on top. A seeded random walk checks the retained canvas against full renders. SVG and PDF export walk the same scene and embed rasters where those formats can't express an effect.
File I/O
.figcraft is the native format (atomic saves, autosave checkpoints, recovery copies after a
crash). Imports: REST API file JSON with variables and images, SVG, raster images, clipboard
content. Exports: PNG, JPG, WebP, SVG, PDF, code. There is no .fig reader yet
(file-format-parity.md).
Robustness
Workspace lints deny unwrap, expect, panic! and friends in shipped code; unsafe_code is
forbidden. figcraft_engine::guard turns a panicking command into an error and rolls the document
back; the desktop app also guards each UI frame. Junk-parameter sweeps run every command with bad
input.
Revision history
| Date | Change | Summary |
|---|---|---|
| 2026-10-10 | minor | Merged main after #101–#103: WASM plugins (plugins area 0% → 35% breadth, 20% ready; ecosystem 5% → 20%) and the multiplayer core without networking (multiplayer 5% → 15% breadth, 5% ready; collaboration 8% → 12%); menu breadth 97.5% → 98.5% (200/203); ready for real work ~46% → ~47%; full parity 420–790 h → 390–745 h; beta 210–380 h → 200–365 h. Fixed the stated weight sums (areas 1–16 sum to 94, total 113; percentages were already computed with those) |
| 2026-10-10 | major | Created from the code as of PR #99 |