Misspelled words in the text being edited get a wavy underline (theme
danger colour); right-click one for suggestions, Ignore (until the app
quits) and Add to dictionary (saved in preferences). Text > Spelling >
Check spelling toggles it, on by default, with a check mark.
The engine stays platform-free: a SpellChecker trait the desktop app
installs (the system's checker through os-spellcheck on Windows and
macOS, in the system's language). Words are checked one at a time and
cached; the edited text is re-checked only when the document changes.
Linux, FreeBSD and the web have no checker yet; scripts written without
spaces (CJK, Thai) are skipped.
Commands: text.spelling, text.spellingSuggestions, text.replaceWord,
spelling.addWord, spelling.ignore. Menu breadth 201/204.
figcraft-server hosts many files over the same hub the app uses for sessions, persists each as a checksummed snapshot plus an append-only log (synced before replies, compacted, damaged files skipped and reported) so restarts lose nothing confirmed, and gives per-file share links with edit or view access (hashed 128-bit tokens, revocable). The app can publish a file to a server and join server-hosted files; view-only tabs refuse edits. Protocol version 2.
* 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>
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>
One keymap (figcraft_engine::keymap) now holds every default shortcut,
grouped like the public shortcut reference, with macOS and Windows/Linux
keys where they differ and canvas / text-editing contexts. The menus,
canvas, control channel, MCP key tool and Keyboard shortcuts panel all
resolve keys there; the catalog keeps only the menu tree.
- Fixed: Cmd/Ctrl+K made a link on the canvas (now Actions; link only
while typing), Shift+1 and Ctrl+Shift+? never matched (key names are
normalised), mask / bring to front / send to back / tidy up / layout
grids on Windows and Linux, distribute on macOS, outlines (Shift+Cmd+O),
pixel grid (Cmd+'), paint bucket B in vector edit mode.
- Added: Cmd/Ctrl+/, + and - zoom, Cmd/Ctrl+0, hold Z zoom tool
(view.zoomAt), two quick digits for exact opacity (object.opacityKeys),
Alt+Shift+U/S/I/E booleans, Alt+/ and Shift+/ remove fill/stroke, pixel
preview, Shift+E / Shift+D / Shift+Cmd+\ panel keys, Ctrl+Tab between
files (doc.nextTab / doc.previousTab), Shift+I, Shift+Space preview,
Home/End frames, Ctrl+C pick colour on macOS.
- Keyboard shortcuts panel: docked at the bottom, a tab per group, search,
keycaps per platform; tooltips show the platform's keys.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M3: colour profiles: sRGB or Display P3 per file, convert or keep values, tagged exports
- color: ColorProfile (srgb, displayP3) with conversions derived from the primaries (shared D65
white and sRGB curve; out-of-gamut channels clip), table-driven pixel conversion, a generated
ICC v4 matrix/TRC profile and PNG cICP code points.
- doc: Document.color_profile (serde default sRGB, written only when set); every colour value
(paints, text runs, effects, grids, page and prototype backgrounds, overlay backdrops, styles,
colour variables, document colours) can be mapped; set_color_profile converts or reinterprets.
- engine: file.colorProfile {profile?, convert?} (File > Color profile...; without a profile it
opens the dialog); document.inspect reports colorProfile.
- render: images are taken as sRGB and managed into the file's space; encode_export tags PNG
(sRGB chunk, or iCCP + cICP for Display P3) and embeds the ICC profile in Display P3 JPG/WebP;
SVG and PDF export Display P3 files converted to sRGB. The CLI exports the same way.
- ui: Color profile dialog (keep appearance / keep values) and a Color profile row on the page
panel; the canvas, presenter, thumbnails, swatches and colour picker show a Display P3 file's
colours converted to the screen's sRGB, and Copy as PNG converts too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: image adjustment presets: built-in, saved in the file, reset
- color: ImageFilters::PRESETS (Vivid, Muted, Warm, Cool, Black & white, Fade, Reset), lookup by
id or label, and sanitized values.
- doc: Document.image_presets (name + adjustments; written only when there are some).
- engine: image.adjust {ids?, target?, index?, preset?, filters?} sets the adjustments of image
paints from a preset (built-in or saved) with values on top, one undo step; image.savePreset,
image.deletePreset and image.presets.
- ui: the colour picker's image section gets a presets menu named after the preset in effect
("Custom" otherwise), saved presets with delete, "Save as preset...", a Reset button, and
aligned adjustment sliders.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: style picker: search, folders, list or swatch grid, edit, create, detach
The Fill, Stroke, Effects and Layout grid sections' Styles button opens a popover laid out like
the variable picker (crates/ui-egui/src/style_picker.rs):
- search over style names; styles grouped by the folders of their `/`-separated names, which
collapse (searching opens them); colour styles as a list or a grid of swatches;
- click applies, a check marks the applied style; the row's edit button (or a right-click) edits
the name, description and values: colour style paints open the colour picker on the style,
effect and grid styles update from the selection; delete;
- `+` creates a style from the selection's current values and opens it for naming; detach;
- colour slots still list colour variables to bind;
- the applied style chip opens the picker; new icons Adjust, ViewList, ViewGrid (drawn in code);
- control channel: ui.set {stylePicker, stylePickerEdit, stylePickerView}.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The selected shapes' outlines split the canvas into regions, computed as the faces of their
arrangement with linesweeper (curves kept, per-shape fill rules, holes and islands handled) and
cached per document version and selection.
- Engine: shapeBuilder.regions / merge / remove / mode commands, ToolId::ShapeBuilder and
tool.shapeBuilder (catalog: Tools > Shape builder, Shift+M). Merge makes one vector from the
picked regions styled like the top-most shape over the first; every shape covering a picked
region loses that area (becomes a vector, or is deleted when nothing is left). One gesture or
command is one undo step. Session::tool_key routes Enter (finish) and vector edit keys.
- Pathops: faces() (arrangement faces with their covering inputs) and union_faces().
- UI: region edges on the canvas, hovered/collected regions hatched (danger colour to remove),
no transform box while building, a toolbar pill with Merge/Remove modes, a hint and Done; shared
pill/tool-cell/divider/Done helpers; original merge/remove icons drawn in code.
- Tests: pathops faces; engine regions, merge, remove, curves, groups, rotation, pointer
click/drag/Alt, modes, Escape/Enter, cache, junk parameters and degenerate geometry.
- Docs: ROADMAP, README, docs/mcp.md, docs/control-protocol.md, docs/parity.md.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Object ▸ Resize to fit (Alt+Shift+Cmd+R, `object.resizeToFit`): frames, sections and
components shrink or grow to their visible contents without moving them on the page; auto
layout frames switch to hug instead.
- Sections get colour swatches (light tints) in the Design panel.
- Frames and sections get a handoff status (`devStatus`: ready for dev / completed) set from
the panel and shown as a badge next to their name on the canvas; `node.get` reports it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>