mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 18:36:44 -06:00
Right-to-left and mixed-direction names in the panels: layers, pages, Assets, tab titles, the file header and canvas frame titles show in visual order (Unicode bidi, joined Arabic letters, mirrored brackets), elided at their end (the left end of a right-to-left name). egui shapes each font run itself, so shown names are laid out a cluster at a time. Rename fields keep egui's editing and draw the shown text over it: the caret, selection, clicks, drags and arrow keys follow what's on screen. System fonts are added for scripts the built-in UI fonts lack. The Typography section's text style browser: list and grid views with each style in its own font, search, folders, this file's and enabled libraries' styles, apply, detach, edit the typography, create from the selected chars, hover to preview on the canvas, and "Mixed" when the chars use several styles. Text styles now link chars (style_id on text styles), so a range can take a style; editing what a style sets unlinks the chars edited; files from before link on open. Live text previews (text.preview / text.endPreview): a hovered text style, font style or named instance and a dragged axis slider show on the canvas without an undo step; release commits one step, Escape cancels. ui.input sends raw window input over the control channel. Visual QA: list markers keep the paragraph's font, renames start with the name selected and centred, Max lines shows while truncating. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
167 lines
9.1 KiB
Markdown
167 lines
9.1 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 whether it is turned to `landscape`, its `deviceColor`) and the `viewport` of the frame
|
|
its screen shows, the presented frame's `frameSize` and, for a responsive prototype, the screen it
|
|
was resized to (`responsive`: `[w, h]`, else `null`), the size option (`scale`:
|
|
`fit|fitWidth|fill|actual`, `deviceFrame`), the `zoom` it shows at, how
|
|
far the view is panned (`pan`, `panRange`) and whether a scroll is still coasting (`flinging`)
|
|
(`null` when nothing plays). `presentOptions` holds the options the next presentation starts with.
|
|
`textPreview` is true while a live text preview shows on the canvas (a hovered text style or named
|
|
instance, a dragged axis slider; see `text.preview` in [mcp.md](mcp.md)).
|
|
|
|
### `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` `Shift+E` `Shift+W` 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.input {events}`
|
|
|
|
Raw window input for the next frame, as if from the mouse and keyboard: it reaches the panels,
|
|
popovers and fields (`ui.pointer` and `ui.key` drive the canvas tools and shortcuts instead).
|
|
`events` is a list of `{move: [x, y]}`, `{down: [x, y], button?: "secondary"}`,
|
|
`{up: [x, y], button?}`, `{key, shift?, alt?, cmd?}` (pressed and released; egui key names such as
|
|
`ArrowLeft`, `Escape`, `A`), `{text}` (typed text) and `{leave: true}` (the pointer left the
|
|
window); positions are window points. Events of one call arrive in the same frame, so send a press
|
|
and its release (or a double-click's two clicks) as separate calls. Returns `{queued}`.
|
|
|
|
```json
|
|
{"id":6,"method":"ui.input","params":{"events":[{"move":[1000,315]}]}}
|
|
{"id":7,"method":"ui.input","params":{"events":[{"down":[1000,315]}]}}
|
|
{"id":8,"method":"ui.input","params":{"events":[{"up":[1000,315]}]}}
|
|
```
|
|
|
|
### `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?, protoEditor?}`
|
|
|
|
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|text`: open the style
|
|
picker for the selection, `text` being the text style browser of the Typography section; `null`
|
|
closes it) with `stylePickerEdit` (a style id or name: open its editor), `stylePickerView`
|
|
(`list|grid`), `protoEditor` (`[layer id, interaction index]`: open that interaction's editor in the
|
|
Prototype panel; `null` closes it). 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.
|