Files
figcraft/docs/cli.md
T
BFlatandClaude Opus 5.5 a5d5c265fd M7: performance: retained canvas renders, virtual layers panel, shared text layouts, cargo xtask bench (#35)
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>
2026-10-10 05:19:05 +07:00

4.3 KiB
Raw Blame History

figcraft-cli

FigCraft without a window: inspect, export, convert and render documents, run command scripts, list commands, serve MCP and measure performance. Build it with cargo build --release -p figcraft-cli (binary target/release/figcraft-cli).

Bad arguments print usage and exit with code 2; failures exit with 1. The CLI never panics on input.

Subcommands

info <file> [--json]

Pages, node counts, top-level frames (id, type, name, size), style/variable/image counts. Repairs of a damaged file and import warnings go to stderr as note: lines (notes with --json).

Every subcommand that reads a file also takes the public REST API file JSON (the response of GET /v1/files/:key or of its nodes endpoint, written to a .json file): it is imported on the fly. Image fills come from images/<imageRef>.png (or .jpg, …) next to the JSON when present, else a grey placeholder.

$ figcraft-cli info app.figcraft
app.figcraft — App (412 nodes, 6 styles, 12 variables, 3 images)
page 1 "Screens": 380 nodes, 4 top-level
  2 FRAME "Home" 393.0×852.0
  …

export <file> -o <out> [--format png|jpg|svg|webp|pdf] [--scale N] [--page NAME] [--node ID|NAME]

  • <out> is a directory: one file per top-level frame of the page (default the first page), named after the frame (Home.png, Settings.png…); with no frames, one file per top-level layer.
  • <out> has an image extension: one file with the given --node, or the whole page composited.
  • --node takes a layer id (12 or 0:12) or a layer name.
  • The format comes from --format, else <out>'s extension, else png. --scale (default 1, max 64) applies to raster formats.
figcraft-cli export app.figcraft -o exports/ --scale 2
figcraft-cli export app.figcraft -o hero.svg --node "Hero"

convert <in> <out>

By extension. Inputs: .figcraft, .json (FigCraft or REST API file JSON), .svg (becomes vectors), .png/.jpg/.webp/.gif/.bmp (an image layer). Outputs: .figcraft, .json (FigCraft JSON, pretty-printed), .svg, .png/.jpg/.webp (the current page rendered at 1x with its background), .pdf (when PDF export is available).

render <file> -o <out.png> [--scale N] [--page NAME]

The whole page (default the first) with its background, as png, jpg or webp. Large pages are scaled down to fit 16384 px.

run <script.json|script.jsonl> [--in <file>] [--out <file>]

Runs commands in a headless session (a new document, or --in), printing one JSON line per step:

{"command":"frame.create","ok":true,"result":{"id":2},"step":0}

A failing step prints "ok": false with the error and exits with code 1 (nothing is saved). --out saves the result by extension like convert.

Script formats: a JSON array of {"command": id, "params": {...}}, an object {"commands": [...]}, or JSON lines (one step per line; blank lines and // lines are skipped).

[
  {"command": "frame.create", "params": {"preset": "iPhone 16", "x": 0, "y": 0, "name": "Home"}},
  {"command": "node.create", "params": {"type": "text", "parent": 2, "x": 24, "y": 64, "text": "Welcome", "props": {"fontSize": 32}}},
  {"command": "layout.add", "params": {"ids": [2], "direction": "vertical"}}
]

Ids are assigned in order (a new document's first page is 1, so the first layer is 2), which lets a script refer to layers it created.

commands [--json] [--filter TEXT]

Every command with its label, shortcut, undoable flag (*) and parameter docs; --json for machines.

mcp [--attach PORT]

Serve MCP on stdio: headless by default, or attached to a running app started with figcraft --control PORT. See mcp.md.

perf [--layers N[,N...]]

Builds synthetic documents of each size (default 1000,10000 layers: screens with an auto layout header, a card group, button instances and a grid of shapes and text) and prints a Markdown table of the hot paths: build + normalise, save/load, 1440×900 renders at 100% and zoomed to fit, hit testing and hover, single-layer edits through the engine (fill, move, text in auto layout, a main component with all its instances), undo/redo, a drag preview frame, normalize after a one-node edit, document.inspect and select-all + move. Use a release build for meaningful numbers; cargo xtask bench runs it at 1k/10k/50k layers together with the UI frame benchmarks.