Files
figcraft/docs/control-protocol.md
T
BFlatandClaude Opus 5.5 b26bbb2da8 M7: pixel-exact icon set (#61)
Every UI icon redrawn on a strict 16 px grid: strokes on pixel centres, fills on pixel edges, and
consistent proportions. Painting snaps the glyph to the pixel grid, scales by whole steps above
16 px, hints straight strokes below it, and keeps strokes one physical pixel wide, so icons stay
sharp at every size and display scale. Concave fills (the move cursor) are triangulated instead of
drawn as convex polygons, which broke them. Icon buttons paint their glyph at its native 16 px.

Redrawn: hand, home, move cursor, comment, moon, undo/redo, rotate/reset, link, corner radius,
flip, eye, lock, clock and the rest of the set. `Icon::ALL` lists every icon; the control channel's
`ui.set {iconGallery: true}` shows them all at 12/16/24/32 px for review.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 09:21:53 +07:00

142 lines
7.4 KiB
Markdown

# FigCraft control protocol
The desktop app can be driven by other programs (tests, scripts, `figcraft-cli mcp --attach`,
sibling Crafting Apps) through a loopback socket:
```sh
cargo run -p figcraft -- --control 7979 [file.figcraft|file.svg|image]
```
The app listens on `127.0.0.1:<port>` only. Each connection carries **newline-delimited JSON**:
one request object per line, one reply line per request, in order.
```text
request: {"id": 1, "method": "engine.execute", "params": {...}}
reply: {"id": 1, "ok": true, "result": ...}
{"id": 1, "ok": false, "error": "message"}
```
Requests are queued and run on the UI thread at the start of the next frame, so replies reflect the
real UI state. A request that the UI doesn't answer within 60 s gets a timeout error. A line that is
not JSON gets `{"id": null, "ok": false, "error": "bad JSON: …"}`. The implementation is in
`crates/ui-egui/src/control.rs` (methods) and `apps/figcraft/src/control_server.rs` (socket).
Coordinates are page units, y down. `x`/`y` in `node.set`/`node.create` are parent-relative.
## Methods
### `engine.execute {command, params?}`
Run any command (the same ids as the menus, shortcuts, Actions palette, CLI and MCP). Programmatic
calls never open dialogs: dialog requests are dropped. Returns the command's result (creation
commands return `{"id": N}`).
```json
{"id":1,"method":"engine.execute","params":{"command":"shape.rectangle","params":{"x":10,"y":10,"width":100,"height":50}}}
{"id":1,"ok":true,"result":{"id":2}}
{"id":2,"method":"engine.execute","params":{"command":"node.set","params":{"ids":[2],"props":{"fill":"#FF6B6B","cornerRadius":8}}}}
{"id":3,"method":"engine.execute","params":{"command":"command.batch","params":{"label":"Tidy","commands":[{"command":"align.left","params":{"ids":[2,3]}},{"command":"distribute.vertical","params":{"ids":[2,3]}}]}}}
```
### `engine.commands {}`
Every command: `[{id, label, params, menu, shortcut, undoable}]`.
### `document.inspect {depth?, allPages?}`
The active document as JSON: name, path, dirty, currentPage, selection, pages (the current page's
layer tree to `depth`, default 8; others summarised unless `allPages`), styles, variable count,
images, the colour profile (`colorProfile`: `srgb` or `displayP3`), the text being edited and the
undo history.
### `node.get {id, depth?}`
One layer (children to `depth`, default 1). Fields: id, name, type, x, y, width, height, rotation,
absoluteBounds, visible, locked, opacity, blend, fills, strokes, strokeWeight, effects, cornerRadius,
layout (including grid `columnTracks` / `rowTracks`), sizing, `minWidth` / `maxWidth` /
`minHeight` / `maxHeight`, `absolute`, constraints (children of plain frames and absolute
children), `gridCell` (a grid auto layout child's column, row and spans), clip, text properties,
component/instance links, overrides, `slot` (a slot layer's property name), boundVariables, interactions, exports. The auto layout props
that `node.set` accepts are listed in [mcp.md](mcp.md#conventions-agents-must-know).
### `ui.inspect {}`
UI state: panels and tabs, current tool, canvas rect, dialogs, view, timings (`frameMs`,
`renderMs`) and what the last canvas render had to do (`canvas.renderWork`: `full`, `partial` with
the `pixels` re-rendered after a pan or an edit, or `reused`). While a prototype plays, `presenter`
(full window) and `preview` (the preview pane) hold the player's state: the `current` frame and its
`name`, `start`, `history`, open `overlays`, the screen's `scroll` offset and `scrollRange`, the
`device` and the `viewport` of the frame its screen shows (`null` when nothing plays).
### `ui.pointer {kind, x, y | sx, sy, to?, steps?, shift?, alt?, cmd?, ctrl?}`
A mouse gesture with the current tool. `kind`: `down`, `move`, `up`, `click`, `double`, `hover`,
`drag` (needs `to: [x, y]`; `steps` intermediate moves, default 8). Position in page coordinates
(`x`, `y`) or screen pixels (`sx`, `sy`). Returns `{selection, tool}`.
```json
{"id":4,"method":"engine.execute","params":{"command":"tool.set","params":{"tool":"ellipse"}}}
{"id":5,"method":"ui.pointer","params":{"kind":"drag","x":200,"y":200,"to":[300,260],"shift":true}}
```
### `ui.key {key, shift?, alt?, cmd?, ctrl?}`
A key as if typed on the canvas (in vector edit mode the edit keys come first: `V` `P` `Q` `X` `Shift+B` tools, arrows nudge points, `Delete`/`Shift+Delete`, `Cmd+A`, `Cmd+J`; with the shape builder, `Enter` finishes): a menu shortcut (`{"key":"G","cmd":true}` groups), a tool key,
`Escape`, `Delete`/`Backspace`, `Enter`, arrows (nudge; shift = big nudge; auto layout children move one place along the flow; with `cmd` they resize the selection). While text is being
edited, keys go to the text editor. Returns `{"command": id}` for shortcuts.
### `ui.type {text}`
Type into the text being edited (start editing with a double-click, `text.edit.begin {id}`, or the
text tool).
### `ui.menu.invoke {id}`
Invoke a menu item exactly like clicking it (this may open a dialog). Returns `{ok, lastError}`.
### `ui.set {rightTab?, leftTab?, theme?, home?, palette?, mainMenu?, colorPicker?, variables?, layerFilter?, collapseLayers?, exportPreview?, preview?, presentScroll?, colorProfileDialog?, stylePicker?, stylePickerEdit?, stylePickerView?, iconGallery?}`
Switch panels and modes: `rightTab` `design|prototype|inspect`, `leftTab` `file|assets`, `theme`,
`home` (bool), `palette` (bool: the Actions palette), `mainMenu` (bool), `iconGallery` (bool:
every UI icon at 12, 16, 24 and 32 px over the window, for reviewing the icon set), `colorPicker`
(`{fills?, index?}`: open the colour picker on the selection's paint; `null` closes it), `variables` (bool,
or a collection id or name to show, or `{library, collection?}` for an enabled library's collection: the
Variables editor), `layerFilter` (string:
open the layers search with that filter; `null` closes it), `collapseLayers` (true: collapse the
layers tree), `exportPreview` (bool: the Design panel Export section's preview), `preview` (true or a
frame id: open the prototype preview pane like `proto.preview`; false or `null` closes it),
`presentScroll` (`[x, y]`: scroll the presented screen, the full window's else the pane's, clamped
to what it can scroll), `colorProfileDialog` (`srgb|displayP3`: open File ▸ Color profile with that
profile chosen; `null` closes it), `stylePicker` (`fill|stroke|effect|grid`: open the style picker
for the selection; `null` closes it) with `stylePickerEdit` (a style id or name: open its editor),
`stylePickerView` (`list|grid`). Returns `ui.inspect` (which reports `stylePicker` and
`stylePickerView`).
### `ui.screenshot {path}`
PNG of the whole window, UI included (needs a visible window). Returns `{path, width, height}`.
### `ui.render {path, scale?, ids?}`
PNG of the document without UI chrome (works without a window). Without `ids` it exports the
selection, else the page's top-level layers. Returns the export result.
### `app.quit {}`
Close the app.
## Example session (shell)
```sh
port=7979
figcraft --control $port &
exec 3<>/dev/tcp/127.0.0.1/$port
echo '{"id":1,"method":"engine.execute","params":{"command":"frame.create","params":{"preset":"iPhone 16","x":0,"y":0}}}' >&3
read -r reply <&3; echo "$reply"
echo '{"id":2,"method":"ui.screenshot","params":{"path":"/tmp/figcraft.png"}}' >&3
read -r reply <&3; echo "$reply"
```
For agents, prefer the MCP server (`figcraft-cli mcp --attach 7979`, see `docs/mcp.md`): it wraps
these methods in documented tools and returns images inline.