From a0f6410397adc52e7ff004e3597892554fdc322a Mon Sep 17 00:00:00 2001 From: Mex <44814932+MMexicann@users.noreply.github.com> Date: Fri, 16 Oct 2026 00:03:04 +0200 Subject: [PATCH 01/38] M4: preserve stroke rings when outlining shapes --- ROADMAP.md | 831 +++++++-------- crates/engine/src/cmd/structure.rs | 1303 +++++++++++------------ crates/engine/tests/outline_stroke.rs | 174 +++ docs/gaps.md | 893 ++++++++-------- docs/roadmap.md | 249 ++--- docs/target-app-parity.md | 1405 +++++++++++++------------ 6 files changed, 2520 insertions(+), 2335 deletions(-) create mode 100644 crates/engine/tests/outline_stroke.rs diff --git a/ROADMAP.md b/ROADMAP.md index 100693e3..29de0e90 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,414 +1,417 @@ -# FigCraft Roadmap - -**Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents) - -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (Typography spacing icons support scrubbing (#373); font scans skip named pipes; system theme on first run; recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading (#207), SVG import budgets (#190), Leptos code (#223), full screen (#227), server snapshots readable again (#188), owner-only preferences (#200), ruzstd 0.9.1 (#182), layer cycle repair (#205), batch rollback, Direct3D 12 on Windows, plugin metering (#192); vector edit mode regions: hover hatch, selection and drag; SVG/PDF exports refuse over-depth documents (#208); bounded MCP/control framing, connections and queues (#201); plugin log limits (#191); live-session revert guard (#198); attached MCP no longer replays after a lost reply (#194); repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7) - -FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow, -native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through -the CLI, a JSON control channel and MCP. This page is the one-page summary; agents choosing work -start with [`docs/gaps.md`](docs/gaps.md). - -## Headline numbers - -| | Value | Kind | -|---|---|---| -| Menu/command breadth | **100%** (211 / 211) | measured, [`docs/parity-checklist.md`](docs/parity-checklist.md) (`cargo xtask parity`) | -| Feature breadth | **~85%** (core editor ~94%) | estimated, [`docs/target-app-parity.md`](docs/target-app-parity.md) | -| Ready for real work | **~53%** | estimated, same doc (written dimension weights and scores) | -| Essentials user | **~59%** | estimated: core features 77.8% × 0.92 launch/stability × 0.94 discoverability × 0.88 opening files people send ([method](docs/target-app-parity.md#essentials-user)) | -| Mainstream practitioner | **~50%** | estimated: weekly areas 72.7% × 0.9 interaction × 0.9 stability × 0.85 file exchange with Figma users ([method](docs/target-app-parity.md#mainstream-practitioner)) | -| Remaining to beta | **~155–280 h** one agent | estimated | -| Remaining to full parity | **~262–519 h** one agent (~83–145 h wall-clock with 5 agents) | estimated | - -### Readiness by audience - -| Audience | Ready % | Opus 5.5 agent wall-clock hours to ~95% | Work that dominates | -|---|---|---|---| -| Full target (ready for real work) | ~53% | ~268–522 h | multiplayer (42–84), localization (29–51), plugins (8–26), the interaction pass (40–60), GPU canvas (30–60), AI (30–60), the `.fig` import's tail (5–10) | -| Mainstream practitioner | ~50% | ~120–229 h | core editor depth incl. the `.fig` import's tail (49–98), the interaction pass (40–60), real-file corpus and platform QA (25–50), checking the rest of the experimental `.fig` export elsewhere: effects, images, prototypes, real files (5–15) | -| Essentials user | ~59% | ~55–100 h | the `.fig` import's tail so the files people send open well (5–10), core tool polish (25–45), interaction pass on the core tools (15–25), stability QA (10–20) | - -Hours are one agent working sequentially, calibrated from this repo's history (about 1 agent-hour -per 1,000 added lines, 1–2.5 h per feature PR, ~7 h per honest depth point in the polish tail; see -[how the hours are calibrated](docs/target-app-parity.md#how-the-hours-are-calibrated)). Each audience's work is a subset of -the next. About 70–80% parallelizes across 4–6 agents on disjoint crates (roughly ÷4 in -wall-clock); the `.fig` reader, the multiplayer core and a plugin runtime each need one owner, and -native-speaker review, real-file licensing and screen-reader QA need people. - -Hours are Opus 5.5 agent wall-clock hours, calibrated from this repo's history: the app was built in -~25 wall-clock hours by 4–6 parallel agents (~110–150 agent-hours, ~1 agent-hour per 1,000 added -lines, a typical feature PR 1–2.5 h). Details and what needs a human: -[`docs/target-app-parity.md`](docs/target-app-parity.md#how-the-hours-are-calibrated). - -## Stage - -**Alpha.** Core workflows exist end to end (frames, shapes, vectors, text, auto layout, components -with variants and overrides, styles, variables, prototyping, Dev Mode, SVG/PNG/JPG/PDF), but -FigCraft opens `.fig` files only best effort, has only a preview of multiplayer, and nothing has been checked -against real Figma documents. Ready-for-real-work is ~53%, inside alpha's ~40–75% band, and the alpha gate passes: every daily core workflow (design screens, build a design system, prototype, hand off to developers) works end to end on FigCraft's own files, which save and reopen; review works only asynchronously and `.fig` import is unproven on real files, both beta gaps ([alpha gate](docs/roadmap.md#alpha-gate)). Beta needs -~75% **and** reliable `.fig` import: the `.fig` import's tail (5–10 h), a real-file corpus with measured -fidelity (15–30 h), the interaction pass (25–40 h of the 40–60), core depth to ~80% (40–75 h), -basic multiplayer on the landed core (30–55 h), stability and platform QA (20–40 h), a partial GPU path (15–30 h); -native speakers' reviews of the six language drafts need people. - -## By dimension - -| Dimension | Ready | Hours | Doc | -|---|---|---|---| -| Features (depth) | 63% (core editor 72%) | 28–67 | [target-app-parity](docs/target-app-parity.md) | -| UI/UX fidelity | 58% | 40–60 | [ui-parity](docs/ui-parity.md) | -| File formats | 60% (`.fig` read best effort with editable vectors; experimental write, round trips through our own importer and written files opened in other software) | 5–10 | [file-format-parity](docs/file-format-parity.md) | -| Collaboration | 13% (core in; live sessions with document sync, cursors and text carets, cursor chat and spotlight, the browser joins, invite links open the app; a self-hosted server with edit and view links and team libraries; preview) | 46–92 | [collaboration-parity](docs/collaboration-parity.md) | -| Stability | 55% | 20–40 | [gaps](docs/gaps.md) | -| Performance | 60% | 30–60 | [hardware-parity](docs/hardware-parity.md) | -| Hardware | 60% | 10–21 | [hardware-parity](docs/hardware-parity.md) | -| Localization | 39% (layer; English; Spanish with a Latin American overlay, French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese drafts; CJK UI fonts) | 29–51 | [localization-parity](docs/localization-parity.md) | -| Platforms | 75% | 10–20 | [hardware-parity](docs/hardware-parity.md) | -| Ecosystem / plugins | 38% (own WASM plugins with panels, settings and widgets; a self-hosted plugin directory) | 8–26 | [gaps](docs/gaps.md) | -| AI features | 0% | 30–60 | [gaps](docs/gaps.md) | - -## Features - -| Area | Breadth | Ready | Hours | Doc | -|---|---|---|---|---| -| Canvas & navigation | 98% | 74% | 1–4 | [ui-parity](docs/ui-parity.md) | -| Selection & transform | 97% | 72% | 3–5 | [ui-parity](docs/ui-parity.md) | -| Layers panel | 99% | 85% | 1–2 | [ui-parity](docs/ui-parity.md) | -| Shapes & vector | 99% | 73% | 1–3 | [target-app-parity](docs/target-app-parity.md) | -| Paint | 90% | 71% | 2–5 | [target-app-parity](docs/target-app-parity.md) | -| Strokes | 95% | 75% | 1–2 | [target-app-parity](docs/target-app-parity.md) | -| Effects | 100% | 75% | 2–5 | [target-app-parity](docs/target-app-parity.md) | -| Frames, groups, sections, masks | 97% | 80% | 1–3 | [target-app-parity](docs/target-app-parity.md) | -| Auto layout & constraints | 98% | 74% | 3–6 | [auto-layout-parity](docs/auto-layout-parity.md) | -| Text | 90% | 70% | 3–7 | [target-app-parity](docs/target-app-parity.md) | -| Components & variants | 96% | 72% | 4–9 | [components-parity](docs/components-parity.md) | -| Styles & variables | 95% | 76% | 2–4 | [components-parity](docs/components-parity.md) | -| Prototyping | 92% | 74% | 2–5 | [prototyping-parity](docs/prototyping-parity.md) | -| Import/export | 86% | 58% | 5–10 | [file-format-parity](docs/file-format-parity.md) | -| Dev Mode / inspect | 96% | 72% | 0–1 | [dev-mode-parity](docs/dev-mode-parity.md) | -| Comments, versions, misc | 87% | 63% | 1–3 | [collaboration-parity](docs/collaboration-parity.md) | -| Multiplayer & sharing | 17% | 5% | 42–84 | [collaboration-parity](docs/collaboration-parity.md) | -| Plugins & widgets | 60% | 35% | 8–26 | [plugins](docs/plugins.md) | -| Team libraries | 70% | 40% | 4–8 | [components-parity](docs/components-parity.md) | -| Figma Draw extras | 88% | 60% | 1–3 | [gaps](docs/gaps.md) | -| AI features | 0% | 0% | 30–60 | [gaps](docs/gaps.md) | - -## Languages - -| Language | Code | Strings | Status | -|---|---|---|---| -| English | en | 100% | full | -| Simplified Chinese | zh-Hans | 100% | draft (needs native review) | -| Spanish | es, es-419 | 100% (es-419: an overlay of 98 entries) | draft (needs native review) | -| Hindi | hi | 0% | none | -| Arabic | ar | 0% | none | -| French | fr | 100% | draft (needs native review) | -| Portuguese (Brazil) | pt-BR | 100% | draft (needs native review) | -| Indonesian | id | 0% | none | -| Japanese | ja | 100% | draft (needs native review) | -| German | de | 100% | draft (needs native review) | -| Korean | ko | 100% | draft (needs native review) | -| Vietnamese | vi | 0% | none | - -Also shipped: Swedish (`sv`: every UI string, 120 of 588 command labels and menu wordings; native review welcome). Figma ships English, Japanese, Korean, French, German, Spanish and Portuguese -(Brazil); FigCraft has the localization layer, English and Spanish (Spain and Latin America), -French, German, Brazilian Portuguese, Japanese, Korean and Simplified Chinese drafts (Preferences ▸ -Language; Traditional Chinese has no catalog; CJK UI -text uses an installed CJK font, loaded only when needed; adding a language: -[`docs/translating.md`](docs/translating.md)). Document text in any script (bidi, Arabic shaping, -IME) already works. Detail: [`docs/localization-parity.md`](docs/localization-parity.md). - -## Upcoming - -Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). - -1. `.fig` import's tail: fixes from real files, connector labels, table cells, filled areas with their own fills (the best-effort import, editable vector networks, the other layer kinds and variable prototype actions landed 2026-10-11): 5–10 h; then checking the rest of the experimental `.fig` export (landed 2026-10-12; written files open and draw in other software) on effects, images, prototypes and real files: 5–15 h. -2. Real-file corpus and measured fidelity: 15–30 h. -3. Tool-by-tool interaction pass against Figma: 40–60 h. -4. Languages: native reviews of the es, fr, de, pt-BR, ja and ko drafts (people), Latin American Spanish (1–2 h), then Simplified Chinese (3–5 h). -5. Multiplayer (M9): accounts and teams on top of the live sessions and the standalone server (transport, hosting, invite links that open the app, document sync, presence with remote text carets, cursor chat and spotlight, per-user undo, reconnect with offline edits, collision-free ids for styles, variables and comments, the browser client, `figcraft-server` with persistence and share links to edit or view, team libraries on the server landed): 42–84 h. - -## Progress log - -- **2026-10-14** · Corner handles win over the corner-radius handles (M1, #292): a press on the inner half of a corner handle's square picked the radius handle 10 px in, so the drag neither resized nor (dragging outward) rounded anything. Inside the drawn square the corner now resizes both sides (in ratio with Shift or the link); further in the radius handle still answers. Regression test in the transform behaviour suite. Numbers unchanged. -- **2026-10-14** · Home screen recent files as cards (M7): each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged. -- **2026-10-13** · Vector edit mode regions (M4): with the Move tool, the filled area under the pointer is hatched with "/" lines in the selection colour, clipped by the area's own fill rule so holes stay clear; the hatch goes near points (3.5 px) and segments (2 px), and its spacing follows octave folding (8 px at every power-of-two zoom), fixed to the shape. A click selects the area with its points (they move with it but keep the unselected look), Shift adds or removes one, a press on a selected area drags the whole selection as one undo step, and a click on an empty spot clears the areas and stays in edit mode. `vector.edit.select {regions}` and `vector.edit.state` (`regions`, `regionVertices`, `regionHover`); `vector_regions_tests` and `regions_tests`. Numbers unchanged. -- **2026-10-14** · SVG/PDF export depth (M0.9, #208): visible descendants deeper than 256 levels below an exported root are refused before bounds or drawing, including masks and raster fallbacks. SVG has a checked API; the engine and CLI report the limit and suggest selecting an inner layer. Existing destination files survive refusal. Boundary, inner-root, hidden-subtree, multi-root/page and command/CLI regressions cover the change. Numbers unchanged. -- **2026-10-13** · MCP/control transport hardening (#201): 32 MiB incoming frames are bounded while reading; oversized frames return an error and close without draining. The opt-in loopback control listener caps active workers at 8, applies 60 s socket read/write inactivity timeouts and caps both request queues at 8, discarding timed-out requests including those deferred to hidden windows. Reader, queue, idle-client and blocked-writer regressions cover the limits. Numbers unchanged. -- **2026-10-13** · Attached MCP (M0.9, #194): a transport failure no longer replays a request that may already have edited the document. The caller gets an outcome-unknown error and must inspect before retrying; the next explicit call reconnects. Loopback tests cover a lost mutation reply, a single applied edit, reconnect and a connection failure before sending. Numbers unchanged. -- **2026-10-13** · Constrain proportions in the Layout panel (M1, #220): with the W/H link on, typing a width or height scales the other side by the layer's own ratio; the property edit dropped the ratio, so the link only worked while dragging resize handles. Both sides in one edit stay as typed, the `w`/`h` aliases work the same, each layer of a multi-selection keeps its own ratio, and a zero side has no ratio to keep. Regression test in the transform behaviour suite. Numbers unchanged. -- **2026-10-13** · Repeat last command (M1, #181): repeated edits keep the underlying command and parameters, so repeated nudges accumulate with one undo step each. A stored self-reference is refused instead of recursing. Regression tests cover three repeats, their undo steps, and missing or self-referencing commands. Numbers unchanged. -- **2026-10-13** · Component playground (M3): with an instance, a component or a component set selected, the Inspect tab shows a Playground: a preview of the component and a control for each variant, boolean and text property. What is tried there changes the preview and the code in the Code section, never the file (no undo step, the canvas instance stays as it is); Reset goes back to the component's or the instance's own values. `component.playground {id?, properties?, language?, path?}` gives the same to scripts: the properties with their values, the code, and a PNG on request (`playground_tests`). Left: instance-swap and slot properties in the playground (the command takes an instance swap; the panel has no control for it), a code connect template's snippet with the tried values. Numbers unchanged. -- **2026-10-13** · Auto layout, on-canvas handles of automatic gaps (M2): the handle between spread rows or columns (and between items with an auto gap) shows the distance the layout gives the gap instead of the stored value, and dragging it makes the gap a fixed one from there, as typing a gap in the panel does; one undo step (`gap_handle_tests`). Before, the drag changed a value that had no effect. The documentation is silent on dragging an automatic gap. Numbers unchanged. -- **2026-10-13** · Variables inside styles (M3): a shared style can bind variables to fields of its value (`style.bind {style, field, variable?}`: a paint style's colours and gradient stops, an effect style's colour, radius, spread and offsets, a grid style's size, count, gutter and offset, a text style's font size, line height, letter spacing, family and style). Every layer using the style shows those fields resolved in its own modes and stays linked when a variable or a mode changes; the style keeps them resolved in the default modes. A style made from a layer keeps that layer's bindings; a new value drops bindings to what is gone. The style editor binds from its property rows (paint colour, shadow colour, blur radius, grid size) and detaches from the variable's chip; the style picker marks styles that use variables. `.fig` export and import carry the bindings on the style, REST layers hand theirs to the style they use. Left: binding text style fields and the other effect and grid fields from the editor (commands only), library variables in the picker, a style's library copy. Numbers unchanged. -- **2026-10-13** · Effects on groups, inner shadows and background blur (M3): a group is no outline of its own, so its inner shadows are cast inwards from around what its layers draw together (their pixels, over them: no shade at a seam between two layers) and its background blur and glass show the backdrop through the outlines of its layers (under them). Canvas and PNG; SVG embeds a group with an inner shadow as a render and its background blur as an image under the layers; PDF places both as images, the blur matching the PNG. Where the group holds one shape the pixels match that shape, or a frame filled like it, carrying the effect (`group_effects_tests` in render, svg and pdf). Offsets and spreads of a group's inner shadow go by whole pixels. Numbers unchanged. -- **2026-10-13** · Text made by commands on a dark page (M1): in the app, `text.create` and `node.create {type: text}` that name no fill give white text on a dark page, by the same rule as the Text tool (the page's colour decides, also inside a frame); a named `fill`, `fills` or `fillStyle` is kept. Sessions without an interface (CLI, MCP, exports) still make black text everywhere. Numbers unchanged. -- **2026-10-13** · Frames and groups, a one-step convert (M1): `object.convertToGroup` and `object.convertToFrame` (the Design panel's type menu on a frame or a group) change the layer in place, its layers staying where they are on the page. A frame turned into a group loses what only a frame has (auto layout, with the positions it gave kept and fill children fixed; fills, strokes, corner radius, clipping, layout grids, guides, scrolling) and takes its layers' box; a group turned into a frame keeps its box, with no fill and no clipping. Instances, main components, layers inside instances and empty frames are refused with the reason, all or nothing; one undo step. Numbers unchanged. -- **2026-10-13** · Prototyping, delays on mouse triggers and manual overlay offsets (M5): mouse enter, leave, down and up wait `delayMs` before their actions run (once while waiting; dropped when the screen or overlay they were on goes away; a pointer that leaves early doesn't cancel, the documentation is silent), and a manual overlay sits at `offset: [x, y]` from the top-left of the layer that opens it (without one, right below it, as before). Delay and X/Y fields in the interaction editor, the player on its own clock, REST import (`delay`, `overlayRelativePosition`), `.fig` import and export (the export's field for mouse delays is unchecked against other software). Numbers unchanged. -- **2026-10-13** · Auto layout, wrapped-line alignment (M2): a wrapping layout's lines (rows of a horizontal one, columns of a vertical one) are packed where the cross alignment says (start, centre, end; before, they always sat at the start), stretch when every child fills across the flow, or take the free space between them (`contentAlign: spaceBetween`, the Design panel's Row/Column gap mode, the alignment pad lighting the whole cross axis). Read from REST JSON and `.fig`, written to `.fig`, `align-content` in CSS, an instance override with the other alignment settings. 13 hand-computed layout cases and command tests. Numbers unchanged. -- **2026-10-13** · `.fig` export: instance sizes, variables, styles (gap 1): instances write what they change from - their main (size, name, paints) as an override addressed by the main's id, what follows for their layers as - derived data and a scale factor when scaled as a whole; variable collections, modes, variables and bindings, - the modes frames and pages set, shared styles with their links and variable-width strokes are written, and the - import reads all of it back. Checked by opening written files in other software: resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width; - number, string and boolean bindings and bindings on effects, layout guides and gradient stops come across as the values they give but did not show as bound there (their entries now also name the layer field they set, not yet checked there), and strokes wider on one side of the path are written as filled outlines, no longer editable strokes there (the stroke itself is kept as plugin data and our own import puts it back; not yet checked there). Numbers unchanged. - text, effect and grid styles, number, string and boolean bindings, bindings on effects, layout guides and gradient stops, and variable-width strokes are not yet checked there. Numbers unchanged. -- **2026-10-13** · Starter templates and first-run help: eight templates built in code (mobile app, landing page, - dashboard, design system starter, wireframe kit, social post sizes, presentation slides, icon set starter), a - Templates tab on the home screen, `template.list` and `file.newFromTemplate` for scripts and agents, and a Getting - started card that can be closed and comes back from the Help menu -- **2026-10-12** · Your online files: a file shared or published online stays yours after its tab closes or the app - quits. The app remembers it (with its owner token, kept with the preferences and never shown) and Home ▸ **Online** - lists what you own: open a file again as its owner, start or stop sharing, copy its code, rename it, remove it from - the server or forget it here. Servers are asked once, when the list opens; tabs mark files in a live session; a - crash reopens such a file from its server and puts back the changes that hadn't reached it. `collab.files`, `collab.openFile`, `collab.forgetFile`; servers need an - upgrade only to tell which files are still there. The six draft catalogs translate every string again -- **2026-10-12** · Everyday editing partials: number fields (scrub rates with Shift and Alt, arrow keys by the nudge - preferences, arithmetic with brackets, relative edits of Mixed values per layer), outline mode's hidden layers and - object bounds, Hide when publishing in the Assets panel, a cursor for every tool and modifier, panel widths that are - kept, and dark pages (white text on them) for files made in the dark theme -- **2026-10-12** · Sharing with a join code: Share ▸ **Online** (through the server in Preferences, built in) or - **Local network**, one Start sharing button and a short code to read out (`K7M-2QX-9TD`; a longer one locally that - carries the host's address); others type it under Join with a code (the popover, the home screen), where a link - works too. The server names links with codes, keeps them hashed and limits wrong ones; `collab.share`, - `collab.join {code}`; servers must be upgraded to hand codes out (older ones keep working with links) -- **2026-10-12** · Text metrics measured: our example documents, re-saved by other software, were read back and - compared layer by layer. Line heights are now whole pixels, advances sit on a 1024-per-em grid and letter spacing - goes between glyphs only; all 230 self-sizing text boxes in the 7 documents match to the pixel (24 of 131 before - in the design-system example). One font family; real files still unchecked (gap 3). No number moves -- **2026-10-12** · Live sessions are per document: every open file has its own session or none, several run at - once (host one, join another, publish a third) and keep syncing behind the tab in front, the Share popover, - people, cursors, chat, spotlight and follow are the file in front's, `collab.peers` lists them all (`sessions`), - and closing a file ends its session only (before, one session followed every tab and starting another replaced it) -- **2026-10-12** · A public `figcraft-server`: `open_publish = true` lets anyone publish without the server key - (Share ▸ Publish to server asks the server first and shows who hosts it, its terms and how long unopened files are - kept), with an owner token shown once that manages the file later (`collab.manageFile`); limits per address, - idle expiry, reports (Share ▸ Report this file…), `block-ip` and `files list --sort`, `GET /healthz`, and - `packaging/server/install.sh` that sets up a Debian/Ubuntu machine with Caddy, ufw and backups - ([server.md](docs/server.md#running-a-public-server)). Numbers unchanged (hosting, not a Figma feature). -- **2026-10-12** · `.fig` export opens elsewhere (gap 1): written files were checked by opening them in other - software: layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft. The - export now says that the document and its pages are visible and opaque, numbers its schema as the format does, - and writes outlines, text settings, standard font style names and line height units; effects such as glass and - noise, images and prototype interactions are still unchecked there, and variables are not written. Numbers - unchanged. -- **2026-10-12** · Experimental `.fig` export (gap 1): `file.export {format: "fig"}`, File ▸ Export… ▸ .fig - (experimental), `figcraft-cli export --format fig` and MCP's `export` write the whole document: layers, - geometry, paints with their images, strokes, effects, styled text, vector networks, auto layout, components and - instances with their overrides, and prototype interactions; what's left out (style links, variables, comments, - Dev Mode data…) comes back as warnings. Checked only by reading the files back with our own importer. File - formats 60%. -- **2026-10-12** · Keyboard shortcut parity (gap 4): a table test with a row for each of the 129 - shortcuts in the public documentation, macOS and Windows/Linux keys each, and the 48 bindings it - is silent on pinned; 126 bound (110 before), Run last plugin moved off a documented conflict on - Windows, 2 missing. New: others' cursors toggle, list levels on Cmd/Ctrl+] and [ while editing, - keyboard box selection on Alt+Space / Ctrl+Space, arrows that pan with nothing selected, Control - suspending snapping on macOS, Space/N/C/F while presenting, the Frame tool on A, pixel grid on - Shift+', link on Shift+Cmd+U, ungroup on Cmd/Ctrl+Backspace, suggest auto layout and go to main - component keys; shifted symbols on other keyboard layouts. -- **2026-10-12** · Plugin directory on `figcraft-server` (gap 6): publish a plugin folder with the server key - (`figcraft-cli plugin publish`), browse a server's plugins in Plugins ▸ Manage plugins ▸ Browse, install after a - confirmation that shows the permissions it asks for, update when a newer version appears, remove; bundles are - checked in full on the server and again in the app, downloads must match their SHA-256, installed plugins live - per user in the same sandbox; `plugin.directory.*` for scripts and MCP; the operator moderates with - `figcraft-server plugins`. Plugins & widgets 60%. -- **2026-10-12** · Plugin widgets and the panel tail (gap 6, plugin API 3; older plugins run unchanged): a widget - plugin puts a layer of its own on the canvas (Plugins ▸ Widgets, `widget.insert`), described by the plugin, - validated and built with locked layers inside; clicking a part it named calls it back with the widget's state - (kept in the layer, so it saves, copies, undoes, exports and syncs) as one undo step, and it redraws; - `widget.click` and `widget.list` for agents; a Counter example. Panels of several plugins open side by side and - plugin colour fields open the colour picker. Plugins & widgets 55%. -- **2026-10-12** · Layers and Assets panels behaviour suite (gap 4): several layers dropped together land - together, Collapse layers keeps the selection's rows open, dragging across eyes or padlocks sets several layers - at once, a Rename layers dialog for several layers (number codes as documented, regular expressions, a start - value), pages dragged to reorder, dividers, "Copy of" duplicates, Assets folders by page, frame and slash names, - and hidden-from-publishing names that decide both ways. -- **2026-10-12** · Vector multi-edit and the vector edit tail (gap 9): several vector layers in edit - mode at once (select, marquee, drag, nudge, delete, mirroring and radius across them), several - Bezier handles dragged together, rounded points beside curved segments, a stroke cap and join per - point, and the Pen connecting to its own points or continuing a selected open path. -- **2026-10-11** · Canvas navigation, guides and grids behaviour suite (gap 4): zoom, panning, the pixel - grid (now from 400%), rulers, guides (top-level frames only; select, Delete, nudge, Remove guide, - Alt-drag copies, Alt measures while dragging), layout grids (Auto counts; hidden on rotated frames), - outline mode (clicks reach layers behind) and measuring, each rule from the documentation. -- **2026-10-11** · Export and inspect behaviour suite (gap 4): exports cover what a layer paints (shadows - only where they fall, true stroke widths, no room for hidden layers, a mask group's mask), slices export what's under - them, layer names with slashes export into folders; export settings gained contents only, include bounding box and - the SVG options (outline text, ids, simplified strokes); Inspect shows colours in hex, RGB, CSS, HSL or HSB, CSS in - rem, blend modes and positions from the frame, and lists icons and images as assets. Dev Mode 70%. -- **2026-10-11** · `.fig` import's tail (gap 1): vectors open as editable vector networks with their - corner radii, handle mirroring and end caps (outlines when the network is missing or damaged); sticky notes, shapes - with text, connectors, tables and similar layers import as frames, text and vectors instead of being dropped; - set-variable and conditional prototype actions come across. File formats 58%, ready ~49%. -- **2026-10-11** · Paint and effects behaviour suite: every documented rule for fills, blend modes, - gradients, images, colour models, shadows, blurs and effect exports is a table-driven case. Fixes: Normal - groups isolate and clipping frames don't, plus darker is drawn, drop shadows hide behind translucent - layers in every export, Fill images reach the box's edges, "Normal" no longer means pass-through. - Paint 70%, effects 75%. -- **2026-10-11** · `.fig` import, best effort (gap 1): `file.open`, `file.import`, drag and drop, the CLI and - MCP open `.fig` files. A generic kiwi decoder reads each file's own schema, and the decoded nodes map by field name - onto the REST importer: layers, paints and styles, text runs, vectors as outlines, auto layout, components and - instances (expanded with their overrides and property values), variables, prototyping and images from the archive; - the rest is a warning. File formats 55%, ready ~49%. -- **2026-10-11** · Gamut warnings (gap 18): the colour picker of a Display P3 file flags colours - outside sRGB and View ▸ Soft-proof sRGB stripes them on the canvas. Showing P3 itself waits on - eframe/egui-wgpu exposing the surface colour space wgpu 30 already supports. -- **2026-10-11** · Several windows (gap 18): File ▸ New window, Open in new window on a tab or a - tab dragged off the strip; each window has its own tabs, panels and canvas, draws only when it - needs to and asks about unsaved changes when it closes (the main window too); the control channel - and MCP open, list, focus and capture windows. Hardware 60%. -- **2026-10-11** · Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls, - certificates always verified; `FIGCRAFT_CA_FILE` for a private authority), releases ship the server for Linux - x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` walks through deploying it on - a rented Linux server (systemd unit, Caddy or nginx, firewall, Docker, upgrades, backups). -- **2026-10-11** · Plugin panels and settings (gap 6, ABI v2; v1 plugins run unchanged): a plugin shows a panel of - fields and buttons drawn with FigCraft's widgets and is called back for each button or changed value (one undo step - per callback, same sandbox limits), `plugin.ui` / `plugin.event` / `plugin.close` drive panels from scripts and MCP, - and per-plugin settings are kept with the preferences; Swatch grid gained a palette panel. Plugins & widgets 45%. -- **2026-10-11** · Canvas fixes: clicks hit boolean groups where their result is (a click in a - subtracted hole reaches what's behind), a layer dragged inside a group stays in it, View ▸ Frame - outlines and View ▸ Slices draw, and a flaky idle test now waits for the render thread (gap 13). -- **2026-10-11** · Team libraries on `figcraft-server` (gap 10): Assets ▸ Libraries ▸ Publish to server keeps - the file's library on the server under the file (per-asset versions; the server's version is followed); other files - enable it by pasting a share link (a view link is enough), get "Library updates available" from a background check - and accept with overrides kept; the server counts which files use which components (Show usage, `library.usage`); - `library.swap` re-points instances, styles and variables to another library by names. Team libraries 70%. -- **2026-10-11** · Animated GIF fills (first frame on the canvas and in exports, played in the - presenter and the preview, waking the window only for the next frame) and TIFF images (gap 12). -- **2026-10-11** · The last three menu items got commands: File ▸ Open recent (kept in the - preferences, missing files greyed, Clear recent), outlines for the selection and mask outlines - (gap 21). Menu breadth 203/203. -- **2026-10-11** · Extended variable collections (a library collection's modes and variables in a - collection of the file, overridden per mode with reset, following library updates), variables - import/export (the REST local variables JSON, merged by name; CSS custom properties), gradient - stops and variant properties bound to variables (gap 14). -- **2026-10-11** · Live boolean editing: operands stay editable layers (double-click or Cmd/Ctrl-click - selects one, moving, resizing or editing its points redraws the result during the drag with every - operand outlined, per-operand operations from the boolean controls, prefix-cached recompute); - Design panel arc fields; the shape builder on strokes and open paths; one-sided variable widths - (gap 13). Feature breadth ~79%. -- **2026-10-10** · WASM plugins (#101: sandboxed wasmi runtime, manifest permissions, Plugins menu, - Run last, Manage, Rust SDK and two examples), docs refresh (#102), the multiplayer core without - networking (#103: client-scoped ids, change sets, server ordering and validation, client - reconciliation, convergence tests). Menu breadth 200/203; ready for real work ~47%. -- **2026-10-10** · Full re-measure against Figma desktop 126.10.7 under the craftrules progress-docs - standard: ready for real work ~46%, feature breadth ~76%, stage alpha; progress docs split out - (`docs/target-app-parity.md`, `gaps.md`, `roadmap.md`, `architecture.md`, the per-area parity - docs); `docs/parity.md` renamed `docs/parity-checklist.md`. -- **2026-10-10** · Day 2 (PRs #1–#99): vector edit mode depth (transform box, point snapping, arc - handles, shape builder, variable width), text depth (variable fonts, bidi, RTL names, style - browser), auto layout nesting and keyboard reordering, slots and library variables, components - at scale (2,000 instances), effects fidelity (glass, noise, texture, progressive blur), - prototyping (conditionals, expressions, device frames, responsive presentation), REST import of - prototypes and variables, SVG import fidelity, PDF print options, Dev Mode (code, annotations, - ready for dev, compare changes), comments polish, version history, keyboard shortcut parity, - accessibility, retained canvas and 50k-layer performance, no redraws while idle; v0.2.0 - released for every platform; multiplayer protocol designed. -- **2026-10-09** · Day 1: foundation (13 crates, layering, command registry, renderer, engine, - UI3 frontend, control channel), CLI and MCP, web build, PDF export, packaging, first depth - passes on text, components, prototyping and comments. - -## Revision history - -| Date | Change | Summary | -|---|---|---| -| 2026-10-15 | minor | M7: system font discovery skips entries observed as non-regular files before opening them, avoiding the reproduced named-pipe stall. Regular-file symlinks remain supported; directory depth, font limits and priorities are unchanged. Numbers unchanged. | -| 2026-10-15 | minor | Typography spacing icons scrub with the shared number-field gesture, modifier rates and one Undo step per drag (#373). Units and selected-character targeting are preserved. Numbers unchanged. | -| 2026-10-14 | minor | System theme on first run (M7): preferences without a theme default to System; the first frame reads the OS appearance directly, including when returning from a manual theme. Saved Light/Dark choices stay explicit; unavailable OS appearance falls back to Light. Numbers unchanged. | -| 2026-10-14 | minor | Home screen recent files as cards (M7): each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged. | -| 2026-10-14 | minor | Lost images reported on loading (#207): a `.figcraft` file whose image member is damaged, too large or missing no longer loads in silence; each lost image (up to 20, then a count) is listed in `repairs` on `file.open` with its hash and the reason, and the app shows it as a toast. Saving still writes the file without the lost images. Numbers unchanged | -| 2026-10-14 | minor | SVG import budgets (#190): gzipped SVG is unpacked by FigCraft within 64 MiB before parsing (it was unpacked without a limit), so the nesting check sees the real document; SVG documents embedded in `` data URIs share one 32 MiB budget and at most 4 levels, and one past a budget is left out. Linked files stay unread. Numbers unchanged | -| 2026-10-14 | minor | Leptos code (#223): `inspect.code`, `inspect.componentCode` and `codeConnect.set` take `language: leptos`: a `#[component]` with `#[prop]` defaults, variant enums and `view!` markup, instances as calls, Code Connect `use` lines; the Dev Mode language list and rem units include it. Numbers unchanged | -| 2026-10-14 | minor | Full screen (#227): View ▸ Full screen (`view.fullscreen {on?}`, F11 on Windows and Linux, Ctrl+Cmd+F on macOS) makes the window fill the screen and leaves it again; FigCraft's own item. Numbers unchanged | -| 2026-10-14 | minor | Colour picker: wrapped rows of page and recent swatches stay inset on both sides instead of running past the picker's right edge. Numbers unchanged | -| 2026-10-14 | minor | Server snapshots (#188): figcraft-server never replaces a readable snapshot with one too large to be read back, and reads snapshots up to twice the file cap (recent batches kept for reconnecting clients may take a file over it). Numbers unchanged | -| 2026-10-14 | minor | Preferences owner-only on Unix (#200): `state.json`, which holds online owner tokens, is written with mode 0600 from the first byte, and an older world-readable file is narrowed. Numbers unchanged | -| 2026-10-14 | minor | ruzstd 0.9.1 (#182): `.fig` import requires the release that fixes RUSTSEC-2026-0344 (a zstd decompression bomb). Numbers unchanged | -| 2026-10-14 | minor | Layer cycles (#205): `validate` reports children listed twice, pages with a parent and layers not reachable from a page (cycles, detached subtrees); `repair` drops them in one linear pass. Numbers unchanged | -| 2026-10-14 | minor | AppImage CLI: `figcraft.AppImage cli ` runs the bundled `figcraft-cli` (MCP server included) on Linux without a second download. Numbers unchanged | -| 2026-10-14 | minor | Boolean outline depth (#193): nested boolean groups share one outline depth budget instead of each level getting its own. Numbers unchanged | -| 2026-10-14 | minor | Command batches roll back as one: a failed `command.batch` (MCP `batch`) restores every open document and its state, and puts the tab in front back. Numbers unchanged | -| 2026-10-14 | minor | Desktop builds no longer rebuild the text crate on every build when the craft-fonts manifest is missing. Numbers unchanged | -| 2026-10-14 | minor | Direct3D 12 on Windows: the desktop app asks wgpu for Direct3D 12 there instead of Vulkan, so it starts without Vulkan loader errors on machines with stray overlay layers. Numbers unchanged | -| 2026-10-14 | minor | Plugin metering (#192): every host call and every byte copied between a plugin and the host costs fuel, so a plugin can't stall the app through cheap-looking host calls. Numbers unchanged | -| 2026-10-14 | minor | SVG/PDF export depth (#208): explicit preflight refusal beyond 256 descendant levels, propagated through public APIs, commands and CLI, with boundary/content and destination-preservation regressions. Numbers unchanged. | -| 2026-10-13 | minor | MCP/control transport hardening (#201): frame limits during reads, bounded connections and both UI request queues, socket inactivity timeouts; percentages and estimates unchanged | -| 2026-10-13 | minor | Plugin log limits (#191): cap each UTF-8 line at 8 KiB, total log bytes at 1 MiB and lines at 1,000 per run/callback before copying guest memory or forwarding to the host. Over-budget runs fail and roll back document edits; callbacks get fresh budgets. Added sandbox boundary/forwarding tests and an engine rollback regression. Percentages and hours unchanged. | -| 2026-10-13 | minor | Live-session revert guard (M9, #198): `file.revert` is refused while its document belongs to a live session, including publishing, reconnecting and hosting before the welcome. The document, previews, branch tabs and session stay intact; leave the session before reverting a local save. Offline revert is unchanged. Scripted access and sync regressions in `live_access_tests`. Numbers unchanged. | -| 2026-10-13 | minor | Repeat last command (M1, #181): keep the underlying command and parameters across repeats, reject self-reference, and retain one undo step per repeated edit. Numbers unchanged. | -| 2026-10-13 | minor | Component playground (M3): with an instance, a component or a component set selected, the Inspect tab shows a Playground: a preview of the component and a control for each variant, boolean and text property. What is tried there changes the preview and the code in the Code section, never the file (no undo step, the canvas instance stays as it is); Reset goes back to the component's or the instance's own values. `component.playground {id?, properties?, language?, path?}` gives the same to scripts: the properties with their values, the code, and a PNG on request (`playground_tests`). Left: instance-swap and slot properties in the playground (the command takes an instance swap; the panel has no control for it), a code connect template's snippet with the tried values. Numbers unchanged. | -| 2026-10-13 | minor | Auto layout, on-canvas handles of automatic gaps (M2): the handle between spread rows or columns (and between items with an auto gap) shows the distance the layout gives the gap instead of the stored value, and dragging it makes the gap a fixed one from there, as typing a gap in the panel does; one undo step (`gap_handle_tests`). Before, the drag changed a value that had no effect. The documentation is silent on dragging an automatic gap. Numbers unchanged. | -| 2026-10-13 | minor | Variables inside styles (M3): a shared style can bind variables to fields of its value (`style.bind {style, field, variable?}`: a paint style's colours and gradient stops, an effect style's colour, radius, spread and offsets, a grid style's size, count, gutter and offset, a text style's font size, line height, letter spacing, family and style). Every layer using the style shows those fields resolved in its own modes and stays linked when a variable or a mode changes; the style keeps them resolved in the default modes. A style made from a layer keeps that layer's bindings; a new value drops bindings to what is gone. The style editor binds from its property rows (paint colour, shadow colour, blur radius, grid size) and detaches from the variable's chip; the style picker marks styles that use variables. `.fig` export and import carry the bindings on the style, REST layers hand theirs to the style they use. Left: binding text style fields and the other effect and grid fields from the editor (commands only), library variables in the picker, a style's library copy. Numbers unchanged. | -| 2026-10-13 | minor | Effects on groups, inner shadows and background blur (M3): a group is no outline of its own, so its inner shadows are cast inwards from around what its layers draw together (their pixels, over them: no shade at a seam between two layers) and its background blur and glass show the backdrop through the outlines of its layers (under them). Canvas and PNG; SVG embeds a group with an inner shadow as a render and its background blur as an image under the layers; PDF places both as images, the blur matching the PNG. Where the group holds one shape the pixels match that shape, or a frame filled like it, carrying the effect (`group_effects_tests` in render, svg and pdf). Offsets and spreads of a group's inner shadow go by whole pixels. Numbers unchanged. | -| 2026-10-13 | minor | Text made by commands on a dark page (M1): in the app, `text.create` and `node.create {type: text}` that name no fill give white text on a dark page, by the same rule as the Text tool (the page's colour decides, also inside a frame); a named `fill`, `fills` or `fillStyle` is kept. Sessions without an interface (CLI, MCP, exports) still make black text everywhere. Numbers unchanged. | -| 2026-10-13 | minor | Frames and groups, a one-step convert (M1): `object.convertToGroup` and `object.convertToFrame` (the Design panel's type menu on a frame or a group) change the layer in place, its layers staying where they are on the page. A frame turned into a group loses what only a frame has (auto layout, with the positions it gave kept and fill children fixed; fills, strokes, corner radius, clipping, layout grids, guides, scrolling) and takes its layers' box; a group turned into a frame keeps its box, with no fill and no clipping. Instances, main components, layers inside instances and empty frames are refused with the reason, all or nothing; one undo step. Numbers unchanged. | -| 2026-10-13 | minor | Prototyping, delays on mouse triggers and manual overlay offsets (M5): mouse enter, leave, down and up wait `delayMs` before their actions run (once while waiting; dropped when the screen or overlay they were on goes away; a pointer that leaves early doesn't cancel, the documentation is silent), and a manual overlay sits at `offset: [x, y]` from the top-left of the layer that opens it (without one, right below it, as before). Delay and X/Y fields in the interaction editor, the player on its own clock, REST import (`delay`, `overlayRelativePosition`), `.fig` import and export (the export's field for mouse delays is unchecked against other software). Numbers unchanged. | -| 2026-10-13 | minor | Auto layout, wrapped-line alignment (M2): a wrapping layout's lines (rows of a horizontal one, columns of a vertical one) are packed where the cross alignment says (start, centre, end; before, they always sat at the start), stretch when every child fills across the flow, or take the free space between them (`contentAlign: spaceBetween`, the Design panel's Row/Column gap mode, the alignment pad lighting the whole cross axis). Read from REST JSON and `.fig`, written to `.fig`, `align-content` in CSS, an instance override with the other alignment settings. 13 hand-computed layout cases and command tests. Numbers unchanged. | -| 2026-10-13 | minor | `.fig` export (gap 1): instance sizes (resized, filling an auto layout, scaled), variables with modes and bindings, shared styles and variable-width strokes are written and read back. Checked by opening written files in other software: resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width; number, string and boolean bindings and bindings on effects, layout guides and gradient stops come across as the values they give but did not show as bound there (their entries now also name the layer field they set, not yet checked there), and strokes wider on one side of the path are written as filled outlines, no longer editable strokes there (the stroke itself is kept as plugin data and our own import puts it back; not yet checked there). Numbers unchanged | -| 2026-10-13 | minor | Starter templates and a first run that explains itself: eight templates built in code through the command layer (mobile app, landing page, dashboard, design system starter, wireframe kit, social post sizes, presentation slides, icon set starter; original and synthetic, deterministic, about 10–190 ms each to build), `template.list` and `file.newFromTemplate` for the app, CLI and MCP, File ▸ New from template…, a Templates tab on the home screen (cards with thumbnails rendered from the documents, search, kind filter, keyboard and screen-reader names) in place of the Community placeholder, a row of templates on Recents while there are no recent files, a Getting started card that can be closed and comes back from Help ▸ Getting started (`help.gettingStarted`), and a first run that opens on the home screen. The Drafts placeholder is gone (the app has no drafts folder). Essentials ~57% → ~59% (discoverability ×0.92 → ×0.94), its hours ~60–110 h → ~55–100 h; other numbers unchanged | -| 2026-10-12 | minor | Your online files (M9): a file put on a server without its key stays its owner's after its tab closes or the app quits. The app records each one with the preferences (server, id, name, owner token, the owner's links, the codes others join with; at most 200; the token never in a command result, `ui.inspect`, a log or debug output) and Home ▸ **Online** lists them: open one as its owner (a new tab, or its tab comes to the front), Start / Stop sharing, Copy code / Copy link, Rename on the server, Remove from server (confirmed), Forget (the file stays on the server). Nothing is asked of a server until the section opens; then one request per server (`Admin owned`, a compatible addition within protocol version 2) says which files are still there, and a server that is offline or older leaves the list as recorded. Tabs of files in a live session carry a mark (shared, or open only to you), toasts about a file that isn't in front name it, and after a crash a recorded file reopens from its server as its owner, its recovery copy kept until its unsent changes are back on the server (or it is opened beside the file). `collab.files`, `collab.openFile`, `collab.forgetFile`; `collab.manageFile` takes its server and token from the record and gains `share` / `unshare`. The draft catalogs (French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese) translate the 71 strings and 5 command labels they lacked and the new ones. Numbers unchanged | -| 2026-10-12 | minor | Everyday editing partials closed (gap 4): number fields scrub a step a point (×10 with Shift, ×0.1 with Alt, on past the screen edge where the platform reports it), step with the arrow keys by the nudge preferences, take brackets, units and relative edits (`+10`, `*2`, `50%`), commit on Tab and nothing when left untouched, and on Mixed apply relative edits to each layer in one undo step; outline mode's Include hidden layers and Include object bounds (`view.outlinesHiddenLayers`, `view.outlinesObjectBounds`), drawn and clickable; Hide when publishing from the Assets panel (`component.hideWhenPublishing`); a cursor for every tool, modifier and hover target (rotate turned to its corner, pen states, eyedropper, comment, copy, not-allowed), with guides and handles keeping theirs; panel widths with limits, double-click reset and the preferences (`view.panelWidths`); a file or page made in the dark theme has a #1E1E1E page (sessions without an interface keep #F5F5F5) and text drawn on a dark page is white. Menu breadth 211/211. UI/UX fidelity 58% and the interaction pass 40–60 h unchanged at this rounding: the side-by-side comparison is what remains | -| 2026-10-12 | minor | Sharing with a join code (M9, from the owner's use: sharing wasn't intuitive): the Share popover is one flow, **Online** (the file is published to the server in Preferences, built in `wss://collab.craftapps.dev/`, and the server names its edit and view links with nine-symbol codes like `K7M-2QX-9TD`) or **Local network** (the app hosts; a 20-symbol code carries its IPv4 address, port and a 44-bit secret, with check bits), one Start sharing button, the code large with Copy code and Copy link, who is here, can edit / can view online, and Stop sharing (online: the others' links and codes are revoked and they are disconnected, while the file stays on the server with its owner in it and can be shared again with new codes; removing it is a separate, confirmed step); **Join with a code** is one field (popover, home screen, Join dialog) for a code of either kind or a link; addresses, keys and ports moved under Advanced and Preferences ▸ Server for online sharing (reset, or empty to turn online sharing off). Protocol additions within version 2 (`Resolve` / `Resolved`, `Admin code`): the server keeps a stretched hash and the wrapped token, never the code, and limits wrong codes (10 a minute per address, a 15-minute lockout after 10 wrong, 1,000 wrong an hour for everyone); codes go with their link and file. Older servers leave the links in place of a code; older apps join by link. `collab.share`, `collab.join {code}`, `code` and `share` in `collab.peers`; the browser joins by code. Numbers unchanged | -| 2026-10-12 | minor | Live sessions per document (M9, a fix from real use): the engine keeps one session per open document instead of one for the app, the link carries several under their own ids (desktop, browser, tests), the window and the `collab.*` commands follow the active document, `collab.peers` gains `session` and `sessions`, `ui.inspect`'s `live` gains `session` and `sessions`; joining opens the session's file in its own tab without touching other sessions and closing a file ends its session only. Numbers unchanged | -| 2026-10-12 | minor | `.fig` export checked by opening written files in other software (gap 1): layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft, after fixes to what the export writes (visible document and pages, the format's schema numbering, outlines, text settings, font style names, line height units); effects such as glass and noise, images and prototype interactions still unchecked there. Numbers unchanged | -| 2026-10-12 | minor | Public `figcraft-server` hardened against resource exhaustion after a security review: messages capped by what a connection is for (a file's size when open), byte-bounded send queues and a bound on bytes waiting for the hub, files checked as they grow (refused batches roll back with the reason; per-address and total caps count growth), the core's recent-batch memory bounded in bytes, no confirmation for changes that couldn't be written, team libraries only in key-published files on public servers with unpacking bounded by the file cap, usage reports kept in memory, layered IPv6 limits (/64, /56, /48), a global publish rate, connections reserved for sessions, per-address download shares, library connections cut on revoke and removal, a private data directory and key, and safer install.sh, systemd and backups. Percentages and hours unchanged | -| 2026-10-12 | minor | A public `figcraft-server` (open hosting): with `open_publish = true` anyone publishes a file without the server key and gets its edit and view links plus an owner token (128 bits, shown once, stored hashed) that lists, makes and revokes its links, renames it or removes it (`collab.manageFile`); limits per address (connections at once and a minute, messages a second, publishes an hour, files and MiB kept; IPv6 by /64; `X-Forwarded-For` believed only from `trusted_proxy`; tables capped at 10,000 addresses) and for everyone (`max_total_mb`), each refusal saying why; files published without the key expire after `idle_days` unopened (the date shown to everyone in the file); moderation (`files list --sort size|age|idle`, `block-ip`, `reports`, a Report button and `collab.report`); the operator's contact and terms shown in the Share dialog; `GET /healthz` on the same port; `packaging/server/install.sh` for a Debian/Ubuntu machine behind Caddy and a nightly backup timer. Protocol additions within version 2, gated like signals (older apps get clear refusals). Hosting infrastructure rather than a Figma feature: percentages and hours unchanged | -| 2026-10-12 | minor | Experimental `.fig` export (gap 1): `file.export {format: "fig"}`, File ▸ Export… ▸ .fig (experimental), the CLI and MCP write the whole document (layer tree, geometry, paints with image bytes, strokes, effects, text with styled runs, vector networks, auto layout, constraints, components, sets, instances with their overrides and property values, prototype interactions) with a kiwi encoder mirroring the decoder and the schema of the fields written; what's left out is a warning; verified only by round trips through our own importer. File formats 59% → 60%; import/export 84%/57% → 86%/58%; file exchange stays ×0.85; mainstream hours ~130–244 h → ~120–229 h; feature breadth (~84%), ready for real work (~53%), essentials (~57%), mainstream (~49%), beta (~155–280 h) and full parity (~265–523 h) unchanged at this rounding | -| 2026-10-12 | minor | Keyboard shortcut parity suite and bindings (gap 4): a table test with one row per shortcut in the public documentation, macOS and Windows/Linux keys each (129 rows: 126 bound, 110 before; 1 moved off a documented conflict; 2 missing; 2 kept by a platform), plus 48 pinned undocumented bindings so every default binding is in the table; newly bound: A (Frame), Shift+' (pixel grid), Alt+Cmd+\ (others' cursors, new `view.multiplayerCursors`), Ctrl+F6, Shift+Cmd+U (link), Cmd/Ctrl+] and [ (list levels while editing, new `text.listIndent` / `text.listOutdent`), Cmd/Ctrl+Backspace (ungroup), Alt+Space / Ctrl+Space (keyboard box selection, Cmd/Ctrl+arrows draw it out), suggest auto layout, go to main component, arrows that pan with nothing selected (new `view.pan`), Control suspending snapping on macOS, Space/N/C/F while presenting; symbols another layout types with Shift count without it; no default binding uses a system-reserved chord (tested). Menu breadth 207/207 (View ▸ Multiplayer cursors). UI/UX fidelity 58% and the interaction pass 40–60 h unchanged at this rounding | -| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Localization 36% → 39%, 31–54 h → 29–51 h; ready for real work unchanged (~53%); full parity ~267–527 h → ~265–524 h | -| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Localization 35% → 36%, 32–55 h → 31–54 h; ready for real work unchanged (~53%); full parity ~268–528 h → ~267–527 h | -| 2026-10-12 | minor | Prototyping tail (gap 15, the part that doesn't need video): key/gamepad triggers take controller buttons (`Gamepad A` … `Gamepad Home`, the standard layout's 17 buttons, from a Controller menu beside the key field; the REST import maps controller triggers instead of skipping them); the player reads controllers on Windows (Windows.Gaming.Input through the `windows` crate the app already used; no new dependency) and in the browser (the Gamepad API), polling only while the prototype has such a trigger and the window has the focus, and the control channel presses buttons (`ui.input {gamepad}`); macOS, Linux and FreeBSD read no controllers yet. Widgets react to clicks in a presented prototype (the canvas's callback and sandbox limits, on the prototype's copy: the file and its history don't change, restarting starts over). `proto.evaluate` computes an expression where a layer's interaction would run (MCP); the mode lookup and set-variable-mode rules are cases in a behaviour suite (they already held). Prototyping breadth 90% → 92%, ready unchanged at 74%, 3–6 h → 2–5 h; area hours 120–260 → 119–259 (core 33–75 → 32–74), Features (depth) 29–68 h → 28–67 h; percentages unchanged at this rounding; full parity ~276–538 h → ~275–537 h | -| 2026-10-12 | minor | Plugin directory on `figcraft-server` (gap 6): the server keeps plugin bundles (manifest, module, up to 32 assets; an uncompressed length-prefixed format named by its SHA-256) published with the server key (`figcraft-cli plugin publish`, `plugin.directory.publish`), checked in full before they are kept (size cap, manifest and plugin API version, a version newer than the one kept, the module compiled but never run, plain asset names) and capped (`max_plugins`, `max_plugin_mb`, `max_plugins_mb`); anyone who reaches the server lists, searches and fetches them (hash-checked again on read, a shared download budget); the operator moderates with `figcraft-server plugins [remove]`. The app's Plugins ▸ Manage plugins gains a Browse tab: a server's plugins with Install / Update / Remove, an install confirmation that shows the permissions asked for, the source and the SHA-256, the download checked against the listed hash before anything is read, plugins kept per user in hash-named folders; `plugin.directory.list` / `install` (needs `confirm: true`) / `publish` / `unpublish` for scripts and MCP. Not in the browser build yet; no public catalogue, publisher accounts, signatures, ratings or background update checks. Plugins & widgets 55%/32% → 60%/35%, 10–30 h → 8–26 h; Ecosystem dimension 34% → 38%; weighted totals unchanged at this rounding; full parity ~281–548 h → ~279–544 h | -| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Localization 30% → 35%, 40–65 h → 32–55 h; ready for real work ~52% → ~53% (52.9 → 53.1); full parity ~284–554 h → ~276–544 h; beta ~162–292 h → ~155–280 h | -| 2026-10-12 | minor | Plugin widgets and the panel tail (gap 6, plugin API 3; v1 and v2 plugins unchanged): a plugin marked as a widget puts a layer of its own on the canvas from Plugins ▸ Widgets or `widget.insert`: it describes the layer (frames with auto layout, text, rectangles, ellipses, images it carries) and FigCraft validates the description (size, layer count, depth, numbers, colours, text and image sizes) and builds it as a frame with locked layers inside; a click on a part the plugin named (on the canvas with the Move tool, a pointing hand over it, or `widget.click`) calls the plugin back in a fresh, metered sandbox with the widget's state, as one undo step, and the widget is redrawn; the state lives in the widget's own layer data, so widgets survive save and reload, copying, undo, export and live sessions; no plugin can write that data or call `widget.*`. `widget.list` lists them. A Counter example widget. Plugin panels of different plugins open side by side, and a colour field's swatch opens the colour picker (the plugin hears of the colour when a drag ends). Plugins & widgets 45%/28% → 55%/32%, 15–40 h → 10–30 h; Ecosystem dimension 28% → 34%; weighted totals unchanged at this rounding; full parity ~286–558 h → ~281–548 h | -| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): every UI string, command label and menu wording (1,899 entries) machine-drafted in each and marked as drafts awaiting native review, falling back to English per key (new strings need only English and Spanish); 0 counts as singular in French and Brazilian Portuguese; System picks the first OS language with a catalog (pt-PT and pt read pt-BR); long translations elide with a tooltip in dropdowns, two-column labels and the home cards. Localization 20% → 30%, 45–75 h → 40–65 h; ready for real work unchanged (~52%); full parity ~289–564 h → ~284–554 h; beta ~170–305 h → ~162–292 h | -| 2026-10-12 | minor | Cursor chat and spotlight in live sessions: `/` opens a chat bubble at your pointer that everyone sees as you type and that fades a few seconds after the last change; a spotlight (your avatar's menu, `collab.spotlight`) makes everyone follow your view and page until they move their own. Ephemeral presence the host validates and rate-limits, passed only to clients that read it (older clients keep working). Multiplayer & sharing breadth 15% → 17%, ready unchanged (5%), 45–90 h → 42–84 h; collaboration 13% unchanged, 49–98 h → 46–92 h; feature breadth (~84%) and ready for real work (~51%) unchanged at this rounding; full parity ~301–587 h → ~298–581 h | -| 2026-10-12 | minor | Layers and Assets panels behaviour suite written from the public documentation (6 engine tests with about 50 table rows and 5 panel tests driven through the UI; about 30 rows that the code before the fixes contradicts) and its fixes (several layers dropped together, Collapse layers, eye and padlock sweeps, Rename layers dialog with the documented number codes, page dragging, dividers, "Copy of" duplicates, Assets folders, hidden-from-publishing names both ways). Layers panel 98%/82% → 99%/85%; percentages and full parity hours unchanged at this rounding | -| 2026-10-12 | minor | Vector multi-edit and the vector edit tail, with a behaviour suite written from the public documentation (5 tests, about 30 table rows and steps, and a junk-parameter sweep of every vector command in multi-edit mode): several vector layers are edited at once (Enter, double-click or `vector.edit.begin {ids}`; a press on another layer's point, handle or segment makes it the active layer; Shift-click, marquee and lasso select across layers; dragging, the arrows, Delete, delete and heal, mirroring, smoothing, corner radius and point caps and joins reach every layer's selected points in one undo step; the pen, bend, paint bucket, cut and eraser work in the active layer); several Bezier handles are selected with Shift-click and dragged together (`vector.edit.moveHandles`); a sharp point next to a curved segment rounds (the curve is cut where the arc meets it and keeps its shape); a point can have its own stroke cap and join (`vector.edit.strokeCap`, `vector.edit.strokeJoin`, the Point section of the Design panel; canvas, PNG, SVG and PDF, outline stroke); the Pen outside edit mode connects to one of its own points and ends the path, and continues a selected open path from an end point. Left: corner smoothing beyond rectangles, the transform box around points of several layers, per-point caps and joins on dashed, variable-width and brush strokes. Shapes & vector 98%/72% → 99%/73%, 2–4 h → 1–3 h; Figma Draw extras 80%/55% → 88%/60%, 2–5 h → 1–3 h (gap 9: pen pressure on macOS and Linux tablets left). Totals recomputed by the doc's method, which rows since the last recompute had left stale: the area table's weighted breadth is 83.7% (~83%; core 94.1%, ~94%) and ready 63.1% (~63%; core 72.1%, ~72%), hours 133–284 (core 38–83); Features (depth) 61% → 63%, 46–94 h → 34–76 h; ready for real work 52.6% (~52%); mainstream weekly areas 71.0% → 72.1% (× 0.9 × 0.9 × 0.85 = 49.7%, ~49%, rounded down); essentials unchanged (~57%); full parity ~306–595 h → ~294–572 h (on top of the rows merged since: ~301–587 h → ~289–564 h; area-table totals recomputed on the merged table and rounded down: breadth ~84%, ready ~63%, core ~94%/~72%, mainstream weekly areas 72.4%, ~49%) | -| 2026-10-12 | minor | Dev resources (gap 16): links for developers (a URL and a name) on any layer: `devResource.add` / `edit` / `remove` / `list`, undoable, saved and synced; an instance also lists its main component's (and that component's set's) links read-only with where they come from, and layers inside an instance show their main layer's; URLs are checked as web links (http or https, a host, no spaces, at most 2,048 characters) and never fetched (a link added without a name is named after its URL), opened only when clicked; the Inspect tab lists, adds, edits and removes them; `get_node` and `inspect_document` return them as `devResources`. The editor extension stays out of scope (the MCP server serves agents and editors). Dev Mode breadth 93% → 96%, ready 70% → 72%, 1–3 h → 0–1 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~304–593 h → ~303–591 h | -| 2026-10-12 | minor | Extended collections' tail (gap 14): variables a library adds to a collection this file extends now count as a library update of their own (listed by name without a copy yet) and arrive inheriting when accepted, instead of waiting for the library's next other update; a library collection that is itself an extended collection can be extended again, values flowing down the chain as each file accepts updates with every level's overrides kept (checked end to end). Styles & variables breadth 94% → 95%, ready unchanged (76%), 3–5 h → 2–4 h; gap 14 1–2 h → 0–1 h (real-library checks left); feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~305–594 h → ~304–593 h | -| 2026-10-12 | minor | A branch's own version history and finer list merges (gap 17): versions saved or autosaved on a branch tab go into the branch (viewed, restored, renamed, deleted and duplicated there, saved in the file with it); merging adds a version to the main file's history and the merged or archived branch keeps its own; fills, strokes and effects merge item by item, conflicting only when the same item changed both ways, an item was removed on one side or both sides added different items. Comments, versions, find, misc breadth 85% → 87%, ready 60% → 61%, 2–5 h → 1–4 h; gap 17 1–3 h → 1–2 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~306–595 h → ~305–594 h (the Features table here still showed 80%/58%/4–8 h from before branches landed) | -| 2026-10-12 | minor | Headline percentages recomputed from the area table by its own method (weights × written scores): feature breadth ~82% → ~84% (core editor ~93% → ~94%), weighted ready ~61% → ~63%, core ready ~70% → ~72%, mainstream weekly areas 71.3% → 72.3% and mainstream ~49% → ~50%; several earlier rows had moved the area scores without carrying them into the totals. Hours are unchanged (left for the next full re-measure) | -| 2026-10-12 | minor | Text lists, hanging punctuation and decoration styles, with a behaviour suite written from the public documentation (7 layout tests and 7 command tests, about 60 table rows): lists are set per paragraph (the list buttons, shortcuts and Type settings act on the paragraphs the selection touches, the whole layer when it isn't being edited), nest up to 8 levels (Tab and Shift+Tab; numbers go 1. a. i., bullets • ◦ ▪, counted per level), Enter on an empty item and Backspace at an item's start step out a level and then out of the list, typing `- `, `* ` or `1. ` at a paragraph's start starts one; list spacing (between items, in place of paragraph spacing), hanging lists (markers outside the box) and hanging punctuation (opening punctuation outside a start-aligned edge, closing punctuation outside an end-aligned one, taking no room when lines wrap); underline and strikethrough style (solid, dotted, wavy), thickness, underline offset, skip ink and colour. Canvas, PNG, SVG and PDF; `node.set` keys (`indentation`, `listSpacing`, `hangingPunctuation`, `hangingList`, `textDecorationStyle`/`Thickness`/`Offset`/`SkipInk`/`Color`), inspect (`lineTypes`, `lineIndentations`), CSS (`text-decoration-style`/`-thickness`/`-color`, `text-underline-offset`, `text-decoration-skip-ink`) and the REST import (`lineTypes` with `lineIndentations` per paragraph, which collapsed to one list style before). Text 87%/68% → 90%/70%, 5–10 h → 3–7 h; mainstream weekly areas 71.0% → 71.2%; other percentages unchanged at this rounding; full parity ~306–595 h → ~304–592 h | -| 2026-10-12 | minor | Comments and version history behaviour suite written from the public documentation (15 tests, about 80 table rows plus stepwise checks; 4 tests failed before the fixes) and its fixes: in a live session, threads made in the session couldn't be replied to, edited, reacted to, resolved, moved, deleted or opened (their ids, from their creator's id space, were refused as out of range); restoring a version brought back that version's comments, losing newer threads and replies (comments now stay, those on layers the version lacks staying where they were); restoring now keeps the replaced design in the history as an autosave first, unless a version of it was just saved or restored; giving an autosave a title makes it a named version (it could be dropped to make room); merging a branch adds the main file as it was and then a `Merged “”` version to its history. Pinned where the documentation is silent: pins go to the innermost frame under them, copies, duplicates and duplicated pages come without comments, replying reopens a resolved thread, versions stay with whoever saved them in a live session (a shared history needs accounts). Left: a branch's own version history, comparing two versions. Comments, versions, misc 85%/60% → 85%/62%, 2–5 h → 2–4 h (this table still showed 80%/58%/4–8 h from before branches landed); percentages unchanged at this rounding; full parity ~306–595 h → ~306–594 h | -| 2026-10-11 | minor | Canvas navigation, rulers, guides, layout grids, outline mode and measuring behaviour suite written from the public documentation (18 tests, about 120 table rows and checks) and its fixes: the pixel grid shows from 400% (it waited for 800%); guides belong to the page or a top-level frame (a guide dropped over a nested frame goes to the frame holding it, and a frame moved into another keeps its guides but they stop showing, snapping and measuring); a guide can be selected, deleted with Delete, nudged with the arrows and removed from the context menu (`guide.select`, `guide.remove` without an index); Alt-dragging a guide drops a copy and holding Alt while dragging one measures to the nearest edges in the frame it crosses; columns and rows count Auto (as many fixed-size ones as fit); layout grids don't show or snap on a rotated frame; in outline mode shapes are clicked on their outlines, reaching layers behind. Left: outline mode's Include hidden layers and Include object bounds. Canvas & navigation 97%/72% → 98%/74%, 2–5 h → 1–4 h (this table still showed 95%/72%/3–6 h from before the outlines landed); percentages unchanged at this rounding; full parity ~320–615 h → ~319–614 h | -| 2026-10-11 | minor | Export and inspect behaviour suite written from the public documentation (139 cases in 19 tests; 47 failed before the fixes) and its fixes: an export covers what the layer paints, so a drop shadow adds room only where it falls, strokes on boxes and ellipses reach their real width (they added up to four times as much), hidden layers take no room and a mask group exports the mask's area; a slice exports everything under it; slashes in layer names export into folders; Copy as PNG copies the whole selection. Added: the export settings' contents only and include bounding box, and for SVG outline text (off writes ``), unique id attributes and simplified strokes (stroke attributes where exact); Inspect colour formats (hex, RGB, CSS, HSL, HSB), CSS in rem, a Blend row, positions measured from the frame around a layer (groups aren't frames), CSS for rotation, blend modes, gradient directions and kinds, text alignment, case and decoration; icons and images in the asset list (`file.exportLayers {assets}`). Import/export 80%/50% → 82%/53% (the row still showed the figures from before `.fig` import); Dev Mode 90%/64% → 93%/70%; mainstream weekly areas 68.1% → 68.4%; percentages and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | `.fig` import's tail (gap 1): vectors open as editable vector networks (points, curved segments, filled areas with their winding, per-point corner radius and handle mirroring, end caps from the end points), falling back to outlines when the network is missing or damaged; sticky notes, shapes with text, connectors, tables, code blocks, slides and similar layers import as the closest frames, text layers and vectors with a warning instead of being dropped; set-variable, set-variable-mode and conditional prototype actions come across. New parsers fuzzed on fixtures generated in code. File formats 55% → 58%, 8–15 h → 5–10 h; import/export breadth 80% → 82%, ready 50% → 54%; feature and area totals and ready for real work (~49%) unchanged; beta ~173–310 h → ~170–305 h; full parity ~321–618 h → ~318–613 h | -| 2026-10-11 | minor | Shapes and vector behaviour suite written from the public documentation and its fixes: rounded vector points are drawn and exported, a boolean group looks like its top layer (a subtraction like the bottom one), Alt draws lines from the centre, stars keep to 3–60 points, delete and heal follows the curve, Shift-click on a point at a corner of the points' box deselects it, outline stroke keeps the stroke's colour style, opacity and effects, the miter field shows degrees, individual corners only for rectangles and frames, Option/Alt+Shift+F flattens, MCP `key` reaches the edit tools. Added: corner radius and star ratio handles, Count and Ratio fields, cut drags that divide shapes into layers, paint bucket drags. Shapes & vector 97%/68% → 98%/72%, 1–3 h → 2–4 h. Totals recomputed from the area table, which earlier rows had left stale: feature breadth ~80% → ~82% (core ~91% → ~93%), Features (depth) 57% → 61% (core 68% → 69%), 60–115 h → 56–109 h, ready for real work ~49% → ~51%, mainstream ~46% → ~48%; full parity ~321–618 h → ~319–615 h (the selection suite's −3/−5 h hadn't reached the totals) | -| 2026-10-11 | minor | Paint and effects behaviour suite written from the public documentation and its fixes: Normal groups and frames isolate their children and clipping frames don't (canvas, SVG, PDF); plus darker drawn on the canvas and in PNG exports; drop shadows hidden behind what the layer covers unless shown behind transparent areas, SVG included, with filter regions that reach as far as the effects; Fill images cover the box to its edges; "Normal" names Normal in commands and the blend menu. Paint 85%/66% → 87%/70% (breadth from the GIF/TIFF row), 5–9 h → 3–6 h; effects 72% → 75%, 3–6 h → 2–5 h; percentages unchanged at this rounding; full parity ~321–618 h → ~319–616 h | -| 2026-10-11 | minor | Frames, groups, sections and masks behaviour suite written from the public documentation (15 frame, group and section tests, 6 mask tests) and its fixes: a layer a mask hides can't be clicked there (clicks reach what's behind; Select layer agrees), a mask group's box and exports are the mask's (plus any layers under it), sections stay on the page or in sections (moves, drags, pastes, grouping and new sections in frames refused or kept on the page), drawing a frame or section around layers puts those fully inside it in it, resizing a section leaves its contents where they are (and a frame resized with Cmd/Ctrl held keeps its layers' sizes too), and X/Y of layers in groups are measured in the frame (groups have no coordinate space). Frames, groups, sections, masks 95%/75% → 97%/80%, 2–4 h → 1–3 h; percentages unchanged at this rounding; full parity ~323–621 h → ~322–620 h | -| 2026-10-11 | minor | Styles and variables behaviour suite written from the public documentation (18 command-level tests) and its fixes: editing a styled fill, stroke, effect or layout grid list (directly, a paint row, an added fill or a bound variable) unlinks that style instead of being overwritten by the next style edit; applying a style replaces variables bound to what it sets; text styles go on text layers only; deleting a variable (or its collection) unbinds the layers using it, which keep their last value, and aliases to it take its values; bindings are type-checked (a colour variable can't drive a width) and number variables fill text content; aliases are checked when a variable is created. Added: duplicate, move (reorder, into a folder) and rename folder for styles, with a Local styles list grouped by type and folder (click to edit, right-click, drag to reorder); duplicate mode, set as default and move column for modes; per-platform code syntax (Web, iOS, Android) used by generated CSS and the variables JSON; font weight, paragraph spacing/indent and text content scopes for number variables. Found missing: variables inside styles. Styles & variables 92%/72% → 94%/76%, 1–2 h → 3–5 h; percentages unchanged at this rounding; full parity ~323–621 h → ~325–624 h | -| 2026-10-11 | minor | Prototyping behaviour suite written from the public documentation (7 model tests, 12 player tests with 21 gestures) and its fixes: on drag starts only for a drag its animation's way and the animation follows the pointer, key combinations (`Shift+K`) fire (and are recorded and imported), an overlay's after-delay timer starts when it opens, instances of interactive components play their variant's interactions and "Change to" changes the right instance, spring presets are physical springs that play until they settle, smart animate overshoots with spring and back easings, fixed layers draw above scrolling content; added scroll to offsets. Prototyping 88%/70% → 90%/74%, 5–9 h → 3–6 h; mainstream weekly areas 68.1% → 68.4%, mainstream ~46% → ~47%; other percentages unchanged at this rounding; full parity ~320–615 h → ~318–612 h | -| 2026-10-11 | minor | Components behaviour suite written from the public documentation (10 tests, 45 table cases) and its fixes: instances now keep overrides of auto layout padding, gap and alignment, layout grids, export settings, prototype interactions and every text property (resizing, paragraph spacing, lists…; these were lost when the main changed); moving, rotating, resizing or re-constraining layers inside an instance, and its auto layout direction, are refused (the instance itself resizes); switching variants keeps an override only where both variants had the same value (text layers keep theirs); swapping from the Assets panel keeps only text overrides; the default variant is the top-left one (instances, Assets thumbnails, code); hidden exposed instances hide their properties; pushing changes from a nested layer goes to the outer main. Added: variant property reordering and duplicate combination warnings. Components 95%/68% → 96%/72%, 6–12 h → 4–9 h; with #133's selection and transform change, core editor ready ~68% → ~69% and mainstream ~46% → ~47% (weekly areas 68.1% → 68.8%); full parity ~320–615 h → ~315–607 h (the totals now also carry #133's area 2 hours) | -| 2026-10-11 | minor | Embedded image colour profiles (ICC in PNG/JPG/WebP/TIFF, PNG sRGB/gAMA/cHRM) converted into the file's colour space when decoded. Paint 85%/66% → 90%/67% (the row had lagged the parity doc), 5–9 h → 3–6 h; totals unchanged at this rounding | -| 2026-10-11 | minor | Layers panel keyboard and stroke variables: F2 renames the selected layer (also from a layer row with the keyboard, which gets it back; Escape there cancels), Shift+F10 (`view.contextMenu`) opens the context menu at the focused row or below the canvas selection and the menu takes the keyboard, F8 (`select.area`) draws a selection box for keyboard-only users (arrows move it, Shift+arrows size it, Enter selects like a marquee; agents pass a box), long lists checked at 10k layers (revealing a layer, expanding or collapsing everything and arrow keys take at most 1.3 ms a frame in the release UI bench; the revealed row takes the keyboard); colour styles on strokes were already complete, and each side's stroke weight now binds to a number variable (`strokeTopWeight`…, bindable fields in the Design panel and REST import). Layers panel 95%/78% → 98%/82%, 2–4 h → 1–2 h; Strokes 90%/72% → 95%/75%, 1–3 h → 1–2 h; mainstream depth 67.8% → 68.1%; feature breadth (~80%), ready for real work (~49%), essentials (~57%) and mainstream (~46%) unchanged at this rounding; full parity ~321–618 h → ~320–615 h | -| 2026-10-11 | minor | Text behaviour suite written from the public documentation (13 text cases, 5 command tests) and its fixes: CJK lines break between ideographs, kana and Hangul and before and after Latin words without breaking inside them, with the line-start and line-end rules (closing punctuation, small kana and the prolonged sound mark never start a line, opening brackets never end one); a style's line height, baseline and decoration come from its own font, so lines with CJK or emoji drawn by fallback fonts keep the spacing of the others; paragraph indent applies to start-aligned text only; carets, Delete and Backspace step over whole grapheme clusters (emoji sequences, flags, combining marks), which also shape with one font; double-click and word moves take one script's run in text without spaces. Added: vertical trim (cap height to baseline; `leadingTrim`, Type settings, REST import, inspect, CSS `text-box`). Text 85%/64% → 87%/68%, 7–13 h → 5–10 h; percentages unchanged at this rounding; full parity ~323–621 h → ~321–618 h | -| 2026-10-11 | minor | Selection and transform behaviour suite written from the public documentation (5 selection tests with 38 table rows, 11 transform tests) and its fixes: Alt-resizing shrinks about the centre too (it could only grow); groups, frames with Scale or stretch constraints and several layers with different rotations scale each layer along its own axes, keeping its rotation and flip (children turned 90° grew along the wrong axis, nested turned groups too); several layers dragged past an edge mirror and flip; a flipped layer reads back the rotation typed; resizes land on whole pixels with the pixel grid on; a press inside a layer smaller than its handles moves it and the nearest handle wins (the first in reach did), with its handles hidden under 16 px; locked layers selected in the layers panel get no handles and aren't dragged with the rest; the background of a top-level frame with layers starts a marquee inside it (it moved the frame); Select inverse skips hidden layers like Select all. Added: equal spacing snapping with labelled gaps, Cmd/Ctrl while resizing a frame ignores its constraints. Selection & transform 95%/68% → 97%/72%, 6–10 h → 3–5 h; core editor ready ~67% → ~68%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~345–666 h → ~342–661 h | -| 2026-10-11 | minor | Windows QA pass (gap 8): everyday workflows on Windows 11 in the desktop app and through MCP; eight fixes: recovery copies of unsaved documents reopen after a crash or a forced quit (`file.recover`), Ctrl moves and deletes by word in text on Windows and Linux, scrubbed fields keep their value, typing on a field Tab reached edits it, Escape closes dialogs, linear gradients keep square bands, panel hints wrap, the window-close question and "Open in new window" are translated. Percentages and hours unchanged | -| 2026-10-11 | minor | Auto layout behaviour suite written from the public documentation (64 layout cases, 8 command tests) and its fixes: a frame is never smaller than its padding, only the frame's inside stroke counts when strokes are included (and new auto layout includes them), auto gaps stop at 0, grid `fr` tracks behave like the CSS `fr` unit (a track never gets less than its content), canvas stacking is drawn, exported and hit (it was stored only). Added: auto gaps evenly and around, vertical wrap, grid cell alignment, strokes and canvas stacking controls. Auto layout 95%/68% → 98%/74%, 6–12 h → 3–6 h; percentages unchanged at this rounding; full parity ~348–672 h → ~345–666 h | -| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | -| 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | -| 2026-10-11 | minor | Localization layer landed (gap 5): per-language catalogs baked in at build time with English text as keys and fallback, placeholders and plurals, command labels by id, Preferences ▸ Language (System follows the OS or the browser) applied live and `pref.language`; English catalog and a complete Spanish draft awaiting native review. Localization 10% → 20%, 60–100 h → 45–75 h; ready for real work unchanged (~47%); full parity ~363–697 h → ~348–672 h; beta ~200–365 h → ~195–355 h | -| 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | -| 2026-10-11 | minor | Plugin panels and settings landed (gap 6, ABI v2; v1 plugins unchanged): a plugin shows a panel of notes, fields (text, number, checkbox, menu, list, colour) and buttons drawn with FigCraft's widgets; a button or a changed value calls it back with every value (same limits per callback, one undo step per callback that edits, rolled back on failure), its module staying loaded while the panel is open; `plugin.ui` / `plugin.event` / `plugin.close` drive panels headless; per-plugin settings (64 KB) kept with the preferences; the Swatch grid example gained a palette panel that remembers the last palette. Plugins & widgets breadth 35% → 45%, ready 20% → 28%, 25–55 h → 15–40 h; ecosystem 20% → 28%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~373–712 h → ~363–697 h | -| 2026-10-11 | minor | Canvas fixes (gap 13 closed): boolean groups are clicked where their result is (a click in a subtracted hole reaches what's behind; with the group or an operand selected its operands are hit, so each can still be picked), a layer dragged inside a group stays in the group (frames and auto layout still take other drops), View ▸ Frame outlines and View ▸ Slices draw on the canvas, and the idle tests wait for the render thread instead of counting frames. Shapes & vector 1–3 h → 1–2 h (vector QA with real files left); percentages, feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~378–722 h → ~375–717 h | -| 2026-10-11 | minor | Stretch and scatter brushes and pen pressure landed (gap 9): eight brushes drawn in code (four stretched along the path, four scattered with spacing, size, rotation and scatter jitter from a seed) in the stroke settings, drawn everywhere strokes are and flattened by outline stroke; the Pencil records pen pressure as the stroke's width profile (Windows pens, browser styluses; macOS and Linux tablets report none) and can draw with a brush; Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–9 h; Strokes 2–4 h → 1–3 h; Hardware 50% → 55%, 15–30 h → 13–27 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~378–722 h → ~358–687 h | -| 2026-10-11 | minor | Text on a path and pattern fills landed (gap 9): text follows a path or shape (start, side, alignment; edited in place; canvas, PNG, SVG and PDF; SVG `` imports as text on a path) and pattern fills tile a layer (grid or half-shifted rows or columns, scale, spacing, alignment; live as the source changes; SVG ``, PDF tiling patterns); Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–10 h; Text 8–14 h → 7–13 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~375–715 h | -| 2026-10-11 | minor | Team libraries on `figcraft-server` landed (gap 10): a file's library is published to the server under the file and enabled in other files by a share link (per-asset versions; updates noticed in the background and accepted with overrides kept), the server counts which files use which components, and a whole library can be swapped for another by names; Team libraries breadth 40% → 70%, ready 25% → 40%, 10–20 h → 4–8 h; collaboration 12% → 13%, 55–110 h → 49–98 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~380–725 h → ~375–715 h | -| 2026-10-11 | minor | Branches landed (gap 17): File ▸ Create branch… copies the file into a branch tab that remembers its base; Review and merge shows both sides' changes (layers with Inspect-style property changes, styles, variables, comments) and the conflicts (the same property changed on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merging is one undo step and archives the branch; Update from main; branches are saved in the `.figcraft` file. Comments, versions, find, misc breadth 80% → 85%, ready 58% → 60%, 4–8 h → 2–5 h; collaboration 12% → 13%; menu breadth 204/204 (the catalog gained File ▸ Create branch…); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~378–722 h | -| 2026-10-11 | minor | Animated GIF fills and TIFF images (gap 12, without video and HEIC): GIF fills keep the original file, show the first frame on the canvas and in exports and play in the presenter and the preview (frames decoded on first play, at most 500 frames and 128 MB; always looping; the window wakes only for a GIF's next frame); TIFF images open, import, place and paste. Paint breadth 85% → 87%, 5–9 h → 4–7 h; gap 12 6–12 h → 5–10 h (video playback and HEIC left, both needing non-Rust decoders); feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | The last three catalogued menu items got commands (gap 21): File ▸ Open recent (recent files kept in the preferences, at most 20, a file that's gone greyed in the menu and dropped when picked, Clear recent), View ▸ Outlines ▸ Show outlines for selection and View ▸ Mask outlines: menu breadth 98.5% → 100% (203/203); Canvas & navigation breadth 95% → 97%, 3–6 h → 2–5 h; feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | Extended collections, variables import/export and gradient stop and variant property bindings landed (gap 14): Styles & variables breadth 85% → 92%, ready 70% → 72%, 4–8 h → 1–2 h; feature breadth unchanged (~79%); ready for real work unchanged (~47%); full parity ~385–730 h → ~380–725 h | -| 2026-10-11 | minor | Collaboration wording after the standalone server: `figcraft-server` keeps many files, persisted as a snapshot plus a log of what changed (compacted, checksummed, damaged files skipped and reported), restarts lose nothing confirmed and apps resume where they were; the app publishes its file to it; share links per file let people edit or only view (the server refuses a viewer's edits), are revocable and stored as hashes; TLS comes from a reverse proxy (the desktop app has none of its own yet); percentages and hours unchanged until the next re-measure | -| 2026-10-11 | minor | Live boolean editing, Design panel arc fields, the shape builder on strokes and open paths and one-sided variable widths landed (gap 13): Shapes & vector breadth 92% → 97%, ready 66% → 68%, 8–14 h → 1–3 h; feature breadth ~78% → ~79%; ready for real work unchanged (~47%); full parity ~390–745 h → ~385–730 h | -| 2026-10-11 | minor | Collaboration wording after the collaboration polish: styles, variables, collections, modes and comments created in a live session take ids from their creator's id space (concurrent creations no longer overwrite each other; the server checks it), others' text carets and selections show on the canvas, and typing in a layer someone else is typing in says so; percentages unchanged | -| 2026-10-11 | minor | Wording only: live sessions (#104), then the browser joining them and `figcraft://` invite links opening the desktop app; percentages and hours unchanged until the next re-measure | -| 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 | minor | Readiness by audience with hours to ~95% (full ~400–750, mainstream ~180–330, essentials ~85–160) | -| 2026-10-10 | minor | Added essentials user (~53%) and mainstream practitioner (~43%); ready for real work unchanged at ~46% | -| 2026-10-10 | minor | Alpha gate (core workflows) checked under the revised stage rules: passes, stays alpha | -| 2026-10-10 | major | Rewritten to the craftrules progress-docs shape after a full re-measure; rubric, area narrative and inventory moved to docs/target-app-parity.md, gaps to docs/gaps.md, milestones to docs/roadmap.md. Depth ~79% (self-graded, 16 areas) → ready for real work ~46% (all dimensions); full parity 115–195 h → 420–790 h | -| 2026-10-10 | minor | Day-2 refreshes after each batch of PRs (depth 58% → 79%) | -| 2026-10-09 | major | First roadmap | +# FigCraft Roadmap + +**Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents) + +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (Outline stroke preserves stroke holes; Typography spacing icons support scrubbing (#373); font scans skip named pipes; system theme on first run; recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading (#207), SVG import budgets (#190), Leptos code (#223), full screen (#227), server snapshots readable again (#188), owner-only preferences (#200), ruzstd 0.9.1 (#182), layer cycle repair (#205), batch rollback, Direct3D 12 on Windows, plugin metering (#192); vector edit mode regions: hover hatch, selection and drag; SVG/PDF exports refuse over-depth documents (#208); bounded MCP/control framing, connections and queues (#201); plugin log limits (#191); live-session revert guard (#198); attached MCP no longer replays after a lost reply (#194); repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7) + +FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow, +native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through +the CLI, a JSON control channel and MCP. This page is the one-page summary; agents choosing work +start with [`docs/gaps.md`](docs/gaps.md). + +## Headline numbers + +| | Value | Kind | +|---|---|---| +| Menu/command breadth | **100%** (211 / 211) | measured, [`docs/parity-checklist.md`](docs/parity-checklist.md) (`cargo xtask parity`) | +| Feature breadth | **~85%** (core editor ~94%) | estimated, [`docs/target-app-parity.md`](docs/target-app-parity.md) | +| Ready for real work | **~53%** | estimated, same doc (written dimension weights and scores) | +| Essentials user | **~59%** | estimated: core features 77.8% × 0.92 launch/stability × 0.94 discoverability × 0.88 opening files people send ([method](docs/target-app-parity.md#essentials-user)) | +| Mainstream practitioner | **~50%** | estimated: weekly areas 72.7% × 0.9 interaction × 0.9 stability × 0.85 file exchange with Figma users ([method](docs/target-app-parity.md#mainstream-practitioner)) | +| Remaining to beta | **~155–280 h** one agent | estimated | +| Remaining to full parity | **~262–519 h** one agent (~83–145 h wall-clock with 5 agents) | estimated | + +### Readiness by audience + +| Audience | Ready % | Opus 5.5 agent wall-clock hours to ~95% | Work that dominates | +|---|---|---|---| +| Full target (ready for real work) | ~53% | ~268–522 h | multiplayer (42–84), localization (29–51), plugins (8–26), the interaction pass (40–60), GPU canvas (30–60), AI (30–60), the `.fig` import's tail (5–10) | +| Mainstream practitioner | ~50% | ~120–229 h | core editor depth incl. the `.fig` import's tail (49–98), the interaction pass (40–60), real-file corpus and platform QA (25–50), checking the rest of the experimental `.fig` export elsewhere: effects, images, prototypes, real files (5–15) | +| Essentials user | ~59% | ~55–100 h | the `.fig` import's tail so the files people send open well (5–10), core tool polish (25–45), interaction pass on the core tools (15–25), stability QA (10–20) | + +Hours are one agent working sequentially, calibrated from this repo's history (about 1 agent-hour +per 1,000 added lines, 1–2.5 h per feature PR, ~7 h per honest depth point in the polish tail; see +[how the hours are calibrated](docs/target-app-parity.md#how-the-hours-are-calibrated)). Each audience's work is a subset of +the next. About 70–80% parallelizes across 4–6 agents on disjoint crates (roughly ÷4 in +wall-clock); the `.fig` reader, the multiplayer core and a plugin runtime each need one owner, and +native-speaker review, real-file licensing and screen-reader QA need people. + +Hours are Opus 5.5 agent wall-clock hours, calibrated from this repo's history: the app was built in +~25 wall-clock hours by 4–6 parallel agents (~110–150 agent-hours, ~1 agent-hour per 1,000 added +lines, a typical feature PR 1–2.5 h). Details and what needs a human: +[`docs/target-app-parity.md`](docs/target-app-parity.md#how-the-hours-are-calibrated). + +## Stage + +**Alpha.** Core workflows exist end to end (frames, shapes, vectors, text, auto layout, components +with variants and overrides, styles, variables, prototyping, Dev Mode, SVG/PNG/JPG/PDF), but +FigCraft opens `.fig` files only best effort, has only a preview of multiplayer, and nothing has been checked +against real Figma documents. Ready-for-real-work is ~53%, inside alpha's ~40–75% band, and the alpha gate passes: every daily core workflow (design screens, build a design system, prototype, hand off to developers) works end to end on FigCraft's own files, which save and reopen; review works only asynchronously and `.fig` import is unproven on real files, both beta gaps ([alpha gate](docs/roadmap.md#alpha-gate)). Beta needs +~75% **and** reliable `.fig` import: the `.fig` import's tail (5–10 h), a real-file corpus with measured +fidelity (15–30 h), the interaction pass (25–40 h of the 40–60), core depth to ~80% (40–75 h), +basic multiplayer on the landed core (30–55 h), stability and platform QA (20–40 h), a partial GPU path (15–30 h); +native speakers' reviews of the six language drafts need people. + +## By dimension + +| Dimension | Ready | Hours | Doc | +|---|---|---|---| +| Features (depth) | 63% (core editor 72%) | 28–67 | [target-app-parity](docs/target-app-parity.md) | +| UI/UX fidelity | 58% | 40–60 | [ui-parity](docs/ui-parity.md) | +| File formats | 60% (`.fig` read best effort with editable vectors; experimental write, round trips through our own importer and written files opened in other software) | 5–10 | [file-format-parity](docs/file-format-parity.md) | +| Collaboration | 13% (core in; live sessions with document sync, cursors and text carets, cursor chat and spotlight, the browser joins, invite links open the app; a self-hosted server with edit and view links and team libraries; preview) | 46–92 | [collaboration-parity](docs/collaboration-parity.md) | +| Stability | 55% | 20–40 | [gaps](docs/gaps.md) | +| Performance | 60% | 30–60 | [hardware-parity](docs/hardware-parity.md) | +| Hardware | 60% | 10–21 | [hardware-parity](docs/hardware-parity.md) | +| Localization | 39% (layer; English; Spanish with a Latin American overlay, French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese drafts; CJK UI fonts) | 29–51 | [localization-parity](docs/localization-parity.md) | +| Platforms | 75% | 10–20 | [hardware-parity](docs/hardware-parity.md) | +| Ecosystem / plugins | 38% (own WASM plugins with panels, settings and widgets; a self-hosted plugin directory) | 8–26 | [gaps](docs/gaps.md) | +| AI features | 0% | 30–60 | [gaps](docs/gaps.md) | + +## Features + +| Area | Breadth | Ready | Hours | Doc | +|---|---|---|---|---| +| Canvas & navigation | 98% | 74% | 1–4 | [ui-parity](docs/ui-parity.md) | +| Selection & transform | 97% | 72% | 3–5 | [ui-parity](docs/ui-parity.md) | +| Layers panel | 99% | 85% | 1–2 | [ui-parity](docs/ui-parity.md) | +| Shapes & vector | 99% | 73% | 1–3 | [target-app-parity](docs/target-app-parity.md) | +| Paint | 90% | 71% | 2–5 | [target-app-parity](docs/target-app-parity.md) | +| Strokes | 95% | 75% | 1–2 | [target-app-parity](docs/target-app-parity.md) | +| Effects | 100% | 75% | 2–5 | [target-app-parity](docs/target-app-parity.md) | +| Frames, groups, sections, masks | 97% | 80% | 1–3 | [target-app-parity](docs/target-app-parity.md) | +| Auto layout & constraints | 98% | 74% | 3–6 | [auto-layout-parity](docs/auto-layout-parity.md) | +| Text | 90% | 70% | 3–7 | [target-app-parity](docs/target-app-parity.md) | +| Components & variants | 96% | 72% | 4–9 | [components-parity](docs/components-parity.md) | +| Styles & variables | 95% | 76% | 2–4 | [components-parity](docs/components-parity.md) | +| Prototyping | 92% | 74% | 2–5 | [prototyping-parity](docs/prototyping-parity.md) | +| Import/export | 86% | 58% | 5–10 | [file-format-parity](docs/file-format-parity.md) | +| Dev Mode / inspect | 96% | 72% | 0–1 | [dev-mode-parity](docs/dev-mode-parity.md) | +| Comments, versions, misc | 87% | 63% | 1–3 | [collaboration-parity](docs/collaboration-parity.md) | +| Multiplayer & sharing | 17% | 5% | 42–84 | [collaboration-parity](docs/collaboration-parity.md) | +| Plugins & widgets | 60% | 35% | 8–26 | [plugins](docs/plugins.md) | +| Team libraries | 70% | 40% | 4–8 | [components-parity](docs/components-parity.md) | +| Figma Draw extras | 88% | 60% | 1–3 | [gaps](docs/gaps.md) | +| AI features | 0% | 0% | 30–60 | [gaps](docs/gaps.md) | + +## Languages + +| Language | Code | Strings | Status | +|---|---|---|---| +| English | en | 100% | full | +| Simplified Chinese | zh-Hans | 100% | draft (needs native review) | +| Spanish | es, es-419 | 100% (es-419: an overlay of 98 entries) | draft (needs native review) | +| Hindi | hi | 0% | none | +| Arabic | ar | 0% | none | +| French | fr | 100% | draft (needs native review) | +| Portuguese (Brazil) | pt-BR | 100% | draft (needs native review) | +| Indonesian | id | 0% | none | +| Japanese | ja | 100% | draft (needs native review) | +| German | de | 100% | draft (needs native review) | +| Korean | ko | 100% | draft (needs native review) | +| Vietnamese | vi | 0% | none | + +Also shipped: Swedish (`sv`: every UI string, 120 of 588 command labels and menu wordings; native review welcome). Figma ships English, Japanese, Korean, French, German, Spanish and Portuguese +(Brazil); FigCraft has the localization layer, English and Spanish (Spain and Latin America), +French, German, Brazilian Portuguese, Japanese, Korean and Simplified Chinese drafts (Preferences ▸ +Language; Traditional Chinese has no catalog; CJK UI +text uses an installed CJK font, loaded only when needed; adding a language: +[`docs/translating.md`](docs/translating.md)). Document text in any script (bidi, Arabic shaping, +IME) already works. Detail: [`docs/localization-parity.md`](docs/localization-parity.md). + +## Upcoming + +Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). + +1. `.fig` import's tail: fixes from real files, connector labels, table cells, filled areas with their own fills (the best-effort import, editable vector networks, the other layer kinds and variable prototype actions landed 2026-10-11): 5–10 h; then checking the rest of the experimental `.fig` export (landed 2026-10-12; written files open and draw in other software) on effects, images, prototypes and real files: 5–15 h. +2. Real-file corpus and measured fidelity: 15–30 h. +3. Tool-by-tool interaction pass against Figma: 40–60 h. +4. Languages: native reviews of the es, fr, de, pt-BR, ja and ko drafts (people), Latin American Spanish (1–2 h), then Simplified Chinese (3–5 h). +5. Multiplayer (M9): accounts and teams on top of the live sessions and the standalone server (transport, hosting, invite links that open the app, document sync, presence with remote text carets, cursor chat and spotlight, per-user undo, reconnect with offline edits, collision-free ids for styles, variables and comments, the browser client, `figcraft-server` with persistence and share links to edit or view, team libraries on the server landed): 42–84 h. + +## Progress log + +- **2026-10-15** · M4: Outline stroke preserves even-odd stroke holes. Pixel comparisons cover alignment, dashes, individual side weights, vector paths and stroke-only opacity; native save/reopen, Undo/Redo and instance opacity inheritance/overrides are covered by `outline_stroke`. Existing estimates unchanged; filled-shape compositing needs separate work. + +- **2026-10-14** · Corner handles win over the corner-radius handles (M1, #292): a press on the inner half of a corner handle's square picked the radius handle 10 px in, so the drag neither resized nor (dragging outward) rounded anything. Inside the drawn square the corner now resizes both sides (in ratio with Shift or the link); further in the radius handle still answers. Regression test in the transform behaviour suite. Numbers unchanged. +- **2026-10-14** · Home screen recent files as cards (M7): each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged. +- **2026-10-13** · Vector edit mode regions (M4): with the Move tool, the filled area under the pointer is hatched with "/" lines in the selection colour, clipped by the area's own fill rule so holes stay clear; the hatch goes near points (3.5 px) and segments (2 px), and its spacing follows octave folding (8 px at every power-of-two zoom), fixed to the shape. A click selects the area with its points (they move with it but keep the unselected look), Shift adds or removes one, a press on a selected area drags the whole selection as one undo step, and a click on an empty spot clears the areas and stays in edit mode. `vector.edit.select {regions}` and `vector.edit.state` (`regions`, `regionVertices`, `regionHover`); `vector_regions_tests` and `regions_tests`. Numbers unchanged. +- **2026-10-14** · SVG/PDF export depth (M0.9, #208): visible descendants deeper than 256 levels below an exported root are refused before bounds or drawing, including masks and raster fallbacks. SVG has a checked API; the engine and CLI report the limit and suggest selecting an inner layer. Existing destination files survive refusal. Boundary, inner-root, hidden-subtree, multi-root/page and command/CLI regressions cover the change. Numbers unchanged. +- **2026-10-13** · MCP/control transport hardening (#201): 32 MiB incoming frames are bounded while reading; oversized frames return an error and close without draining. The opt-in loopback control listener caps active workers at 8, applies 60 s socket read/write inactivity timeouts and caps both request queues at 8, discarding timed-out requests including those deferred to hidden windows. Reader, queue, idle-client and blocked-writer regressions cover the limits. Numbers unchanged. +- **2026-10-13** · Attached MCP (M0.9, #194): a transport failure no longer replays a request that may already have edited the document. The caller gets an outcome-unknown error and must inspect before retrying; the next explicit call reconnects. Loopback tests cover a lost mutation reply, a single applied edit, reconnect and a connection failure before sending. Numbers unchanged. +- **2026-10-13** · Constrain proportions in the Layout panel (M1, #220): with the W/H link on, typing a width or height scales the other side by the layer's own ratio; the property edit dropped the ratio, so the link only worked while dragging resize handles. Both sides in one edit stay as typed, the `w`/`h` aliases work the same, each layer of a multi-selection keeps its own ratio, and a zero side has no ratio to keep. Regression test in the transform behaviour suite. Numbers unchanged. +- **2026-10-13** · Repeat last command (M1, #181): repeated edits keep the underlying command and parameters, so repeated nudges accumulate with one undo step each. A stored self-reference is refused instead of recursing. Regression tests cover three repeats, their undo steps, and missing or self-referencing commands. Numbers unchanged. +- **2026-10-13** · Component playground (M3): with an instance, a component or a component set selected, the Inspect tab shows a Playground: a preview of the component and a control for each variant, boolean and text property. What is tried there changes the preview and the code in the Code section, never the file (no undo step, the canvas instance stays as it is); Reset goes back to the component's or the instance's own values. `component.playground {id?, properties?, language?, path?}` gives the same to scripts: the properties with their values, the code, and a PNG on request (`playground_tests`). Left: instance-swap and slot properties in the playground (the command takes an instance swap; the panel has no control for it), a code connect template's snippet with the tried values. Numbers unchanged. +- **2026-10-13** · Auto layout, on-canvas handles of automatic gaps (M2): the handle between spread rows or columns (and between items with an auto gap) shows the distance the layout gives the gap instead of the stored value, and dragging it makes the gap a fixed one from there, as typing a gap in the panel does; one undo step (`gap_handle_tests`). Before, the drag changed a value that had no effect. The documentation is silent on dragging an automatic gap. Numbers unchanged. +- **2026-10-13** · Variables inside styles (M3): a shared style can bind variables to fields of its value (`style.bind {style, field, variable?}`: a paint style's colours and gradient stops, an effect style's colour, radius, spread and offsets, a grid style's size, count, gutter and offset, a text style's font size, line height, letter spacing, family and style). Every layer using the style shows those fields resolved in its own modes and stays linked when a variable or a mode changes; the style keeps them resolved in the default modes. A style made from a layer keeps that layer's bindings; a new value drops bindings to what is gone. The style editor binds from its property rows (paint colour, shadow colour, blur radius, grid size) and detaches from the variable's chip; the style picker marks styles that use variables. `.fig` export and import carry the bindings on the style, REST layers hand theirs to the style they use. Left: binding text style fields and the other effect and grid fields from the editor (commands only), library variables in the picker, a style's library copy. Numbers unchanged. +- **2026-10-13** · Effects on groups, inner shadows and background blur (M3): a group is no outline of its own, so its inner shadows are cast inwards from around what its layers draw together (their pixels, over them: no shade at a seam between two layers) and its background blur and glass show the backdrop through the outlines of its layers (under them). Canvas and PNG; SVG embeds a group with an inner shadow as a render and its background blur as an image under the layers; PDF places both as images, the blur matching the PNG. Where the group holds one shape the pixels match that shape, or a frame filled like it, carrying the effect (`group_effects_tests` in render, svg and pdf). Offsets and spreads of a group's inner shadow go by whole pixels. Numbers unchanged. +- **2026-10-13** · Text made by commands on a dark page (M1): in the app, `text.create` and `node.create {type: text}` that name no fill give white text on a dark page, by the same rule as the Text tool (the page's colour decides, also inside a frame); a named `fill`, `fills` or `fillStyle` is kept. Sessions without an interface (CLI, MCP, exports) still make black text everywhere. Numbers unchanged. +- **2026-10-13** · Frames and groups, a one-step convert (M1): `object.convertToGroup` and `object.convertToFrame` (the Design panel's type menu on a frame or a group) change the layer in place, its layers staying where they are on the page. A frame turned into a group loses what only a frame has (auto layout, with the positions it gave kept and fill children fixed; fills, strokes, corner radius, clipping, layout grids, guides, scrolling) and takes its layers' box; a group turned into a frame keeps its box, with no fill and no clipping. Instances, main components, layers inside instances and empty frames are refused with the reason, all or nothing; one undo step. Numbers unchanged. +- **2026-10-13** · Prototyping, delays on mouse triggers and manual overlay offsets (M5): mouse enter, leave, down and up wait `delayMs` before their actions run (once while waiting; dropped when the screen or overlay they were on goes away; a pointer that leaves early doesn't cancel, the documentation is silent), and a manual overlay sits at `offset: [x, y]` from the top-left of the layer that opens it (without one, right below it, as before). Delay and X/Y fields in the interaction editor, the player on its own clock, REST import (`delay`, `overlayRelativePosition`), `.fig` import and export (the export's field for mouse delays is unchecked against other software). Numbers unchanged. +- **2026-10-13** · Auto layout, wrapped-line alignment (M2): a wrapping layout's lines (rows of a horizontal one, columns of a vertical one) are packed where the cross alignment says (start, centre, end; before, they always sat at the start), stretch when every child fills across the flow, or take the free space between them (`contentAlign: spaceBetween`, the Design panel's Row/Column gap mode, the alignment pad lighting the whole cross axis). Read from REST JSON and `.fig`, written to `.fig`, `align-content` in CSS, an instance override with the other alignment settings. 13 hand-computed layout cases and command tests. Numbers unchanged. +- **2026-10-13** · `.fig` export: instance sizes, variables, styles (gap 1): instances write what they change from + their main (size, name, paints) as an override addressed by the main's id, what follows for their layers as + derived data and a scale factor when scaled as a whole; variable collections, modes, variables and bindings, + the modes frames and pages set, shared styles with their links and variable-width strokes are written, and the + import reads all of it back. Checked by opening written files in other software: resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width; + number, string and boolean bindings and bindings on effects, layout guides and gradient stops come across as the values they give but did not show as bound there (their entries now also name the layer field they set, not yet checked there), and strokes wider on one side of the path are written as filled outlines, no longer editable strokes there (the stroke itself is kept as plugin data and our own import puts it back; not yet checked there). Numbers unchanged. + text, effect and grid styles, number, string and boolean bindings, bindings on effects, layout guides and gradient stops, and variable-width strokes are not yet checked there. Numbers unchanged. +- **2026-10-13** · Starter templates and first-run help: eight templates built in code (mobile app, landing page, + dashboard, design system starter, wireframe kit, social post sizes, presentation slides, icon set starter), a + Templates tab on the home screen, `template.list` and `file.newFromTemplate` for scripts and agents, and a Getting + started card that can be closed and comes back from the Help menu +- **2026-10-12** · Your online files: a file shared or published online stays yours after its tab closes or the app + quits. The app remembers it (with its owner token, kept with the preferences and never shown) and Home ▸ **Online** + lists what you own: open a file again as its owner, start or stop sharing, copy its code, rename it, remove it from + the server or forget it here. Servers are asked once, when the list opens; tabs mark files in a live session; a + crash reopens such a file from its server and puts back the changes that hadn't reached it. `collab.files`, `collab.openFile`, `collab.forgetFile`; servers need an + upgrade only to tell which files are still there. The six draft catalogs translate every string again +- **2026-10-12** · Everyday editing partials: number fields (scrub rates with Shift and Alt, arrow keys by the nudge + preferences, arithmetic with brackets, relative edits of Mixed values per layer), outline mode's hidden layers and + object bounds, Hide when publishing in the Assets panel, a cursor for every tool and modifier, panel widths that are + kept, and dark pages (white text on them) for files made in the dark theme +- **2026-10-12** · Sharing with a join code: Share ▸ **Online** (through the server in Preferences, built in) or + **Local network**, one Start sharing button and a short code to read out (`K7M-2QX-9TD`; a longer one locally that + carries the host's address); others type it under Join with a code (the popover, the home screen), where a link + works too. The server names links with codes, keeps them hashed and limits wrong ones; `collab.share`, + `collab.join {code}`; servers must be upgraded to hand codes out (older ones keep working with links) +- **2026-10-12** · Text metrics measured: our example documents, re-saved by other software, were read back and + compared layer by layer. Line heights are now whole pixels, advances sit on a 1024-per-em grid and letter spacing + goes between glyphs only; all 230 self-sizing text boxes in the 7 documents match to the pixel (24 of 131 before + in the design-system example). One font family; real files still unchecked (gap 3). No number moves +- **2026-10-12** · Live sessions are per document: every open file has its own session or none, several run at + once (host one, join another, publish a third) and keep syncing behind the tab in front, the Share popover, + people, cursors, chat, spotlight and follow are the file in front's, `collab.peers` lists them all (`sessions`), + and closing a file ends its session only (before, one session followed every tab and starting another replaced it) +- **2026-10-12** · A public `figcraft-server`: `open_publish = true` lets anyone publish without the server key + (Share ▸ Publish to server asks the server first and shows who hosts it, its terms and how long unopened files are + kept), with an owner token shown once that manages the file later (`collab.manageFile`); limits per address, + idle expiry, reports (Share ▸ Report this file…), `block-ip` and `files list --sort`, `GET /healthz`, and + `packaging/server/install.sh` that sets up a Debian/Ubuntu machine with Caddy, ufw and backups + ([server.md](docs/server.md#running-a-public-server)). Numbers unchanged (hosting, not a Figma feature). +- **2026-10-12** · `.fig` export opens elsewhere (gap 1): written files were checked by opening them in other + software: layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft. The + export now says that the document and its pages are visible and opaque, numbers its schema as the format does, + and writes outlines, text settings, standard font style names and line height units; effects such as glass and + noise, images and prototype interactions are still unchecked there, and variables are not written. Numbers + unchanged. +- **2026-10-12** · Experimental `.fig` export (gap 1): `file.export {format: "fig"}`, File ▸ Export… ▸ .fig + (experimental), `figcraft-cli export --format fig` and MCP's `export` write the whole document: layers, + geometry, paints with their images, strokes, effects, styled text, vector networks, auto layout, components and + instances with their overrides, and prototype interactions; what's left out (style links, variables, comments, + Dev Mode data…) comes back as warnings. Checked only by reading the files back with our own importer. File + formats 60%. +- **2026-10-12** · Keyboard shortcut parity (gap 4): a table test with a row for each of the 129 + shortcuts in the public documentation, macOS and Windows/Linux keys each, and the 48 bindings it + is silent on pinned; 126 bound (110 before), Run last plugin moved off a documented conflict on + Windows, 2 missing. New: others' cursors toggle, list levels on Cmd/Ctrl+] and [ while editing, + keyboard box selection on Alt+Space / Ctrl+Space, arrows that pan with nothing selected, Control + suspending snapping on macOS, Space/N/C/F while presenting, the Frame tool on A, pixel grid on + Shift+', link on Shift+Cmd+U, ungroup on Cmd/Ctrl+Backspace, suggest auto layout and go to main + component keys; shifted symbols on other keyboard layouts. +- **2026-10-12** · Plugin directory on `figcraft-server` (gap 6): publish a plugin folder with the server key + (`figcraft-cli plugin publish`), browse a server's plugins in Plugins ▸ Manage plugins ▸ Browse, install after a + confirmation that shows the permissions it asks for, update when a newer version appears, remove; bundles are + checked in full on the server and again in the app, downloads must match their SHA-256, installed plugins live + per user in the same sandbox; `plugin.directory.*` for scripts and MCP; the operator moderates with + `figcraft-server plugins`. Plugins & widgets 60%. +- **2026-10-12** · Plugin widgets and the panel tail (gap 6, plugin API 3; older plugins run unchanged): a widget + plugin puts a layer of its own on the canvas (Plugins ▸ Widgets, `widget.insert`), described by the plugin, + validated and built with locked layers inside; clicking a part it named calls it back with the widget's state + (kept in the layer, so it saves, copies, undoes, exports and syncs) as one undo step, and it redraws; + `widget.click` and `widget.list` for agents; a Counter example. Panels of several plugins open side by side and + plugin colour fields open the colour picker. Plugins & widgets 55%. +- **2026-10-12** · Layers and Assets panels behaviour suite (gap 4): several layers dropped together land + together, Collapse layers keeps the selection's rows open, dragging across eyes or padlocks sets several layers + at once, a Rename layers dialog for several layers (number codes as documented, regular expressions, a start + value), pages dragged to reorder, dividers, "Copy of" duplicates, Assets folders by page, frame and slash names, + and hidden-from-publishing names that decide both ways. +- **2026-10-12** · Vector multi-edit and the vector edit tail (gap 9): several vector layers in edit + mode at once (select, marquee, drag, nudge, delete, mirroring and radius across them), several + Bezier handles dragged together, rounded points beside curved segments, a stroke cap and join per + point, and the Pen connecting to its own points or continuing a selected open path. +- **2026-10-11** · Canvas navigation, guides and grids behaviour suite (gap 4): zoom, panning, the pixel + grid (now from 400%), rulers, guides (top-level frames only; select, Delete, nudge, Remove guide, + Alt-drag copies, Alt measures while dragging), layout grids (Auto counts; hidden on rotated frames), + outline mode (clicks reach layers behind) and measuring, each rule from the documentation. +- **2026-10-11** · Export and inspect behaviour suite (gap 4): exports cover what a layer paints (shadows + only where they fall, true stroke widths, no room for hidden layers, a mask group's mask), slices export what's under + them, layer names with slashes export into folders; export settings gained contents only, include bounding box and + the SVG options (outline text, ids, simplified strokes); Inspect shows colours in hex, RGB, CSS, HSL or HSB, CSS in + rem, blend modes and positions from the frame, and lists icons and images as assets. Dev Mode 70%. +- **2026-10-11** · `.fig` import's tail (gap 1): vectors open as editable vector networks with their + corner radii, handle mirroring and end caps (outlines when the network is missing or damaged); sticky notes, shapes + with text, connectors, tables and similar layers import as frames, text and vectors instead of being dropped; + set-variable and conditional prototype actions come across. File formats 58%, ready ~49%. +- **2026-10-11** · Paint and effects behaviour suite: every documented rule for fills, blend modes, + gradients, images, colour models, shadows, blurs and effect exports is a table-driven case. Fixes: Normal + groups isolate and clipping frames don't, plus darker is drawn, drop shadows hide behind translucent + layers in every export, Fill images reach the box's edges, "Normal" no longer means pass-through. + Paint 70%, effects 75%. +- **2026-10-11** · `.fig` import, best effort (gap 1): `file.open`, `file.import`, drag and drop, the CLI and + MCP open `.fig` files. A generic kiwi decoder reads each file's own schema, and the decoded nodes map by field name + onto the REST importer: layers, paints and styles, text runs, vectors as outlines, auto layout, components and + instances (expanded with their overrides and property values), variables, prototyping and images from the archive; + the rest is a warning. File formats 55%, ready ~49%. +- **2026-10-11** · Gamut warnings (gap 18): the colour picker of a Display P3 file flags colours + outside sRGB and View ▸ Soft-proof sRGB stripes them on the canvas. Showing P3 itself waits on + eframe/egui-wgpu exposing the surface colour space wgpu 30 already supports. +- **2026-10-11** · Several windows (gap 18): File ▸ New window, Open in new window on a tab or a + tab dragged off the strip; each window has its own tabs, panels and canvas, draws only when it + needs to and asks about unsaved changes when it closes (the main window too); the control channel + and MCP open, list, focus and capture windows. Hardware 60%. +- **2026-10-11** · Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls, + certificates always verified; `FIGCRAFT_CA_FILE` for a private authority), releases ship the server for Linux + x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` walks through deploying it on + a rented Linux server (systemd unit, Caddy or nginx, firewall, Docker, upgrades, backups). +- **2026-10-11** · Plugin panels and settings (gap 6, ABI v2; v1 plugins run unchanged): a plugin shows a panel of + fields and buttons drawn with FigCraft's widgets and is called back for each button or changed value (one undo step + per callback, same sandbox limits), `plugin.ui` / `plugin.event` / `plugin.close` drive panels from scripts and MCP, + and per-plugin settings are kept with the preferences; Swatch grid gained a palette panel. Plugins & widgets 45%. +- **2026-10-11** · Canvas fixes: clicks hit boolean groups where their result is (a click in a + subtracted hole reaches what's behind), a layer dragged inside a group stays in it, View ▸ Frame + outlines and View ▸ Slices draw, and a flaky idle test now waits for the render thread (gap 13). +- **2026-10-11** · Team libraries on `figcraft-server` (gap 10): Assets ▸ Libraries ▸ Publish to server keeps + the file's library on the server under the file (per-asset versions; the server's version is followed); other files + enable it by pasting a share link (a view link is enough), get "Library updates available" from a background check + and accept with overrides kept; the server counts which files use which components (Show usage, `library.usage`); + `library.swap` re-points instances, styles and variables to another library by names. Team libraries 70%. +- **2026-10-11** · Animated GIF fills (first frame on the canvas and in exports, played in the + presenter and the preview, waking the window only for the next frame) and TIFF images (gap 12). +- **2026-10-11** · The last three menu items got commands: File ▸ Open recent (kept in the + preferences, missing files greyed, Clear recent), outlines for the selection and mask outlines + (gap 21). Menu breadth 203/203. +- **2026-10-11** · Extended variable collections (a library collection's modes and variables in a + collection of the file, overridden per mode with reset, following library updates), variables + import/export (the REST local variables JSON, merged by name; CSS custom properties), gradient + stops and variant properties bound to variables (gap 14). +- **2026-10-11** · Live boolean editing: operands stay editable layers (double-click or Cmd/Ctrl-click + selects one, moving, resizing or editing its points redraws the result during the drag with every + operand outlined, per-operand operations from the boolean controls, prefix-cached recompute); + Design panel arc fields; the shape builder on strokes and open paths; one-sided variable widths + (gap 13). Feature breadth ~79%. +- **2026-10-10** · WASM plugins (#101: sandboxed wasmi runtime, manifest permissions, Plugins menu, + Run last, Manage, Rust SDK and two examples), docs refresh (#102), the multiplayer core without + networking (#103: client-scoped ids, change sets, server ordering and validation, client + reconciliation, convergence tests). Menu breadth 200/203; ready for real work ~47%. +- **2026-10-10** · Full re-measure against Figma desktop 126.10.7 under the craftrules progress-docs + standard: ready for real work ~46%, feature breadth ~76%, stage alpha; progress docs split out + (`docs/target-app-parity.md`, `gaps.md`, `roadmap.md`, `architecture.md`, the per-area parity + docs); `docs/parity.md` renamed `docs/parity-checklist.md`. +- **2026-10-10** · Day 2 (PRs #1–#99): vector edit mode depth (transform box, point snapping, arc + handles, shape builder, variable width), text depth (variable fonts, bidi, RTL names, style + browser), auto layout nesting and keyboard reordering, slots and library variables, components + at scale (2,000 instances), effects fidelity (glass, noise, texture, progressive blur), + prototyping (conditionals, expressions, device frames, responsive presentation), REST import of + prototypes and variables, SVG import fidelity, PDF print options, Dev Mode (code, annotations, + ready for dev, compare changes), comments polish, version history, keyboard shortcut parity, + accessibility, retained canvas and 50k-layer performance, no redraws while idle; v0.2.0 + released for every platform; multiplayer protocol designed. +- **2026-10-09** · Day 1: foundation (13 crates, layering, command registry, renderer, engine, + UI3 frontend, control channel), CLI and MCP, web build, PDF export, packaging, first depth + passes on text, components, prototyping and comments. + +## Revision history + +| Date | Change | Summary | +|---|---|---| +| 2026-10-15 | minor | M4: Outline stroke preserves even-odd stroke holes. Pixel comparisons cover alignment, dashes, individual side weights, vector paths and stroke-only opacity; native save/reopen, Undo/Redo and instance opacity inheritance/overrides are covered by `outline_stroke`. Existing estimates unchanged; filled-shape compositing needs separate work. | +| 2026-10-15 | minor | M7: system font discovery skips entries observed as non-regular files before opening them, avoiding the reproduced named-pipe stall. Regular-file symlinks remain supported; directory depth, font limits and priorities are unchanged. Numbers unchanged. | +| 2026-10-15 | minor | Typography spacing icons scrub with the shared number-field gesture, modifier rates and one Undo step per drag (#373). Units and selected-character targeting are preserved. Numbers unchanged. | +| 2026-10-14 | minor | System theme on first run (M7): preferences without a theme default to System; the first frame reads the OS appearance directly, including when returning from a manual theme. Saved Light/Dark choices stay explicit; unavailable OS appearance falls back to Light. Numbers unchanged. | +| 2026-10-14 | minor | Home screen recent files as cards (M7): each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged. | +| 2026-10-14 | minor | Lost images reported on loading (#207): a `.figcraft` file whose image member is damaged, too large or missing no longer loads in silence; each lost image (up to 20, then a count) is listed in `repairs` on `file.open` with its hash and the reason, and the app shows it as a toast. Saving still writes the file without the lost images. Numbers unchanged | +| 2026-10-14 | minor | SVG import budgets (#190): gzipped SVG is unpacked by FigCraft within 64 MiB before parsing (it was unpacked without a limit), so the nesting check sees the real document; SVG documents embedded in `` data URIs share one 32 MiB budget and at most 4 levels, and one past a budget is left out. Linked files stay unread. Numbers unchanged | +| 2026-10-14 | minor | Leptos code (#223): `inspect.code`, `inspect.componentCode` and `codeConnect.set` take `language: leptos`: a `#[component]` with `#[prop]` defaults, variant enums and `view!` markup, instances as calls, Code Connect `use` lines; the Dev Mode language list and rem units include it. Numbers unchanged | +| 2026-10-14 | minor | Full screen (#227): View ▸ Full screen (`view.fullscreen {on?}`, F11 on Windows and Linux, Ctrl+Cmd+F on macOS) makes the window fill the screen and leaves it again; FigCraft's own item. Numbers unchanged | +| 2026-10-14 | minor | Colour picker: wrapped rows of page and recent swatches stay inset on both sides instead of running past the picker's right edge. Numbers unchanged | +| 2026-10-14 | minor | Server snapshots (#188): figcraft-server never replaces a readable snapshot with one too large to be read back, and reads snapshots up to twice the file cap (recent batches kept for reconnecting clients may take a file over it). Numbers unchanged | +| 2026-10-14 | minor | Preferences owner-only on Unix (#200): `state.json`, which holds online owner tokens, is written with mode 0600 from the first byte, and an older world-readable file is narrowed. Numbers unchanged | +| 2026-10-14 | minor | ruzstd 0.9.1 (#182): `.fig` import requires the release that fixes RUSTSEC-2026-0344 (a zstd decompression bomb). Numbers unchanged | +| 2026-10-14 | minor | Layer cycles (#205): `validate` reports children listed twice, pages with a parent and layers not reachable from a page (cycles, detached subtrees); `repair` drops them in one linear pass. Numbers unchanged | +| 2026-10-14 | minor | AppImage CLI: `figcraft.AppImage cli ` runs the bundled `figcraft-cli` (MCP server included) on Linux without a second download. Numbers unchanged | +| 2026-10-14 | minor | Boolean outline depth (#193): nested boolean groups share one outline depth budget instead of each level getting its own. Numbers unchanged | +| 2026-10-14 | minor | Command batches roll back as one: a failed `command.batch` (MCP `batch`) restores every open document and its state, and puts the tab in front back. Numbers unchanged | +| 2026-10-14 | minor | Desktop builds no longer rebuild the text crate on every build when the craft-fonts manifest is missing. Numbers unchanged | +| 2026-10-14 | minor | Direct3D 12 on Windows: the desktop app asks wgpu for Direct3D 12 there instead of Vulkan, so it starts without Vulkan loader errors on machines with stray overlay layers. Numbers unchanged | +| 2026-10-14 | minor | Plugin metering (#192): every host call and every byte copied between a plugin and the host costs fuel, so a plugin can't stall the app through cheap-looking host calls. Numbers unchanged | +| 2026-10-14 | minor | SVG/PDF export depth (#208): explicit preflight refusal beyond 256 descendant levels, propagated through public APIs, commands and CLI, with boundary/content and destination-preservation regressions. Numbers unchanged. | +| 2026-10-13 | minor | MCP/control transport hardening (#201): frame limits during reads, bounded connections and both UI request queues, socket inactivity timeouts; percentages and estimates unchanged | +| 2026-10-13 | minor | Plugin log limits (#191): cap each UTF-8 line at 8 KiB, total log bytes at 1 MiB and lines at 1,000 per run/callback before copying guest memory or forwarding to the host. Over-budget runs fail and roll back document edits; callbacks get fresh budgets. Added sandbox boundary/forwarding tests and an engine rollback regression. Percentages and hours unchanged. | +| 2026-10-13 | minor | Live-session revert guard (M9, #198): `file.revert` is refused while its document belongs to a live session, including publishing, reconnecting and hosting before the welcome. The document, previews, branch tabs and session stay intact; leave the session before reverting a local save. Offline revert is unchanged. Scripted access and sync regressions in `live_access_tests`. Numbers unchanged. | +| 2026-10-13 | minor | Repeat last command (M1, #181): keep the underlying command and parameters across repeats, reject self-reference, and retain one undo step per repeated edit. Numbers unchanged. | +| 2026-10-13 | minor | Component playground (M3): with an instance, a component or a component set selected, the Inspect tab shows a Playground: a preview of the component and a control for each variant, boolean and text property. What is tried there changes the preview and the code in the Code section, never the file (no undo step, the canvas instance stays as it is); Reset goes back to the component's or the instance's own values. `component.playground {id?, properties?, language?, path?}` gives the same to scripts: the properties with their values, the code, and a PNG on request (`playground_tests`). Left: instance-swap and slot properties in the playground (the command takes an instance swap; the panel has no control for it), a code connect template's snippet with the tried values. Numbers unchanged. | +| 2026-10-13 | minor | Auto layout, on-canvas handles of automatic gaps (M2): the handle between spread rows or columns (and between items with an auto gap) shows the distance the layout gives the gap instead of the stored value, and dragging it makes the gap a fixed one from there, as typing a gap in the panel does; one undo step (`gap_handle_tests`). Before, the drag changed a value that had no effect. The documentation is silent on dragging an automatic gap. Numbers unchanged. | +| 2026-10-13 | minor | Variables inside styles (M3): a shared style can bind variables to fields of its value (`style.bind {style, field, variable?}`: a paint style's colours and gradient stops, an effect style's colour, radius, spread and offsets, a grid style's size, count, gutter and offset, a text style's font size, line height, letter spacing, family and style). Every layer using the style shows those fields resolved in its own modes and stays linked when a variable or a mode changes; the style keeps them resolved in the default modes. A style made from a layer keeps that layer's bindings; a new value drops bindings to what is gone. The style editor binds from its property rows (paint colour, shadow colour, blur radius, grid size) and detaches from the variable's chip; the style picker marks styles that use variables. `.fig` export and import carry the bindings on the style, REST layers hand theirs to the style they use. Left: binding text style fields and the other effect and grid fields from the editor (commands only), library variables in the picker, a style's library copy. Numbers unchanged. | +| 2026-10-13 | minor | Effects on groups, inner shadows and background blur (M3): a group is no outline of its own, so its inner shadows are cast inwards from around what its layers draw together (their pixels, over them: no shade at a seam between two layers) and its background blur and glass show the backdrop through the outlines of its layers (under them). Canvas and PNG; SVG embeds a group with an inner shadow as a render and its background blur as an image under the layers; PDF places both as images, the blur matching the PNG. Where the group holds one shape the pixels match that shape, or a frame filled like it, carrying the effect (`group_effects_tests` in render, svg and pdf). Offsets and spreads of a group's inner shadow go by whole pixels. Numbers unchanged. | +| 2026-10-13 | minor | Text made by commands on a dark page (M1): in the app, `text.create` and `node.create {type: text}` that name no fill give white text on a dark page, by the same rule as the Text tool (the page's colour decides, also inside a frame); a named `fill`, `fills` or `fillStyle` is kept. Sessions without an interface (CLI, MCP, exports) still make black text everywhere. Numbers unchanged. | +| 2026-10-13 | minor | Frames and groups, a one-step convert (M1): `object.convertToGroup` and `object.convertToFrame` (the Design panel's type menu on a frame or a group) change the layer in place, its layers staying where they are on the page. A frame turned into a group loses what only a frame has (auto layout, with the positions it gave kept and fill children fixed; fills, strokes, corner radius, clipping, layout grids, guides, scrolling) and takes its layers' box; a group turned into a frame keeps its box, with no fill and no clipping. Instances, main components, layers inside instances and empty frames are refused with the reason, all or nothing; one undo step. Numbers unchanged. | +| 2026-10-13 | minor | Prototyping, delays on mouse triggers and manual overlay offsets (M5): mouse enter, leave, down and up wait `delayMs` before their actions run (once while waiting; dropped when the screen or overlay they were on goes away; a pointer that leaves early doesn't cancel, the documentation is silent), and a manual overlay sits at `offset: [x, y]` from the top-left of the layer that opens it (without one, right below it, as before). Delay and X/Y fields in the interaction editor, the player on its own clock, REST import (`delay`, `overlayRelativePosition`), `.fig` import and export (the export's field for mouse delays is unchecked against other software). Numbers unchanged. | +| 2026-10-13 | minor | Auto layout, wrapped-line alignment (M2): a wrapping layout's lines (rows of a horizontal one, columns of a vertical one) are packed where the cross alignment says (start, centre, end; before, they always sat at the start), stretch when every child fills across the flow, or take the free space between them (`contentAlign: spaceBetween`, the Design panel's Row/Column gap mode, the alignment pad lighting the whole cross axis). Read from REST JSON and `.fig`, written to `.fig`, `align-content` in CSS, an instance override with the other alignment settings. 13 hand-computed layout cases and command tests. Numbers unchanged. | +| 2026-10-13 | minor | `.fig` export (gap 1): instance sizes (resized, filling an auto layout, scaled), variables with modes and bindings, shared styles and variable-width strokes are written and read back. Checked by opening written files in other software: resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width; number, string and boolean bindings and bindings on effects, layout guides and gradient stops come across as the values they give but did not show as bound there (their entries now also name the layer field they set, not yet checked there), and strokes wider on one side of the path are written as filled outlines, no longer editable strokes there (the stroke itself is kept as plugin data and our own import puts it back; not yet checked there). Numbers unchanged | +| 2026-10-13 | minor | Starter templates and a first run that explains itself: eight templates built in code through the command layer (mobile app, landing page, dashboard, design system starter, wireframe kit, social post sizes, presentation slides, icon set starter; original and synthetic, deterministic, about 10–190 ms each to build), `template.list` and `file.newFromTemplate` for the app, CLI and MCP, File ▸ New from template…, a Templates tab on the home screen (cards with thumbnails rendered from the documents, search, kind filter, keyboard and screen-reader names) in place of the Community placeholder, a row of templates on Recents while there are no recent files, a Getting started card that can be closed and comes back from Help ▸ Getting started (`help.gettingStarted`), and a first run that opens on the home screen. The Drafts placeholder is gone (the app has no drafts folder). Essentials ~57% → ~59% (discoverability ×0.92 → ×0.94), its hours ~60–110 h → ~55–100 h; other numbers unchanged | +| 2026-10-12 | minor | Your online files (M9): a file put on a server without its key stays its owner's after its tab closes or the app quits. The app records each one with the preferences (server, id, name, owner token, the owner's links, the codes others join with; at most 200; the token never in a command result, `ui.inspect`, a log or debug output) and Home ▸ **Online** lists them: open one as its owner (a new tab, or its tab comes to the front), Start / Stop sharing, Copy code / Copy link, Rename on the server, Remove from server (confirmed), Forget (the file stays on the server). Nothing is asked of a server until the section opens; then one request per server (`Admin owned`, a compatible addition within protocol version 2) says which files are still there, and a server that is offline or older leaves the list as recorded. Tabs of files in a live session carry a mark (shared, or open only to you), toasts about a file that isn't in front name it, and after a crash a recorded file reopens from its server as its owner, its recovery copy kept until its unsent changes are back on the server (or it is opened beside the file). `collab.files`, `collab.openFile`, `collab.forgetFile`; `collab.manageFile` takes its server and token from the record and gains `share` / `unshare`. The draft catalogs (French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese) translate the 71 strings and 5 command labels they lacked and the new ones. Numbers unchanged | +| 2026-10-12 | minor | Everyday editing partials closed (gap 4): number fields scrub a step a point (×10 with Shift, ×0.1 with Alt, on past the screen edge where the platform reports it), step with the arrow keys by the nudge preferences, take brackets, units and relative edits (`+10`, `*2`, `50%`), commit on Tab and nothing when left untouched, and on Mixed apply relative edits to each layer in one undo step; outline mode's Include hidden layers and Include object bounds (`view.outlinesHiddenLayers`, `view.outlinesObjectBounds`), drawn and clickable; Hide when publishing from the Assets panel (`component.hideWhenPublishing`); a cursor for every tool, modifier and hover target (rotate turned to its corner, pen states, eyedropper, comment, copy, not-allowed), with guides and handles keeping theirs; panel widths with limits, double-click reset and the preferences (`view.panelWidths`); a file or page made in the dark theme has a #1E1E1E page (sessions without an interface keep #F5F5F5) and text drawn on a dark page is white. Menu breadth 211/211. UI/UX fidelity 58% and the interaction pass 40–60 h unchanged at this rounding: the side-by-side comparison is what remains | +| 2026-10-12 | minor | Sharing with a join code (M9, from the owner's use: sharing wasn't intuitive): the Share popover is one flow, **Online** (the file is published to the server in Preferences, built in `wss://collab.craftapps.dev/`, and the server names its edit and view links with nine-symbol codes like `K7M-2QX-9TD`) or **Local network** (the app hosts; a 20-symbol code carries its IPv4 address, port and a 44-bit secret, with check bits), one Start sharing button, the code large with Copy code and Copy link, who is here, can edit / can view online, and Stop sharing (online: the others' links and codes are revoked and they are disconnected, while the file stays on the server with its owner in it and can be shared again with new codes; removing it is a separate, confirmed step); **Join with a code** is one field (popover, home screen, Join dialog) for a code of either kind or a link; addresses, keys and ports moved under Advanced and Preferences ▸ Server for online sharing (reset, or empty to turn online sharing off). Protocol additions within version 2 (`Resolve` / `Resolved`, `Admin code`): the server keeps a stretched hash and the wrapped token, never the code, and limits wrong codes (10 a minute per address, a 15-minute lockout after 10 wrong, 1,000 wrong an hour for everyone); codes go with their link and file. Older servers leave the links in place of a code; older apps join by link. `collab.share`, `collab.join {code}`, `code` and `share` in `collab.peers`; the browser joins by code. Numbers unchanged | +| 2026-10-12 | minor | Live sessions per document (M9, a fix from real use): the engine keeps one session per open document instead of one for the app, the link carries several under their own ids (desktop, browser, tests), the window and the `collab.*` commands follow the active document, `collab.peers` gains `session` and `sessions`, `ui.inspect`'s `live` gains `session` and `sessions`; joining opens the session's file in its own tab without touching other sessions and closing a file ends its session only. Numbers unchanged | +| 2026-10-12 | minor | `.fig` export checked by opening written files in other software (gap 1): layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft, after fixes to what the export writes (visible document and pages, the format's schema numbering, outlines, text settings, font style names, line height units); effects such as glass and noise, images and prototype interactions still unchecked there. Numbers unchanged | +| 2026-10-12 | minor | Public `figcraft-server` hardened against resource exhaustion after a security review: messages capped by what a connection is for (a file's size when open), byte-bounded send queues and a bound on bytes waiting for the hub, files checked as they grow (refused batches roll back with the reason; per-address and total caps count growth), the core's recent-batch memory bounded in bytes, no confirmation for changes that couldn't be written, team libraries only in key-published files on public servers with unpacking bounded by the file cap, usage reports kept in memory, layered IPv6 limits (/64, /56, /48), a global publish rate, connections reserved for sessions, per-address download shares, library connections cut on revoke and removal, a private data directory and key, and safer install.sh, systemd and backups. Percentages and hours unchanged | +| 2026-10-12 | minor | A public `figcraft-server` (open hosting): with `open_publish = true` anyone publishes a file without the server key and gets its edit and view links plus an owner token (128 bits, shown once, stored hashed) that lists, makes and revokes its links, renames it or removes it (`collab.manageFile`); limits per address (connections at once and a minute, messages a second, publishes an hour, files and MiB kept; IPv6 by /64; `X-Forwarded-For` believed only from `trusted_proxy`; tables capped at 10,000 addresses) and for everyone (`max_total_mb`), each refusal saying why; files published without the key expire after `idle_days` unopened (the date shown to everyone in the file); moderation (`files list --sort size|age|idle`, `block-ip`, `reports`, a Report button and `collab.report`); the operator's contact and terms shown in the Share dialog; `GET /healthz` on the same port; `packaging/server/install.sh` for a Debian/Ubuntu machine behind Caddy and a nightly backup timer. Protocol additions within version 2, gated like signals (older apps get clear refusals). Hosting infrastructure rather than a Figma feature: percentages and hours unchanged | +| 2026-10-12 | minor | Experimental `.fig` export (gap 1): `file.export {format: "fig"}`, File ▸ Export… ▸ .fig (experimental), the CLI and MCP write the whole document (layer tree, geometry, paints with image bytes, strokes, effects, text with styled runs, vector networks, auto layout, constraints, components, sets, instances with their overrides and property values, prototype interactions) with a kiwi encoder mirroring the decoder and the schema of the fields written; what's left out is a warning; verified only by round trips through our own importer. File formats 59% → 60%; import/export 84%/57% → 86%/58%; file exchange stays ×0.85; mainstream hours ~130–244 h → ~120–229 h; feature breadth (~84%), ready for real work (~53%), essentials (~57%), mainstream (~49%), beta (~155–280 h) and full parity (~265–523 h) unchanged at this rounding | +| 2026-10-12 | minor | Keyboard shortcut parity suite and bindings (gap 4): a table test with one row per shortcut in the public documentation, macOS and Windows/Linux keys each (129 rows: 126 bound, 110 before; 1 moved off a documented conflict; 2 missing; 2 kept by a platform), plus 48 pinned undocumented bindings so every default binding is in the table; newly bound: A (Frame), Shift+' (pixel grid), Alt+Cmd+\ (others' cursors, new `view.multiplayerCursors`), Ctrl+F6, Shift+Cmd+U (link), Cmd/Ctrl+] and [ (list levels while editing, new `text.listIndent` / `text.listOutdent`), Cmd/Ctrl+Backspace (ungroup), Alt+Space / Ctrl+Space (keyboard box selection, Cmd/Ctrl+arrows draw it out), suggest auto layout, go to main component, arrows that pan with nothing selected (new `view.pan`), Control suspending snapping on macOS, Space/N/C/F while presenting; symbols another layout types with Shift count without it; no default binding uses a system-reserved chord (tested). Menu breadth 207/207 (View ▸ Multiplayer cursors). UI/UX fidelity 58% and the interaction pass 40–60 h unchanged at this rounding | +| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Localization 36% → 39%, 31–54 h → 29–51 h; ready for real work unchanged (~53%); full parity ~267–527 h → ~265–524 h | +| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Localization 35% → 36%, 32–55 h → 31–54 h; ready for real work unchanged (~53%); full parity ~268–528 h → ~267–527 h | +| 2026-10-12 | minor | Prototyping tail (gap 15, the part that doesn't need video): key/gamepad triggers take controller buttons (`Gamepad A` … `Gamepad Home`, the standard layout's 17 buttons, from a Controller menu beside the key field; the REST import maps controller triggers instead of skipping them); the player reads controllers on Windows (Windows.Gaming.Input through the `windows` crate the app already used; no new dependency) and in the browser (the Gamepad API), polling only while the prototype has such a trigger and the window has the focus, and the control channel presses buttons (`ui.input {gamepad}`); macOS, Linux and FreeBSD read no controllers yet. Widgets react to clicks in a presented prototype (the canvas's callback and sandbox limits, on the prototype's copy: the file and its history don't change, restarting starts over). `proto.evaluate` computes an expression where a layer's interaction would run (MCP); the mode lookup and set-variable-mode rules are cases in a behaviour suite (they already held). Prototyping breadth 90% → 92%, ready unchanged at 74%, 3–6 h → 2–5 h; area hours 120–260 → 119–259 (core 33–75 → 32–74), Features (depth) 29–68 h → 28–67 h; percentages unchanged at this rounding; full parity ~276–538 h → ~275–537 h | +| 2026-10-12 | minor | Plugin directory on `figcraft-server` (gap 6): the server keeps plugin bundles (manifest, module, up to 32 assets; an uncompressed length-prefixed format named by its SHA-256) published with the server key (`figcraft-cli plugin publish`, `plugin.directory.publish`), checked in full before they are kept (size cap, manifest and plugin API version, a version newer than the one kept, the module compiled but never run, plain asset names) and capped (`max_plugins`, `max_plugin_mb`, `max_plugins_mb`); anyone who reaches the server lists, searches and fetches them (hash-checked again on read, a shared download budget); the operator moderates with `figcraft-server plugins [remove]`. The app's Plugins ▸ Manage plugins gains a Browse tab: a server's plugins with Install / Update / Remove, an install confirmation that shows the permissions asked for, the source and the SHA-256, the download checked against the listed hash before anything is read, plugins kept per user in hash-named folders; `plugin.directory.list` / `install` (needs `confirm: true`) / `publish` / `unpublish` for scripts and MCP. Not in the browser build yet; no public catalogue, publisher accounts, signatures, ratings or background update checks. Plugins & widgets 55%/32% → 60%/35%, 10–30 h → 8–26 h; Ecosystem dimension 34% → 38%; weighted totals unchanged at this rounding; full parity ~281–548 h → ~279–544 h | +| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Localization 30% → 35%, 40–65 h → 32–55 h; ready for real work ~52% → ~53% (52.9 → 53.1); full parity ~284–554 h → ~276–544 h; beta ~162–292 h → ~155–280 h | +| 2026-10-12 | minor | Plugin widgets and the panel tail (gap 6, plugin API 3; v1 and v2 plugins unchanged): a plugin marked as a widget puts a layer of its own on the canvas from Plugins ▸ Widgets or `widget.insert`: it describes the layer (frames with auto layout, text, rectangles, ellipses, images it carries) and FigCraft validates the description (size, layer count, depth, numbers, colours, text and image sizes) and builds it as a frame with locked layers inside; a click on a part the plugin named (on the canvas with the Move tool, a pointing hand over it, or `widget.click`) calls the plugin back in a fresh, metered sandbox with the widget's state, as one undo step, and the widget is redrawn; the state lives in the widget's own layer data, so widgets survive save and reload, copying, undo, export and live sessions; no plugin can write that data or call `widget.*`. `widget.list` lists them. A Counter example widget. Plugin panels of different plugins open side by side, and a colour field's swatch opens the colour picker (the plugin hears of the colour when a drag ends). Plugins & widgets 45%/28% → 55%/32%, 15–40 h → 10–30 h; Ecosystem dimension 28% → 34%; weighted totals unchanged at this rounding; full parity ~286–558 h → ~281–548 h | +| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): every UI string, command label and menu wording (1,899 entries) machine-drafted in each and marked as drafts awaiting native review, falling back to English per key (new strings need only English and Spanish); 0 counts as singular in French and Brazilian Portuguese; System picks the first OS language with a catalog (pt-PT and pt read pt-BR); long translations elide with a tooltip in dropdowns, two-column labels and the home cards. Localization 20% → 30%, 45–75 h → 40–65 h; ready for real work unchanged (~52%); full parity ~289–564 h → ~284–554 h; beta ~170–305 h → ~162–292 h | +| 2026-10-12 | minor | Cursor chat and spotlight in live sessions: `/` opens a chat bubble at your pointer that everyone sees as you type and that fades a few seconds after the last change; a spotlight (your avatar's menu, `collab.spotlight`) makes everyone follow your view and page until they move their own. Ephemeral presence the host validates and rate-limits, passed only to clients that read it (older clients keep working). Multiplayer & sharing breadth 15% → 17%, ready unchanged (5%), 45–90 h → 42–84 h; collaboration 13% unchanged, 49–98 h → 46–92 h; feature breadth (~84%) and ready for real work (~51%) unchanged at this rounding; full parity ~301–587 h → ~298–581 h | +| 2026-10-12 | minor | Layers and Assets panels behaviour suite written from the public documentation (6 engine tests with about 50 table rows and 5 panel tests driven through the UI; about 30 rows that the code before the fixes contradicts) and its fixes (several layers dropped together, Collapse layers, eye and padlock sweeps, Rename layers dialog with the documented number codes, page dragging, dividers, "Copy of" duplicates, Assets folders, hidden-from-publishing names both ways). Layers panel 98%/82% → 99%/85%; percentages and full parity hours unchanged at this rounding | +| 2026-10-12 | minor | Vector multi-edit and the vector edit tail, with a behaviour suite written from the public documentation (5 tests, about 30 table rows and steps, and a junk-parameter sweep of every vector command in multi-edit mode): several vector layers are edited at once (Enter, double-click or `vector.edit.begin {ids}`; a press on another layer's point, handle or segment makes it the active layer; Shift-click, marquee and lasso select across layers; dragging, the arrows, Delete, delete and heal, mirroring, smoothing, corner radius and point caps and joins reach every layer's selected points in one undo step; the pen, bend, paint bucket, cut and eraser work in the active layer); several Bezier handles are selected with Shift-click and dragged together (`vector.edit.moveHandles`); a sharp point next to a curved segment rounds (the curve is cut where the arc meets it and keeps its shape); a point can have its own stroke cap and join (`vector.edit.strokeCap`, `vector.edit.strokeJoin`, the Point section of the Design panel; canvas, PNG, SVG and PDF, outline stroke); the Pen outside edit mode connects to one of its own points and ends the path, and continues a selected open path from an end point. Left: corner smoothing beyond rectangles, the transform box around points of several layers, per-point caps and joins on dashed, variable-width and brush strokes. Shapes & vector 98%/72% → 99%/73%, 2–4 h → 1–3 h; Figma Draw extras 80%/55% → 88%/60%, 2–5 h → 1–3 h (gap 9: pen pressure on macOS and Linux tablets left). Totals recomputed by the doc's method, which rows since the last recompute had left stale: the area table's weighted breadth is 83.7% (~83%; core 94.1%, ~94%) and ready 63.1% (~63%; core 72.1%, ~72%), hours 133–284 (core 38–83); Features (depth) 61% → 63%, 46–94 h → 34–76 h; ready for real work 52.6% (~52%); mainstream weekly areas 71.0% → 72.1% (× 0.9 × 0.9 × 0.85 = 49.7%, ~49%, rounded down); essentials unchanged (~57%); full parity ~306–595 h → ~294–572 h (on top of the rows merged since: ~301–587 h → ~289–564 h; area-table totals recomputed on the merged table and rounded down: breadth ~84%, ready ~63%, core ~94%/~72%, mainstream weekly areas 72.4%, ~49%) | +| 2026-10-12 | minor | Dev resources (gap 16): links for developers (a URL and a name) on any layer: `devResource.add` / `edit` / `remove` / `list`, undoable, saved and synced; an instance also lists its main component's (and that component's set's) links read-only with where they come from, and layers inside an instance show their main layer's; URLs are checked as web links (http or https, a host, no spaces, at most 2,048 characters) and never fetched (a link added without a name is named after its URL), opened only when clicked; the Inspect tab lists, adds, edits and removes them; `get_node` and `inspect_document` return them as `devResources`. The editor extension stays out of scope (the MCP server serves agents and editors). Dev Mode breadth 93% → 96%, ready 70% → 72%, 1–3 h → 0–1 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~304–593 h → ~303–591 h | +| 2026-10-12 | minor | Extended collections' tail (gap 14): variables a library adds to a collection this file extends now count as a library update of their own (listed by name without a copy yet) and arrive inheriting when accepted, instead of waiting for the library's next other update; a library collection that is itself an extended collection can be extended again, values flowing down the chain as each file accepts updates with every level's overrides kept (checked end to end). Styles & variables breadth 94% → 95%, ready unchanged (76%), 3–5 h → 2–4 h; gap 14 1–2 h → 0–1 h (real-library checks left); feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~305–594 h → ~304–593 h | +| 2026-10-12 | minor | A branch's own version history and finer list merges (gap 17): versions saved or autosaved on a branch tab go into the branch (viewed, restored, renamed, deleted and duplicated there, saved in the file with it); merging adds a version to the main file's history and the merged or archived branch keeps its own; fills, strokes and effects merge item by item, conflicting only when the same item changed both ways, an item was removed on one side or both sides added different items. Comments, versions, find, misc breadth 85% → 87%, ready 60% → 61%, 2–5 h → 1–4 h; gap 17 1–3 h → 1–2 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~306–595 h → ~305–594 h (the Features table here still showed 80%/58%/4–8 h from before branches landed) | +| 2026-10-12 | minor | Headline percentages recomputed from the area table by its own method (weights × written scores): feature breadth ~82% → ~84% (core editor ~93% → ~94%), weighted ready ~61% → ~63%, core ready ~70% → ~72%, mainstream weekly areas 71.3% → 72.3% and mainstream ~49% → ~50%; several earlier rows had moved the area scores without carrying them into the totals. Hours are unchanged (left for the next full re-measure) | +| 2026-10-12 | minor | Text lists, hanging punctuation and decoration styles, with a behaviour suite written from the public documentation (7 layout tests and 7 command tests, about 60 table rows): lists are set per paragraph (the list buttons, shortcuts and Type settings act on the paragraphs the selection touches, the whole layer when it isn't being edited), nest up to 8 levels (Tab and Shift+Tab; numbers go 1. a. i., bullets • ◦ ▪, counted per level), Enter on an empty item and Backspace at an item's start step out a level and then out of the list, typing `- `, `* ` or `1. ` at a paragraph's start starts one; list spacing (between items, in place of paragraph spacing), hanging lists (markers outside the box) and hanging punctuation (opening punctuation outside a start-aligned edge, closing punctuation outside an end-aligned one, taking no room when lines wrap); underline and strikethrough style (solid, dotted, wavy), thickness, underline offset, skip ink and colour. Canvas, PNG, SVG and PDF; `node.set` keys (`indentation`, `listSpacing`, `hangingPunctuation`, `hangingList`, `textDecorationStyle`/`Thickness`/`Offset`/`SkipInk`/`Color`), inspect (`lineTypes`, `lineIndentations`), CSS (`text-decoration-style`/`-thickness`/`-color`, `text-underline-offset`, `text-decoration-skip-ink`) and the REST import (`lineTypes` with `lineIndentations` per paragraph, which collapsed to one list style before). Text 87%/68% → 90%/70%, 5–10 h → 3–7 h; mainstream weekly areas 71.0% → 71.2%; other percentages unchanged at this rounding; full parity ~306–595 h → ~304–592 h | +| 2026-10-12 | minor | Comments and version history behaviour suite written from the public documentation (15 tests, about 80 table rows plus stepwise checks; 4 tests failed before the fixes) and its fixes: in a live session, threads made in the session couldn't be replied to, edited, reacted to, resolved, moved, deleted or opened (their ids, from their creator's id space, were refused as out of range); restoring a version brought back that version's comments, losing newer threads and replies (comments now stay, those on layers the version lacks staying where they were); restoring now keeps the replaced design in the history as an autosave first, unless a version of it was just saved or restored; giving an autosave a title makes it a named version (it could be dropped to make room); merging a branch adds the main file as it was and then a `Merged “”` version to its history. Pinned where the documentation is silent: pins go to the innermost frame under them, copies, duplicates and duplicated pages come without comments, replying reopens a resolved thread, versions stay with whoever saved them in a live session (a shared history needs accounts). Left: a branch's own version history, comparing two versions. Comments, versions, misc 85%/60% → 85%/62%, 2–5 h → 2–4 h (this table still showed 80%/58%/4–8 h from before branches landed); percentages unchanged at this rounding; full parity ~306–595 h → ~306–594 h | +| 2026-10-11 | minor | Canvas navigation, rulers, guides, layout grids, outline mode and measuring behaviour suite written from the public documentation (18 tests, about 120 table rows and checks) and its fixes: the pixel grid shows from 400% (it waited for 800%); guides belong to the page or a top-level frame (a guide dropped over a nested frame goes to the frame holding it, and a frame moved into another keeps its guides but they stop showing, snapping and measuring); a guide can be selected, deleted with Delete, nudged with the arrows and removed from the context menu (`guide.select`, `guide.remove` without an index); Alt-dragging a guide drops a copy and holding Alt while dragging one measures to the nearest edges in the frame it crosses; columns and rows count Auto (as many fixed-size ones as fit); layout grids don't show or snap on a rotated frame; in outline mode shapes are clicked on their outlines, reaching layers behind. Left: outline mode's Include hidden layers and Include object bounds. Canvas & navigation 97%/72% → 98%/74%, 2–5 h → 1–4 h (this table still showed 95%/72%/3–6 h from before the outlines landed); percentages unchanged at this rounding; full parity ~320–615 h → ~319–614 h | +| 2026-10-11 | minor | Export and inspect behaviour suite written from the public documentation (139 cases in 19 tests; 47 failed before the fixes) and its fixes: an export covers what the layer paints, so a drop shadow adds room only where it falls, strokes on boxes and ellipses reach their real width (they added up to four times as much), hidden layers take no room and a mask group exports the mask's area; a slice exports everything under it; slashes in layer names export into folders; Copy as PNG copies the whole selection. Added: the export settings' contents only and include bounding box, and for SVG outline text (off writes ``), unique id attributes and simplified strokes (stroke attributes where exact); Inspect colour formats (hex, RGB, CSS, HSL, HSB), CSS in rem, a Blend row, positions measured from the frame around a layer (groups aren't frames), CSS for rotation, blend modes, gradient directions and kinds, text alignment, case and decoration; icons and images in the asset list (`file.exportLayers {assets}`). Import/export 80%/50% → 82%/53% (the row still showed the figures from before `.fig` import); Dev Mode 90%/64% → 93%/70%; mainstream weekly areas 68.1% → 68.4%; percentages and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | `.fig` import's tail (gap 1): vectors open as editable vector networks (points, curved segments, filled areas with their winding, per-point corner radius and handle mirroring, end caps from the end points), falling back to outlines when the network is missing or damaged; sticky notes, shapes with text, connectors, tables, code blocks, slides and similar layers import as the closest frames, text layers and vectors with a warning instead of being dropped; set-variable, set-variable-mode and conditional prototype actions come across. New parsers fuzzed on fixtures generated in code. File formats 55% → 58%, 8–15 h → 5–10 h; import/export breadth 80% → 82%, ready 50% → 54%; feature and area totals and ready for real work (~49%) unchanged; beta ~173–310 h → ~170–305 h; full parity ~321–618 h → ~318–613 h | +| 2026-10-11 | minor | Shapes and vector behaviour suite written from the public documentation and its fixes: rounded vector points are drawn and exported, a boolean group looks like its top layer (a subtraction like the bottom one), Alt draws lines from the centre, stars keep to 3–60 points, delete and heal follows the curve, Shift-click on a point at a corner of the points' box deselects it, outline stroke keeps the stroke's colour style, opacity and effects, the miter field shows degrees, individual corners only for rectangles and frames, Option/Alt+Shift+F flattens, MCP `key` reaches the edit tools. Added: corner radius and star ratio handles, Count and Ratio fields, cut drags that divide shapes into layers, paint bucket drags. Shapes & vector 97%/68% → 98%/72%, 1–3 h → 2–4 h. Totals recomputed from the area table, which earlier rows had left stale: feature breadth ~80% → ~82% (core ~91% → ~93%), Features (depth) 57% → 61% (core 68% → 69%), 60–115 h → 56–109 h, ready for real work ~49% → ~51%, mainstream ~46% → ~48%; full parity ~321–618 h → ~319–615 h (the selection suite's −3/−5 h hadn't reached the totals) | +| 2026-10-11 | minor | Paint and effects behaviour suite written from the public documentation and its fixes: Normal groups and frames isolate their children and clipping frames don't (canvas, SVG, PDF); plus darker drawn on the canvas and in PNG exports; drop shadows hidden behind what the layer covers unless shown behind transparent areas, SVG included, with filter regions that reach as far as the effects; Fill images cover the box to its edges; "Normal" names Normal in commands and the blend menu. Paint 85%/66% → 87%/70% (breadth from the GIF/TIFF row), 5–9 h → 3–6 h; effects 72% → 75%, 3–6 h → 2–5 h; percentages unchanged at this rounding; full parity ~321–618 h → ~319–616 h | +| 2026-10-11 | minor | Frames, groups, sections and masks behaviour suite written from the public documentation (15 frame, group and section tests, 6 mask tests) and its fixes: a layer a mask hides can't be clicked there (clicks reach what's behind; Select layer agrees), a mask group's box and exports are the mask's (plus any layers under it), sections stay on the page or in sections (moves, drags, pastes, grouping and new sections in frames refused or kept on the page), drawing a frame or section around layers puts those fully inside it in it, resizing a section leaves its contents where they are (and a frame resized with Cmd/Ctrl held keeps its layers' sizes too), and X/Y of layers in groups are measured in the frame (groups have no coordinate space). Frames, groups, sections, masks 95%/75% → 97%/80%, 2–4 h → 1–3 h; percentages unchanged at this rounding; full parity ~323–621 h → ~322–620 h | +| 2026-10-11 | minor | Styles and variables behaviour suite written from the public documentation (18 command-level tests) and its fixes: editing a styled fill, stroke, effect or layout grid list (directly, a paint row, an added fill or a bound variable) unlinks that style instead of being overwritten by the next style edit; applying a style replaces variables bound to what it sets; text styles go on text layers only; deleting a variable (or its collection) unbinds the layers using it, which keep their last value, and aliases to it take its values; bindings are type-checked (a colour variable can't drive a width) and number variables fill text content; aliases are checked when a variable is created. Added: duplicate, move (reorder, into a folder) and rename folder for styles, with a Local styles list grouped by type and folder (click to edit, right-click, drag to reorder); duplicate mode, set as default and move column for modes; per-platform code syntax (Web, iOS, Android) used by generated CSS and the variables JSON; font weight, paragraph spacing/indent and text content scopes for number variables. Found missing: variables inside styles. Styles & variables 92%/72% → 94%/76%, 1–2 h → 3–5 h; percentages unchanged at this rounding; full parity ~323–621 h → ~325–624 h | +| 2026-10-11 | minor | Prototyping behaviour suite written from the public documentation (7 model tests, 12 player tests with 21 gestures) and its fixes: on drag starts only for a drag its animation's way and the animation follows the pointer, key combinations (`Shift+K`) fire (and are recorded and imported), an overlay's after-delay timer starts when it opens, instances of interactive components play their variant's interactions and "Change to" changes the right instance, spring presets are physical springs that play until they settle, smart animate overshoots with spring and back easings, fixed layers draw above scrolling content; added scroll to offsets. Prototyping 88%/70% → 90%/74%, 5–9 h → 3–6 h; mainstream weekly areas 68.1% → 68.4%, mainstream ~46% → ~47%; other percentages unchanged at this rounding; full parity ~320–615 h → ~318–612 h | +| 2026-10-11 | minor | Components behaviour suite written from the public documentation (10 tests, 45 table cases) and its fixes: instances now keep overrides of auto layout padding, gap and alignment, layout grids, export settings, prototype interactions and every text property (resizing, paragraph spacing, lists…; these were lost when the main changed); moving, rotating, resizing or re-constraining layers inside an instance, and its auto layout direction, are refused (the instance itself resizes); switching variants keeps an override only where both variants had the same value (text layers keep theirs); swapping from the Assets panel keeps only text overrides; the default variant is the top-left one (instances, Assets thumbnails, code); hidden exposed instances hide their properties; pushing changes from a nested layer goes to the outer main. Added: variant property reordering and duplicate combination warnings. Components 95%/68% → 96%/72%, 6–12 h → 4–9 h; with #133's selection and transform change, core editor ready ~68% → ~69% and mainstream ~46% → ~47% (weekly areas 68.1% → 68.8%); full parity ~320–615 h → ~315–607 h (the totals now also carry #133's area 2 hours) | +| 2026-10-11 | minor | Embedded image colour profiles (ICC in PNG/JPG/WebP/TIFF, PNG sRGB/gAMA/cHRM) converted into the file's colour space when decoded. Paint 85%/66% → 90%/67% (the row had lagged the parity doc), 5–9 h → 3–6 h; totals unchanged at this rounding | +| 2026-10-11 | minor | Layers panel keyboard and stroke variables: F2 renames the selected layer (also from a layer row with the keyboard, which gets it back; Escape there cancels), Shift+F10 (`view.contextMenu`) opens the context menu at the focused row or below the canvas selection and the menu takes the keyboard, F8 (`select.area`) draws a selection box for keyboard-only users (arrows move it, Shift+arrows size it, Enter selects like a marquee; agents pass a box), long lists checked at 10k layers (revealing a layer, expanding or collapsing everything and arrow keys take at most 1.3 ms a frame in the release UI bench; the revealed row takes the keyboard); colour styles on strokes were already complete, and each side's stroke weight now binds to a number variable (`strokeTopWeight`…, bindable fields in the Design panel and REST import). Layers panel 95%/78% → 98%/82%, 2–4 h → 1–2 h; Strokes 90%/72% → 95%/75%, 1–3 h → 1–2 h; mainstream depth 67.8% → 68.1%; feature breadth (~80%), ready for real work (~49%), essentials (~57%) and mainstream (~46%) unchanged at this rounding; full parity ~321–618 h → ~320–615 h | +| 2026-10-11 | minor | Text behaviour suite written from the public documentation (13 text cases, 5 command tests) and its fixes: CJK lines break between ideographs, kana and Hangul and before and after Latin words without breaking inside them, with the line-start and line-end rules (closing punctuation, small kana and the prolonged sound mark never start a line, opening brackets never end one); a style's line height, baseline and decoration come from its own font, so lines with CJK or emoji drawn by fallback fonts keep the spacing of the others; paragraph indent applies to start-aligned text only; carets, Delete and Backspace step over whole grapheme clusters (emoji sequences, flags, combining marks), which also shape with one font; double-click and word moves take one script's run in text without spaces. Added: vertical trim (cap height to baseline; `leadingTrim`, Type settings, REST import, inspect, CSS `text-box`). Text 85%/64% → 87%/68%, 7–13 h → 5–10 h; percentages unchanged at this rounding; full parity ~323–621 h → ~321–618 h | +| 2026-10-11 | minor | Selection and transform behaviour suite written from the public documentation (5 selection tests with 38 table rows, 11 transform tests) and its fixes: Alt-resizing shrinks about the centre too (it could only grow); groups, frames with Scale or stretch constraints and several layers with different rotations scale each layer along its own axes, keeping its rotation and flip (children turned 90° grew along the wrong axis, nested turned groups too); several layers dragged past an edge mirror and flip; a flipped layer reads back the rotation typed; resizes land on whole pixels with the pixel grid on; a press inside a layer smaller than its handles moves it and the nearest handle wins (the first in reach did), with its handles hidden under 16 px; locked layers selected in the layers panel get no handles and aren't dragged with the rest; the background of a top-level frame with layers starts a marquee inside it (it moved the frame); Select inverse skips hidden layers like Select all. Added: equal spacing snapping with labelled gaps, Cmd/Ctrl while resizing a frame ignores its constraints. Selection & transform 95%/68% → 97%/72%, 6–10 h → 3–5 h; core editor ready ~67% → ~68%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~345–666 h → ~342–661 h | +| 2026-10-11 | minor | Windows QA pass (gap 8): everyday workflows on Windows 11 in the desktop app and through MCP; eight fixes: recovery copies of unsaved documents reopen after a crash or a forced quit (`file.recover`), Ctrl moves and deletes by word in text on Windows and Linux, scrubbed fields keep their value, typing on a field Tab reached edits it, Escape closes dialogs, linear gradients keep square bands, panel hints wrap, the window-close question and "Open in new window" are translated. Percentages and hours unchanged | +| 2026-10-11 | minor | Auto layout behaviour suite written from the public documentation (64 layout cases, 8 command tests) and its fixes: a frame is never smaller than its padding, only the frame's inside stroke counts when strokes are included (and new auto layout includes them), auto gaps stop at 0, grid `fr` tracks behave like the CSS `fr` unit (a track never gets less than its content), canvas stacking is drawn, exported and hit (it was stored only). Added: auto gaps evenly and around, vertical wrap, grid cell alignment, strokes and canvas stacking controls. Auto layout 95%/68% → 98%/74%, 6–12 h → 3–6 h; percentages unchanged at this rounding; full parity ~348–672 h → ~345–666 h | +| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | +| 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | +| 2026-10-11 | minor | Localization layer landed (gap 5): per-language catalogs baked in at build time with English text as keys and fallback, placeholders and plurals, command labels by id, Preferences ▸ Language (System follows the OS or the browser) applied live and `pref.language`; English catalog and a complete Spanish draft awaiting native review. Localization 10% → 20%, 60–100 h → 45–75 h; ready for real work unchanged (~47%); full parity ~363–697 h → ~348–672 h; beta ~200–365 h → ~195–355 h | +| 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | +| 2026-10-11 | minor | Plugin panels and settings landed (gap 6, ABI v2; v1 plugins unchanged): a plugin shows a panel of notes, fields (text, number, checkbox, menu, list, colour) and buttons drawn with FigCraft's widgets; a button or a changed value calls it back with every value (same limits per callback, one undo step per callback that edits, rolled back on failure), its module staying loaded while the panel is open; `plugin.ui` / `plugin.event` / `plugin.close` drive panels headless; per-plugin settings (64 KB) kept with the preferences; the Swatch grid example gained a palette panel that remembers the last palette. Plugins & widgets breadth 35% → 45%, ready 20% → 28%, 25–55 h → 15–40 h; ecosystem 20% → 28%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~373–712 h → ~363–697 h | +| 2026-10-11 | minor | Canvas fixes (gap 13 closed): boolean groups are clicked where their result is (a click in a subtracted hole reaches what's behind; with the group or an operand selected its operands are hit, so each can still be picked), a layer dragged inside a group stays in the group (frames and auto layout still take other drops), View ▸ Frame outlines and View ▸ Slices draw on the canvas, and the idle tests wait for the render thread instead of counting frames. Shapes & vector 1–3 h → 1–2 h (vector QA with real files left); percentages, feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~378–722 h → ~375–717 h | +| 2026-10-11 | minor | Stretch and scatter brushes and pen pressure landed (gap 9): eight brushes drawn in code (four stretched along the path, four scattered with spacing, size, rotation and scatter jitter from a seed) in the stroke settings, drawn everywhere strokes are and flattened by outline stroke; the Pencil records pen pressure as the stroke's width profile (Windows pens, browser styluses; macOS and Linux tablets report none) and can draw with a brush; Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–9 h; Strokes 2–4 h → 1–3 h; Hardware 50% → 55%, 15–30 h → 13–27 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~378–722 h → ~358–687 h | +| 2026-10-11 | minor | Text on a path and pattern fills landed (gap 9): text follows a path or shape (start, side, alignment; edited in place; canvas, PNG, SVG and PDF; SVG `` imports as text on a path) and pattern fills tile a layer (grid or half-shifted rows or columns, scale, spacing, alignment; live as the source changes; SVG ``, PDF tiling patterns); Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–10 h; Text 8–14 h → 7–13 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~375–715 h | +| 2026-10-11 | minor | Team libraries on `figcraft-server` landed (gap 10): a file's library is published to the server under the file and enabled in other files by a share link (per-asset versions; updates noticed in the background and accepted with overrides kept), the server counts which files use which components, and a whole library can be swapped for another by names; Team libraries breadth 40% → 70%, ready 25% → 40%, 10–20 h → 4–8 h; collaboration 12% → 13%, 55–110 h → 49–98 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~380–725 h → ~375–715 h | +| 2026-10-11 | minor | Branches landed (gap 17): File ▸ Create branch… copies the file into a branch tab that remembers its base; Review and merge shows both sides' changes (layers with Inspect-style property changes, styles, variables, comments) and the conflicts (the same property changed on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merging is one undo step and archives the branch; Update from main; branches are saved in the `.figcraft` file. Comments, versions, find, misc breadth 80% → 85%, ready 58% → 60%, 4–8 h → 2–5 h; collaboration 12% → 13%; menu breadth 204/204 (the catalog gained File ▸ Create branch…); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~378–722 h | +| 2026-10-11 | minor | Animated GIF fills and TIFF images (gap 12, without video and HEIC): GIF fills keep the original file, show the first frame on the canvas and in exports and play in the presenter and the preview (frames decoded on first play, at most 500 frames and 128 MB; always looping; the window wakes only for a GIF's next frame); TIFF images open, import, place and paste. Paint breadth 85% → 87%, 5–9 h → 4–7 h; gap 12 6–12 h → 5–10 h (video playback and HEIC left, both needing non-Rust decoders); feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | The last three catalogued menu items got commands (gap 21): File ▸ Open recent (recent files kept in the preferences, at most 20, a file that's gone greyed in the menu and dropped when picked, Clear recent), View ▸ Outlines ▸ Show outlines for selection and View ▸ Mask outlines: menu breadth 98.5% → 100% (203/203); Canvas & navigation breadth 95% → 97%, 3–6 h → 2–5 h; feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | Extended collections, variables import/export and gradient stop and variant property bindings landed (gap 14): Styles & variables breadth 85% → 92%, ready 70% → 72%, 4–8 h → 1–2 h; feature breadth unchanged (~79%); ready for real work unchanged (~47%); full parity ~385–730 h → ~380–725 h | +| 2026-10-11 | minor | Collaboration wording after the standalone server: `figcraft-server` keeps many files, persisted as a snapshot plus a log of what changed (compacted, checksummed, damaged files skipped and reported), restarts lose nothing confirmed and apps resume where they were; the app publishes its file to it; share links per file let people edit or only view (the server refuses a viewer's edits), are revocable and stored as hashes; TLS comes from a reverse proxy (the desktop app has none of its own yet); percentages and hours unchanged until the next re-measure | +| 2026-10-11 | minor | Live boolean editing, Design panel arc fields, the shape builder on strokes and open paths and one-sided variable widths landed (gap 13): Shapes & vector breadth 92% → 97%, ready 66% → 68%, 8–14 h → 1–3 h; feature breadth ~78% → ~79%; ready for real work unchanged (~47%); full parity ~390–745 h → ~385–730 h | +| 2026-10-11 | minor | Collaboration wording after the collaboration polish: styles, variables, collections, modes and comments created in a live session take ids from their creator's id space (concurrent creations no longer overwrite each other; the server checks it), others' text carets and selections show on the canvas, and typing in a layer someone else is typing in says so; percentages unchanged | +| 2026-10-11 | minor | Wording only: live sessions (#104), then the browser joining them and `figcraft://` invite links opening the desktop app; percentages and hours unchanged until the next re-measure | +| 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 | minor | Readiness by audience with hours to ~95% (full ~400–750, mainstream ~180–330, essentials ~85–160) | +| 2026-10-10 | minor | Added essentials user (~53%) and mainstream practitioner (~43%); ready for real work unchanged at ~46% | +| 2026-10-10 | minor | Alpha gate (core workflows) checked under the revised stage rules: passes, stays alpha | +| 2026-10-10 | major | Rewritten to the craftrules progress-docs shape after a full re-measure; rubric, area narrative and inventory moved to docs/target-app-parity.md, gaps to docs/gaps.md, milestones to docs/roadmap.md. Depth ~79% (self-graded, 16 areas) → ready for real work ~46% (all dimensions); full parity 115–195 h → 420–790 h | +| 2026-10-10 | minor | Day-2 refreshes after each batch of PRs (depth 58% → 79%) | +| 2026-10-09 | major | First roadmap | diff --git a/crates/engine/src/cmd/structure.rs b/crates/engine/src/cmd/structure.rs index bed70d3d..fd766735 100644 --- a/crates/engine/src/cmd/structure.rs +++ b/crates/engine/src/cmd/structure.rs @@ -1,650 +1,653 @@ -//! Structure: groups, frames, masks, boolean groups, flatten, outline stroke, auto layout, pages. - -use figcraft_color::{Color, Paint}; -use figcraft_doc::*; -use figcraft_geom::{Affine, BezPath, Rect, Shape}; -use serde_json::{Value, json}; - -use super::edit::{roots, z_sorted}; -use super::{CommandSpec, ok}; -use crate::params::*; -use crate::{EngineError, Result, Session, bad, spec}; - -pub fn specs() -> Vec { - vec![ - spec!("object.group", "Group selection", "{ids?, name?}", true, |s, p| wrap(s, p, Wrap::Group)), - spec!("object.ungroup", "Ungroup selection", "{ids?}", true, ungroup), - spec!("object.frameSelection", "Frame selection", "{ids?, name?}", true, |s, p| wrap(s, p, Wrap::Frame)), - spec!("object.removeFrame", "Remove frame", "{ids?}", true, ungroup), - spec!( - "object.convertToGroup", - "Convert to group", - "{ids?} (frames become groups in place, their layers staying where they are; what only a frame has goes: auto layout, fills, strokes, corner radius, clipping, layout grids, guides and scrolling. Groups are left alone; instances, main components, layers inside instances and empty frames are refused)", - true, - |s, p| convert(s, p, "object.convertToGroup", false) - ), - spec!( - "object.convertToFrame", - "Convert to frame", - "{ids?} (groups become frames of the same box in place, their layers staying where they are: no fill, not clipping. Frames are left alone; instances, main components and layers inside instances are refused)", - true, - |s, p| convert(s, p, "object.convertToFrame", true) - ), - spec!( - "object.wrapInSection", - "Wrap in new section", - "{ids?, name?} (layers on the page or in a section; the section wraps them with some room)", - true, - |s, p| wrap(s, p, Wrap::Section) - ), - spec!( - "object.resizeToFit", - "Resize to fit", - "{ids?} (frames, sections and components shrink or grow to their contents; the contents stay put)", - true, - resize_to_fit - ), - spec!("object.mask", "Use as mask", "{ids?}", true, mask), - spec!("object.releaseMask", "Release mask", "{ids?}", true, |s, p| { - for i in req_targets("object.releaseMask", s, p)? { - s.doc_mut()?.get_mut(i)?.is_mask = false; - } - ok() - }), - spec!("bool.union", "Union selection", BOOL_PARAMS, true, |s, p| crate::booleans::apply(s, "bool.union", p, BoolOp::Union)), - spec!("bool.subtract", "Subtract selection", BOOL_PARAMS, true, |s, p| crate::booleans::apply(s, "bool.subtract", p, BoolOp::Subtract)), - spec!("bool.intersect", "Intersect selection", BOOL_PARAMS, true, |s, p| crate::booleans::apply(s, "bool.intersect", p, BoolOp::Intersect)), - spec!("bool.exclude", "Exclude selection", BOOL_PARAMS, true, |s, p| crate::booleans::apply(s, "bool.exclude", p, BoolOp::Exclude)), - spec!("bool.flatten", "Flatten boolean", "{ids?}", true, flatten), - spec!("vector.flatten", "Flatten", "{ids?}", true, flatten), - spec!("vector.outlineStroke", "Outline stroke", "{ids?}", true, outline_stroke), - spec!("layout.add", "Add auto layout", "{ids?, direction? (horizontal|vertical|grid)}", true, layout_add), - spec!("layout.remove", "Remove auto layout", "{ids?}", true, |s, p| { - for i in req_targets("layout.remove", s, p)? { - // Hugging needs auto layout and the children that filled have nothing to fill: both - // become fixed (the frame keeps filling its own auto layout parent). - let doc = s.doc_mut()?; - doc.get_mut(i)?.layout = None; - figcraft_layout::coherent_sizing(doc, i); - } - ok() - }), - spec!("layout.suggest", "Suggest auto layout", "{ids?}", true, layout_add), - spec!("object.moveToPage", "Move to page", "{ids?, page}", true, move_to_page), - spec!("object.setThumbnail", "Set as thumbnail", "{id?}", true, |s, p| { - let id = targets(s, p).first().copied().ok_or_else(|| bad("object.setThumbnail", "select a frame"))?; - let doc = s.doc_mut()?; - for n in doc.ids().collect::>() { - if let Some(node) = doc.node(n) - && node.plugin_data.contains_key("figcraft.thumbnail") - { - doc.get_mut(n)?.plugin_data.remove("figcraft.thumbnail"); - } - } - doc.get_mut(id)?.plugin_data.insert("figcraft.thumbnail".into(), "true".into()); - ok() - }), - spec!("object.rasterize", "Rasterize selection", "{ids?, scale? (default 2)}", true, rasterize), - spec!("vector.reverse", "Reverse path direction", "{ids?}", true, |s, p| { - for i in req_targets("vector.reverse", s, p)? { - if let NodeKind::Vector { network } = &mut s.doc_mut()?.get_mut(i)?.kind { - for seg in &mut network.segments { - std::mem::swap(&mut seg.a, &mut seg.b); - std::mem::swap(&mut seg.ta, &mut seg.tb); - } - } - } - ok() - }), - spec!("vector.simplify", "Simplify vector", "{ids?, tolerance?}", true, simplify), - spec!("vector.offset", "Offset path", "{ids?, offset}", true, offset_path), - spec!("vector.join", "Join selection", "{ids?} (in vector edit mode: joins the selected points)", true, |s, p| { - crate::vectoredit::join_in_edit_mode(s, false).unwrap_or_else(|| join(s, p)) - }), - spec!("vector.smoothJoin", "Smooth join selection", "{ids?} (in vector edit mode: joins the selected points smoothly)", true, |s, p| { - crate::vectoredit::join_in_edit_mode(s, true).unwrap_or_else(|| join(s, p)) - }), - ] -} - -enum Wrap { - Group, - Frame, - Section, - Bool(BoolOp), -} - -/// Room a section made around layers leaves on each side. -const SECTION_PAD: f64 = 40.0; - -/// What the boolean commands do with what they're given. -const BOOL_PARAMS: &str = "{ids?} — layers inside boolean groups: sets their own operation (how each combines with the operands below it); one boolean group: sets its operation (every operand follows it); otherwise wraps the layers in a new boolean group"; - -/// Wrap the selection in a new boolean group doing `op`. -pub(crate) fn wrap_boolean(s: &mut Session, p: &Value, op: BoolOp) -> Result { - wrap(s, p, Wrap::Bool(op)) -} - -/// Wrap the selection in a new container at the topmost selected layer's place. -fn wrap(s: &mut Session, p: &Value, w: Wrap) -> Result { - let ids = req_targets("object.group", s, p)?; - let doc = s.doc_mut()?; - let ids = z_sorted(doc, &roots(doc, &ids)); - let top = *ids.last().ok_or_else(|| bad("object.group", "nothing selected"))?; - let parent = doc.get(top)?.parent.ok_or_else(|| bad("object.group", "can't group a page"))?; - // Sections sit on the page or in sections, never in frames or groups. - let board = |doc: &Document, i: NodeId| { - doc.node(i).and_then(|n| n.parent).and_then(|p| doc.node(p)).is_some_and(|p| p.is_page() || matches!(p.kind, NodeKind::Section { .. })) - }; - if matches!(w, Wrap::Section) && !ids.iter().all(|i| board(doc, *i)) { - return Err(bad("object.wrapInSection", "only layers on the page or in a section can be wrapped in a section")); - } - if !matches!(w, Wrap::Section) && ids.iter().any(|i| doc.node(*i).is_some_and(|n| matches!(n.kind, NodeKind::Section { .. }))) { - return Err(bad("object.group", "sections can't go in groups or frames")); - } - let index = doc.index_in_parent(top).map(|i| i + 1); - let mut bounds = doc.bounds_of(&ids).unwrap_or_default(); - if matches!(w, Wrap::Section) { - bounds = bounds.inflate(SECTION_PAD, SECTION_PAD); - } - let pw = doc.world_transform(parent); - let local = figcraft_geom::transform_rect_bbox(pw.inverse(), bounds); - let (kind, base) = match &w { - Wrap::Group => (NodeKind::Group, "Group"), - Wrap::Frame => (NodeKind::Frame, "Frame"), - Wrap::Section => (NodeKind::Section { contents_hidden: false }, "Section"), - Wrap::Bool(op) => (NodeKind::BooleanOp { op: *op }, op.label()), - }; - let name = string(p, "name").unwrap_or_else(|| if matches!(w, Wrap::Bool(_)) { base.to_string() } else { doc.next_name(base) }); - let gid = doc.alloc_id(); - let mut g = match w { - Wrap::Section => Node::new_section(gid, name, local.width(), local.height()), - _ => Node::new(gid, kind, name, local.width(), local.height()), - }; - g.transform = Affine::translate((local.x0, local.y0)); - if let Wrap::Bool(op) = w { - // A boolean group looks like its top layer; a subtraction like the bottom one it cuts. - let source = if op == BoolOp::Subtract { ids.first() } else { ids.last() }; - if let Some(first) = source.and_then(|i| doc.node(*i)) { - g.fills = if first.fills.is_empty() { vec![Paint::solid(Color::DEFAULT_SHAPE)] } else { first.fills.clone() }; - g.strokes = first.strokes.clone(); - g.stroke = first.stroke.clone(); - g.effects = first.effects.clone(); - } - g.blend = figcraft_color::BlendMode::Normal; - } - if let Wrap::Frame = w { - g.clip = true; - } - // Insert the container where the topmost layer is. - doc.insert(g, parent, index)?; - for i in &ids { - doc.reparent(*i, gid, None)?; - } - if !matches!(w, Wrap::Frame | Wrap::Section) { - let _ = doc.fit_to_children(gid); - } - s.set_selection(vec![gid]); - Ok(json!({"id": gid})) -} - -/// Turn frames into groups or groups into frames in place (same id, name, layers, effects, -/// opacity, exports and interactions), the layers inside staying where they are on the page. -/// Nothing changes unless every layer can convert. -fn convert(s: &mut Session, p: &Value, cmd: &'static str, to_frame: bool) -> Result { - let ids = req_targets(cmd, s, p)?; - let doc = s.doc_mut()?; - let mut todo = Vec::new(); - for i in ids { - let n = doc.get(i)?; - let refuse = |why: &str| Err(bad(cmd, format!("\"{}\" {why}", n.name))); - match (&n.kind, to_frame) { - (NodeKind::Instance { .. }, _) => return refuse("is an instance: its layers come from its main component (detach it first)"), - (NodeKind::Component { .. }, _) => return refuse("is a main component, which stays a frame"), - (NodeKind::Frame | NodeKind::Group, _) if crate::components::owning_instance(doc, i).is_some() => { - return refuse("is inside an instance: edit its main component, or detach the instance"); - } - (NodeKind::Frame, false) if n.children.is_empty() => return refuse("is empty: a group needs layers"), - (NodeKind::Frame, false) | (NodeKind::Group, true) => todo.push(i), - (NodeKind::Frame | NodeKind::Group, _) => {} - _ => return refuse("is neither a frame nor a group"), - } - } - for i in &todo { - if to_frame { - // The group's box as it is; nothing painted or clipped, so the look stays. - let n = doc.get_mut(*i)?; - n.kind = NodeKind::Frame; - n.clip = false; - n.fills.clear(); - n.strokes.clear(); - continue; - } - let n = doc.get_mut(*i)?; - n.kind = NodeKind::Group; - (n.layout, n.clip, n.overflow) = (None, false, proto::Overflow::None); - n.fills.clear(); - n.strokes.clear(); - n.grids.clear(); - n.guides.clear(); - n.corners = Corners::default(); - (n.styles.fill, n.styles.stroke, n.styles.grid) = (None, None, None); - // Variables bound to what went would bring it back. - n.bound.retain(|k, _| matches!(k.as_str(), "opacity" | "visible")); - let kids = n.children.clone(); - for k in kids { - // Out of the flow there is nothing to fill or to be absolute against. - doc.get_mut(k)?.child_layout.absolute = false; - } - figcraft_layout::coherent_sizing(doc, *i); - doc.fit_to_children(*i)?; - } - Ok(json!({"ids": todo})) -} - -fn ungroup(s: &mut Session, p: &Value) -> Result { - let ids = req_targets("object.ungroup", s, p)?; - let doc = s.doc_mut()?; - let mut out = Vec::new(); - for g in ids { - let n = doc.get(g)?; - if !(matches!(n.kind, NodeKind::Group | NodeKind::BooleanOp { .. } | NodeKind::Frame | NodeKind::Section { .. })) { - continue; - } - let Some(parent) = n.parent else { continue }; - let idx = doc.index_in_parent(g).unwrap_or(0); - let kids = n.children.clone(); - // Ungrouping a group keeps its opacity on the children (multiplied). - let op = n.opacity; - for (k, c) in kids.iter().enumerate() { - doc.reparent(*c, parent, Some(idx + k))?; - // Released operands no longer combine with anything. - if doc.get(*c)?.bool_op.is_some() { - doc.get_mut(*c)?.bool_op = None; - } - if op < 1.0 { - let cn = doc.get_mut(*c)?; - cn.opacity *= op; - } - out.push(*c); - } - doc.remove(g)?; - } - s.set_selection(out.clone()); - Ok(json!({"ids": out})) -} - -fn mask(s: &mut Session, p: &Value) -> Result { - let ids = req_targets("object.mask", s, p)?; - if ids.len() == 1 { - let doc = s.doc_mut()?; - let id = ids.first().copied().unwrap_or(NodeId(0)); - let n = doc.get_mut(id)?; - n.is_mask = !n.is_mask; - return ok(); - } - // Group, then the bottom-most child is the mask. - let r = wrap(s, &json!({"ids": ids, "name": "Mask group"}), Wrap::Group)?; - let gid = id_of(&r["id"]).ok_or_else(|| EngineError::Other("group failed".into()))?; - let doc = s.doc_mut()?; - if let Some(first) = doc.get(gid)?.children.first().copied() { - doc.get_mut(first)?.is_mask = true; - } - Ok(r) -} - -/// The outline of a node in its parent's space (shapes, text, booleans, groups). -pub fn outline_in_parent(doc: &Document, id: NodeId) -> Option { - let n = doc.node(id)?; - let (mut p, _) = figcraft_render::subtree_outline(doc, n, 0)?; - p.apply_affine(n.transform); - Some(p) -} - -fn flatten(s: &mut Session, p: &Value) -> Result { - let ids = req_targets("vector.flatten", s, p)?; - let doc = s.doc_mut()?; - let ids = z_sorted(doc, &roots(doc, &ids)); - let top = *ids.last().ok_or_else(|| bad("vector.flatten", "nothing selected"))?; - let parent = doc.get(top)?.parent.ok_or_else(|| bad("vector.flatten", "can't flatten a page"))?; - let index = doc.index_in_parent(top).map(|i| i + 1); - let pw = doc.world_transform(parent); - let mut path = BezPath::new(); - let mut even = false; - for i in &ids { - let Some(n) = doc.node(*i) else { continue }; - // Booleans flatten to their result; strokes don't get outlined. - let Some((mut op, rule)) = (match &n.kind { - NodeKind::Group | NodeKind::Frame | NodeKind::Component { .. } | NodeKind::Instance { .. } => figcraft_render::subtree_outline(doc, n, 0), - _ => figcraft_render::node_outline(doc, n), - }) else { - continue; - }; - even |= rule == figcraft_render::FillRule::EvenOdd; - // Into the common parent's space. - let w = doc.world_transform(*i); - op.apply_affine(pw.inverse() * w); - path.extend(op.elements().iter().copied()); - } - if path.elements().is_empty() { - return Err(EngineError::Disabled("vector.flatten".into(), "nothing to flatten".into())); - } - let first = ids.first().and_then(|i| doc.node(*i)).cloned(); - let bb = path.bounding_box(); - path.apply_affine(Affine::translate((-bb.x0, -bb.y0))); - let mut net = VectorNetwork::from_path(&path); - for r in &mut net.regions { - r.winding = if even { WindingRule::EvenOdd } else { WindingRule::NonZero }; - } - let vid = doc.alloc_id(); - let name = first.as_ref().map(|f| f.name.clone()).unwrap_or_else(|| "Vector".into()); - let mut v = Node::new(vid, NodeKind::Vector { network: net }, name, bb.width(), bb.height()); - v.transform = Affine::translate((bb.x0, bb.y0)); - if let Some(f) = first { - v.fills = if matches!(f.kind, NodeKind::Group | NodeKind::Line) && f.fills.is_empty() { - vec![Paint::solid(Color::DEFAULT_SHAPE)] - } else { - f.fills.clone() - }; - v.strokes = f.strokes.clone(); - v.stroke = StrokeProps { align: StrokeAlign::Center, ..f.stroke.clone() }; - v.effects = f.effects.clone(); - v.opacity = f.opacity; - v.blend = if matches!(f.blend, figcraft_color::BlendMode::PassThrough) { figcraft_color::BlendMode::Normal } else { f.blend }; - } - doc.insert(v, parent, index)?; - for i in &ids { - doc.remove(*i)?; - } - s.set_selection(vec![vid]); - Ok(json!({"id": vid})) -} - -fn outline_stroke(s: &mut Session, p: &Value) -> Result { - let ids = req_targets("vector.outlineStroke", s, p)?; - let mut out = Vec::new(); - for i in ids { - let doc = s.doc_mut()?; - let n = doc.get(i)?.clone(); - if !n.has_visible_stroke() { - continue; - } - let closed = figcraft_doc::geometry::is_closed(&n); - let Some(geom) = (match &n.kind { - NodeKind::Text(_) | NodeKind::BooleanOp { .. } => figcraft_render::node_outline(doc, &n).map(|(p, _)| p), - _ => figcraft_doc::geometry::stroke_geometry(&n), - }) else { - continue; - }; - let area = figcraft_render::draw::stroke_area(&n, &geom, closed); - let mut path = (*area).clone(); - // Union the pieces so the outline is one clean shape. - if let Ok(u) = figcraft_pathops::simplify(&path) { - path = u; - } - let bb = path.bounding_box(); - path.apply_affine(Affine::translate((-bb.x0, -bb.y0))); - let mut net = VectorNetwork::from_path(&path); - for r in &mut net.regions { - r.winding = WindingRule::EvenOdd; - } - let parent = n.parent.ok_or_else(|| bad("vector.outlineStroke", "page"))?; - let idx = doc.index_in_parent(i); - let vid = doc.alloc_id(); - let mut v = Node::new(vid, NodeKind::Vector { network: net }, n.name.clone(), bb.width(), bb.height()); - v.transform = n.transform * Affine::translate((bb.x0, bb.y0)); - // The stroke's paints (and its colour style) fill the outline. - v.fills = n.strokes.clone(); - v.strokes.clear(); - v.styles.fill = n.styles.stroke; - if n.fills.iter().any(|f| f.visible) && closed { - // Keep the fill as its own layer under the outline, grouped. - doc.insert(v, parent, idx.map(|x| x + 1))?; - let fill_id = i; - { - let f = doc.get_mut(fill_id)?; - f.strokes.clear(); - } - let r = wrap(s, &json!({"ids": [fill_id, vid], "name": n.name}), Wrap::Group)?; - if let Some(g) = id_of(&r["id"]) { - out.push(g); - } - continue; - } - // Standing in for the layer, the outline keeps how it is drawn and placed. - (v.opacity, v.blend, v.effects, v.styles.effect) = (n.opacity, n.blend, n.effects.clone(), n.styles.effect); - (v.constraints, v.child_layout, v.visible, v.locked) = (n.constraints, n.child_layout.clone(), n.visible, n.locked); - doc.insert(v, parent, idx.map(|x| x + 1))?; - doc.remove(i)?; - out.push(vid); - } - s.set_selection(out.clone()); - Ok(json!({"ids": out})) -} - -/// Infer direction, gap and padding from where children are (Figma's "Add auto layout"). -fn infer_layout(doc: &Document, frame: NodeId) -> AutoLayout { - let Some(f) = doc.node(frame) else { return AutoLayout::default() }; - let boxes: Vec = - f.children.iter().filter_map(|c| doc.node(*c)).map(|c| figcraft_geom::transform_rect_bbox(c.transform, c.local_rect())).collect(); - if boxes.is_empty() { - return AutoLayout { padding: [10.0; 4], gap: 10.0, ..Default::default() }; - } - let span_x = boxes.iter().map(|b| b.x1).fold(f64::MIN, f64::max) - boxes.iter().map(|b| b.x0).fold(f64::MAX, f64::min); - let span_y = boxes.iter().map(|b| b.y1).fold(f64::MIN, f64::max) - boxes.iter().map(|b| b.y0).fold(f64::MAX, f64::min); - let vertical = span_y > span_x; - let mut sorted = boxes.clone(); - sorted.sort_by(|a, b| if vertical { a.y0.total_cmp(&b.y0) } else { a.x0.total_cmp(&b.x0) }); - let gaps: Vec = - sorted.windows(2).filter_map(|w| Some(if vertical { w.get(1)?.y0 - w.first()?.y1 } else { w.get(1)?.x0 - w.first()?.x1 })).collect(); - let gap = if gaps.is_empty() { 10.0 } else { (gaps.iter().sum::() / gaps.len() as f64).max(0.0).round() }; - let all = boxes.iter().copied().reduce(|a, b| a.union(b)).unwrap_or_default(); - let pad = [all.y0.max(0.0), (f.width - all.x1).max(0.0), (f.height - all.y1).max(0.0), all.x0.max(0.0)].map(f64::round); - AutoLayout { mode: if vertical { LayoutMode::Vertical } else { LayoutMode::Horizontal }, gap, padding: pad, ..Default::default() } -} - -fn layout_add(s: &mut Session, p: &Value) -> Result { - let ids = req_targets("layout.add", s, p)?; - let dir = string(p, "direction"); - let doc = s.doc()?; - let all_frames = ids.iter().all(|i| doc.node(*i).is_some_and(|n| n.kind.is_frame_like() && n.layout.is_none())); - let targets = if all_frames { - ids - } else { - // Wrap non-frames in a new frame without a fill, then add auto layout to it. - let r = wrap(s, &json!({"ids": ids}), Wrap::Frame)?; - let fid = id_of(&r["id"]).ok_or_else(|| EngineError::Other("wrap failed".into()))?; - let f = s.doc_mut()?.get_mut(fid)?; - f.fills.clear(); - f.clip = false; - vec![fid] - }; - let doc = s.doc_mut()?; - for f in &targets { - let mut al = infer_layout(doc, *f); - if let Some(d) = &dir { - al.mode = match d.to_ascii_lowercase().as_str() { - "vertical" | "column" => LayoutMode::Vertical, - "grid" => LayoutMode::Grid, - _ => LayoutMode::Horizontal, - }; - } - // Children in reading order along the axis. - let mut kids = doc.get(*f)?.children.clone(); - let vertical = al.mode == LayoutMode::Vertical; - kids.sort_by(|a, b| { - let pa = doc.node(*a).map(|n| figcraft_geom::transform_rect_bbox(n.transform, n.local_rect())).unwrap_or_default(); - let pb = doc.node(*b).map(|n| figcraft_geom::transform_rect_bbox(n.transform, n.local_rect())).unwrap_or_default(); - if vertical { pa.y0.total_cmp(&pb.y0) } else { pa.x0.total_cmp(&pb.x0) } - }); - let n = doc.get_mut(*f)?; - n.children = kids; - n.layout = Some(al); - n.child_layout.sizing_h = Sizing::Hug; - n.child_layout.sizing_v = Sizing::Hug; - } - s.set_selection(targets.clone()); - Ok(json!({"ids": targets})) -} - -fn move_to_page(s: &mut Session, p: &Value) -> Result { - let ids = req_targets("object.moveToPage", s, p)?; - let page = req_id("object.moveToPage", p, "page")?; - let doc = s.doc_mut()?; - if !doc.node(page).is_some_and(Node::is_page) { - return Err(bad("object.moveToPage", format!("{page} is not a page"))); - } - if doc.is_divider(page) { - return Err(bad("object.moveToPage", "a divider holds no layers")); - } - for i in z_sorted(doc, &roots(doc, &ids)) { - doc.reparent(i, page, None)?; - } - s.set_selection(vec![]); - ok() -} - -fn rasterize(s: &mut Session, p: &Value) -> Result { - let ids = req_targets("object.rasterize", s, p)?; - let scale = num_or(p, "scale", 2.0); - let mut out = Vec::new(); - for i in ids { - let doc = s.doc()?; - let Some(pix) = figcraft_render::render_node(doc, i, scale) else { continue }; - let png = figcraft_render::encode_png(&pix).map_err(EngineError::Other)?; - let b = figcraft_render::export_bounds(doc, i).unwrap_or_default(); - let parent = doc.get(i)?.parent.ok_or_else(|| bad("object.rasterize", "page"))?; - let idx = doc.index_in_parent(i); - let pw = doc.world_transform(parent); - let name = doc.get(i)?.name.clone(); - let docm = s.doc_mut()?; - let (hash, _, _) = figcraft_format::add_image(docm, png).map_err(|e| EngineError::Other(e.to_string()))?; - let rid = docm.alloc_id(); - let mut r = Node::new(rid, NodeKind::Rectangle, name, b.width(), b.height()); - r.transform = pw.inverse() * Affine::translate((b.x0, b.y0)); - r.fills = vec![Paint::image(hash, figcraft_color::ImageScaleMode::Fill)]; - docm.insert(r, parent, idx.map(|x| x + 1))?; - docm.remove(i)?; - out.push(rid); - } - s.set_selection(out.clone()); - Ok(json!({"ids": out})) -} - -fn simplify(s: &mut Session, p: &Value) -> Result { - let tol = num_or(p, "tolerance", 0.5).max(0.01); - for i in req_targets("vector.simplify", s, p)? { - let doc = s.doc_mut()?; - let n = doc.get_mut(i)?; - if let NodeKind::Vector { network } = &mut n.kind { - let fill = network.fill_path(); - let src = if fill.elements().is_empty() { network.stroke_path() } else { fill }; - let simplified = figcraft_geom::kurbo::simplify::simplify_bezpath(src.iter(), tol, &Default::default()); - let regions = !network.regions.is_empty(); - let mut net = VectorNetwork::from_path(&simplified); - if !regions { - net.regions.clear(); - } - *network = net; - } - } - ok() -} - -fn offset_path(s: &mut Session, p: &Value) -> Result { - let d = req_num("vector.offset", p, "offset")?; - for i in req_targets("vector.offset", s, p)? { - let doc = s.doc_mut()?; - let n = doc.get(i)?.clone(); - let Some((path, _)) = figcraft_render::node_outline(doc, &n) else { continue }; - let props = StrokeProps { weight: d.abs() * 2.0, join: StrokeJoin::Round, align: StrokeAlign::Center, ..Default::default() }; - let ring = figcraft_pathops::stroke_center(&path, d.abs() * 2.0, &props); - let res = if d >= 0.0 { - figcraft_pathops::boolean(&[path, ring], BoolOp::Union) - } else { - figcraft_pathops::boolean(&[path, ring], BoolOp::Subtract) - }; - let Ok(mut res) = res else { continue }; - let bb = res.bounding_box(); - res.apply_affine(Affine::translate((-bb.x0, -bb.y0))); - let nm = doc.get_mut(i)?; - let mut net = VectorNetwork::from_path(&res); - for r in &mut net.regions { - r.winding = WindingRule::EvenOdd; - } - nm.kind = NodeKind::Vector { network: net }; - nm.transform *= Affine::translate((bb.x0, bb.y0)); - nm.width = bb.width(); - nm.height = bb.height(); - } - ok() -} - -/// Join open vectors whose end points touch into one vector. -fn join(s: &mut Session, p: &Value) -> Result { - let ids = req_targets("vector.join", s, p)?; - if ids.len() < 2 { - // Close a single open path. - if let Some(i) = ids.first() { - let n = s.doc_mut()?.get_mut(*i)?; - if let NodeKind::Vector { network } = &mut n.kind - && network.regions.is_empty() - && network.vertices.len() > 2 - { - let last = network.vertices.len() - 1; - network.segments.push(Segment::line(last, 0)); - let all: Vec = (0..network.segments.len()).collect(); - network.regions.push(Region { loops: vec![all], winding: WindingRule::NonZero }); - } - } - return ok(); - } - flatten(s, &json!({"ids": ids})) -} - -/// Fit containers to their visible children's bounds, keeping the children where they are on the -/// page (auto layout frames hug instead); rotated containers fit in their own axes. -fn resize_to_fit(s: &mut Session, p: &Value) -> Result { - let cmd = "object.resizeToFit"; - let ids = req_targets(cmd, s, p)?; - let doc = s.doc_mut()?; - let mut fitted = Vec::new(); - for id in ids { - let Some(n) = doc.node(id).cloned() else { continue }; - let container = n.kind.is_frame_like() || matches!(n.kind, NodeKind::Section { .. }); - if !container || n.children.is_empty() || matches!(n.kind, NodeKind::Instance { .. }) { - continue; - } - // Auto layout frames fit by hugging their contents. - if n.layout.is_some() { - let node = doc.get_mut(id)?; - node.child_layout.sizing_h = Sizing::Hug; - node.child_layout.sizing_v = Sizing::Hug; - fitted.push(id); - continue; - } - // Children's bounds in the container's own coordinates. - let mut b: Option = None; - for c in n.children.iter().filter_map(|c| doc.node(*c)).filter(|c| c.visible) { - let r = figcraft_geom::transform_rect_bbox(c.transform, c.local_rect()); - if r.x0.is_finite() && r.y0.is_finite() && r.x1.is_finite() && r.y1.is_finite() { - b = Some(b.map_or(r, |u| u.union(r))); - } - } - let Some(b) = b else { continue }; - let (w, h) = (b.width().max(0.01), b.height().max(0.01)); - // Move the container by the offset, and its children back by it. - let shift = Affine::translate((b.x0, b.y0)); - let node = doc.get_mut(id)?; - node.transform *= shift; - node.width = w; - node.height = h; - for c in &n.children { - if let Ok(cn) = doc.get_mut(*c) { - cn.transform = shift.inverse() * cn.transform; - } - } - fitted.push(id); - } - Ok(json!({"ids": fitted})) -} +//! Structure: groups, frames, masks, boolean groups, flatten, outline stroke, auto layout, pages. + +use figcraft_color::{Color, Paint}; +use figcraft_doc::*; +use figcraft_geom::{Affine, BezPath, Rect, Shape}; +use serde_json::{Value, json}; + +use super::edit::{roots, z_sorted}; +use super::{CommandSpec, ok}; +use crate::params::*; +use crate::{EngineError, Result, Session, bad, spec}; + +pub fn specs() -> Vec { + vec![ + spec!("object.group", "Group selection", "{ids?, name?}", true, |s, p| wrap(s, p, Wrap::Group)), + spec!("object.ungroup", "Ungroup selection", "{ids?}", true, ungroup), + spec!("object.frameSelection", "Frame selection", "{ids?, name?}", true, |s, p| wrap(s, p, Wrap::Frame)), + spec!("object.removeFrame", "Remove frame", "{ids?}", true, ungroup), + spec!( + "object.convertToGroup", + "Convert to group", + "{ids?} (frames become groups in place, their layers staying where they are; what only a frame has goes: auto layout, fills, strokes, corner radius, clipping, layout grids, guides and scrolling. Groups are left alone; instances, main components, layers inside instances and empty frames are refused)", + true, + |s, p| convert(s, p, "object.convertToGroup", false) + ), + spec!( + "object.convertToFrame", + "Convert to frame", + "{ids?} (groups become frames of the same box in place, their layers staying where they are: no fill, not clipping. Frames are left alone; instances, main components and layers inside instances are refused)", + true, + |s, p| convert(s, p, "object.convertToFrame", true) + ), + spec!( + "object.wrapInSection", + "Wrap in new section", + "{ids?, name?} (layers on the page or in a section; the section wraps them with some room)", + true, + |s, p| wrap(s, p, Wrap::Section) + ), + spec!( + "object.resizeToFit", + "Resize to fit", + "{ids?} (frames, sections and components shrink or grow to their contents; the contents stay put)", + true, + resize_to_fit + ), + spec!("object.mask", "Use as mask", "{ids?}", true, mask), + spec!("object.releaseMask", "Release mask", "{ids?}", true, |s, p| { + for i in req_targets("object.releaseMask", s, p)? { + s.doc_mut()?.get_mut(i)?.is_mask = false; + } + ok() + }), + spec!("bool.union", "Union selection", BOOL_PARAMS, true, |s, p| crate::booleans::apply(s, "bool.union", p, BoolOp::Union)), + spec!("bool.subtract", "Subtract selection", BOOL_PARAMS, true, |s, p| crate::booleans::apply(s, "bool.subtract", p, BoolOp::Subtract)), + spec!("bool.intersect", "Intersect selection", BOOL_PARAMS, true, |s, p| crate::booleans::apply(s, "bool.intersect", p, BoolOp::Intersect)), + spec!("bool.exclude", "Exclude selection", BOOL_PARAMS, true, |s, p| crate::booleans::apply(s, "bool.exclude", p, BoolOp::Exclude)), + spec!("bool.flatten", "Flatten boolean", "{ids?}", true, flatten), + spec!("vector.flatten", "Flatten", "{ids?}", true, flatten), + spec!("vector.outlineStroke", "Outline stroke", "{ids?}", true, outline_stroke), + spec!("layout.add", "Add auto layout", "{ids?, direction? (horizontal|vertical|grid)}", true, layout_add), + spec!("layout.remove", "Remove auto layout", "{ids?}", true, |s, p| { + for i in req_targets("layout.remove", s, p)? { + // Hugging needs auto layout and the children that filled have nothing to fill: both + // become fixed (the frame keeps filling its own auto layout parent). + let doc = s.doc_mut()?; + doc.get_mut(i)?.layout = None; + figcraft_layout::coherent_sizing(doc, i); + } + ok() + }), + spec!("layout.suggest", "Suggest auto layout", "{ids?}", true, layout_add), + spec!("object.moveToPage", "Move to page", "{ids?, page}", true, move_to_page), + spec!("object.setThumbnail", "Set as thumbnail", "{id?}", true, |s, p| { + let id = targets(s, p).first().copied().ok_or_else(|| bad("object.setThumbnail", "select a frame"))?; + let doc = s.doc_mut()?; + for n in doc.ids().collect::>() { + if let Some(node) = doc.node(n) + && node.plugin_data.contains_key("figcraft.thumbnail") + { + doc.get_mut(n)?.plugin_data.remove("figcraft.thumbnail"); + } + } + doc.get_mut(id)?.plugin_data.insert("figcraft.thumbnail".into(), "true".into()); + ok() + }), + spec!("object.rasterize", "Rasterize selection", "{ids?, scale? (default 2)}", true, rasterize), + spec!("vector.reverse", "Reverse path direction", "{ids?}", true, |s, p| { + for i in req_targets("vector.reverse", s, p)? { + if let NodeKind::Vector { network } = &mut s.doc_mut()?.get_mut(i)?.kind { + for seg in &mut network.segments { + std::mem::swap(&mut seg.a, &mut seg.b); + std::mem::swap(&mut seg.ta, &mut seg.tb); + } + } + } + ok() + }), + spec!("vector.simplify", "Simplify vector", "{ids?, tolerance?}", true, simplify), + spec!("vector.offset", "Offset path", "{ids?, offset}", true, offset_path), + spec!("vector.join", "Join selection", "{ids?} (in vector edit mode: joins the selected points)", true, |s, p| { + crate::vectoredit::join_in_edit_mode(s, false).unwrap_or_else(|| join(s, p)) + }), + spec!("vector.smoothJoin", "Smooth join selection", "{ids?} (in vector edit mode: joins the selected points smoothly)", true, |s, p| { + crate::vectoredit::join_in_edit_mode(s, true).unwrap_or_else(|| join(s, p)) + }), + ] +} + +enum Wrap { + Group, + Frame, + Section, + Bool(BoolOp), +} + +/// Room a section made around layers leaves on each side. +const SECTION_PAD: f64 = 40.0; + +/// What the boolean commands do with what they're given. +const BOOL_PARAMS: &str = "{ids?} — layers inside boolean groups: sets their own operation (how each combines with the operands below it); one boolean group: sets its operation (every operand follows it); otherwise wraps the layers in a new boolean group"; + +/// Wrap the selection in a new boolean group doing `op`. +pub(crate) fn wrap_boolean(s: &mut Session, p: &Value, op: BoolOp) -> Result { + wrap(s, p, Wrap::Bool(op)) +} + +/// Wrap the selection in a new container at the topmost selected layer's place. +fn wrap(s: &mut Session, p: &Value, w: Wrap) -> Result { + let ids = req_targets("object.group", s, p)?; + let doc = s.doc_mut()?; + let ids = z_sorted(doc, &roots(doc, &ids)); + let top = *ids.last().ok_or_else(|| bad("object.group", "nothing selected"))?; + let parent = doc.get(top)?.parent.ok_or_else(|| bad("object.group", "can't group a page"))?; + // Sections sit on the page or in sections, never in frames or groups. + let board = |doc: &Document, i: NodeId| { + doc.node(i).and_then(|n| n.parent).and_then(|p| doc.node(p)).is_some_and(|p| p.is_page() || matches!(p.kind, NodeKind::Section { .. })) + }; + if matches!(w, Wrap::Section) && !ids.iter().all(|i| board(doc, *i)) { + return Err(bad("object.wrapInSection", "only layers on the page or in a section can be wrapped in a section")); + } + if !matches!(w, Wrap::Section) && ids.iter().any(|i| doc.node(*i).is_some_and(|n| matches!(n.kind, NodeKind::Section { .. }))) { + return Err(bad("object.group", "sections can't go in groups or frames")); + } + let index = doc.index_in_parent(top).map(|i| i + 1); + let mut bounds = doc.bounds_of(&ids).unwrap_or_default(); + if matches!(w, Wrap::Section) { + bounds = bounds.inflate(SECTION_PAD, SECTION_PAD); + } + let pw = doc.world_transform(parent); + let local = figcraft_geom::transform_rect_bbox(pw.inverse(), bounds); + let (kind, base) = match &w { + Wrap::Group => (NodeKind::Group, "Group"), + Wrap::Frame => (NodeKind::Frame, "Frame"), + Wrap::Section => (NodeKind::Section { contents_hidden: false }, "Section"), + Wrap::Bool(op) => (NodeKind::BooleanOp { op: *op }, op.label()), + }; + let name = string(p, "name").unwrap_or_else(|| if matches!(w, Wrap::Bool(_)) { base.to_string() } else { doc.next_name(base) }); + let gid = doc.alloc_id(); + let mut g = match w { + Wrap::Section => Node::new_section(gid, name, local.width(), local.height()), + _ => Node::new(gid, kind, name, local.width(), local.height()), + }; + g.transform = Affine::translate((local.x0, local.y0)); + if let Wrap::Bool(op) = w { + // A boolean group looks like its top layer; a subtraction like the bottom one it cuts. + let source = if op == BoolOp::Subtract { ids.first() } else { ids.last() }; + if let Some(first) = source.and_then(|i| doc.node(*i)) { + g.fills = if first.fills.is_empty() { vec![Paint::solid(Color::DEFAULT_SHAPE)] } else { first.fills.clone() }; + g.strokes = first.strokes.clone(); + g.stroke = first.stroke.clone(); + g.effects = first.effects.clone(); + } + g.blend = figcraft_color::BlendMode::Normal; + } + if let Wrap::Frame = w { + g.clip = true; + } + // Insert the container where the topmost layer is. + doc.insert(g, parent, index)?; + for i in &ids { + doc.reparent(*i, gid, None)?; + } + if !matches!(w, Wrap::Frame | Wrap::Section) { + let _ = doc.fit_to_children(gid); + } + s.set_selection(vec![gid]); + Ok(json!({"id": gid})) +} + +/// Turn frames into groups or groups into frames in place (same id, name, layers, effects, +/// opacity, exports and interactions), the layers inside staying where they are on the page. +/// Nothing changes unless every layer can convert. +fn convert(s: &mut Session, p: &Value, cmd: &'static str, to_frame: bool) -> Result { + let ids = req_targets(cmd, s, p)?; + let doc = s.doc_mut()?; + let mut todo = Vec::new(); + for i in ids { + let n = doc.get(i)?; + let refuse = |why: &str| Err(bad(cmd, format!("\"{}\" {why}", n.name))); + match (&n.kind, to_frame) { + (NodeKind::Instance { .. }, _) => return refuse("is an instance: its layers come from its main component (detach it first)"), + (NodeKind::Component { .. }, _) => return refuse("is a main component, which stays a frame"), + (NodeKind::Frame | NodeKind::Group, _) if crate::components::owning_instance(doc, i).is_some() => { + return refuse("is inside an instance: edit its main component, or detach the instance"); + } + (NodeKind::Frame, false) if n.children.is_empty() => return refuse("is empty: a group needs layers"), + (NodeKind::Frame, false) | (NodeKind::Group, true) => todo.push(i), + (NodeKind::Frame | NodeKind::Group, _) => {} + _ => return refuse("is neither a frame nor a group"), + } + } + for i in &todo { + if to_frame { + // The group's box as it is; nothing painted or clipped, so the look stays. + let n = doc.get_mut(*i)?; + n.kind = NodeKind::Frame; + n.clip = false; + n.fills.clear(); + n.strokes.clear(); + continue; + } + let n = doc.get_mut(*i)?; + n.kind = NodeKind::Group; + (n.layout, n.clip, n.overflow) = (None, false, proto::Overflow::None); + n.fills.clear(); + n.strokes.clear(); + n.grids.clear(); + n.guides.clear(); + n.corners = Corners::default(); + (n.styles.fill, n.styles.stroke, n.styles.grid) = (None, None, None); + // Variables bound to what went would bring it back. + n.bound.retain(|k, _| matches!(k.as_str(), "opacity" | "visible")); + let kids = n.children.clone(); + for k in kids { + // Out of the flow there is nothing to fill or to be absolute against. + doc.get_mut(k)?.child_layout.absolute = false; + } + figcraft_layout::coherent_sizing(doc, *i); + doc.fit_to_children(*i)?; + } + Ok(json!({"ids": todo})) +} + +fn ungroup(s: &mut Session, p: &Value) -> Result { + let ids = req_targets("object.ungroup", s, p)?; + let doc = s.doc_mut()?; + let mut out = Vec::new(); + for g in ids { + let n = doc.get(g)?; + if !(matches!(n.kind, NodeKind::Group | NodeKind::BooleanOp { .. } | NodeKind::Frame | NodeKind::Section { .. })) { + continue; + } + let Some(parent) = n.parent else { continue }; + let idx = doc.index_in_parent(g).unwrap_or(0); + let kids = n.children.clone(); + // Ungrouping a group keeps its opacity on the children (multiplied). + let op = n.opacity; + for (k, c) in kids.iter().enumerate() { + doc.reparent(*c, parent, Some(idx + k))?; + // Released operands no longer combine with anything. + if doc.get(*c)?.bool_op.is_some() { + doc.get_mut(*c)?.bool_op = None; + } + if op < 1.0 { + let cn = doc.get_mut(*c)?; + cn.opacity *= op; + } + out.push(*c); + } + doc.remove(g)?; + } + s.set_selection(out.clone()); + Ok(json!({"ids": out})) +} + +fn mask(s: &mut Session, p: &Value) -> Result { + let ids = req_targets("object.mask", s, p)?; + if ids.len() == 1 { + let doc = s.doc_mut()?; + let id = ids.first().copied().unwrap_or(NodeId(0)); + let n = doc.get_mut(id)?; + n.is_mask = !n.is_mask; + return ok(); + } + // Group, then the bottom-most child is the mask. + let r = wrap(s, &json!({"ids": ids, "name": "Mask group"}), Wrap::Group)?; + let gid = id_of(&r["id"]).ok_or_else(|| EngineError::Other("group failed".into()))?; + let doc = s.doc_mut()?; + if let Some(first) = doc.get(gid)?.children.first().copied() { + doc.get_mut(first)?.is_mask = true; + } + Ok(r) +} + +/// The outline of a node in its parent's space (shapes, text, booleans, groups). +pub fn outline_in_parent(doc: &Document, id: NodeId) -> Option { + let n = doc.node(id)?; + let (mut p, _) = figcraft_render::subtree_outline(doc, n, 0)?; + p.apply_affine(n.transform); + Some(p) +} + +fn flatten(s: &mut Session, p: &Value) -> Result { + let ids = req_targets("vector.flatten", s, p)?; + let doc = s.doc_mut()?; + let ids = z_sorted(doc, &roots(doc, &ids)); + let top = *ids.last().ok_or_else(|| bad("vector.flatten", "nothing selected"))?; + let parent = doc.get(top)?.parent.ok_or_else(|| bad("vector.flatten", "can't flatten a page"))?; + let index = doc.index_in_parent(top).map(|i| i + 1); + let pw = doc.world_transform(parent); + let mut path = BezPath::new(); + let mut even = false; + for i in &ids { + let Some(n) = doc.node(*i) else { continue }; + // Booleans flatten to their result; strokes don't get outlined. + let Some((mut op, rule)) = (match &n.kind { + NodeKind::Group | NodeKind::Frame | NodeKind::Component { .. } | NodeKind::Instance { .. } => figcraft_render::subtree_outline(doc, n, 0), + _ => figcraft_render::node_outline(doc, n), + }) else { + continue; + }; + even |= rule == figcraft_render::FillRule::EvenOdd; + // Into the common parent's space. + let w = doc.world_transform(*i); + op.apply_affine(pw.inverse() * w); + path.extend(op.elements().iter().copied()); + } + if path.elements().is_empty() { + return Err(EngineError::Disabled("vector.flatten".into(), "nothing to flatten".into())); + } + let first = ids.first().and_then(|i| doc.node(*i)).cloned(); + let bb = path.bounding_box(); + path.apply_affine(Affine::translate((-bb.x0, -bb.y0))); + let mut net = VectorNetwork::from_path(&path); + for r in &mut net.regions { + r.winding = if even { WindingRule::EvenOdd } else { WindingRule::NonZero }; + } + let vid = doc.alloc_id(); + let name = first.as_ref().map(|f| f.name.clone()).unwrap_or_else(|| "Vector".into()); + let mut v = Node::new(vid, NodeKind::Vector { network: net }, name, bb.width(), bb.height()); + v.transform = Affine::translate((bb.x0, bb.y0)); + if let Some(f) = first { + v.fills = if matches!(f.kind, NodeKind::Group | NodeKind::Line) && f.fills.is_empty() { + vec![Paint::solid(Color::DEFAULT_SHAPE)] + } else { + f.fills.clone() + }; + v.strokes = f.strokes.clone(); + v.stroke = StrokeProps { align: StrokeAlign::Center, ..f.stroke.clone() }; + v.effects = f.effects.clone(); + v.opacity = f.opacity; + v.blend = if matches!(f.blend, figcraft_color::BlendMode::PassThrough) { figcraft_color::BlendMode::Normal } else { f.blend }; + } + doc.insert(v, parent, index)?; + for i in &ids { + doc.remove(*i)?; + } + s.set_selection(vec![vid]); + Ok(json!({"id": vid})) +} + +fn outline_stroke(s: &mut Session, p: &Value) -> Result { + let ids = req_targets("vector.outlineStroke", s, p)?; + let mut out = Vec::new(); + for i in ids { + let doc = s.doc_mut()?; + let n = doc.get(i)?.clone(); + if !n.has_visible_stroke() { + continue; + } + let closed = figcraft_doc::geometry::is_closed(&n); + let Some((area, rule)) = (match &n.kind { + NodeKind::Text(_) => figcraft_render::node_outline(doc, &n) + .map(|(p, _)| (figcraft_render::draw::stroke_area(&n, &p, closed), figcraft_render::FillRule::Winding)), + _ => figcraft_render::stroke_fill(doc, &n), + }) else { + continue; + }; + let mut path = (*area).clone(); + // Simplify non-zero strokes into even-odd contours. Inside/outside rings already use + // even-odd: interpreting their same-direction contours as non-zero would fill the hole. + if rule == figcraft_render::FillRule::Winding + && let Ok(u) = figcraft_pathops::simplify(&path) + { + path = u; + } + let bb = path.bounding_box(); + path.apply_affine(Affine::translate((-bb.x0, -bb.y0))); + let mut net = VectorNetwork::from_path(&path); + for r in &mut net.regions { + r.winding = WindingRule::EvenOdd; + } + let parent = n.parent.ok_or_else(|| bad("vector.outlineStroke", "page"))?; + let idx = doc.index_in_parent(i); + let vid = doc.alloc_id(); + let mut v = Node::new(vid, NodeKind::Vector { network: net }, n.name.clone(), bb.width(), bb.height()); + v.transform = n.transform * Affine::translate((bb.x0, bb.y0)); + // The stroke's paints (and its colour style) fill the outline. + v.fills = n.strokes.clone(); + v.strokes.clear(); + v.styles.fill = n.styles.stroke; + if n.fills.iter().any(|f| f.visible) && closed { + // Keep the fill as its own layer under the outline, grouped. + doc.insert(v, parent, idx.map(|x| x + 1))?; + let fill_id = i; + { + let f = doc.get_mut(fill_id)?; + f.strokes.clear(); + } + let r = wrap(s, &json!({"ids": [fill_id, vid], "name": n.name}), Wrap::Group)?; + if let Some(g) = id_of(&r["id"]) { + out.push(g); + } + continue; + } + // Standing in for the layer, the outline keeps how it is drawn and placed. + (v.opacity, v.blend, v.effects, v.styles.effect) = (n.opacity, n.blend, n.effects.clone(), n.styles.effect); + (v.constraints, v.child_layout, v.visible, v.locked) = (n.constraints, n.child_layout.clone(), n.visible, n.locked); + doc.insert(v, parent, idx.map(|x| x + 1))?; + doc.remove(i)?; + out.push(vid); + } + s.set_selection(out.clone()); + Ok(json!({"ids": out})) +} + +/// Infer direction, gap and padding from where children are (Figma's "Add auto layout"). +fn infer_layout(doc: &Document, frame: NodeId) -> AutoLayout { + let Some(f) = doc.node(frame) else { return AutoLayout::default() }; + let boxes: Vec = + f.children.iter().filter_map(|c| doc.node(*c)).map(|c| figcraft_geom::transform_rect_bbox(c.transform, c.local_rect())).collect(); + if boxes.is_empty() { + return AutoLayout { padding: [10.0; 4], gap: 10.0, ..Default::default() }; + } + let span_x = boxes.iter().map(|b| b.x1).fold(f64::MIN, f64::max) - boxes.iter().map(|b| b.x0).fold(f64::MAX, f64::min); + let span_y = boxes.iter().map(|b| b.y1).fold(f64::MIN, f64::max) - boxes.iter().map(|b| b.y0).fold(f64::MAX, f64::min); + let vertical = span_y > span_x; + let mut sorted = boxes.clone(); + sorted.sort_by(|a, b| if vertical { a.y0.total_cmp(&b.y0) } else { a.x0.total_cmp(&b.x0) }); + let gaps: Vec = + sorted.windows(2).filter_map(|w| Some(if vertical { w.get(1)?.y0 - w.first()?.y1 } else { w.get(1)?.x0 - w.first()?.x1 })).collect(); + let gap = if gaps.is_empty() { 10.0 } else { (gaps.iter().sum::() / gaps.len() as f64).max(0.0).round() }; + let all = boxes.iter().copied().reduce(|a, b| a.union(b)).unwrap_or_default(); + let pad = [all.y0.max(0.0), (f.width - all.x1).max(0.0), (f.height - all.y1).max(0.0), all.x0.max(0.0)].map(f64::round); + AutoLayout { mode: if vertical { LayoutMode::Vertical } else { LayoutMode::Horizontal }, gap, padding: pad, ..Default::default() } +} + +fn layout_add(s: &mut Session, p: &Value) -> Result { + let ids = req_targets("layout.add", s, p)?; + let dir = string(p, "direction"); + let doc = s.doc()?; + let all_frames = ids.iter().all(|i| doc.node(*i).is_some_and(|n| n.kind.is_frame_like() && n.layout.is_none())); + let targets = if all_frames { + ids + } else { + // Wrap non-frames in a new frame without a fill, then add auto layout to it. + let r = wrap(s, &json!({"ids": ids}), Wrap::Frame)?; + let fid = id_of(&r["id"]).ok_or_else(|| EngineError::Other("wrap failed".into()))?; + let f = s.doc_mut()?.get_mut(fid)?; + f.fills.clear(); + f.clip = false; + vec![fid] + }; + let doc = s.doc_mut()?; + for f in &targets { + let mut al = infer_layout(doc, *f); + if let Some(d) = &dir { + al.mode = match d.to_ascii_lowercase().as_str() { + "vertical" | "column" => LayoutMode::Vertical, + "grid" => LayoutMode::Grid, + _ => LayoutMode::Horizontal, + }; + } + // Children in reading order along the axis. + let mut kids = doc.get(*f)?.children.clone(); + let vertical = al.mode == LayoutMode::Vertical; + kids.sort_by(|a, b| { + let pa = doc.node(*a).map(|n| figcraft_geom::transform_rect_bbox(n.transform, n.local_rect())).unwrap_or_default(); + let pb = doc.node(*b).map(|n| figcraft_geom::transform_rect_bbox(n.transform, n.local_rect())).unwrap_or_default(); + if vertical { pa.y0.total_cmp(&pb.y0) } else { pa.x0.total_cmp(&pb.x0) } + }); + let n = doc.get_mut(*f)?; + n.children = kids; + n.layout = Some(al); + n.child_layout.sizing_h = Sizing::Hug; + n.child_layout.sizing_v = Sizing::Hug; + } + s.set_selection(targets.clone()); + Ok(json!({"ids": targets})) +} + +fn move_to_page(s: &mut Session, p: &Value) -> Result { + let ids = req_targets("object.moveToPage", s, p)?; + let page = req_id("object.moveToPage", p, "page")?; + let doc = s.doc_mut()?; + if !doc.node(page).is_some_and(Node::is_page) { + return Err(bad("object.moveToPage", format!("{page} is not a page"))); + } + if doc.is_divider(page) { + return Err(bad("object.moveToPage", "a divider holds no layers")); + } + for i in z_sorted(doc, &roots(doc, &ids)) { + doc.reparent(i, page, None)?; + } + s.set_selection(vec![]); + ok() +} + +fn rasterize(s: &mut Session, p: &Value) -> Result { + let ids = req_targets("object.rasterize", s, p)?; + let scale = num_or(p, "scale", 2.0); + let mut out = Vec::new(); + for i in ids { + let doc = s.doc()?; + let Some(pix) = figcraft_render::render_node(doc, i, scale) else { continue }; + let png = figcraft_render::encode_png(&pix).map_err(EngineError::Other)?; + let b = figcraft_render::export_bounds(doc, i).unwrap_or_default(); + let parent = doc.get(i)?.parent.ok_or_else(|| bad("object.rasterize", "page"))?; + let idx = doc.index_in_parent(i); + let pw = doc.world_transform(parent); + let name = doc.get(i)?.name.clone(); + let docm = s.doc_mut()?; + let (hash, _, _) = figcraft_format::add_image(docm, png).map_err(|e| EngineError::Other(e.to_string()))?; + let rid = docm.alloc_id(); + let mut r = Node::new(rid, NodeKind::Rectangle, name, b.width(), b.height()); + r.transform = pw.inverse() * Affine::translate((b.x0, b.y0)); + r.fills = vec![Paint::image(hash, figcraft_color::ImageScaleMode::Fill)]; + docm.insert(r, parent, idx.map(|x| x + 1))?; + docm.remove(i)?; + out.push(rid); + } + s.set_selection(out.clone()); + Ok(json!({"ids": out})) +} + +fn simplify(s: &mut Session, p: &Value) -> Result { + let tol = num_or(p, "tolerance", 0.5).max(0.01); + for i in req_targets("vector.simplify", s, p)? { + let doc = s.doc_mut()?; + let n = doc.get_mut(i)?; + if let NodeKind::Vector { network } = &mut n.kind { + let fill = network.fill_path(); + let src = if fill.elements().is_empty() { network.stroke_path() } else { fill }; + let simplified = figcraft_geom::kurbo::simplify::simplify_bezpath(src.iter(), tol, &Default::default()); + let regions = !network.regions.is_empty(); + let mut net = VectorNetwork::from_path(&simplified); + if !regions { + net.regions.clear(); + } + *network = net; + } + } + ok() +} + +fn offset_path(s: &mut Session, p: &Value) -> Result { + let d = req_num("vector.offset", p, "offset")?; + for i in req_targets("vector.offset", s, p)? { + let doc = s.doc_mut()?; + let n = doc.get(i)?.clone(); + let Some((path, _)) = figcraft_render::node_outline(doc, &n) else { continue }; + let props = StrokeProps { weight: d.abs() * 2.0, join: StrokeJoin::Round, align: StrokeAlign::Center, ..Default::default() }; + let ring = figcraft_pathops::stroke_center(&path, d.abs() * 2.0, &props); + let res = if d >= 0.0 { + figcraft_pathops::boolean(&[path, ring], BoolOp::Union) + } else { + figcraft_pathops::boolean(&[path, ring], BoolOp::Subtract) + }; + let Ok(mut res) = res else { continue }; + let bb = res.bounding_box(); + res.apply_affine(Affine::translate((-bb.x0, -bb.y0))); + let nm = doc.get_mut(i)?; + let mut net = VectorNetwork::from_path(&res); + for r in &mut net.regions { + r.winding = WindingRule::EvenOdd; + } + nm.kind = NodeKind::Vector { network: net }; + nm.transform *= Affine::translate((bb.x0, bb.y0)); + nm.width = bb.width(); + nm.height = bb.height(); + } + ok() +} + +/// Join open vectors whose end points touch into one vector. +fn join(s: &mut Session, p: &Value) -> Result { + let ids = req_targets("vector.join", s, p)?; + if ids.len() < 2 { + // Close a single open path. + if let Some(i) = ids.first() { + let n = s.doc_mut()?.get_mut(*i)?; + if let NodeKind::Vector { network } = &mut n.kind + && network.regions.is_empty() + && network.vertices.len() > 2 + { + let last = network.vertices.len() - 1; + network.segments.push(Segment::line(last, 0)); + let all: Vec = (0..network.segments.len()).collect(); + network.regions.push(Region { loops: vec![all], winding: WindingRule::NonZero }); + } + } + return ok(); + } + flatten(s, &json!({"ids": ids})) +} + +/// Fit containers to their visible children's bounds, keeping the children where they are on the +/// page (auto layout frames hug instead); rotated containers fit in their own axes. +fn resize_to_fit(s: &mut Session, p: &Value) -> Result { + let cmd = "object.resizeToFit"; + let ids = req_targets(cmd, s, p)?; + let doc = s.doc_mut()?; + let mut fitted = Vec::new(); + for id in ids { + let Some(n) = doc.node(id).cloned() else { continue }; + let container = n.kind.is_frame_like() || matches!(n.kind, NodeKind::Section { .. }); + if !container || n.children.is_empty() || matches!(n.kind, NodeKind::Instance { .. }) { + continue; + } + // Auto layout frames fit by hugging their contents. + if n.layout.is_some() { + let node = doc.get_mut(id)?; + node.child_layout.sizing_h = Sizing::Hug; + node.child_layout.sizing_v = Sizing::Hug; + fitted.push(id); + continue; + } + // Children's bounds in the container's own coordinates. + let mut b: Option = None; + for c in n.children.iter().filter_map(|c| doc.node(*c)).filter(|c| c.visible) { + let r = figcraft_geom::transform_rect_bbox(c.transform, c.local_rect()); + if r.x0.is_finite() && r.y0.is_finite() && r.x1.is_finite() && r.y1.is_finite() { + b = Some(b.map_or(r, |u| u.union(r))); + } + } + let Some(b) = b else { continue }; + let (w, h) = (b.width().max(0.01), b.height().max(0.01)); + // Move the container by the offset, and its children back by it. + let shift = Affine::translate((b.x0, b.y0)); + let node = doc.get_mut(id)?; + node.transform *= shift; + node.width = w; + node.height = h; + for c in &n.children { + if let Ok(cn) = doc.get_mut(*c) { + cn.transform = shift.inverse() * cn.transform; + } + } + fitted.push(id); + } + Ok(json!({"ids": fitted})) +} diff --git a/crates/engine/tests/outline_stroke.rs b/crates/engine/tests/outline_stroke.rs new file mode 100644 index 00000000..49e8a8b2 --- /dev/null +++ b/crates/engine/tests/outline_stroke.rs @@ -0,0 +1,174 @@ +//! Outline stroke keeps the renderer's holes and existing property ownership. +#[cfg(test)] +mod tests { + use figcraft_doc::{Document, NodeId, NodeKind}; + use figcraft_engine::Session; + use figcraft_render::{Options, View, render_view}; + use serde_json::{Value, json}; + + fn run(s: &mut Session, command: &str, params: Value) -> Value { + s.execute(command, ¶ms).unwrap() + } + + fn rectangle(s: &mut Session, opacity: f64) -> NodeId { + NodeId( + run( + s, + "shape.rectangle", + json!({ + "x": 24, "y": 24, "width": 64, "height": 64, + "props": {"fill": "#FF0000", "stroke": "#0000FF", "strokeWeight": 8, "opacity": opacity} + }), + )["id"] + .as_u64() + .unwrap(), + ) + } + + fn pixels(doc: &Document) -> Vec { + render_view(doc, doc.first_page().unwrap(), &View { origin: (0.0, 0.0), scale: 1.0, width: 112, height: 112 }, &Options::default()) + .unwrap() + .data() + .to_vec() + } + + fn outlined(s: &mut Session, id: NodeId) -> NodeId { + NodeId(run(s, "vector.outlineStroke", json!({"ids": [id]}))["ids"][0].as_u64().unwrap()) + } + + fn same_pixels(actual: &[u8], expected: &[u8]) { + assert_eq!(actual.len(), expected.len()); + let differences: Vec<_> = actual.iter().zip(expected).enumerate().filter(|(_, (a, b))| a != b).take(8).collect(); + assert!(differences.is_empty(), "rendered channels changed (byte, actual/expected): {differences:?}"); + } + + #[test] + fn outline_keeps_rings_through_undo_and_native_reopen() { + for vector in [false, true] { + let mut s = Session::new(); + let mut id = rectangle(&mut s, 1.0); + if vector { + id = NodeId(run(&mut s, "vector.flatten", json!({"ids": [id]}))["id"].as_u64().unwrap()); + } + let before = s.doc().unwrap().clone(); + let expected = pixels(&before); + let depth = s.undo_depth(); + let group = outlined(&mut s, id); + let after = s.doc().unwrap().clone(); + same_pixels(&pixels(&after), &expected); + assert!(after.node(id).unwrap().strokes.is_empty()); + assert_eq!(s.selection(), vec![group]); + assert_eq!(s.undo_depth(), (depth.0 + 1, 0)); + let (loaded, notes) = figcraft_format::load_bytes(&figcraft_format::save_bytes(&after, None).unwrap()).unwrap(); + assert!(notes.is_empty(), "{notes:?}"); + same_pixels(&pixels(&loaded), &expected); + assert!(s.undo().unwrap()); + assert_eq!(serde_json::to_value(s.doc().unwrap()).unwrap(), serde_json::to_value(&before).unwrap()); + assert!(s.redo().unwrap()); + assert_eq!(serde_json::to_value(s.doc().unwrap()).unwrap(), serde_json::to_value(&after).unwrap()); + } + } + + #[test] + fn stroke_only_keeps_holes_at_each_opacity() { + for opacity in [0.0, 0.25, 0.5, 1.0] { + let mut s = Session::new(); + let id = rectangle(&mut s, opacity); + run(&mut s, "object.removeFill", json!({"ids": [id]})); + let before = pixels(s.doc().unwrap()); + let vector = outlined(&mut s, id); + let doc = s.doc().unwrap(); + assert!(matches!(doc.node(vector).unwrap().kind, NodeKind::Vector { .. })); + assert_eq!(doc.node(vector).unwrap().opacity, opacity as f32); + same_pixels(&pixels(doc), &before); + } + } + + #[test] + fn outline_keeps_stroke_holes_and_alignment() { + for props in [ + json!({"strokeAlign": "inside"}), + json!({"strokeAlign": "outside"}), + json!({"strokeAlign": "center"}), + json!({"strokeSides": [2, 4, 8, 16]}), + json!({"strokeAlign": "center", "dashes": [8, 4]}), + ] { + for filled in [false, true] { + let mut s = Session::new(); + let id = rectangle(&mut s, 1.0); + run(&mut s, "node.set", json!({"ids": [id], "props": props})); + if !filled { + run(&mut s, "object.removeFill", json!({"ids": [id]})); + } + let before = pixels(s.doc().unwrap()); + outlined(&mut s, id); + same_pixels(&pixels(s.doc().unwrap()), &before); + } + } + } + + #[test] + fn outlining_an_instance_keeps_its_inherited_opacity_link() { + let mut s = Session::new(); + let component = NodeId( + run( + &mut s, + "frame.create", + json!({ + "width": 64, "height": 64, + "props": {"fill": "#FF0000", "stroke": "#0000FF", "strokeWeight": 8, "opacity": 0.5} + }), + )["id"] + .as_u64() + .unwrap(), + ); + run(&mut s, "component.create", json!({"ids": [component]})); + let instance = NodeId(run(&mut s, "instance.create", json!({"component": component, "x": 200}))["id"].as_u64().unwrap()); + outlined(&mut s, instance); + assert_eq!(s.doc().unwrap().node(instance).unwrap().opacity, 0.5); + assert!(!s.doc().unwrap().node(instance).unwrap().overrides.contains("opacity")); + run(&mut s, "node.set", json!({"id": component, "props": {"opacity": 0.25}})); + let n = s.doc().unwrap().node(instance).unwrap(); + assert_eq!(n.opacity, 0.25, "the instance still follows its main"); + assert!(!n.overrides.contains("opacity")); + } + + #[test] + fn outlining_a_main_layer_keeps_its_instance_override() { + let mut s = Session::new(); + let frame = NodeId(run(&mut s, "frame.create", json!({"width": 100, "height": 100}))["id"].as_u64().unwrap()); + let layer = NodeId( + run( + &mut s, + "shape.rectangle", + json!({ + "parent": frame, "x": 24, "y": 24, "width": 64, "height": 64, + "props": {"fill": "#FF0000", "stroke": "#0000FF", "strokeWeight": 8} + }), + )["id"] + .as_u64() + .unwrap(), + ); + let component = run(&mut s, "component.create", json!({"ids": [frame]}))["id"].clone(); + run(&mut s, "instance.create", json!({"component": component, "x": 200})); + let copy = s.doc().unwrap().nodes().find(|n| n.source == Some(layer)).unwrap().id; + run(&mut s, "node.set", json!({"id": copy, "props": {"opacity": 0.25}})); + let before = s.doc().unwrap().clone(); + let view = View { origin: (0.0, 0.0), scale: 1.0, width: 312, height: 112 }; + let expected = render_view(&before, s.page().unwrap(), &view, &Options::default()).unwrap(); + let group = outlined(&mut s, layer); + let doc = s.doc().unwrap(); + let actual = render_view(doc, s.page().unwrap(), &view, &Options::default()).unwrap(); + same_pixels(actual.data(), expected.data()); + let instance_group = doc.node(copy).unwrap(); + assert!(matches!(instance_group.kind, NodeKind::Group)); + assert_eq!(instance_group.source, Some(group)); + assert_eq!(instance_group.opacity, 0.25); + assert!(instance_group.overrides.contains("opacity")); + assert!(s.undo().unwrap()); + assert_eq!(serde_json::to_value(s.doc().unwrap()).unwrap(), serde_json::to_value(&before).unwrap()); + assert!(s.redo().unwrap()); + let actual = render_view(s.doc().unwrap(), s.page().unwrap(), &view, &Options::default()).unwrap(); + same_pixels(actual.data(), expected.data()); + } +} diff --git a/docs/gaps.md b/docs/gaps.md index 07192dfd..96620b05 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,445 +1,448 @@ -# Where FigCraft falls short of Figma - -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (reviewed Russian catalog and integer plurals; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail) · **Target:** Figma Design (desktop app 126.10.7) - -Every known shortfall against Figma, one entry each, ranked by how much it stops a Figma user from -doing real work in FigCraft. Agents: pick work from the top unless the owner says otherwise. The -numbers behind the ranking are in [`target-app-parity.md`](target-app-parity.md). Hours are one -Opus 5.5 agent working sequentially, tests included, calibrated as described there. "Estimated" -everywhere unless marked measured. - -## Ranked - -No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passes. Beta blockers are marked. - -| # | Gap | Kind | Hours | Doc | -|---|---|---|---|---| -| 1 | `.fig` files: best-effort import with editable vector networks and stand-ins for non-design layers landed, and an experimental export (round trips through our own importer; written files opened in other software, where layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft, instances keep their size and variables with modes and colour styles come across); real-file checks and the small tail left (**beta blocker** until checked on real files, gap 3) | File format | 5–10 | [file-format-parity](file-format-parity.md) | -| 2 | Multiplayer and sharing are a preview, without accounts (**beta blocker**) | Feature | 42–84 | [collaboration-parity](collaboration-parity.md) | -| 3 | Nothing checked against real Figma files | Fidelity | 15–30 | [target-app-parity](target-app-parity.md) | -| 4 | Interaction fidelity not compared tool by tool | UI/UX | 40–60 | [ui-parity](ui-parity.md) | -| 5 | Localization: English, reviewed Russian, plus unreviewed Spanish (and Latin American Spanish), French, German, Brazilian Portuguese, Japanese, Korean and Simplified Chinese drafts | Localization | 29–51 | [localization-parity](localization-parity.md) | -| 6 | Plugins: widgets take clicks only, a self-hosted directory but no public catalogue; Figma's plugins don't run | Ecosystem | 8–26 | [target-app-parity](target-app-parity.md) | -| 7 | CPU canvas where Figma uses the GPU | Hardware/performance | 30–60 | [hardware-parity](hardware-parity.md) | -| 8 | Platform QA: Linux and the browser left (Windows pass done 2026-10-11) | Stability/platforms | 10–20 | [hardware-parity](hardware-parity.md) | -| 9 | Figma Draw: pen pressure on macOS/Linux tablets (multi-edit, brushes, pressure, text on a path and pattern fills landed) | Feature | 1–3 | [target-app-parity](target-app-parity.md) | -| 10 | Team libraries: no team-wide lists or accounts; analytics are counts | Feature | 4–8 | [components-parity](components-parity.md) | -| 11 | Spell check | Feature | 4–8 | [target-app-parity](target-app-parity.md) | -| 12 | Video fills; HEIC images | File format | 5–10 | [file-format-parity](file-format-parity.md) | -| 13 | Vector tail: none left (boolean hit testing on the result landed 2026-10-11) | Feature | — | [target-app-parity](target-app-parity.md) | -| 14 | Variables: extended collections checked on real libraries (the tail landed 2026-10-12) | Feature | 0–1 | [components-parity](components-parity.md) | -| 15 | Prototyping: media triggers and actions; controllers on macOS, Linux and FreeBSD (controller buttons on key/gamepad triggers, widgets in prototypes landed) | Feature | 2–5 | [prototyping-parity](prototyping-parity.md) | -| 16 | Dev Mode: import lines inside generated layer trees; the editor extension is left to the MCP server (code connect, Dev Mode plugins and dev resources landed) | Feature | 0–1 | [dev-mode-parity](dev-mode-parity.md) | -| 17 | Branching: branches in live sessions, lists other than fills, strokes and effects merged whole (a branch's own version history and per-item list merges landed 2026-10-12) | Feature | 1–2 | [collaboration-parity](collaboration-parity.md) | -| 18 | Display P3 on screen: blocked upstream (egui-wgpu picks an 8-bit sRGB surface); gamut warning and soft-proof sRGB landed, as did several windows | Hardware | 4–8 | [hardware-parity](hardware-parity.md) | -| 19 | AI features | AI | 30–60 | [target-app-parity](target-app-parity.md) | -| 20 | Mobile apps (view, mirror, comment) | Platforms | 20–40 | [hardware-parity](hardware-parity.md) | -| 21 | Menu items without a command: none left (landed 2026-10-11) | Feature | — | [parity-checklist](parity-checklist.md) | -| 22 | Screen readers never run; browser has no accessibility tree | UI/UX | 4–8 | [ui-parity](ui-parity.md) | -| 23 | CMYK output intent for print PDFs | File format | 2–4 | [file-format-parity](file-format-parity.md) | - -## Entries - -### 1. `.fig` files: the import's tail -- **Landed (2026-10-11):** `.fig` files open (best effort): the zip, its deflate/zstd chunks and the kiwi schema are decoded generically and mapped by field name onto the REST importer, so pages, the layer tree, geometry, paints (styles resolved), strokes, effects, text with styled runs, vectors, auto layout (grid included), constraints, components, sets, instances (expanded from their main with overrides, derived layout and property values), styles, variables with modes and bindings, prototyping and image fills from the archive come across; anything else is a warning (`crates/format/src/fig`). Then (2026-10-11): vectors open as editable vector networks (per-point corner radius, handle mirroring, end caps; outlines when the network blob is missing or damaged), sticky notes, shapes with text, connectors, tables, code blocks, slides and similar layers import as the closest frames, text layers and vectors with a warning, and set-variable, set-variable-mode and conditional prototype actions come across. `file.open`, - `file.import`, drag and drop, the CLI and MCP's `open_file` read `.fig`; the file is never saved - over. -- **Landed (2026-10-12):** an experimental export: `file.export {format: "fig"}`, File ▸ Export… ▸ - .fig (experimental), `figcraft-cli export --format fig` and MCP's `export` write the whole document - with a kiwi encoder mirroring the decoder and a schema of the fields written: the layer tree, - geometry, paints with image bytes, strokes, effects, text with styled runs, vector networks, auto - layout, constraints, components, sets, instances with their overrides and property values, and - prototype interactions; shared style links, variables, comments, Dev Mode data and the rest are - left out with a warning ([file-format-parity](file-format-parity.md#export-to-fig-experimental)). - The import gained ellipse arcs, boolean operation types and component set descriptions. Then - (2026-10-12): written files were checked by opening them in other software: layers, fills, - gradients, strokes, shadows, vectors, components and text draw as in FigCraft, after the export - learned to say that the document and its pages are visible and opaque, to number its schema as - the format does, to write outlines, text settings, standard font style names and line height - units. Then (2026-10-13): instances keep their own size (an override addressed by the main's id, - derived data for their layers, a scale factor), variables with modes and bindings, shared styles - and variable-width strokes are written and read back; checked there: resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width; number, string and boolean bindings and bindings on effects, layout guides and gradient stops come across as the values they give but did not show as bound there (their entries now also name the layer field they set, not yet checked there), and strokes wider on one side of the path are written as filled outlines, no longer editable strokes there (the stroke itself is kept as plugin data and our own import puts it back; not yet checked there), - like effects such as glass and noise, images and prototype interactions. -- **Missing:** connector labels, elbowed or curved connectors without a saved path (drawn straight), - table cells beyond their own layers, filled areas of a vector with their own fills, per-point stroke - joins, variables bound to fields the editor can't bind, fixed or content-sized grid tracks and video - fills aren't read; text lays out again with - local fonts. Nothing has been checked against real-world files yet (gap 3). The export is checked - only against our own importer: whether other apps open its files is unknown. -- **Evidence:** fixtures generated in code (`crates/format/src/fig/tests.rs` and `tail_tests.rs`: kiwi - round trips, every mapped feature, vector network blobs, truncation, bit flips and junk never panic); `crates/engine/src/fig_import_tests.rs`. - Export: 60 generated documents round trip through the importer layer by layer, written files - decode with the generic decoder and list parents first, the encoder agrees with the decoder, - junk documents never panic (`crates/format/src/fig/export_tests.rs`, - `crates/engine/src/fig_export_tests.rs`). -- **Impact:** existing work can be brought across, but only real files will show how much of it. -- **Estimate:** 5–10 h for the read tail (real-file fixes, connector labels, table cells); checking the export against real files a further 5–15 h (with gap 3). - -### 2. Multiplayer and sharing are a preview, without accounts -- **Missing:** accounts, teams and projects, per-person permissions, audio, - shared comments and version history. Comments and versions are local to the file. -- **Evidence:** the core landed in #103 (`crates/collab`: client-scoped ids, change sets, server - ordering and validation, client reconciliation, binary messages, randomized convergence tests over - real commands), and live sessions run over the network (`crates/collab-net`: hosting from the - app, invite links that open the app, document sync between apps, live cursors, selections, - text carets, avatars and follow, cursor chat and spotlight, and the browser build joins) with per-person undo, refused - edits rolled back with a reason, reconnects that resume and send what was made offline, and - styles, variables and comments that several people create at once all kept; sessions are per - document (each open file has its own or none, several at once, closing a file ends only its - own); files are shared with a short join code (online through the server in Preferences, or on - the local network with a code that carries the host's address) and joined by typing it; the app - remembers the files it put online and lists them on the home screen (open again as owner, stop - and start sharing, rename, remove, forget), per computer; a standalone - `figcraft-server` keeps many files (snapshot plus log, compacted; restarts lose nothing - confirmed), takes files published from the app and hands out share links that let people edit - or only view; the desktop app and the browser reach it over `wss://` through a TLS reverse - proxy, and releases ship it with a deployment guide (`docs/server.md`). But there are no - accounts (a link is the access) (`docs/collab-protocol.md`). -- **Impact:** Figma's defining feature; most teams use it every day. Solo designers can work - without it. -- **Estimate:** 42–84 h: accounts, teams and per-person permissions, shared comments and versions - (numbers from before the server landed; re-measured next). - -### 3. Nothing checked against real Figma files -- **Missing:** a corpus of real documents (design systems, app screens, prototypes) imported and - compared with Figma's own PNG/SVG/PDF exports. -- **Evidence:** every corpus in the repo is synthetic (`crates/svg/tests/corpus`, the synthetic - design system in `crates/engine/src/synthetic_ds.rs`). Scores were self-graded by the agents that - built the features. -- **Impact:** unknown fidelity in auto layout sizing, text metrics, effects and component - resolution on real files; the numbers in this repo can't be trusted beyond "estimated" until it - exists. -- **Measured so far:** text box sizes and auto layout results of our own example documents, - against the same documents re-saved by other software (which lays text out again). Three text - rules explained every difference and are applied (whole-pixel line heights, advances on a - 1024-per-em grid, letter spacing between glyphs only): all 230 self-sizing text boxes in 7 - documents now come out to the pixel (24 of 131 before, in the design-system example), and 208 of - its 231 other layers match in size and position (115 before). The other 23 hang on one grid - auto layout frame whose row positions came back as written while its cells got shorter. One font - family (Inter), left-aligned text; real files, other fonts, effects and component resolution are - still unmeasured (`crates/text/src/metrics_tests.rs`, and a local check over such documents in - `crates/engine/src/text_metrics_local_tests.rs`). -- **Estimate:** 15–30 h once `.fig` or REST import of real files is available; needs files whose - licence allows use (owner to supply or approve; never committed if not redistributable, see - craftrules `standards/test-corpora.md`). - -### 4. Interaction fidelity not compared tool by tool -- **Missing:** a side-by-side pass of every tool and panel: cursors, modifier keys while dragging, - hover states, popovers, menus, snapping thresholds. -- **Landed (2026-10-11):** behaviour suites written from the public documentation for auto layout, - selection and transform, text, and canvas navigation with rulers, guides, layout grids, outline - mode and measuring, then (2026-10-12) comments and version history, and the layers and Assets - panels (`crates/engine/src/*_behaviour_tests.rs`, `crates/ui-egui/src/layers_behaviour_tests.rs`), - each with its fixes; then keyboard shortcuts per platform (`shortcut_parity_tests.rs`: 129 - documented rows, 126 bound, 1 moved off a documented conflict, 2 missing, 48 undocumented - bindings pinned), with 16 rows newly bound, Control suspending snapping on macOS and shifted - symbols on other keyboard layouts; then the everyday partials of - [`ui-parity.md`](ui-parity.md): number fields, outline mode's options, Hide when publishing, a - cursor for every tool and modifier, panel widths, dark pages (`field_tests`, `cursor_tests`, - `cursors_tests`, `outline_options_tests`, `hide_publishing_tests`, `panel_width_tests`, - `theme_pages_tests`). -- **Evidence:** ROADMAP item 7 ("Left: tool by tool, panel by panel against the incumbent"). -- **Impact:** Figma users notice the feel within minutes; it decides whether they stay. -- **Estimate:** 40–60 h. Detail in [`ui-parity.md`](ui-parity.md). - -### 5. Localization: English, reviewed Russian, plus unreviewed drafts -- **Missing:** native reviews of the Spanish, French, German, Brazilian Portuguese, Japanese, - Korean and Simplified Chinese drafts and the Latin American Spanish overlay, the 25 newest strings - in five drafts, Hindi, Arabic, Indonesian and Vietnamese, localized engine messages and number - formatting; CJK UI text on the web without the craft-fonts CJK build. -- **Evidence (measured from source):** the layer landed 2026-10-11: catalogs in - `crates/ui-egui/locales/` (1,371 UI keys plus 524 command labels and 29 menu wordings), - Preferences ▸ Language, `pref.language`; `en.toml` is complete, and `es.toml`, `fr.toml`, - `de.toml`, `pt-BR.toml`, `ja.toml`, `ko.toml` and `zh-Hans.toml` (2026-10-12) are machine drafts - (all but `zh-Hans.toml` lack the 25 newest strings), with `es-419.toml` a Latin American overlay - listing only what differs from Spanish (Spanish and Russian are held to completeness by the check; the - others show English for strings added later). Russian adds all 2,165 current entries, - reviewed and approved by VanTan, with one/few/many integer plurals. - CJK UI text uses an installed CJK face the desktop app loads when something on screen needs it - (`crates/ui-egui/src/cjk.rs`). Engine error messages and Dev Mode values stay English. -- **Impact:** non-English designers get machine drafts nobody has reviewed yet, and English - errors. -- **Estimate:** 3–7 h per language plus native review, 4–6 h for engine messages and numbers; - 29–51 h for the key languages left and the drafts' follow-ups. Detail in - [`localization-parity.md`](localization-parity.md); how to add one in - [`translating.md`](translating.md). - -### 6. Plugins: widgets take clicks only, a self-hosted directory but no public catalogue; Figma's plugins don't run -- **Landed (#101):** sandboxed WebAssembly plugins (wasmi, metered, memory cap, no WASI, also in - the browser) acting through the command registry, one undo step per run, manifest permissions, - Plugins menu, Run last plugin, Manage plugins, `plugin.*` commands, a Rust SDK and two examples - ([`plugins.md`](plugins.md)). Plugin panels and settings (ABI v2, 2026-10-11): a plugin shows a - panel of fields and buttons drawn with FigCraft's widgets and is called back for each button or - changed value (one undo step per callback, same sandbox limits), driven headless with - `plugin.ui` / `plugin.event` / `plugin.close`; per-plugin settings kept with the preferences. - Widgets (plugin API 3, 2026-10-12): a plugin draws a layer of its own on the canvas (Plugins ▸ - Widgets, `widget.insert`), validated and built with locked layers inside, called back for clicks - on the parts it names (one undo step each, a fresh metered sandbox, the state kept in the layer - so it saves, copies, undoes, exports and syncs), `widget.click` / `widget.list` for agents, a - Counter example; panels of several plugins side by side; the colour picker on plugin colour - fields. A plugin directory on `figcraft-server` (2026-10-12): bundles published with the server - key and checked in full (`figcraft-cli plugin publish`), browsed, installed after a permissions - confirmation and updated from Plugins ▸ Manage plugins ▸ Browse, downloads checked against their - SHA-256, `plugin.directory.*` for scripts and MCP, moderation with `figcraft-server plugins`. -- **Missing:** a public catalogue and Community files, publisher accounts and signatures, ratings - and install counts, background update checks, the directory in the browser build; widgets take only clicks (no - text input, property menus, hover or timers) and draw only frames, text, shapes and images; - no images or sliders in panels. Figma's own plugins are JavaScript against Figma's plugin API and - don't run. -- **Impact:** teams that depend on specific Figma plugins (icons, content, accessibility checks) - can't bring them. FigCraft's CLI, control channel and MCP server cover agent automation better - than Figma does. -- **Estimate:** 8–26 h for the missing pieces of FigCraft's own API. Running Figma's plugins - needs a JS engine and an API shim: **owner decision needed**. - -### 7. CPU canvas where Figma uses the GPU -- **Missing:** GPU rendering of the canvas. FigCraft renders with tiny-skia on the CPU (banded, - parallel, retained) and shows the image through wgpu. A new zoom can't reuse the retained - image; when renders from scratch take a frame or more, the whole view is previewed with a - sixteenth of the pixels first, then refined. -- **Evidence (measured, `cargo xtask bench`):** a 50k-layer page ~8 ms zoomed out with levels of - detail, single-layer edits at 10k layers ~0.2 ms; full 1440×900 renders 7–15 ms at 10k layers. - Heavy effects (glass, background blur, noise) and 4K/5K canvases at 2x are where CPU time grows. -- **Impact:** large files and effect-heavy designs; battery on laptops. -- **Estimate:** 30–60 h for a GPU path (vello or wgpu shaders) held to the CPU renderer as the - oracle. - -### 8. Platform QA: Linux and the browser left -- **Missing:** regular runs on Linux and the browser; real screen readers; a repeat of the Windows - pass on other machines (scale factors above 100%, pen tablets, file drops from Explorer). -- **Evidence:** CI tests on Linux only (`.github/workflows/ci.yml`); release builds exist for every - platform (v0.2.0) but no issue has been filed yet. Windows: QA pass done 2026-10-11, findings in - PR (Windows 11, the desktop app driven over the control channel, the same workflow through MCP): - eight bugs fixed, among them unsaved changes lost when the app was killed or crashed, - Ctrl+arrows and Ctrl+Backspace acting on the whole line in text, scrubbed fields snapping back - and skewed linear gradients. -- **Impact:** unknown bugs on the platforms where FigCraft is most attractive (Linux has no Figma - desktop app). -- **Estimate:** 10–20 h, partly human (hardware). - -### 9. Figma Draw: tablet pressure on macOS and Linux -- **Missing:** pen pressure on macOS and Linux tablets (winit reports none there). -- **Landed (2026-10-12):** multi-edit: several vector layers in edit mode at once (Enter or - double-click with them selected, `vector.edit.begin {ids}`); points selected, marqueed, dragged, - nudged, deleted, mirrored and rounded across layers in one undo step; the pen, bend, paint bucket, - cut and eraser work in the layer last pressed (`vector_multi_behaviour_tests`). -- **Landed:** text on a path (`text.onPath`, `text.pathOptions`, `text.removeFromPath`; the - Design panel's Typography section; canvas, PNG, SVG and PDF as outlines; SVG `` imports - as text on a path), pattern fills (`PaintKind::Pattern`, `fill.pattern`, the colour picker's - Pattern tab; SVG ``, PDF tiling patterns), stretch and scatter brushes (`stroke.brush`, - eight presets drawn in code; canvas, PNG, SVG, PDF and outline stroke) and pen pressure for the - Pencil (Windows pens and browser styluses; recorded as the stroke's width profile, which brushes - follow). -- **Evidence:** winit reports no pressure on macOS and Linux tablets. -- **Impact:** illustration-flavoured work moved into Figma Draw in 2025. -- **Estimate:** 1–3 h once winit (or a platform tablet API behind it) reports pressure there. - -### 10. Team libraries: no team-wide lists or accounts; analytics are counts -- **Missing:** libraries listed for a whole team or organization (each file enables a library by - its link), accounts and per-person library permissions, analytics beyond instance counts per - component and file (insertions and detaches over time), update notices pushed by the server - (apps ask every 30 seconds while the Assets panel shows). -- **Evidence:** a file's library is published to `figcraft-server` under the file and enabled in - other files by a share link, with per-asset versions, updates with overrides kept, usage counts - and swapping a whole library by names ([`server.md`](server.md#team-libraries), - [`components-parity.md`](components-parity.md)). -- **Impact:** design-system teams. -- **Estimate:** 4–8 h, the accounts part after gap 2. - -### 11. Spell check -- **Missing:** spelling underline and suggestions while editing text. -- **Evidence:** no spell-check code in the repo. -- **Estimate:** 4–8 h with a permissively licensed dictionary and checker (provenance per - `ATTRIBUTION.md`). - -### 12. Video fills; HEIC images -- **Landed (2026-10-11):** animated GIF fills (the file kept as is, the first frame on the canvas - and in exports, played in the presenter and the preview) and TIFF images. -- **Missing:** Figma takes JPG, PNG, HEIC, WebP, GIF and TIFF images and MP4, MOV and WebM video - (help center, checked 2026-10-10). FigCraft decodes PNG, JPEG, WebP, GIF, TIFF and BMP; HEIC and - video need decoders that are C libraries (libheif, FFmpeg) or each platform's media framework. -- **Estimate:** 5–10 h (video playback in the presenter is most of it; it can reuse the GIF clock). - -### 13. Vector tail: none left -- **Landed (2026-10-12):** several Bezier handles selected with Shift-click and dragged together, - sharp points beside curved segments rounded, a stroke cap and join per point (canvas, PNG, SVG, - PDF, outline stroke), the Pen outside edit mode connecting to its own points and continuing a - selected open path. Left: corner smoothing beyond rectangles and frames, the transform box around - points of several layers, per-point caps and joins on dashed, variable-width and brush strokes. -- **Landed (2026-10-11):** boolean groups are clicked where their result is (a click in a - subtracted hole reaches what's behind; with the group or an operand selected its operands are - hit, so each can still be picked; "Select layer" lists the group only where its result is), live - boolean editing (operands selected by double-click or Cmd/Ctrl-click; - moving, resizing or editing an operand's points redraws the result during the drag, with every - operand outlined; per-operand operations from the boolean controls and `node.set`; a drag frame - recombines only the operands from the edited one up), Design panel Arc fields (Start, Sweep, - Ratio; Mixed across ellipses; bindable), the shape builder on strokes and open paths (strokes take - part as their outlined areas), one-sided variable widths (Alt-drag a width handle; canvas, PNG, - SVG, PDF and outline stroke). -- **Estimate:** none left (vector QA with real files is part of gap 3). - -### 14. Variables: extended collections on real libraries -- **Landed:** extended collections (`library.extendCollection`: a library collection's modes and - variables in a collection of this file, values overridden per mode with reset, following library - updates, variables of its own), variables import/export (`variable.export` / `variable.import`: - the REST local variables JSON, merged by name; CSS custom properties), gradient stops and variant - properties bound to variables. -- **Landed (2026-10-12):** variables a library adds to a collection this file extends are a library - update of their own (`library.updates` lists them by name, without an `id` until they have a - copy) and arrive inheriting when accepted; a library collection that is itself an extended - collection (a brand theme extending a core one) can be extended again, its values flowing down - the chain as each file accepts updates, every level's overrides kept. -- **Missing:** checked with synthetic files only; extending a collection of the same file isn't - offered (extending takes library collections). -- **Estimate:** 0–1 h. - -### 15. Prototyping: media triggers and actions, controllers off Windows -- **Landed (2026-10-12):** key/gamepad triggers take controller buttons (`Gamepad A` … `Gamepad - Home`, the standard layout's 17 buttons; a Controller menu beside the key field; REST files' - controller triggers import); the player reads controllers on Windows and in the browser (Gamepad - API) and takes `ui.input {gamepad}` on the control channel; widgets react to clicks in a - presented prototype, on the prototype's own copy; `proto.evaluate` computes expressions (mode - lookups included) where a layer's interaction would run (`crates/engine/src/proto_tail_tests.rs`, - `crates/ui-egui/src/present_tail_tests.rs`, `crates/format/src/rest/gamepad_tests.rs`). -- **Missing:** video triggers (on media hit/end) and play/pause/mute/seek actions (the REST - importer warns and skips them, `crates/format/src/rest/proto.rs`); controllers on macOS, Linux - and FreeBSD (the usual Rust controller library links libudev on Linux; a pure-Rust reader per - platform, or an owner decision on that dependency); PlayStation and Switch button labels. -- **Estimate:** 2–5 h, the media part after video fills (gap 12). - -### 16. Dev Mode: the tail after dev resources -- **Landed (2026-10-11):** code connect: a component or set links to its code component per - language (React, SwiftUI, Compose): an import path, the name and a template whose `{property}` - and `{property ? "a" : "b"}` placeholders take the instance's values, escaped per language and - never executed. `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet - instead of generated code (inside generated layer trees too); the panel's Connect to code edits - it with a live preview. Links are stored on the component (saved, synced, undoable) and travel - as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`, validated and capped) - so code owners can version them. Plugins whose manifest asks only to read run from the Inspect - tab. -- **Landed (2026-10-12):** dev resources: links for developers (a URL and a name) on any layer - (`devResource.add` / `edit` / `remove` / `list`, undoable, saved and synced). An instance also - lists its main component's links (and its set's) read-only with where they come from; layers - inside an instance show their main layer's. URLs are checked as web links (http or https, a - host, no spaces, capped) and never fetched: a link added without a name is named after its URL, - and a link opens only when clicked. The Inspect tab lists, adds, edits and removes them; - `get_node` / `inspect_document` return them as `devResources`. -- **Missing:** import lines for connected instances inside a - generated layer tree (only the selected layer's own import is shown); publishing mappings from - the code repository with a CLI. An editor extension is out of scope: the MCP server already - serves design data, code and the connected snippets to agents and editors. -- **Estimate:** 0–1 h. - -### 17. Branching and merging -- **Landed (2026-10-11):** branches of a file (`branch.*`): File ▸ Create branch… (or the Branches - list in version history) copies the file into a branch tab that remembers the file as it was (its - base) and makes its ids in its own id space; edits on the branch are saved with the main file. - Review and merge (the banner on a branch tab) shows the main file and the branch side by side or - as an overlay, what changed on each side since the base (layers with their property changes as - Inspect shows them; styles, variables, comments) and the conflicts: the same property changed on - both sides, or a layer deleted on one side while the other edited it or what's inside it. Each - conflict is resolved for the branch or main; nothing conflicting merges until it is. Merging is - one undo step on the main file and archives the branch; Update from main brings main's changes - into the branch the same way; branches can be archived without merging. Three-way merge over the - collaboration change sets (`figcraft_collab::merge`), fuzz-tested with random real commands. -- **Landed (2026-10-12):** a branch's own version history: versions saved on a branch tab (by hand - or autosaved) go into the branch, not the main file, and are viewed, restored (one undo step on - the branch), renamed, deleted and duplicated there; they are saved in the file with the branch. - Merging adds a version to the main file's history (Merged "name"); the merged or archived - branch keeps its own history and opens with it. Fills, strokes and effects merge item by item: - different items changed on each side, or items added at the end on one side, merge without a - conflict; only the same item changed both ways, an item removed on one side, or different items - added on both sides conflict (as the whole list). -- **Missing:** branches of a file in a live session are refused; other lists (text runs, export - settings, guides…) still conflict as a whole; the branch history has no entries of its own for - its creation or updates from main (only saved versions, autosaves and the merge on main). -- **Estimate:** 1–2 h. - -### 18. Display P3 on screen (several windows landed) -- **Landed (2026-10-11):** several windows: File ▸ New window, a tab's Open in new window or a tab - dragged off the strip; each window has its own tabs, panels and canvas and draws only when it - needs to; closing one asks about unsaved changes; `window.new`, `window.list`, `window.focus` and - `window` on any control request. -- **Missing:** wide-gamut colour on the canvas (egui draws sRGB, so P3 colours outside sRGB clip on - screen; exports keep them); a screen-reader tree in windows other than the main one (eframe 0.36 - gives only the main window one); window positions restored at the next start. -- **Estimate:** 4–8 h. - -### 19. AI features -- **Missing:** Figma AI (first draft, rename layers, remove background, image generation and - editing, visual search, Make). -- **Estimate:** 30–60 h with openly licensed local models. **Owner decision needed.** - -### 20. Mobile apps -- **Missing:** Figma's iOS and Android apps view and comment on files and mirror prototypes. - FigCraft's web build runs in mobile browsers but is not designed for touch. -- **Estimate:** 20–40 h for a touch-first viewer and mirror. - -### 21. Menu items without a command (measured): none left -- **Landed (2026-10-11):** `file.openRecent` (File ▸ Open recent: recent files kept in the - preferences, at most 20, greyed when the file is gone and dropped when picked, Clear recent), - `view.outlinesSelection` (outline mode for the selected layers only) and `view.maskOutlines`; - the plugin pair landed in #101. Every item in the menu catalog has a command - ([`parity-checklist.md`](parity-checklist.md)). -- **Estimate:** none left. - -### 22. Screen readers never run; browser has no accessibility tree -- **Evidence:** AccessKit names and keyboard navigation landed (#96), checked by tests only; - eframe's web backend doesn't send the tree yet. -- **Estimate:** 4–8 h plus human QA with VoiceOver, NVDA, Narrator and Orca. - -### 23. CMYK output intent for print PDFs -- **Evidence:** CMYK conversion is a documented naive formula; no CMYK ICC profile can ship in the - repository (licensing). -- **Estimate:** 2–4 h once an openly licensed profile is found. - -## Revision history - -| Date | Change | Summary | -|---|---|---| -| 2026-10-14 | minor | Russian catalog (M9), reviewed and approved by VanTan: all 2,165 current entries, Русский in Preferences and regional ru tags. Russian integer plurals use one/few/many; contributor display name VanTan added. Existing estimates unchanged. | -| 2026-10-13 | minor | Gap 1: the experimental `.fig` export writes instance sizes (resized, filling an auto layout, scaled), variables with modes and bindings, shared styles and variable-width strokes, and the import reads them back. Checked by opening written files in other software: resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width; number, string and boolean bindings and bindings on effects, layout guides and gradient stops come across as the values they give but did not show as bound there (their entries now also name the layer field they set, not yet checked there), and strokes wider on one side of the path are written as filled outlines, no longer editable strokes there (the stroke itself is kept as plugin data and our own import puts it back; not yet checked there). Hours and the ranking unchanged | -| 2026-10-12 | minor | Gap 2: your online files landed (the app records the files it put on a server with their owner tokens, Home ▸ Online lists them and opens them again as their owner, with sharing, rename, removal and forget from the list; one request per server tells which are still there; tabs mark files in a live session). What is left is unchanged: accounts, teams and per-person permissions, shared comments and versions. Hours unchanged | -| 2026-10-12 | minor | Gap 2: sharing with a join code (Share ▸ Online or Local network, one Start sharing button, a short code to give others, one join field for a code or a link; the server names links with codes and limits wrong ones) added to the landed list; usability, so the estimate and ranking are unchanged | -| 2026-10-12 | minor | Gap 3: first measured fidelity data. Text box sizes and auto layout results of our example documents were compared with the same documents re-saved by other software; three text rules (whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only) were found and applied, and all 230 self-sizing text boxes in 7 documents now match to the pixel (one font family). Real files are still unchecked: hours and the ranking unchanged | -| 2026-10-12 | minor | Gap 2: live sessions are per document (each open file has its own session or none, several run at once, the window and the `collab.*` commands follow the file in front, closing a file ends its session only) added to the landed list; a fix, so the estimate and ranking are unchanged | -| 2026-10-12 | minor | Gap 1: the experimental `.fig` export now opens elsewhere: written files were checked by opening them in other software: layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft; effects such as glass and noise, images and prototype interactions are still unchecked there, and variables are not written. Hours and the ranking unchanged | -| 2026-10-12 | minor | Gap 1: an experimental `.fig` export landed (the whole document, from the same public format notes as the import; checked only by round trips through our own importer); export hours 15–30 h → 5–15 h (checks against real files); gap 1's read tail (5–10 h) and the ranking unchanged | -| 2026-10-12 | minor | Gap 4: keyboard shortcut parity suite per platform (129 documented rows: 126 bound, 1 moved, 2 missing; 48 undocumented bindings pinned) added to the landed list; estimate and ranking unchanged | -| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Gap 5 31–54 h → 29–51 h, rank unchanged | -| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Gap 5 32–55 h → 31–54 h, rank unchanged | -| 2026-10-12 | minor | Gap 15: controller buttons on key/gamepad triggers (Windows and the browser), widgets reacting in prototypes and `proto.evaluate` landed; gap 15 now media plus controllers off Windows, 3–6 h → 2–5 h; ranking unchanged | -| 2026-10-12 | minor | Plugin directory on `figcraft-server` (publish with the server key, browse, install after a permissions confirmation, hash-checked downloads, updates by version, operator moderation) added to gap 6's landed list; gap 6 10–30 h → 8–26 h; ranking unchanged | -| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Gap 5 40–65 h → 32–55 h, rank unchanged | -| 2026-10-12 | minor | Plugin widgets (API 3) and the panel tail (panels side by side, colour picker on plugin fields) added to gap 6's landed list; gap 6 now 10–30 h; ranking unchanged | -| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): every UI string, command label and menu wording (1,899 entries) machine-drafted in each and marked as drafts awaiting native review, falling back to English per key (new strings need only English and Spanish); 0 counts as singular in French and Brazilian Portuguese; System picks the first OS language with a catalog (pt-PT and pt read pt-BR); long translations elide with a tooltip in dropdowns, two-column labels and the home cards. Gap 5 45–75 h → 40–65 h, rank unchanged | -| 2026-10-12 | minor | Cursor chat and spotlight in live sessions (gap 2): `/` opens a chat bubble at your pointer, a spotlight makes everyone follow your view; ephemeral presence the host validates and rate-limits. Gap 2 45–90 h → 42–84 h | -| 2026-10-12 | minor | Layers and Assets panels behaviour suite and fixes added to gap 4's landed list; ranking and hours unchanged | -| 2026-10-12 | minor | Vector multi-edit and the vector edit tail, with a behaviour suite written from the public documentation (5 tests, about 30 table rows and steps, and a junk-parameter sweep of every vector command in multi-edit mode): several vector layers are edited at once (Enter, double-click or `vector.edit.begin {ids}`; a press on another layer's point, handle or segment makes it the active layer; Shift-click, marquee and lasso select across layers; dragging, the arrows, Delete, delete and heal, mirroring, smoothing, corner radius and point caps and joins reach every layer's selected points in one undo step; the pen, bend, paint bucket, cut and eraser work in the active layer); several Bezier handles are selected with Shift-click and dragged together (`vector.edit.moveHandles`); a sharp point next to a curved segment rounds (the curve is cut where the arc meets it and keeps its shape); a point can have its own stroke cap and join (`vector.edit.strokeCap`, `vector.edit.strokeJoin`, the Point section of the Design panel; canvas, PNG, SVG and PDF, outline stroke); the Pen outside edit mode connects to one of its own points and ends the path, and continues a selected open path from an end point. Left: corner smoothing beyond rectangles, the transform box around points of several layers, per-point caps and joins on dashed, variable-width and brush strokes. Gap 9 2–5 h → 1–3 h (pen pressure on macOS and Linux tablets left, blocked on winit). Shapes & vector 98%/72% → 99%/73%, 2–4 h → 1–3 h; Figma Draw extras 80%/55% → 88%/60%, 2–5 h → 1–3 h (gap 9: pen pressure on macOS and Linux tablets left). Totals recomputed by the doc's method, which rows since the last recompute had left stale: the area table's weighted breadth is 83.7% (~83%; core 94.1%, ~94%) and ready 63.1% (~63%; core 72.1%, ~72%), hours 133–284 (core 38–83); Features (depth) 61% → 63%, 46–94 h → 34–76 h; ready for real work 52.6% (~52%); mainstream weekly areas 71.0% → 72.1% (× 0.9 × 0.9 × 0.85 = 49.7%, ~49%, rounded down); essentials unchanged (~57%); full parity ~306–595 h → ~294–572 h (on top of the rows merged since: ~301–587 h → ~289–564 h; area-table totals recomputed on the merged table and rounded down: breadth ~84%, ready ~63%, core ~94%/~72%, mainstream weekly areas 72.4%, ~49%) | -| 2026-10-12 | minor | Dev resources (gap 16): links for developers (a URL and a name) on any layer: `devResource.add` / `edit` / `remove` / `list`, undoable, saved and synced; an instance also lists its main component's (and that component's set's) links read-only with where they come from, and layers inside an instance show their main layer's; URLs are checked as web links (http or https, a host, no spaces, at most 2,048 characters) and never fetched (a link added without a name is named after its URL), opened only when clicked; the Inspect tab lists, adds, edits and removes them; `get_node` and `inspect_document` return them as `devResources`. The editor extension stays out of scope (the MCP server serves agents and editors). Dev Mode breadth 93% → 96%, ready 70% → 72%, 1–3 h → 0–1 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~304–593 h → ~303–591 h | -| 2026-10-12 | minor | Extended collections' tail (gap 14): variables a library adds to a collection this file extends now count as a library update of their own (listed by name without a copy yet) and arrive inheriting when accepted, instead of waiting for the library's next other update; a library collection that is itself an extended collection can be extended again, values flowing down the chain as each file accepts updates with every level's overrides kept (checked end to end). Styles & variables breadth 94% → 95%, ready unchanged (76%), 3–5 h → 2–4 h; gap 14 1–2 h → 0–1 h (real-library checks left); feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~305–594 h → ~304–593 h | -| 2026-10-12 | minor | A branch's own version history and finer list merges (gap 17): versions saved or autosaved on a branch tab go into the branch (viewed, restored, renamed, deleted and duplicated there, saved in the file with it); merging adds a version to the main file's history and the merged or archived branch keeps its own; fills, strokes and effects merge item by item, conflicting only when the same item changed both ways, an item was removed on one side or both sides added different items. Comments, versions, find, misc breadth 85% → 87%, ready 60% → 61%, 2–5 h → 1–4 h; gap 17 1–3 h → 1–2 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~306–595 h → ~305–594 h | -| 2026-10-12 | minor | Comments and version history behaviour suite and fixes added to gap 4's landed list (gap 17 unchanged: a branch's own version history is still left); ranking and hours unchanged | -| 2026-10-11 | minor | `.fig` import's tail (gap 1): vectors open as editable vector networks (points, curved segments, filled areas with their winding, per-point corner radius and handle mirroring, end caps from the end points), falling back to outlines when the network is missing or damaged; sticky notes, shapes with text, connectors, tables, code blocks, slides and similar layers import as the closest frames, text layers and vectors with a warning instead of being dropped; set-variable, set-variable-mode and conditional prototype actions come across. New parsers fuzzed on fixtures generated in code. File formats 55% → 58%, 8–15 h → 5–10 h; import/export breadth 80% → 82%, ready 50% → 54%; feature and area totals and ready for real work (~49%) unchanged; beta ~173–310 h → ~170–305 h; full parity ~321–618 h → ~318–613 h | -| 2026-10-11 | minor | `.fig` import, best effort (gap 1): `.fig` files open (best effort): the zip, its deflate/zstd chunks and the kiwi schema are decoded generically and mapped by field name onto the REST importer, so pages, the layer tree, geometry, paints (styles resolved), strokes, effects, text with styled runs, vectors (as outlines), auto layout (grid included), constraints, components, sets, instances (expanded from their main with overrides, derived layout and property values), styles, variables with modes and bindings, prototyping and image fills from the archive come across; anything else is a warning. Gap 1 30–60 h → 8–15 h (vector networks, other layer kinds, real-file checks; export 15–30 h on top, as before) | -| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | -| 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | -| 2026-10-11 | minor | Localization layer landed (gap 5): per-language catalogs baked in at build time with English text as keys and fallback, placeholders and plurals, command labels by id, Preferences ▸ Language (System follows the OS or the browser) applied live and `pref.language`; English catalog and a complete Spanish draft awaiting native review. Localization 10% → 20%, 60–100 h → 45–75 h; ready for real work unchanged (~47%); full parity ~363–697 h → ~348–672 h | -| 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | -| 2026-10-11 | minor | Plugin panels and settings landed (gap 6, ABI v2; v1 plugins unchanged): a plugin shows a panel of notes, fields (text, number, checkbox, menu, list, colour) and buttons drawn with FigCraft's widgets; a button or a changed value calls it back with every value (same limits per callback, one undo step per callback that edits, rolled back on failure), its module staying loaded while the panel is open; `plugin.ui` / `plugin.event` / `plugin.close` drive panels headless; per-plugin settings (64 KB) kept with the preferences; the Swatch grid example gained a palette panel that remembers the last palette. Plugins & widgets breadth 35% → 45%, ready 20% → 28%, 25–55 h → 15–40 h; ecosystem 20% → 28%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~373–712 h → ~363–697 h | -| 2026-10-11 | minor | Canvas fixes (gap 13 closed): boolean groups are clicked where their result is (a click in a subtracted hole reaches what's behind; with the group or an operand selected its operands are hit, so each can still be picked), a layer dragged inside a group stays in the group (frames and auto layout still take other drops), View ▸ Frame outlines and View ▸ Slices draw on the canvas, and the idle tests wait for the render thread instead of counting frames. Shapes & vector 1–3 h → 1–2 h (vector QA with real files left); percentages, feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~378–722 h → ~375–717 h | -| 2026-10-11 | minor | Stretch and scatter brushes and pen pressure landed (gap 9): eight brushes drawn in code (four stretched along the path, four scattered with spacing, size, rotation and scatter jitter from a seed) in the stroke settings, drawn everywhere strokes are and flattened by outline stroke; the Pencil records pen pressure as the stroke's width profile (Windows pens, browser styluses; macOS and Linux tablets report none) and can draw with a brush; Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–9 h; Strokes 2–4 h → 1–3 h; Hardware 50% → 55%, 15–30 h → 13–27 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~378–722 h → ~373–712 h | -| 2026-10-11 | minor | Text on a path and pattern fills landed (gap 9): text follows a path or shape (start, side, alignment; edited in place; canvas, PNG, SVG and PDF; SVG `` imports as text on a path) and pattern fills tile a layer (grid or half-shifted rows or columns, scale, spacing, alignment; live as the source changes; SVG ``, PDF tiling patterns); Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–10 h; Text 8–14 h → 7–13 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~375–715 h | -| 2026-10-11 | minor | Team libraries on `figcraft-server` landed (gap 10): a file's library is published to the server under the file and enabled in other files by a share link (per-asset versions; updates noticed in the background and accepted with overrides kept), the server counts which files use which components, and a whole library can be swapped for another by names; Team libraries breadth 40% → 70%, ready 25% → 40%, 10–20 h → 4–8 h; collaboration 12% → 13%, 55–110 h → 49–98 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~380–725 h → ~375–715 h | -| 2026-10-11 | minor | Branches landed (gap 17): File ▸ Create branch… copies the file into a branch tab that remembers its base; Review and merge shows both sides' changes (layers with Inspect-style property changes, styles, variables, comments) and the conflicts (the same property changed on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merging is one undo step and archives the branch; Update from main; branches are saved in the `.figcraft` file. Comments, versions, find, misc breadth 80% → 85%, ready 58% → 60%, 4–8 h → 2–5 h; collaboration 12% → 13%; menu breadth 204/204 (the catalog gained File ▸ Create branch…); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~378–722 h | -| 2026-10-11 | minor | Animated GIF fills and TIFF images (gap 12, without video and HEIC): GIF fills keep the original file, show the first frame on the canvas and in exports and play in the presenter and the preview (frames decoded on first play, at most 500 frames and 128 MB; always looping; the window wakes only for a GIF's next frame); TIFF images open, import, place and paste. Paint breadth 85% → 87%, 5–9 h → 4–7 h; gap 12 6–12 h → 5–10 h (video playback and HEIC left, both needing non-Rust decoders); feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | The last three catalogued menu items got commands (gap 21): File ▸ Open recent (recent files kept in the preferences, at most 20, a file that's gone greyed in the menu and dropped when picked, Clear recent), View ▸ Outlines ▸ Show outlines for selection and View ▸ Mask outlines: menu breadth 98.5% → 100% (203/203); Canvas & navigation breadth 95% → 97%, 3–6 h → 2–5 h; feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | Extended collections, variables import/export and gradient stop and variant property bindings landed (gap 14): Styles & variables breadth 85% → 92%, ready 70% → 72%, 4–8 h → 1–2 h; feature breadth unchanged (~79%); ready for real work unchanged (~47%); full parity ~385–730 h → ~380–725 h | -| 2026-10-11 | minor | Collaboration wording after the standalone server: `figcraft-server` keeps many files, persisted as a snapshot plus a log of what changed (compacted, checksummed, damaged files skipped and reported), restarts lose nothing confirmed and apps resume where they were; the app publishes its file to it; share links per file let people edit or only view (the server refuses a viewer's edits), are revocable and stored as hashes; TLS comes from a reverse proxy (the desktop app has none of its own yet); percentages and hours unchanged until the next re-measure | -| 2026-10-11 | minor | Live boolean editing, Design panel arc fields, the shape builder on strokes and open paths and one-sided variable widths landed (gap 13): Shapes & vector breadth 92% → 97%, ready 66% → 68%, 8–14 h → 1–3 h; feature breadth ~78% → ~79%; ready for real work unchanged (~47%); full parity ~390–745 h → ~385–730 h | -| 2026-10-11 | minor | Collaboration wording after the collaboration polish: styles, variables, collections, modes and comments created in a live session take ids from their creator's id space (concurrent creations no longer overwrite each other; the server checks it), others' text carets and selections show on the canvas, and typing in a layer someone else is typing in says so; percentages unchanged | -| 2026-10-11 | minor | Wording only: live sessions (#104), then the browser joining them and `figcraft://` invite links opening the desktop app; percentages and hours unchanged until the next re-measure | -| 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 | minor | Marked beta blockers; no alpha blockers (the alpha gate passes) | -| 2026-10-10 | major | Created from the 2026-10-10 re-measure against Figma desktop 126.10.7 (replaces the gap list that lived in ROADMAP.md) | +# Where FigCraft falls short of Figma + +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (Outline stroke preserves stroke holes; reviewed Russian catalog and integer plurals; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail) · **Target:** Figma Design (desktop app 126.10.7) + +Every known shortfall against Figma, one entry each, ranked by how much it stops a Figma user from +doing real work in FigCraft. Agents: pick work from the top unless the owner says otherwise. The +numbers behind the ranking are in [`target-app-parity.md`](target-app-parity.md). Hours are one +Opus 5.5 agent working sequentially, tests included, calibrated as described there. "Estimated" +everywhere unless marked measured. + +## Ranked + +No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passes. Beta blockers are marked. + +| # | Gap | Kind | Hours | Doc | +|---|---|---|---|---| +| 1 | `.fig` files: best-effort import with editable vector networks and stand-ins for non-design layers landed, and an experimental export (round trips through our own importer; written files opened in other software, where layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft, instances keep their size and variables with modes and colour styles come across); real-file checks and the small tail left (**beta blocker** until checked on real files, gap 3) | File format | 5–10 | [file-format-parity](file-format-parity.md) | +| 2 | Multiplayer and sharing are a preview, without accounts (**beta blocker**) | Feature | 42–84 | [collaboration-parity](collaboration-parity.md) | +| 3 | Nothing checked against real Figma files | Fidelity | 15–30 | [target-app-parity](target-app-parity.md) | +| 4 | Interaction fidelity not compared tool by tool | UI/UX | 40–60 | [ui-parity](ui-parity.md) | +| 5 | Localization: English, reviewed Russian, plus unreviewed Spanish (and Latin American Spanish), French, German, Brazilian Portuguese, Japanese, Korean and Simplified Chinese drafts | Localization | 29–51 | [localization-parity](localization-parity.md) | +| 6 | Plugins: widgets take clicks only, a self-hosted directory but no public catalogue; Figma's plugins don't run | Ecosystem | 8–26 | [target-app-parity](target-app-parity.md) | +| 7 | CPU canvas where Figma uses the GPU | Hardware/performance | 30–60 | [hardware-parity](hardware-parity.md) | +| 8 | Platform QA: Linux and the browser left (Windows pass done 2026-10-11) | Stability/platforms | 10–20 | [hardware-parity](hardware-parity.md) | +| 9 | Figma Draw: pen pressure on macOS/Linux tablets (multi-edit, brushes, pressure, text on a path and pattern fills landed) | Feature | 1–3 | [target-app-parity](target-app-parity.md) | +| 10 | Team libraries: no team-wide lists or accounts; analytics are counts | Feature | 4–8 | [components-parity](components-parity.md) | +| 11 | Spell check | Feature | 4–8 | [target-app-parity](target-app-parity.md) | +| 12 | Video fills; HEIC images | File format | 5–10 | [file-format-parity](file-format-parity.md) | +| 13 | Vector tail: outline-stroke compositing (stroke rings preserved) | Feature | — | [target-app-parity](target-app-parity.md) | +| 14 | Variables: extended collections checked on real libraries (the tail landed 2026-10-12) | Feature | 0–1 | [components-parity](components-parity.md) | +| 15 | Prototyping: media triggers and actions; controllers on macOS, Linux and FreeBSD (controller buttons on key/gamepad triggers, widgets in prototypes landed) | Feature | 2–5 | [prototyping-parity](prototyping-parity.md) | +| 16 | Dev Mode: import lines inside generated layer trees; the editor extension is left to the MCP server (code connect, Dev Mode plugins and dev resources landed) | Feature | 0–1 | [dev-mode-parity](dev-mode-parity.md) | +| 17 | Branching: branches in live sessions, lists other than fills, strokes and effects merged whole (a branch's own version history and per-item list merges landed 2026-10-12) | Feature | 1–2 | [collaboration-parity](collaboration-parity.md) | +| 18 | Display P3 on screen: blocked upstream (egui-wgpu picks an 8-bit sRGB surface); gamut warning and soft-proof sRGB landed, as did several windows | Hardware | 4–8 | [hardware-parity](hardware-parity.md) | +| 19 | AI features | AI | 30–60 | [target-app-parity](target-app-parity.md) | +| 20 | Mobile apps (view, mirror, comment) | Platforms | 20–40 | [hardware-parity](hardware-parity.md) | +| 21 | Menu items without a command: none left (landed 2026-10-11) | Feature | — | [parity-checklist](parity-checklist.md) | +| 22 | Screen readers never run; browser has no accessibility tree | UI/UX | 4–8 | [ui-parity](ui-parity.md) | +| 23 | CMYK output intent for print PDFs | File format | 2–4 | [file-format-parity](file-format-parity.md) | + +## Entries + +### 1. `.fig` files: the import's tail +- **Landed (2026-10-11):** `.fig` files open (best effort): the zip, its deflate/zstd chunks and the kiwi schema are decoded generically and mapped by field name onto the REST importer, so pages, the layer tree, geometry, paints (styles resolved), strokes, effects, text with styled runs, vectors, auto layout (grid included), constraints, components, sets, instances (expanded from their main with overrides, derived layout and property values), styles, variables with modes and bindings, prototyping and image fills from the archive come across; anything else is a warning (`crates/format/src/fig`). Then (2026-10-11): vectors open as editable vector networks (per-point corner radius, handle mirroring, end caps; outlines when the network blob is missing or damaged), sticky notes, shapes with text, connectors, tables, code blocks, slides and similar layers import as the closest frames, text layers and vectors with a warning, and set-variable, set-variable-mode and conditional prototype actions come across. `file.open`, + `file.import`, drag and drop, the CLI and MCP's `open_file` read `.fig`; the file is never saved + over. +- **Landed (2026-10-12):** an experimental export: `file.export {format: "fig"}`, File ▸ Export… ▸ + .fig (experimental), `figcraft-cli export --format fig` and MCP's `export` write the whole document + with a kiwi encoder mirroring the decoder and a schema of the fields written: the layer tree, + geometry, paints with image bytes, strokes, effects, text with styled runs, vector networks, auto + layout, constraints, components, sets, instances with their overrides and property values, and + prototype interactions; shared style links, variables, comments, Dev Mode data and the rest are + left out with a warning ([file-format-parity](file-format-parity.md#export-to-fig-experimental)). + The import gained ellipse arcs, boolean operation types and component set descriptions. Then + (2026-10-12): written files were checked by opening them in other software: layers, fills, + gradients, strokes, shadows, vectors, components and text draw as in FigCraft, after the export + learned to say that the document and its pages are visible and opaque, to number its schema as + the format does, to write outlines, text settings, standard font style names and line height + units. Then (2026-10-13): instances keep their own size (an override addressed by the main's id, + derived data for their layers, a scale factor), variables with modes and bindings, shared styles + and variable-width strokes are written and read back; checked there: resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width; number, string and boolean bindings and bindings on effects, layout guides and gradient stops come across as the values they give but did not show as bound there (their entries now also name the layer field they set, not yet checked there), and strokes wider on one side of the path are written as filled outlines, no longer editable strokes there (the stroke itself is kept as plugin data and our own import puts it back; not yet checked there), + like effects such as glass and noise, images and prototype interactions. +- **Missing:** connector labels, elbowed or curved connectors without a saved path (drawn straight), + table cells beyond their own layers, filled areas of a vector with their own fills, per-point stroke + joins, variables bound to fields the editor can't bind, fixed or content-sized grid tracks and video + fills aren't read; text lays out again with + local fonts. Nothing has been checked against real-world files yet (gap 3). The export is checked + only against our own importer: whether other apps open its files is unknown. +- **Evidence:** fixtures generated in code (`crates/format/src/fig/tests.rs` and `tail_tests.rs`: kiwi + round trips, every mapped feature, vector network blobs, truncation, bit flips and junk never panic); `crates/engine/src/fig_import_tests.rs`. + Export: 60 generated documents round trip through the importer layer by layer, written files + decode with the generic decoder and list parents first, the encoder agrees with the decoder, + junk documents never panic (`crates/format/src/fig/export_tests.rs`, + `crates/engine/src/fig_export_tests.rs`). +- **Impact:** existing work can be brought across, but only real files will show how much of it. +- **Estimate:** 5–10 h for the read tail (real-file fixes, connector labels, table cells); checking the export against real files a further 5–15 h (with gap 3). + +### 2. Multiplayer and sharing are a preview, without accounts +- **Missing:** accounts, teams and projects, per-person permissions, audio, + shared comments and version history. Comments and versions are local to the file. +- **Evidence:** the core landed in #103 (`crates/collab`: client-scoped ids, change sets, server + ordering and validation, client reconciliation, binary messages, randomized convergence tests over + real commands), and live sessions run over the network (`crates/collab-net`: hosting from the + app, invite links that open the app, document sync between apps, live cursors, selections, + text carets, avatars and follow, cursor chat and spotlight, and the browser build joins) with per-person undo, refused + edits rolled back with a reason, reconnects that resume and send what was made offline, and + styles, variables and comments that several people create at once all kept; sessions are per + document (each open file has its own or none, several at once, closing a file ends only its + own); files are shared with a short join code (online through the server in Preferences, or on + the local network with a code that carries the host's address) and joined by typing it; the app + remembers the files it put online and lists them on the home screen (open again as owner, stop + and start sharing, rename, remove, forget), per computer; a standalone + `figcraft-server` keeps many files (snapshot plus log, compacted; restarts lose nothing + confirmed), takes files published from the app and hands out share links that let people edit + or only view; the desktop app and the browser reach it over `wss://` through a TLS reverse + proxy, and releases ship it with a deployment guide (`docs/server.md`). But there are no + accounts (a link is the access) (`docs/collab-protocol.md`). +- **Impact:** Figma's defining feature; most teams use it every day. Solo designers can work + without it. +- **Estimate:** 42–84 h: accounts, teams and per-person permissions, shared comments and versions + (numbers from before the server landed; re-measured next). + +### 3. Nothing checked against real Figma files +- **Missing:** a corpus of real documents (design systems, app screens, prototypes) imported and + compared with Figma's own PNG/SVG/PDF exports. +- **Evidence:** every corpus in the repo is synthetic (`crates/svg/tests/corpus`, the synthetic + design system in `crates/engine/src/synthetic_ds.rs`). Scores were self-graded by the agents that + built the features. +- **Impact:** unknown fidelity in auto layout sizing, text metrics, effects and component + resolution on real files; the numbers in this repo can't be trusted beyond "estimated" until it + exists. +- **Measured so far:** text box sizes and auto layout results of our own example documents, + against the same documents re-saved by other software (which lays text out again). Three text + rules explained every difference and are applied (whole-pixel line heights, advances on a + 1024-per-em grid, letter spacing between glyphs only): all 230 self-sizing text boxes in 7 + documents now come out to the pixel (24 of 131 before, in the design-system example), and 208 of + its 231 other layers match in size and position (115 before). The other 23 hang on one grid + auto layout frame whose row positions came back as written while its cells got shorter. One font + family (Inter), left-aligned text; real files, other fonts, effects and component resolution are + still unmeasured (`crates/text/src/metrics_tests.rs`, and a local check over such documents in + `crates/engine/src/text_metrics_local_tests.rs`). +- **Estimate:** 15–30 h once `.fig` or REST import of real files is available; needs files whose + licence allows use (owner to supply or approve; never committed if not redistributable, see + craftrules `standards/test-corpora.md`). + +### 4. Interaction fidelity not compared tool by tool +- **Missing:** a side-by-side pass of every tool and panel: cursors, modifier keys while dragging, + hover states, popovers, menus, snapping thresholds. +- **Landed (2026-10-11):** behaviour suites written from the public documentation for auto layout, + selection and transform, text, and canvas navigation with rulers, guides, layout grids, outline + mode and measuring, then (2026-10-12) comments and version history, and the layers and Assets + panels (`crates/engine/src/*_behaviour_tests.rs`, `crates/ui-egui/src/layers_behaviour_tests.rs`), + each with its fixes; then keyboard shortcuts per platform (`shortcut_parity_tests.rs`: 129 + documented rows, 126 bound, 1 moved off a documented conflict, 2 missing, 48 undocumented + bindings pinned), with 16 rows newly bound, Control suspending snapping on macOS and shifted + symbols on other keyboard layouts; then the everyday partials of + [`ui-parity.md`](ui-parity.md): number fields, outline mode's options, Hide when publishing, a + cursor for every tool and modifier, panel widths, dark pages (`field_tests`, `cursor_tests`, + `cursors_tests`, `outline_options_tests`, `hide_publishing_tests`, `panel_width_tests`, + `theme_pages_tests`). +- **Evidence:** ROADMAP item 7 ("Left: tool by tool, panel by panel against the incumbent"). +- **Impact:** Figma users notice the feel within minutes; it decides whether they stay. +- **Estimate:** 40–60 h. Detail in [`ui-parity.md`](ui-parity.md). + +### 5. Localization: English, reviewed Russian, plus unreviewed drafts +- **Missing:** native reviews of the Spanish, French, German, Brazilian Portuguese, Japanese, + Korean and Simplified Chinese drafts and the Latin American Spanish overlay, the 25 newest strings + in five drafts, Hindi, Arabic, Indonesian and Vietnamese, localized engine messages and number + formatting; CJK UI text on the web without the craft-fonts CJK build. +- **Evidence (measured from source):** the layer landed 2026-10-11: catalogs in + `crates/ui-egui/locales/` (1,371 UI keys plus 524 command labels and 29 menu wordings), + Preferences ▸ Language, `pref.language`; `en.toml` is complete, and `es.toml`, `fr.toml`, + `de.toml`, `pt-BR.toml`, `ja.toml`, `ko.toml` and `zh-Hans.toml` (2026-10-12) are machine drafts + (all but `zh-Hans.toml` lack the 25 newest strings), with `es-419.toml` a Latin American overlay + listing only what differs from Spanish (Spanish and Russian are held to completeness by the check; the + others show English for strings added later). Russian adds all 2,165 current entries, + reviewed and approved by VanTan, with one/few/many integer plurals. + CJK UI text uses an installed CJK face the desktop app loads when something on screen needs it + (`crates/ui-egui/src/cjk.rs`). Engine error messages and Dev Mode values stay English. +- **Impact:** non-English designers get machine drafts nobody has reviewed yet, and English + errors. +- **Estimate:** 3–7 h per language plus native review, 4–6 h for engine messages and numbers; + 29–51 h for the key languages left and the drafts' follow-ups. Detail in + [`localization-parity.md`](localization-parity.md); how to add one in + [`translating.md`](translating.md). + +### 6. Plugins: widgets take clicks only, a self-hosted directory but no public catalogue; Figma's plugins don't run +- **Landed (#101):** sandboxed WebAssembly plugins (wasmi, metered, memory cap, no WASI, also in + the browser) acting through the command registry, one undo step per run, manifest permissions, + Plugins menu, Run last plugin, Manage plugins, `plugin.*` commands, a Rust SDK and two examples + ([`plugins.md`](plugins.md)). Plugin panels and settings (ABI v2, 2026-10-11): a plugin shows a + panel of fields and buttons drawn with FigCraft's widgets and is called back for each button or + changed value (one undo step per callback, same sandbox limits), driven headless with + `plugin.ui` / `plugin.event` / `plugin.close`; per-plugin settings kept with the preferences. + Widgets (plugin API 3, 2026-10-12): a plugin draws a layer of its own on the canvas (Plugins ▸ + Widgets, `widget.insert`), validated and built with locked layers inside, called back for clicks + on the parts it names (one undo step each, a fresh metered sandbox, the state kept in the layer + so it saves, copies, undoes, exports and syncs), `widget.click` / `widget.list` for agents, a + Counter example; panels of several plugins side by side; the colour picker on plugin colour + fields. A plugin directory on `figcraft-server` (2026-10-12): bundles published with the server + key and checked in full (`figcraft-cli plugin publish`), browsed, installed after a permissions + confirmation and updated from Plugins ▸ Manage plugins ▸ Browse, downloads checked against their + SHA-256, `plugin.directory.*` for scripts and MCP, moderation with `figcraft-server plugins`. +- **Missing:** a public catalogue and Community files, publisher accounts and signatures, ratings + and install counts, background update checks, the directory in the browser build; widgets take only clicks (no + text input, property menus, hover or timers) and draw only frames, text, shapes and images; + no images or sliders in panels. Figma's own plugins are JavaScript against Figma's plugin API and + don't run. +- **Impact:** teams that depend on specific Figma plugins (icons, content, accessibility checks) + can't bring them. FigCraft's CLI, control channel and MCP server cover agent automation better + than Figma does. +- **Estimate:** 8–26 h for the missing pieces of FigCraft's own API. Running Figma's plugins + needs a JS engine and an API shim: **owner decision needed**. + +### 7. CPU canvas where Figma uses the GPU +- **Missing:** GPU rendering of the canvas. FigCraft renders with tiny-skia on the CPU (banded, + parallel, retained) and shows the image through wgpu. A new zoom can't reuse the retained + image; when renders from scratch take a frame or more, the whole view is previewed with a + sixteenth of the pixels first, then refined. +- **Evidence (measured, `cargo xtask bench`):** a 50k-layer page ~8 ms zoomed out with levels of + detail, single-layer edits at 10k layers ~0.2 ms; full 1440×900 renders 7–15 ms at 10k layers. + Heavy effects (glass, background blur, noise) and 4K/5K canvases at 2x are where CPU time grows. +- **Impact:** large files and effect-heavy designs; battery on laptops. +- **Estimate:** 30–60 h for a GPU path (vello or wgpu shaders) held to the CPU renderer as the + oracle. + +### 8. Platform QA: Linux and the browser left +- **Missing:** regular runs on Linux and the browser; real screen readers; a repeat of the Windows + pass on other machines (scale factors above 100%, pen tablets, file drops from Explorer). +- **Evidence:** CI tests on Linux only (`.github/workflows/ci.yml`); release builds exist for every + platform (v0.2.0) but no issue has been filed yet. Windows: QA pass done 2026-10-11, findings in + PR (Windows 11, the desktop app driven over the control channel, the same workflow through MCP): + eight bugs fixed, among them unsaved changes lost when the app was killed or crashed, + Ctrl+arrows and Ctrl+Backspace acting on the whole line in text, scrubbed fields snapping back + and skewed linear gradients. +- **Impact:** unknown bugs on the platforms where FigCraft is most attractive (Linux has no Figma + desktop app). +- **Estimate:** 10–20 h, partly human (hardware). + +### 9. Figma Draw: tablet pressure on macOS and Linux +- **Missing:** pen pressure on macOS and Linux tablets (winit reports none there). +- **Landed (2026-10-12):** multi-edit: several vector layers in edit mode at once (Enter or + double-click with them selected, `vector.edit.begin {ids}`); points selected, marqueed, dragged, + nudged, deleted, mirrored and rounded across layers in one undo step; the pen, bend, paint bucket, + cut and eraser work in the layer last pressed (`vector_multi_behaviour_tests`). +- **Landed:** text on a path (`text.onPath`, `text.pathOptions`, `text.removeFromPath`; the + Design panel's Typography section; canvas, PNG, SVG and PDF as outlines; SVG `` imports + as text on a path), pattern fills (`PaintKind::Pattern`, `fill.pattern`, the colour picker's + Pattern tab; SVG ``, PDF tiling patterns), stretch and scatter brushes (`stroke.brush`, + eight presets drawn in code; canvas, PNG, SVG, PDF and outline stroke) and pen pressure for the + Pencil (Windows pens and browser styluses; recorded as the stroke's width profile, which brushes + follow). +- **Evidence:** winit reports no pressure on macOS and Linux tablets. +- **Impact:** illustration-flavoured work moved into Figma Draw in 2025. +- **Estimate:** 1–3 h once winit (or a platform tablet API behind it) reports pressure there. + +### 10. Team libraries: no team-wide lists or accounts; analytics are counts +- **Missing:** libraries listed for a whole team or organization (each file enables a library by + its link), accounts and per-person library permissions, analytics beyond instance counts per + component and file (insertions and detaches over time), update notices pushed by the server + (apps ask every 30 seconds while the Assets panel shows). +- **Evidence:** a file's library is published to `figcraft-server` under the file and enabled in + other files by a share link, with per-asset versions, updates with overrides kept, usage counts + and swapping a whole library by names ([`server.md`](server.md#team-libraries), + [`components-parity.md`](components-parity.md)). +- **Impact:** design-system teams. +- **Estimate:** 4–8 h, the accounts part after gap 2. + +### 11. Spell check +- **Missing:** spelling underline and suggestions while editing text. +- **Evidence:** no spell-check code in the repo. +- **Estimate:** 4–8 h with a permissively licensed dictionary and checker (provenance per + `ATTRIBUTION.md`). + +### 12. Video fills; HEIC images +- **Landed (2026-10-11):** animated GIF fills (the file kept as is, the first frame on the canvas + and in exports, played in the presenter and the preview) and TIFF images. +- **Missing:** Figma takes JPG, PNG, HEIC, WebP, GIF and TIFF images and MP4, MOV and WebM video + (help center, checked 2026-10-10). FigCraft decodes PNG, JPEG, WebP, GIF, TIFF and BMP; HEIC and + video need decoders that are C libraries (libheif, FFmpeg) or each platform's media framework. +- **Estimate:** 5–10 h (video playback in the presenter is most of it; it can reuse the GIF clock). + +### 13. Vector tail: outline-stroke compositing +- **Landed (2026-10-15):** Outline stroke keeps the renderer's even-odd stroke rings; regression pixels, Undo/Redo, native save/reopen and instance opacity inheritance/overrides in `outline_stroke`. +- **Missing:** preserving filled-shape opacity, blend and effects on the composite. A follow-up must preserve component inheritance and references when deciding which node owns these properties. +- **Landed (2026-10-12):** several Bezier handles selected with Shift-click and dragged together, + sharp points beside curved segments rounded, a stroke cap and join per point (canvas, PNG, SVG, + PDF, outline stroke), the Pen outside edit mode connecting to its own points and continuing a + selected open path. Left: corner smoothing beyond rectangles and frames, the transform box around + points of several layers, per-point caps and joins on dashed, variable-width and brush strokes. +- **Landed (2026-10-11):** boolean groups are clicked where their result is (a click in a + subtracted hole reaches what's behind; with the group or an operand selected its operands are + hit, so each can still be picked; "Select layer" lists the group only where its result is), live + boolean editing (operands selected by double-click or Cmd/Ctrl-click; + moving, resizing or editing an operand's points redraws the result during the drag, with every + operand outlined; per-operand operations from the boolean controls and `node.set`; a drag frame + recombines only the operands from the edited one up), Design panel Arc fields (Start, Sweep, + Ratio; Mixed across ellipses; bindable), the shape builder on strokes and open paths (strokes take + part as their outlined areas), one-sided variable widths (Alt-drag a width handle; canvas, PNG, + SVG, PDF and outline stroke). +- **Estimate:** compositing not yet re-estimated; vector QA with real files remains part of gap 3. + +### 14. Variables: extended collections on real libraries +- **Landed:** extended collections (`library.extendCollection`: a library collection's modes and + variables in a collection of this file, values overridden per mode with reset, following library + updates, variables of its own), variables import/export (`variable.export` / `variable.import`: + the REST local variables JSON, merged by name; CSS custom properties), gradient stops and variant + properties bound to variables. +- **Landed (2026-10-12):** variables a library adds to a collection this file extends are a library + update of their own (`library.updates` lists them by name, without an `id` until they have a + copy) and arrive inheriting when accepted; a library collection that is itself an extended + collection (a brand theme extending a core one) can be extended again, its values flowing down + the chain as each file accepts updates, every level's overrides kept. +- **Missing:** checked with synthetic files only; extending a collection of the same file isn't + offered (extending takes library collections). +- **Estimate:** 0–1 h. + +### 15. Prototyping: media triggers and actions, controllers off Windows +- **Landed (2026-10-12):** key/gamepad triggers take controller buttons (`Gamepad A` … `Gamepad + Home`, the standard layout's 17 buttons; a Controller menu beside the key field; REST files' + controller triggers import); the player reads controllers on Windows and in the browser (Gamepad + API) and takes `ui.input {gamepad}` on the control channel; widgets react to clicks in a + presented prototype, on the prototype's own copy; `proto.evaluate` computes expressions (mode + lookups included) where a layer's interaction would run (`crates/engine/src/proto_tail_tests.rs`, + `crates/ui-egui/src/present_tail_tests.rs`, `crates/format/src/rest/gamepad_tests.rs`). +- **Missing:** video triggers (on media hit/end) and play/pause/mute/seek actions (the REST + importer warns and skips them, `crates/format/src/rest/proto.rs`); controllers on macOS, Linux + and FreeBSD (the usual Rust controller library links libudev on Linux; a pure-Rust reader per + platform, or an owner decision on that dependency); PlayStation and Switch button labels. +- **Estimate:** 2–5 h, the media part after video fills (gap 12). + +### 16. Dev Mode: the tail after dev resources +- **Landed (2026-10-11):** code connect: a component or set links to its code component per + language (React, SwiftUI, Compose): an import path, the name and a template whose `{property}` + and `{property ? "a" : "b"}` placeholders take the instance's values, escaped per language and + never executed. `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet + instead of generated code (inside generated layer trees too); the panel's Connect to code edits + it with a live preview. Links are stored on the component (saved, synced, undoable) and travel + as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`, validated and capped) + so code owners can version them. Plugins whose manifest asks only to read run from the Inspect + tab. +- **Landed (2026-10-12):** dev resources: links for developers (a URL and a name) on any layer + (`devResource.add` / `edit` / `remove` / `list`, undoable, saved and synced). An instance also + lists its main component's links (and its set's) read-only with where they come from; layers + inside an instance show their main layer's. URLs are checked as web links (http or https, a + host, no spaces, capped) and never fetched: a link added without a name is named after its URL, + and a link opens only when clicked. The Inspect tab lists, adds, edits and removes them; + `get_node` / `inspect_document` return them as `devResources`. +- **Missing:** import lines for connected instances inside a + generated layer tree (only the selected layer's own import is shown); publishing mappings from + the code repository with a CLI. An editor extension is out of scope: the MCP server already + serves design data, code and the connected snippets to agents and editors. +- **Estimate:** 0–1 h. + +### 17. Branching and merging +- **Landed (2026-10-11):** branches of a file (`branch.*`): File ▸ Create branch… (or the Branches + list in version history) copies the file into a branch tab that remembers the file as it was (its + base) and makes its ids in its own id space; edits on the branch are saved with the main file. + Review and merge (the banner on a branch tab) shows the main file and the branch side by side or + as an overlay, what changed on each side since the base (layers with their property changes as + Inspect shows them; styles, variables, comments) and the conflicts: the same property changed on + both sides, or a layer deleted on one side while the other edited it or what's inside it. Each + conflict is resolved for the branch or main; nothing conflicting merges until it is. Merging is + one undo step on the main file and archives the branch; Update from main brings main's changes + into the branch the same way; branches can be archived without merging. Three-way merge over the + collaboration change sets (`figcraft_collab::merge`), fuzz-tested with random real commands. +- **Landed (2026-10-12):** a branch's own version history: versions saved on a branch tab (by hand + or autosaved) go into the branch, not the main file, and are viewed, restored (one undo step on + the branch), renamed, deleted and duplicated there; they are saved in the file with the branch. + Merging adds a version to the main file's history (Merged "name"); the merged or archived + branch keeps its own history and opens with it. Fills, strokes and effects merge item by item: + different items changed on each side, or items added at the end on one side, merge without a + conflict; only the same item changed both ways, an item removed on one side, or different items + added on both sides conflict (as the whole list). +- **Missing:** branches of a file in a live session are refused; other lists (text runs, export + settings, guides…) still conflict as a whole; the branch history has no entries of its own for + its creation or updates from main (only saved versions, autosaves and the merge on main). +- **Estimate:** 1–2 h. + +### 18. Display P3 on screen (several windows landed) +- **Landed (2026-10-11):** several windows: File ▸ New window, a tab's Open in new window or a tab + dragged off the strip; each window has its own tabs, panels and canvas and draws only when it + needs to; closing one asks about unsaved changes; `window.new`, `window.list`, `window.focus` and + `window` on any control request. +- **Missing:** wide-gamut colour on the canvas (egui draws sRGB, so P3 colours outside sRGB clip on + screen; exports keep them); a screen-reader tree in windows other than the main one (eframe 0.36 + gives only the main window one); window positions restored at the next start. +- **Estimate:** 4–8 h. + +### 19. AI features +- **Missing:** Figma AI (first draft, rename layers, remove background, image generation and + editing, visual search, Make). +- **Estimate:** 30–60 h with openly licensed local models. **Owner decision needed.** + +### 20. Mobile apps +- **Missing:** Figma's iOS and Android apps view and comment on files and mirror prototypes. + FigCraft's web build runs in mobile browsers but is not designed for touch. +- **Estimate:** 20–40 h for a touch-first viewer and mirror. + +### 21. Menu items without a command (measured): none left +- **Landed (2026-10-11):** `file.openRecent` (File ▸ Open recent: recent files kept in the + preferences, at most 20, greyed when the file is gone and dropped when picked, Clear recent), + `view.outlinesSelection` (outline mode for the selected layers only) and `view.maskOutlines`; + the plugin pair landed in #101. Every item in the menu catalog has a command + ([`parity-checklist.md`](parity-checklist.md)). +- **Estimate:** none left. + +### 22. Screen readers never run; browser has no accessibility tree +- **Evidence:** AccessKit names and keyboard navigation landed (#96), checked by tests only; + eframe's web backend doesn't send the tree yet. +- **Estimate:** 4–8 h plus human QA with VoiceOver, NVDA, Narrator and Orca. + +### 23. CMYK output intent for print PDFs +- **Evidence:** CMYK conversion is a documented naive formula; no CMYK ICC profile can ship in the + repository (licensing). +- **Estimate:** 2–4 h once an openly licensed profile is found. + +## Revision history + +| Date | Change | Summary | +|---|---|---| +| 2026-10-15 | minor | M4: Outline stroke preserves even-odd stroke holes. Pixel comparisons cover alignment, dashes, individual side weights, vector paths and stroke-only opacity; native save/reopen, Undo/Redo and instance opacity inheritance/overrides are covered by `outline_stroke`. Existing estimates unchanged; filled-shape compositing needs separate work. | +| 2026-10-14 | minor | Russian catalog (M9), reviewed and approved by VanTan: all 2,165 current entries, Русский in Preferences and regional ru tags. Russian integer plurals use one/few/many; contributor display name VanTan added. Existing estimates unchanged. | +| 2026-10-13 | minor | Gap 1: the experimental `.fig` export writes instance sizes (resized, filling an auto layout, scaled), variables with modes and bindings, shared styles and variable-width strokes, and the import reads them back. Checked by opening written files in other software: resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width; number, string and boolean bindings and bindings on effects, layout guides and gradient stops come across as the values they give but did not show as bound there (their entries now also name the layer field they set, not yet checked there), and strokes wider on one side of the path are written as filled outlines, no longer editable strokes there (the stroke itself is kept as plugin data and our own import puts it back; not yet checked there). Hours and the ranking unchanged | +| 2026-10-12 | minor | Gap 2: your online files landed (the app records the files it put on a server with their owner tokens, Home ▸ Online lists them and opens them again as their owner, with sharing, rename, removal and forget from the list; one request per server tells which are still there; tabs mark files in a live session). What is left is unchanged: accounts, teams and per-person permissions, shared comments and versions. Hours unchanged | +| 2026-10-12 | minor | Gap 2: sharing with a join code (Share ▸ Online or Local network, one Start sharing button, a short code to give others, one join field for a code or a link; the server names links with codes and limits wrong ones) added to the landed list; usability, so the estimate and ranking are unchanged | +| 2026-10-12 | minor | Gap 3: first measured fidelity data. Text box sizes and auto layout results of our example documents were compared with the same documents re-saved by other software; three text rules (whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only) were found and applied, and all 230 self-sizing text boxes in 7 documents now match to the pixel (one font family). Real files are still unchecked: hours and the ranking unchanged | +| 2026-10-12 | minor | Gap 2: live sessions are per document (each open file has its own session or none, several run at once, the window and the `collab.*` commands follow the file in front, closing a file ends its session only) added to the landed list; a fix, so the estimate and ranking are unchanged | +| 2026-10-12 | minor | Gap 1: the experimental `.fig` export now opens elsewhere: written files were checked by opening them in other software: layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft; effects such as glass and noise, images and prototype interactions are still unchecked there, and variables are not written. Hours and the ranking unchanged | +| 2026-10-12 | minor | Gap 1: an experimental `.fig` export landed (the whole document, from the same public format notes as the import; checked only by round trips through our own importer); export hours 15–30 h → 5–15 h (checks against real files); gap 1's read tail (5–10 h) and the ranking unchanged | +| 2026-10-12 | minor | Gap 4: keyboard shortcut parity suite per platform (129 documented rows: 126 bound, 1 moved, 2 missing; 48 undocumented bindings pinned) added to the landed list; estimate and ranking unchanged | +| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Gap 5 31–54 h → 29–51 h, rank unchanged | +| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Gap 5 32–55 h → 31–54 h, rank unchanged | +| 2026-10-12 | minor | Gap 15: controller buttons on key/gamepad triggers (Windows and the browser), widgets reacting in prototypes and `proto.evaluate` landed; gap 15 now media plus controllers off Windows, 3–6 h → 2–5 h; ranking unchanged | +| 2026-10-12 | minor | Plugin directory on `figcraft-server` (publish with the server key, browse, install after a permissions confirmation, hash-checked downloads, updates by version, operator moderation) added to gap 6's landed list; gap 6 10–30 h → 8–26 h; ranking unchanged | +| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Gap 5 40–65 h → 32–55 h, rank unchanged | +| 2026-10-12 | minor | Plugin widgets (API 3) and the panel tail (panels side by side, colour picker on plugin fields) added to gap 6's landed list; gap 6 now 10–30 h; ranking unchanged | +| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): every UI string, command label and menu wording (1,899 entries) machine-drafted in each and marked as drafts awaiting native review, falling back to English per key (new strings need only English and Spanish); 0 counts as singular in French and Brazilian Portuguese; System picks the first OS language with a catalog (pt-PT and pt read pt-BR); long translations elide with a tooltip in dropdowns, two-column labels and the home cards. Gap 5 45–75 h → 40–65 h, rank unchanged | +| 2026-10-12 | minor | Cursor chat and spotlight in live sessions (gap 2): `/` opens a chat bubble at your pointer, a spotlight makes everyone follow your view; ephemeral presence the host validates and rate-limits. Gap 2 45–90 h → 42–84 h | +| 2026-10-12 | minor | Layers and Assets panels behaviour suite and fixes added to gap 4's landed list; ranking and hours unchanged | +| 2026-10-12 | minor | Vector multi-edit and the vector edit tail, with a behaviour suite written from the public documentation (5 tests, about 30 table rows and steps, and a junk-parameter sweep of every vector command in multi-edit mode): several vector layers are edited at once (Enter, double-click or `vector.edit.begin {ids}`; a press on another layer's point, handle or segment makes it the active layer; Shift-click, marquee and lasso select across layers; dragging, the arrows, Delete, delete and heal, mirroring, smoothing, corner radius and point caps and joins reach every layer's selected points in one undo step; the pen, bend, paint bucket, cut and eraser work in the active layer); several Bezier handles are selected with Shift-click and dragged together (`vector.edit.moveHandles`); a sharp point next to a curved segment rounds (the curve is cut where the arc meets it and keeps its shape); a point can have its own stroke cap and join (`vector.edit.strokeCap`, `vector.edit.strokeJoin`, the Point section of the Design panel; canvas, PNG, SVG and PDF, outline stroke); the Pen outside edit mode connects to one of its own points and ends the path, and continues a selected open path from an end point. Left: corner smoothing beyond rectangles, the transform box around points of several layers, per-point caps and joins on dashed, variable-width and brush strokes. Gap 9 2–5 h → 1–3 h (pen pressure on macOS and Linux tablets left, blocked on winit). Shapes & vector 98%/72% → 99%/73%, 2–4 h → 1–3 h; Figma Draw extras 80%/55% → 88%/60%, 2–5 h → 1–3 h (gap 9: pen pressure on macOS and Linux tablets left). Totals recomputed by the doc's method, which rows since the last recompute had left stale: the area table's weighted breadth is 83.7% (~83%; core 94.1%, ~94%) and ready 63.1% (~63%; core 72.1%, ~72%), hours 133–284 (core 38–83); Features (depth) 61% → 63%, 46–94 h → 34–76 h; ready for real work 52.6% (~52%); mainstream weekly areas 71.0% → 72.1% (× 0.9 × 0.9 × 0.85 = 49.7%, ~49%, rounded down); essentials unchanged (~57%); full parity ~306–595 h → ~294–572 h (on top of the rows merged since: ~301–587 h → ~289–564 h; area-table totals recomputed on the merged table and rounded down: breadth ~84%, ready ~63%, core ~94%/~72%, mainstream weekly areas 72.4%, ~49%) | +| 2026-10-12 | minor | Dev resources (gap 16): links for developers (a URL and a name) on any layer: `devResource.add` / `edit` / `remove` / `list`, undoable, saved and synced; an instance also lists its main component's (and that component's set's) links read-only with where they come from, and layers inside an instance show their main layer's; URLs are checked as web links (http or https, a host, no spaces, at most 2,048 characters) and never fetched (a link added without a name is named after its URL), opened only when clicked; the Inspect tab lists, adds, edits and removes them; `get_node` and `inspect_document` return them as `devResources`. The editor extension stays out of scope (the MCP server serves agents and editors). Dev Mode breadth 93% → 96%, ready 70% → 72%, 1–3 h → 0–1 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~304–593 h → ~303–591 h | +| 2026-10-12 | minor | Extended collections' tail (gap 14): variables a library adds to a collection this file extends now count as a library update of their own (listed by name without a copy yet) and arrive inheriting when accepted, instead of waiting for the library's next other update; a library collection that is itself an extended collection can be extended again, values flowing down the chain as each file accepts updates with every level's overrides kept (checked end to end). Styles & variables breadth 94% → 95%, ready unchanged (76%), 3–5 h → 2–4 h; gap 14 1–2 h → 0–1 h (real-library checks left); feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~305–594 h → ~304–593 h | +| 2026-10-12 | minor | A branch's own version history and finer list merges (gap 17): versions saved or autosaved on a branch tab go into the branch (viewed, restored, renamed, deleted and duplicated there, saved in the file with it); merging adds a version to the main file's history and the merged or archived branch keeps its own; fills, strokes and effects merge item by item, conflicting only when the same item changed both ways, an item was removed on one side or both sides added different items. Comments, versions, find, misc breadth 85% → 87%, ready 60% → 61%, 2–5 h → 1–4 h; gap 17 1–3 h → 1–2 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~306–595 h → ~305–594 h | +| 2026-10-12 | minor | Comments and version history behaviour suite and fixes added to gap 4's landed list (gap 17 unchanged: a branch's own version history is still left); ranking and hours unchanged | +| 2026-10-11 | minor | `.fig` import's tail (gap 1): vectors open as editable vector networks (points, curved segments, filled areas with their winding, per-point corner radius and handle mirroring, end caps from the end points), falling back to outlines when the network is missing or damaged; sticky notes, shapes with text, connectors, tables, code blocks, slides and similar layers import as the closest frames, text layers and vectors with a warning instead of being dropped; set-variable, set-variable-mode and conditional prototype actions come across. New parsers fuzzed on fixtures generated in code. File formats 55% → 58%, 8–15 h → 5–10 h; import/export breadth 80% → 82%, ready 50% → 54%; feature and area totals and ready for real work (~49%) unchanged; beta ~173–310 h → ~170–305 h; full parity ~321–618 h → ~318–613 h | +| 2026-10-11 | minor | `.fig` import, best effort (gap 1): `.fig` files open (best effort): the zip, its deflate/zstd chunks and the kiwi schema are decoded generically and mapped by field name onto the REST importer, so pages, the layer tree, geometry, paints (styles resolved), strokes, effects, text with styled runs, vectors (as outlines), auto layout (grid included), constraints, components, sets, instances (expanded from their main with overrides, derived layout and property values), styles, variables with modes and bindings, prototyping and image fills from the archive come across; anything else is a warning. Gap 1 30–60 h → 8–15 h (vector networks, other layer kinds, real-file checks; export 15–30 h on top, as before) | +| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | +| 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | +| 2026-10-11 | minor | Localization layer landed (gap 5): per-language catalogs baked in at build time with English text as keys and fallback, placeholders and plurals, command labels by id, Preferences ▸ Language (System follows the OS or the browser) applied live and `pref.language`; English catalog and a complete Spanish draft awaiting native review. Localization 10% → 20%, 60–100 h → 45–75 h; ready for real work unchanged (~47%); full parity ~363–697 h → ~348–672 h | +| 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | +| 2026-10-11 | minor | Plugin panels and settings landed (gap 6, ABI v2; v1 plugins unchanged): a plugin shows a panel of notes, fields (text, number, checkbox, menu, list, colour) and buttons drawn with FigCraft's widgets; a button or a changed value calls it back with every value (same limits per callback, one undo step per callback that edits, rolled back on failure), its module staying loaded while the panel is open; `plugin.ui` / `plugin.event` / `plugin.close` drive panels headless; per-plugin settings (64 KB) kept with the preferences; the Swatch grid example gained a palette panel that remembers the last palette. Plugins & widgets breadth 35% → 45%, ready 20% → 28%, 25–55 h → 15–40 h; ecosystem 20% → 28%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~373–712 h → ~363–697 h | +| 2026-10-11 | minor | Canvas fixes (gap 13 closed): boolean groups are clicked where their result is (a click in a subtracted hole reaches what's behind; with the group or an operand selected its operands are hit, so each can still be picked), a layer dragged inside a group stays in the group (frames and auto layout still take other drops), View ▸ Frame outlines and View ▸ Slices draw on the canvas, and the idle tests wait for the render thread instead of counting frames. Shapes & vector 1–3 h → 1–2 h (vector QA with real files left); percentages, feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~378–722 h → ~375–717 h | +| 2026-10-11 | minor | Stretch and scatter brushes and pen pressure landed (gap 9): eight brushes drawn in code (four stretched along the path, four scattered with spacing, size, rotation and scatter jitter from a seed) in the stroke settings, drawn everywhere strokes are and flattened by outline stroke; the Pencil records pen pressure as the stroke's width profile (Windows pens, browser styluses; macOS and Linux tablets report none) and can draw with a brush; Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–9 h; Strokes 2–4 h → 1–3 h; Hardware 50% → 55%, 15–30 h → 13–27 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~378–722 h → ~373–712 h | +| 2026-10-11 | minor | Text on a path and pattern fills landed (gap 9): text follows a path or shape (start, side, alignment; edited in place; canvas, PNG, SVG and PDF; SVG `` imports as text on a path) and pattern fills tile a layer (grid or half-shifted rows or columns, scale, spacing, alignment; live as the source changes; SVG ``, PDF tiling patterns); Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–10 h; Text 8–14 h → 7–13 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~375–715 h | +| 2026-10-11 | minor | Team libraries on `figcraft-server` landed (gap 10): a file's library is published to the server under the file and enabled in other files by a share link (per-asset versions; updates noticed in the background and accepted with overrides kept), the server counts which files use which components, and a whole library can be swapped for another by names; Team libraries breadth 40% → 70%, ready 25% → 40%, 10–20 h → 4–8 h; collaboration 12% → 13%, 55–110 h → 49–98 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~380–725 h → ~375–715 h | +| 2026-10-11 | minor | Branches landed (gap 17): File ▸ Create branch… copies the file into a branch tab that remembers its base; Review and merge shows both sides' changes (layers with Inspect-style property changes, styles, variables, comments) and the conflicts (the same property changed on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merging is one undo step and archives the branch; Update from main; branches are saved in the `.figcraft` file. Comments, versions, find, misc breadth 80% → 85%, ready 58% → 60%, 4–8 h → 2–5 h; collaboration 12% → 13%; menu breadth 204/204 (the catalog gained File ▸ Create branch…); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~378–722 h | +| 2026-10-11 | minor | Animated GIF fills and TIFF images (gap 12, without video and HEIC): GIF fills keep the original file, show the first frame on the canvas and in exports and play in the presenter and the preview (frames decoded on first play, at most 500 frames and 128 MB; always looping; the window wakes only for a GIF's next frame); TIFF images open, import, place and paste. Paint breadth 85% → 87%, 5–9 h → 4–7 h; gap 12 6–12 h → 5–10 h (video playback and HEIC left, both needing non-Rust decoders); feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | The last three catalogued menu items got commands (gap 21): File ▸ Open recent (recent files kept in the preferences, at most 20, a file that's gone greyed in the menu and dropped when picked, Clear recent), View ▸ Outlines ▸ Show outlines for selection and View ▸ Mask outlines: menu breadth 98.5% → 100% (203/203); Canvas & navigation breadth 95% → 97%, 3–6 h → 2–5 h; feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | Extended collections, variables import/export and gradient stop and variant property bindings landed (gap 14): Styles & variables breadth 85% → 92%, ready 70% → 72%, 4–8 h → 1–2 h; feature breadth unchanged (~79%); ready for real work unchanged (~47%); full parity ~385–730 h → ~380–725 h | +| 2026-10-11 | minor | Collaboration wording after the standalone server: `figcraft-server` keeps many files, persisted as a snapshot plus a log of what changed (compacted, checksummed, damaged files skipped and reported), restarts lose nothing confirmed and apps resume where they were; the app publishes its file to it; share links per file let people edit or only view (the server refuses a viewer's edits), are revocable and stored as hashes; TLS comes from a reverse proxy (the desktop app has none of its own yet); percentages and hours unchanged until the next re-measure | +| 2026-10-11 | minor | Live boolean editing, Design panel arc fields, the shape builder on strokes and open paths and one-sided variable widths landed (gap 13): Shapes & vector breadth 92% → 97%, ready 66% → 68%, 8–14 h → 1–3 h; feature breadth ~78% → ~79%; ready for real work unchanged (~47%); full parity ~390–745 h → ~385–730 h | +| 2026-10-11 | minor | Collaboration wording after the collaboration polish: styles, variables, collections, modes and comments created in a live session take ids from their creator's id space (concurrent creations no longer overwrite each other; the server checks it), others' text carets and selections show on the canvas, and typing in a layer someone else is typing in says so; percentages unchanged | +| 2026-10-11 | minor | Wording only: live sessions (#104), then the browser joining them and `figcraft://` invite links opening the desktop app; percentages and hours unchanged until the next re-measure | +| 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 | minor | Marked beta blockers; no alpha blockers (the alpha gate passes) | +| 2026-10-10 | major | Created from the 2026-10-10 re-measure against Figma desktop 126.10.7 (replaces the gap list that lived in ROADMAP.md) | diff --git a/docs/roadmap.md b/docs/roadmap.md index 12f2a5fb..336eb1ad 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,124 +1,125 @@ -# FigCraft roadmap: milestones and what's next - -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading, SVG import budgets, Leptos code, full screen, server snapshots, owner-only preferences, ruzstd 0.9.1, layer cycle repair, batch rollback, Direct3D 12 on Windows, plugin metering; vector edit mode regions: hover hatch, selection and drag; repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; `.fig` import, best effort; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%) · **Target:** Figma Design (desktop app 126.10.7) - -Forward-looking plan. Where FigCraft stands today: [`ROADMAP.md`](../ROADMAP.md) and -[`target-app-parity.md`](target-app-parity.md); the full ranked list: [`gaps.md`](gaps.md). Hours -are one Opus 5.5 agent; most milestones split across 4–6 agents on disjoint crates. There is no -`cargo xtask scorecard` in this repo; the measured number is `cargo xtask parity`. - -## Current focus - -1. **`.fig` import's tail**: editable vector networks, the skipped layer kinds that matter and - fixes from real files (the best-effort import landed 2026-10-11): 8–15 h. -2. **Real-file corpus and fidelity measurement**: import real files, compare against Figma's own - exports, turn estimated percentages into measured ones: 15–30 h. -3. **Tool-by-tool interaction pass** against the running Figma ([`ui-parity.md`](ui-parity.md)): - 40–60 h, parallel by tool. -4. **Languages** on the landed localization layer: native reviews of the es, fr, de, pt-BR, ja, - ko and zh-Hans drafts and the es-419 overlay (people), then the other key languages: 3–7 h per language - plus native review. -5. **Multiplayer M9**: accounts and teams on top of the live sessions and the standalone server (the WebSocket transport, hosting from the app, invite links that open the app, document sync, live cursors, selections, text carets, avatars and follow, per-user undo, reconnect with offline edits, collision-free ids for styles, variables, collections and comments, joining from the browser, and `figcraft-server` with persistence, publishing from the app and share links to edit or view landed), per [`collab-protocol.md`](collab-protocol.md) and [`server.md`](server.md): 45–90 h. - -## Alpha gate - -The core workflows a Figma Design user runs every day, checked end to end on macOS (the main -platform) with work saved to `.figcraft` and reopened. Evidence is the repo's tests and the -builders' side-by-side checks with synthetic files; nothing has been run on real Figma files yet. - -| Workflow | Works end to end? | Evidence | Hours to pass | -|---|---|---|---| -| Design screens: frames, shapes, vectors, text, auto layout, styles; save, reopen, export assets | yes | tools and panels in `crates/engine` and `crates/ui-egui`; `.figcraft` round trips (`crates/format/src/large_tests.rs`); PNG/JPG/SVG/PDF export tests | — | -| Build and use a design system: components, variants, properties, variables and modes, libraries | yes | `components_scale_tests.rs` (300 components, 2,000 instances), `library_tests.rs`, `library_variables_tests.rs`; libraries are local files, not shared | — | -| Prototype and present: interactions, flows, overlays, smart animate, variables | yes | `proto_*_tests.rs`, `present_*_tests.rs` | — | -| Hand off to developers: Dev Mode inspect, code, measurements, asset export, ready for dev | yes | `devinspect_tests.rs`, `component_codegen_tests.rs`, `devcompare_tests.rs`, `annotations_tests.rs` | — | -| Review with others: comments, versions, sharing the work | partial (not blocking) | comments with threads, mentions and resolve plus version history work inside a file (`comments_tests.rs`, `version_tests.rs`); the file is shared by passing it along; live sessions (preview: hosted from the desktop app or a self-hosted `figcraft-server` with edit and view links, joined from the app or the browser, undo per person); no accounts or teams ([collaboration-parity](collaboration-parity.md)) | beta gap: 45–90 | -| Continue existing Figma work (open `.fig`) | partial (beta gap) | best-effort `.fig` import (`crates/format/src/fig`, tested on generated fixtures, not yet on real files); REST JSON import works for files fetched through Figma's API ([file-format-parity](file-format-parity.md)) | beta gap: 8–15 | - -**Passes, so FigCraft stays alpha:** every daily design workflow works end to end on FigCraft's -own files, which save and reopen, and ready for real work (~47%) is above the ~40% bar. The two -gaps are real but don't break a workflow done in FigCraft: review works asynchronously through -comments in a shared file, and opening `.fig` is the step that moves work across from Figma, which -the standard requires for beta, not alpha. Both stay at the top of [`gaps.md`](gaps.md) as beta -blockers. - -## Milestones - -| Milestone | Status | Left | Hours | -|---|---|---|---| -| M0 Foundation: workspace, model, renderer, engine, UI shell, app, control channel | done (2026-10-09) | — | — | -| M0.9 CLI + MCP, web build, release packaging, PDF export | done (v0.2.0 released 2026-10-10 for every platform) | — | — | -| M1 Core editing completeness (tools, panels, menus, clipboard, files) | ~77% | interaction pass (Open recent, the outline mode variants and keyboard box selection landed 2026-10-11; repeat commands preserve the underlying edit and undo steps, #181; the Layout panel's constrain-proportions link scales the other side, #220) | 6–12 + pass | -| M2 Layout depth | ~80% | grid `fr` rows, real-file QA | 6–12 | -| M3 Components & styles depth | ~78% (extended collections, variables import/export, gradient stop and variant bindings landed 2026-10-11) | real libraries, component playground | 7–14 | -| M4 Vector & text depth | ~80% (live booleans, arc fields, shape builder on strokes, boolean hit testing on the result, one-sided widths, text on a path, pattern fills, brushes and pen pressure landed 2026-10-11; vector edit mode regions 2026-10-13) | spell check, Figma Draw multi-edit | 7–18 | -| M5 Prototype & present | ~85% | media, controllers on macOS/Linux/FreeBSD, separate window | 2–5 | -| M6 Interop & Dev Mode | ~65% (code connect, Dev Mode plugins and best-effort `.fig` import landed 2026-10-11) | `.fig` import's tail, video/HEIC (animated GIFs and TIFF landed 2026-10-11), CMYK intent | 15–30 | -| M7 Release & fidelity (signed builds, look & feel, shortcuts) | builds done; fidelity pass started | tool-by-tool pass | 40–60 | -| M8 Hardening & performance (bench, retained canvas, accessibility, idle) | ~60% | real-file corpus, platform QA, GPU canvas, screen readers | 60–110 | -| M9 Real-time collaboration | core done (#103: ids, change sets, server ordering, convergence); live sessions (WebSocket transport with limits and reconnect, hosting from the app, invite links, document sync between apps, cursors, selections, avatars, follow, `collab.*` commands); per-user undo, rejection handling, offline queue and resuming after a reconnect; styles, variables, collections, modes and comments created in a session get ids from their creator's space; others' text carets and selections on the canvas; the browser client (joins desktop-hosted sessions); `figcraft://` links open the app and join (Windows registers per user; macOS and Linux packages declare the scheme); the standalone `figcraft-server` (persistence with compaction, restarts that lose nothing confirmed, publishing from the app, share links to edit or view, revocable); team libraries kept on the server and enabled by a share link, with usage counts, and swapping a library; `wss://` from the desktop app, server releases and a deployment guide | accounts, teams | 45–90 | -| M10 Localization (new) | not started | layer + 11 languages | 60–100 | -| M11 Plugins | runtime, API, menu, manager and SDK done (#101); plugin panels and settings (ABI v2); widgets (API 3), panels side by side, colour picker on plugin fields; a plugin directory on `figcraft-server` (publish, browse, install, update) | a public catalogue, publisher accounts and signatures, the directory in the browser, richer widget events; Figma plugin compatibility is an owner decision | 8–26 | -| M12 AI features (new, owner decision) | not started | local models | 30–60 | - -## Next, after the current focus - -- Team libraries for a whole team (lists, accounts and permissions) and deeper library analytics (4–8 h, after - accounts), branching (8–15 h). -- GPU canvas held to the CPU renderer (30–60 h). -- Figma Draw multi-edit and pen pressure on macOS/Linux tablets (2–5 h). -- Mobile viewer and prototype mirror (20–40 h). - -## Revision history - -| Date | Change | Summary | -|---|---|---| -| 2026-10-14 | minor | M7: the Small caps button in Type settings showed a box where the UI font has no small-capital `ɢ` (builds without Inter use egui's fonts); its sample is now a capital and a smaller capital, drawn from ordinary letters every UI font has. Numbers unchanged | -| 2026-10-14 | minor | M7: Home screen recent files as cards: each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged. | -| 2026-10-14 | minor | Lost images reported on loading (#207): a `.figcraft` file whose image member is damaged, too large or missing no longer loads in silence; each lost image (up to 20, then a count) is listed in `repairs` on `file.open` with its hash and the reason, and the app shows it as a toast. Saving still writes the file without the lost images. Numbers unchanged | -| 2026-10-14 | minor | SVG import budgets (#190): gzipped SVG is unpacked by FigCraft within 64 MiB before parsing (it was unpacked without a limit), so the nesting check sees the real document; SVG documents embedded in `` data URIs share one 32 MiB budget and at most 4 levels, and one past a budget is left out. Linked files stay unread. Numbers unchanged | -| 2026-10-14 | minor | Leptos code (#223): `inspect.code`, `inspect.componentCode` and `codeConnect.set` take `language: leptos`: a `#[component]` with `#[prop]` defaults, variant enums and `view!` markup, instances as calls, Code Connect `use` lines; the Dev Mode language list and rem units include it. Numbers unchanged | -| 2026-10-14 | minor | Full screen (#227): View ▸ Full screen (`view.fullscreen {on?}`, F11 on Windows and Linux, Ctrl+Cmd+F on macOS) makes the window fill the screen and leaves it again; FigCraft's own item. Numbers unchanged | -| 2026-10-14 | minor | Colour picker: wrapped rows of page and recent swatches stay inset on both sides instead of running past the picker's right edge. Numbers unchanged | -| 2026-10-14 | minor | Server snapshots (#188): figcraft-server never replaces a readable snapshot with one too large to be read back, and reads snapshots up to twice the file cap (recent batches kept for reconnecting clients may take a file over it). Numbers unchanged | -| 2026-10-14 | minor | Preferences owner-only on Unix (#200): `state.json`, which holds online owner tokens, is written with mode 0600 from the first byte, and an older world-readable file is narrowed. Numbers unchanged | -| 2026-10-14 | minor | ruzstd 0.9.1 (#182): `.fig` import requires the release that fixes RUSTSEC-2026-0344 (a zstd decompression bomb). Numbers unchanged | -| 2026-10-14 | minor | Layer cycles (#205): `validate` reports children listed twice, pages with a parent and layers not reachable from a page (cycles, detached subtrees); `repair` drops them in one linear pass. Numbers unchanged | -| 2026-10-14 | minor | AppImage CLI: `figcraft.AppImage cli ` runs the bundled `figcraft-cli` (MCP server included) on Linux without a second download. Numbers unchanged | -| 2026-10-14 | minor | Boolean outline depth (#193): nested boolean groups share one outline depth budget instead of each level getting its own. Numbers unchanged | -| 2026-10-14 | minor | Command batches roll back as one: a failed `command.batch` (MCP `batch`) restores every open document and its state, and puts the tab in front back. Numbers unchanged | -| 2026-10-14 | minor | Desktop builds no longer rebuild the text crate on every build when the craft-fonts manifest is missing. Numbers unchanged | -| 2026-10-14 | minor | Direct3D 12 on Windows: the desktop app asks wgpu for Direct3D 12 there instead of Vulkan, so it starts without Vulkan loader errors on machines with stray overlay layers. Numbers unchanged | -| 2026-10-14 | minor | Plugin metering (#192): every host call and every byte copied between a plugin and the host costs fuel, so a plugin can't stall the app through cheap-looking host calls. Numbers unchanged | -| 2026-10-13 | minor | M4: Vector edit mode regions: the filled area under the pointer is hatched (clear of points and segments, holes left out, octave-folded spacing fixed to the shape), and a click, Shift-click or drag selects and moves areas with their points (`vector_regions_tests`, `regions_tests`). Estimates unchanged. | -| 2026-10-13 | minor | M1, #220: The Layout panel's constrain-proportions link keeps the ratio on a W or H edit; the property edit dropped it, so the link only worked while dragging resize handles. Both sides in one edit stay as typed, the `w`/`h` aliases work the same, each layer of a multi-selection keeps its own ratio, and a zero side has no ratio to keep (transform behaviour suite). Estimates unchanged. | -| 2026-10-13 | minor | M1, #181: Repeat last command keeps the underlying command and parameters instead of recording itself; a stored self-reference is refused. Three cumulative repeats and their individual undo steps are covered by regression tests. Estimates unchanged. | -| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Focus item 4 adds the zh-Hans draft's review | -| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Focus item 4 no longer lists Latin American Spanish | -| 2026-10-12 | minor | M5: prototyping tail: controller buttons on key/gamepad triggers (Windows and the browser), widgets reacting in prototypes, `proto.evaluate`; M5's tail 3–6 h → 2–5 h | -| 2026-10-12 | minor | M11: a plugin directory on `figcraft-server` (bundles published with the server key and checked in full, browsed and installed from Plugins ▸ Manage plugins after a permissions confirmation, hash-checked downloads, updates by version, `plugin.directory.*`, `figcraft-cli plugin publish`); 10–30 h → 8–26 h | -| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Current focus 4 now lists the drafts' native reviews | -| 2026-10-12 | minor | M11: plugin widgets (API 3: plugin-drawn layers on the canvas, redrawn on clicks from state kept in the document), panels of several plugins side by side and the colour picker on plugin colour fields; 15–40 h → 10–30 h | -| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): every UI string, command label and menu wording (1,899 entries) machine-drafted in each and marked as drafts awaiting native review, falling back to English per key (new strings need only English and Spanish); 0 counts as singular in French and Brazilian Portuguese; System picks the first OS language with a catalog (pt-PT and pt read pt-BR); long translations elide with a tooltip in dropdowns, two-column labels and the home cards. Current focus 4 now lists Japanese and Korean | -| 2026-10-11 | minor | `.fig` import, best effort: current focus 1 and the alpha gate's `.fig` row move to the import's tail (30–60 h → 8–15 h); M6 ~55% → ~65%, 37–75 h → 15–30 h | -| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | -| 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | -| 2026-10-11 | minor | Localization layer landed (gap 5): per-language catalogs baked in at build time with English text as keys and fallback, placeholders and plurals, command labels by id, Preferences ▸ Language (System follows the OS or the browser) applied live and `pref.language`; English catalog and a complete Spanish draft awaiting native review. Localization 10% → 20%, 60–100 h → 45–75 h; ready for real work unchanged (~47%); full parity ~363–697 h → ~348–672 h; beta ~200–365 h → ~195–355 h | -| 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | -| 2026-10-11 | minor | Plugin panels and settings landed (gap 6, ABI v2; v1 plugins unchanged): a plugin shows a panel of notes, fields (text, number, checkbox, menu, list, colour) and buttons drawn with FigCraft's widgets; a button or a changed value calls it back with every value (same limits per callback, one undo step per callback that edits, rolled back on failure), its module staying loaded while the panel is open; `plugin.ui` / `plugin.event` / `plugin.close` drive panels headless; per-plugin settings (64 KB) kept with the preferences; the Swatch grid example gained a palette panel that remembers the last palette. Plugins & widgets breadth 35% → 45%, ready 20% → 28%, 25–55 h → 15–40 h; ecosystem 20% → 28%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~373–712 h → ~363–697 h | -| 2026-10-11 | minor | Canvas fixes (gap 13 closed): boolean groups are clicked where their result is (a click in a subtracted hole reaches what's behind; with the group or an operand selected its operands are hit, so each can still be picked), a layer dragged inside a group stays in the group (frames and auto layout still take other drops), View ▸ Frame outlines and View ▸ Slices draw on the canvas, and the idle tests wait for the render thread instead of counting frames. Shapes & vector 1–3 h → 1–2 h (vector QA with real files left); percentages, feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~378–722 h → ~375–717 h | -| 2026-10-11 | minor | Stretch and scatter brushes and pen pressure landed (gap 9): eight brushes drawn in code (four stretched along the path, four scattered with spacing, size, rotation and scatter jitter from a seed) in the stroke settings, drawn everywhere strokes are and flattened by outline stroke; the Pencil records pen pressure as the stroke's width profile (Windows pens, browser styluses; macOS and Linux tablets report none) and can draw with a brush; Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–9 h; Strokes 2–4 h → 1–3 h; Hardware 50% → 55%, 15–30 h → 13–27 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~378–722 h → ~373–712 h | -| 2026-10-11 | minor | Text on a path and pattern fills landed (gap 9): text follows a path or shape (start, side, alignment; edited in place; canvas, PNG, SVG and PDF; SVG `` imports as text on a path) and pattern fills tile a layer (grid or half-shifted rows or columns, scale, spacing, alignment; live as the source changes; SVG ``, PDF tiling patterns); Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–10 h; Text 8–14 h → 7–13 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~375–715 h | -| 2026-10-11 | minor | Team libraries on `figcraft-server` landed (gap 10): a file's library is published to the server under the file and enabled in other files by a share link (per-asset versions; updates noticed in the background and accepted with overrides kept), the server counts which files use which components, and a whole library can be swapped for another by names; Team libraries breadth 40% → 70%, ready 25% → 40%, 10–20 h → 4–8 h; collaboration 12% → 13%, 55–110 h → 49–98 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~380–725 h → ~375–715 h | -| 2026-10-11 | minor | Branches landed (gap 17): File ▸ Create branch… copies the file into a branch tab that remembers its base; Review and merge shows both sides' changes (layers with Inspect-style property changes, styles, variables, comments) and the conflicts (the same property changed on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merging is one undo step and archives the branch; Update from main; branches are saved in the `.figcraft` file. Comments, versions, find, misc breadth 80% → 85%, ready 58% → 60%, 4–8 h → 2–5 h; collaboration 12% → 13%; menu breadth 204/204 (the catalog gained File ▸ Create branch…); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~378–722 h | -| 2026-10-11 | minor | Animated GIF fills and TIFF images (gap 12, without video and HEIC): GIF fills keep the original file, show the first frame on the canvas and in exports and play in the presenter and the preview (frames decoded on first play, at most 500 frames and 128 MB; always looping; the window wakes only for a GIF's next frame); TIFF images open, import, place and paste. Paint breadth 85% → 87%, 5–9 h → 4–7 h; gap 12 6–12 h → 5–10 h (video playback and HEIC left, both needing non-Rust decoders); feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | The last three catalogued menu items got commands (gap 21): File ▸ Open recent (recent files kept in the preferences, at most 20, a file that's gone greyed in the menu and dropped when picked, Clear recent), View ▸ Outlines ▸ Show outlines for selection and View ▸ Mask outlines: menu breadth 98.5% → 100% (203/203); Canvas & navigation breadth 95% → 97%, 3–6 h → 2–5 h; feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | Extended collections, variables import/export and gradient stop and variant property bindings landed (gap 14): Styles & variables breadth 85% → 92%, ready 70% → 72%, 4–8 h → 1–2 h; feature breadth unchanged (~79%); ready for real work unchanged (~47%); full parity ~385–730 h → ~380–725 h | -| 2026-10-11 | minor | Collaboration wording after the standalone server: `figcraft-server` keeps many files, persisted as a snapshot plus a log of what changed (compacted, checksummed, damaged files skipped and reported), restarts lose nothing confirmed and apps resume where they were; the app publishes its file to it; share links per file let people edit or only view (the server refuses a viewer's edits), are revocable and stored as hashes; TLS comes from a reverse proxy (the desktop app has none of its own yet); percentages and hours unchanged until the next re-measure | -| 2026-10-11 | minor | Live boolean editing, Design panel arc fields, the shape builder on strokes and open paths and one-sided variable widths landed (gap 13): Shapes & vector breadth 92% → 97%, ready 66% → 68%, 8–14 h → 1–3 h; feature breadth ~78% → ~79%; ready for real work unchanged (~47%); full parity ~390–745 h → ~385–730 h | -| 2026-10-11 | minor | Collaboration wording after the collaboration polish: styles, variables, collections, modes and comments created in a live session take ids from their creator's id space (concurrent creations no longer overwrite each other; the server checks it), others' text carets and selections show on the canvas, and typing in a layer someone else is typing in says so; percentages unchanged | -| 2026-10-11 | minor | Wording only: live sessions (#104), then the browser joining them and `figcraft://` invite links opening the desktop app; percentages and hours unchanged until the next re-measure | -| 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 | minor | Added the alpha gate (core-workflow table): passes, stage stays alpha | -| 2026-10-10 | major | Created: milestone table moved from ROADMAP.md with statuses re-checked; added M10–M12; current focus re-planned | +# FigCraft roadmap: milestones and what's next + +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (Outline stroke preserves stroke holes; recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading, SVG import budgets, Leptos code, full screen, server snapshots, owner-only preferences, ruzstd 0.9.1, layer cycle repair, batch rollback, Direct3D 12 on Windows, plugin metering; vector edit mode regions: hover hatch, selection and drag; repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; `.fig` import, best effort; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%) · **Target:** Figma Design (desktop app 126.10.7) + +Forward-looking plan. Where FigCraft stands today: [`ROADMAP.md`](../ROADMAP.md) and +[`target-app-parity.md`](target-app-parity.md); the full ranked list: [`gaps.md`](gaps.md). Hours +are one Opus 5.5 agent; most milestones split across 4–6 agents on disjoint crates. There is no +`cargo xtask scorecard` in this repo; the measured number is `cargo xtask parity`. + +## Current focus + +1. **`.fig` import's tail**: editable vector networks, the skipped layer kinds that matter and + fixes from real files (the best-effort import landed 2026-10-11): 8–15 h. +2. **Real-file corpus and fidelity measurement**: import real files, compare against Figma's own + exports, turn estimated percentages into measured ones: 15–30 h. +3. **Tool-by-tool interaction pass** against the running Figma ([`ui-parity.md`](ui-parity.md)): + 40–60 h, parallel by tool. +4. **Languages** on the landed localization layer: native reviews of the es, fr, de, pt-BR, ja, + ko and zh-Hans drafts and the es-419 overlay (people), then the other key languages: 3–7 h per language + plus native review. +5. **Multiplayer M9**: accounts and teams on top of the live sessions and the standalone server (the WebSocket transport, hosting from the app, invite links that open the app, document sync, live cursors, selections, text carets, avatars and follow, per-user undo, reconnect with offline edits, collision-free ids for styles, variables, collections and comments, joining from the browser, and `figcraft-server` with persistence, publishing from the app and share links to edit or view landed), per [`collab-protocol.md`](collab-protocol.md) and [`server.md`](server.md): 45–90 h. + +## Alpha gate + +The core workflows a Figma Design user runs every day, checked end to end on macOS (the main +platform) with work saved to `.figcraft` and reopened. Evidence is the repo's tests and the +builders' side-by-side checks with synthetic files; nothing has been run on real Figma files yet. + +| Workflow | Works end to end? | Evidence | Hours to pass | +|---|---|---|---| +| Design screens: frames, shapes, vectors, text, auto layout, styles; save, reopen, export assets | yes | tools and panels in `crates/engine` and `crates/ui-egui`; `.figcraft` round trips (`crates/format/src/large_tests.rs`); PNG/JPG/SVG/PDF export tests | — | +| Build and use a design system: components, variants, properties, variables and modes, libraries | yes | `components_scale_tests.rs` (300 components, 2,000 instances), `library_tests.rs`, `library_variables_tests.rs`; libraries are local files, not shared | — | +| Prototype and present: interactions, flows, overlays, smart animate, variables | yes | `proto_*_tests.rs`, `present_*_tests.rs` | — | +| Hand off to developers: Dev Mode inspect, code, measurements, asset export, ready for dev | yes | `devinspect_tests.rs`, `component_codegen_tests.rs`, `devcompare_tests.rs`, `annotations_tests.rs` | — | +| Review with others: comments, versions, sharing the work | partial (not blocking) | comments with threads, mentions and resolve plus version history work inside a file (`comments_tests.rs`, `version_tests.rs`); the file is shared by passing it along; live sessions (preview: hosted from the desktop app or a self-hosted `figcraft-server` with edit and view links, joined from the app or the browser, undo per person); no accounts or teams ([collaboration-parity](collaboration-parity.md)) | beta gap: 45–90 | +| Continue existing Figma work (open `.fig`) | partial (beta gap) | best-effort `.fig` import (`crates/format/src/fig`, tested on generated fixtures, not yet on real files); REST JSON import works for files fetched through Figma's API ([file-format-parity](file-format-parity.md)) | beta gap: 8–15 | + +**Passes, so FigCraft stays alpha:** every daily design workflow works end to end on FigCraft's +own files, which save and reopen, and ready for real work (~47%) is above the ~40% bar. The two +gaps are real but don't break a workflow done in FigCraft: review works asynchronously through +comments in a shared file, and opening `.fig` is the step that moves work across from Figma, which +the standard requires for beta, not alpha. Both stay at the top of [`gaps.md`](gaps.md) as beta +blockers. + +## Milestones + +| Milestone | Status | Left | Hours | +|---|---|---|---| +| M0 Foundation: workspace, model, renderer, engine, UI shell, app, control channel | done (2026-10-09) | — | — | +| M0.9 CLI + MCP, web build, release packaging, PDF export | done (v0.2.0 released 2026-10-10 for every platform) | — | — | +| M1 Core editing completeness (tools, panels, menus, clipboard, files) | ~77% | interaction pass (Open recent, the outline mode variants and keyboard box selection landed 2026-10-11; repeat commands preserve the underlying edit and undo steps, #181; the Layout panel's constrain-proportions link scales the other side, #220) | 6–12 + pass | +| M2 Layout depth | ~80% | grid `fr` rows, real-file QA | 6–12 | +| M3 Components & styles depth | ~78% (extended collections, variables import/export, gradient stop and variant bindings landed 2026-10-11) | real libraries, component playground | 7–14 | +| M4 Vector & text depth | ~80% (live booleans, arc fields, shape builder on strokes, boolean hit testing on the result, one-sided widths, text on a path, pattern fills, brushes and pen pressure landed 2026-10-11; vector edit mode regions 2026-10-13; stroke holes preserved 2026-10-15) | outline-stroke compositing, spell check, Figma Draw multi-edit | 7–18 | +| M5 Prototype & present | ~85% | media, controllers on macOS/Linux/FreeBSD, separate window | 2–5 | +| M6 Interop & Dev Mode | ~65% (code connect, Dev Mode plugins and best-effort `.fig` import landed 2026-10-11) | `.fig` import's tail, video/HEIC (animated GIFs and TIFF landed 2026-10-11), CMYK intent | 15–30 | +| M7 Release & fidelity (signed builds, look & feel, shortcuts) | builds done; fidelity pass started | tool-by-tool pass | 40–60 | +| M8 Hardening & performance (bench, retained canvas, accessibility, idle) | ~60% | real-file corpus, platform QA, GPU canvas, screen readers | 60–110 | +| M9 Real-time collaboration | core done (#103: ids, change sets, server ordering, convergence); live sessions (WebSocket transport with limits and reconnect, hosting from the app, invite links, document sync between apps, cursors, selections, avatars, follow, `collab.*` commands); per-user undo, rejection handling, offline queue and resuming after a reconnect; styles, variables, collections, modes and comments created in a session get ids from their creator's space; others' text carets and selections on the canvas; the browser client (joins desktop-hosted sessions); `figcraft://` links open the app and join (Windows registers per user; macOS and Linux packages declare the scheme); the standalone `figcraft-server` (persistence with compaction, restarts that lose nothing confirmed, publishing from the app, share links to edit or view, revocable); team libraries kept on the server and enabled by a share link, with usage counts, and swapping a library; `wss://` from the desktop app, server releases and a deployment guide | accounts, teams | 45–90 | +| M10 Localization (new) | not started | layer + 11 languages | 60–100 | +| M11 Plugins | runtime, API, menu, manager and SDK done (#101); plugin panels and settings (ABI v2); widgets (API 3), panels side by side, colour picker on plugin fields; a plugin directory on `figcraft-server` (publish, browse, install, update) | a public catalogue, publisher accounts and signatures, the directory in the browser, richer widget events; Figma plugin compatibility is an owner decision | 8–26 | +| M12 AI features (new, owner decision) | not started | local models | 30–60 | + +## Next, after the current focus + +- Team libraries for a whole team (lists, accounts and permissions) and deeper library analytics (4–8 h, after + accounts), branching (8–15 h). +- GPU canvas held to the CPU renderer (30–60 h). +- Figma Draw multi-edit and pen pressure on macOS/Linux tablets (2–5 h). +- Mobile viewer and prototype mirror (20–40 h). + +## Revision history + +| Date | Change | Summary | +|---|---|---| +| 2026-10-15 | minor | M4: Outline stroke preserves even-odd stroke holes. Pixel comparisons cover alignment, dashes, individual side weights, vector paths and stroke-only opacity; native save/reopen, Undo/Redo and instance opacity inheritance/overrides are covered by `outline_stroke`. Existing estimates unchanged; filled-shape compositing needs separate work. | +| 2026-10-14 | minor | M7: the Small caps button in Type settings showed a box where the UI font has no small-capital `ɢ` (builds without Inter use egui's fonts); its sample is now a capital and a smaller capital, drawn from ordinary letters every UI font has. Numbers unchanged | +| 2026-10-14 | minor | M7: Home screen recent files as cards: each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged. | +| 2026-10-14 | minor | Lost images reported on loading (#207): a `.figcraft` file whose image member is damaged, too large or missing no longer loads in silence; each lost image (up to 20, then a count) is listed in `repairs` on `file.open` with its hash and the reason, and the app shows it as a toast. Saving still writes the file without the lost images. Numbers unchanged | +| 2026-10-14 | minor | SVG import budgets (#190): gzipped SVG is unpacked by FigCraft within 64 MiB before parsing (it was unpacked without a limit), so the nesting check sees the real document; SVG documents embedded in `` data URIs share one 32 MiB budget and at most 4 levels, and one past a budget is left out. Linked files stay unread. Numbers unchanged | +| 2026-10-14 | minor | Leptos code (#223): `inspect.code`, `inspect.componentCode` and `codeConnect.set` take `language: leptos`: a `#[component]` with `#[prop]` defaults, variant enums and `view!` markup, instances as calls, Code Connect `use` lines; the Dev Mode language list and rem units include it. Numbers unchanged | +| 2026-10-14 | minor | Full screen (#227): View ▸ Full screen (`view.fullscreen {on?}`, F11 on Windows and Linux, Ctrl+Cmd+F on macOS) makes the window fill the screen and leaves it again; FigCraft's own item. Numbers unchanged | +| 2026-10-14 | minor | Colour picker: wrapped rows of page and recent swatches stay inset on both sides instead of running past the picker's right edge. Numbers unchanged | +| 2026-10-14 | minor | Server snapshots (#188): figcraft-server never replaces a readable snapshot with one too large to be read back, and reads snapshots up to twice the file cap (recent batches kept for reconnecting clients may take a file over it). Numbers unchanged | +| 2026-10-14 | minor | Preferences owner-only on Unix (#200): `state.json`, which holds online owner tokens, is written with mode 0600 from the first byte, and an older world-readable file is narrowed. Numbers unchanged | +| 2026-10-14 | minor | ruzstd 0.9.1 (#182): `.fig` import requires the release that fixes RUSTSEC-2026-0344 (a zstd decompression bomb). Numbers unchanged | +| 2026-10-14 | minor | Layer cycles (#205): `validate` reports children listed twice, pages with a parent and layers not reachable from a page (cycles, detached subtrees); `repair` drops them in one linear pass. Numbers unchanged | +| 2026-10-14 | minor | AppImage CLI: `figcraft.AppImage cli ` runs the bundled `figcraft-cli` (MCP server included) on Linux without a second download. Numbers unchanged | +| 2026-10-14 | minor | Boolean outline depth (#193): nested boolean groups share one outline depth budget instead of each level getting its own. Numbers unchanged | +| 2026-10-14 | minor | Command batches roll back as one: a failed `command.batch` (MCP `batch`) restores every open document and its state, and puts the tab in front back. Numbers unchanged | +| 2026-10-14 | minor | Desktop builds no longer rebuild the text crate on every build when the craft-fonts manifest is missing. Numbers unchanged | +| 2026-10-14 | minor | Direct3D 12 on Windows: the desktop app asks wgpu for Direct3D 12 there instead of Vulkan, so it starts without Vulkan loader errors on machines with stray overlay layers. Numbers unchanged | +| 2026-10-14 | minor | Plugin metering (#192): every host call and every byte copied between a plugin and the host costs fuel, so a plugin can't stall the app through cheap-looking host calls. Numbers unchanged | +| 2026-10-13 | minor | M4: Vector edit mode regions: the filled area under the pointer is hatched (clear of points and segments, holes left out, octave-folded spacing fixed to the shape), and a click, Shift-click or drag selects and moves areas with their points (`vector_regions_tests`, `regions_tests`). Estimates unchanged. | +| 2026-10-13 | minor | M1, #220: The Layout panel's constrain-proportions link keeps the ratio on a W or H edit; the property edit dropped it, so the link only worked while dragging resize handles. Both sides in one edit stay as typed, the `w`/`h` aliases work the same, each layer of a multi-selection keeps its own ratio, and a zero side has no ratio to keep (transform behaviour suite). Estimates unchanged. | +| 2026-10-13 | minor | M1, #181: Repeat last command keeps the underlying command and parameters instead of recording itself; a stored self-reference is refused. Three cumulative repeats and their individual undo steps are covered by regression tests. Estimates unchanged. | +| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Focus item 4 adds the zh-Hans draft's review | +| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Focus item 4 no longer lists Latin American Spanish | +| 2026-10-12 | minor | M5: prototyping tail: controller buttons on key/gamepad triggers (Windows and the browser), widgets reacting in prototypes, `proto.evaluate`; M5's tail 3–6 h → 2–5 h | +| 2026-10-12 | minor | M11: a plugin directory on `figcraft-server` (bundles published with the server key and checked in full, browsed and installed from Plugins ▸ Manage plugins after a permissions confirmation, hash-checked downloads, updates by version, `plugin.directory.*`, `figcraft-cli plugin publish`); 10–30 h → 8–26 h | +| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Current focus 4 now lists the drafts' native reviews | +| 2026-10-12 | minor | M11: plugin widgets (API 3: plugin-drawn layers on the canvas, redrawn on clicks from state kept in the document), panels of several plugins side by side and the colour picker on plugin colour fields; 15–40 h → 10–30 h | +| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): every UI string, command label and menu wording (1,899 entries) machine-drafted in each and marked as drafts awaiting native review, falling back to English per key (new strings need only English and Spanish); 0 counts as singular in French and Brazilian Portuguese; System picks the first OS language with a catalog (pt-PT and pt read pt-BR); long translations elide with a tooltip in dropdowns, two-column labels and the home cards. Current focus 4 now lists Japanese and Korean | +| 2026-10-11 | minor | `.fig` import, best effort: current focus 1 and the alpha gate's `.fig` row move to the import's tail (30–60 h → 8–15 h); M6 ~55% → ~65%, 37–75 h → 15–30 h | +| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | +| 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | +| 2026-10-11 | minor | Localization layer landed (gap 5): per-language catalogs baked in at build time with English text as keys and fallback, placeholders and plurals, command labels by id, Preferences ▸ Language (System follows the OS or the browser) applied live and `pref.language`; English catalog and a complete Spanish draft awaiting native review. Localization 10% → 20%, 60–100 h → 45–75 h; ready for real work unchanged (~47%); full parity ~363–697 h → ~348–672 h; beta ~200–365 h → ~195–355 h | +| 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | +| 2026-10-11 | minor | Plugin panels and settings landed (gap 6, ABI v2; v1 plugins unchanged): a plugin shows a panel of notes, fields (text, number, checkbox, menu, list, colour) and buttons drawn with FigCraft's widgets; a button or a changed value calls it back with every value (same limits per callback, one undo step per callback that edits, rolled back on failure), its module staying loaded while the panel is open; `plugin.ui` / `plugin.event` / `plugin.close` drive panels headless; per-plugin settings (64 KB) kept with the preferences; the Swatch grid example gained a palette panel that remembers the last palette. Plugins & widgets breadth 35% → 45%, ready 20% → 28%, 25–55 h → 15–40 h; ecosystem 20% → 28%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~373–712 h → ~363–697 h | +| 2026-10-11 | minor | Canvas fixes (gap 13 closed): boolean groups are clicked where their result is (a click in a subtracted hole reaches what's behind; with the group or an operand selected its operands are hit, so each can still be picked), a layer dragged inside a group stays in the group (frames and auto layout still take other drops), View ▸ Frame outlines and View ▸ Slices draw on the canvas, and the idle tests wait for the render thread instead of counting frames. Shapes & vector 1–3 h → 1–2 h (vector QA with real files left); percentages, feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~378–722 h → ~375–717 h | +| 2026-10-11 | minor | Stretch and scatter brushes and pen pressure landed (gap 9): eight brushes drawn in code (four stretched along the path, four scattered with spacing, size, rotation and scatter jitter from a seed) in the stroke settings, drawn everywhere strokes are and flattened by outline stroke; the Pencil records pen pressure as the stroke's width profile (Windows pens, browser styluses; macOS and Linux tablets report none) and can draw with a brush; Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–9 h; Strokes 2–4 h → 1–3 h; Hardware 50% → 55%, 15–30 h → 13–27 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~378–722 h → ~373–712 h | +| 2026-10-11 | minor | Text on a path and pattern fills landed (gap 9): text follows a path or shape (start, side, alignment; edited in place; canvas, PNG, SVG and PDF; SVG `` imports as text on a path) and pattern fills tile a layer (grid or half-shifted rows or columns, scale, spacing, alignment; live as the source changes; SVG ``, PDF tiling patterns); Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–10 h; Text 8–14 h → 7–13 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~375–715 h | +| 2026-10-11 | minor | Team libraries on `figcraft-server` landed (gap 10): a file's library is published to the server under the file and enabled in other files by a share link (per-asset versions; updates noticed in the background and accepted with overrides kept), the server counts which files use which components, and a whole library can be swapped for another by names; Team libraries breadth 40% → 70%, ready 25% → 40%, 10–20 h → 4–8 h; collaboration 12% → 13%, 55–110 h → 49–98 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~380–725 h → ~375–715 h | +| 2026-10-11 | minor | Branches landed (gap 17): File ▸ Create branch… copies the file into a branch tab that remembers its base; Review and merge shows both sides' changes (layers with Inspect-style property changes, styles, variables, comments) and the conflicts (the same property changed on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merging is one undo step and archives the branch; Update from main; branches are saved in the `.figcraft` file. Comments, versions, find, misc breadth 80% → 85%, ready 58% → 60%, 4–8 h → 2–5 h; collaboration 12% → 13%; menu breadth 204/204 (the catalog gained File ▸ Create branch…); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~378–722 h | +| 2026-10-11 | minor | Animated GIF fills and TIFF images (gap 12, without video and HEIC): GIF fills keep the original file, show the first frame on the canvas and in exports and play in the presenter and the preview (frames decoded on first play, at most 500 frames and 128 MB; always looping; the window wakes only for a GIF's next frame); TIFF images open, import, place and paste. Paint breadth 85% → 87%, 5–9 h → 4–7 h; gap 12 6–12 h → 5–10 h (video playback and HEIC left, both needing non-Rust decoders); feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | The last three catalogued menu items got commands (gap 21): File ▸ Open recent (recent files kept in the preferences, at most 20, a file that's gone greyed in the menu and dropped when picked, Clear recent), View ▸ Outlines ▸ Show outlines for selection and View ▸ Mask outlines: menu breadth 98.5% → 100% (203/203); Canvas & navigation breadth 95% → 97%, 3–6 h → 2–5 h; feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | Extended collections, variables import/export and gradient stop and variant property bindings landed (gap 14): Styles & variables breadth 85% → 92%, ready 70% → 72%, 4–8 h → 1–2 h; feature breadth unchanged (~79%); ready for real work unchanged (~47%); full parity ~385–730 h → ~380–725 h | +| 2026-10-11 | minor | Collaboration wording after the standalone server: `figcraft-server` keeps many files, persisted as a snapshot plus a log of what changed (compacted, checksummed, damaged files skipped and reported), restarts lose nothing confirmed and apps resume where they were; the app publishes its file to it; share links per file let people edit or only view (the server refuses a viewer's edits), are revocable and stored as hashes; TLS comes from a reverse proxy (the desktop app has none of its own yet); percentages and hours unchanged until the next re-measure | +| 2026-10-11 | minor | Live boolean editing, Design panel arc fields, the shape builder on strokes and open paths and one-sided variable widths landed (gap 13): Shapes & vector breadth 92% → 97%, ready 66% → 68%, 8–14 h → 1–3 h; feature breadth ~78% → ~79%; ready for real work unchanged (~47%); full parity ~390–745 h → ~385–730 h | +| 2026-10-11 | minor | Collaboration wording after the collaboration polish: styles, variables, collections, modes and comments created in a live session take ids from their creator's id space (concurrent creations no longer overwrite each other; the server checks it), others' text carets and selections show on the canvas, and typing in a layer someone else is typing in says so; percentages unchanged | +| 2026-10-11 | minor | Wording only: live sessions (#104), then the browser joining them and `figcraft://` invite links opening the desktop app; percentages and hours unchanged until the next re-measure | +| 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 | minor | Added the alpha gate (core-workflow table): passes, stage stays alpha | +| 2026-10-10 | major | Created: milestone table moved from ROADMAP.md with statuses re-checked; added M10–M12; current focus re-planned | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index e15ddaed..1f17a8c0 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,702 +1,703 @@ -# FigCraft parity with Figma - -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS) - -The authoritative assessment of how close FigCraft is to Figma Design. [`ROADMAP.md`](../ROADMAP.md) -summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu checklist is -[`parity-checklist.md`](parity-checklist.md). Deeper checklists: -[`ui-parity.md`](ui-parity.md), [`file-format-parity.md`](file-format-parity.md), -[`hardware-parity.md`](hardware-parity.md), [`localization-parity.md`](localization-parity.md), -[`auto-layout-parity.md`](auto-layout-parity.md), [`components-parity.md`](components-parity.md), -[`prototyping-parity.md`](prototyping-parity.md), [`dev-mode-parity.md`](dev-mode-parity.md), -[`collaboration-parity.md`](collaboration-parity.md). - -## Headline - -| Number | Value | Kind | -|---|---|---| -| Menu/command breadth | **100%** (211 / 211 catalogued menu items have a live command) | **measured** (`cargo xtask parity`, 2026-10-12) | -| Feature breadth (does each Figma Design feature exist, weighted) | **~85%** (core editor areas 1–16: ~94%) | estimated from the area table below | -| Ready for real work | **~53%** | estimated (dimension table below: written weights × written per-dimension scores) | -| Essentials user | **~59%** | estimated ([below](#essentials-user): core features 77.8% × discounts 0.92 × 0.94 × 0.88) | -| Mainstream practitioner | **~50%** | estimated ([below](#mainstream-practitioner): weekly areas 72.7% × discounts 0.9 × 0.9 × 0.85) | -| Remaining to beta | **~155–280 h** one Opus 5.5 agent (~32–64 h wall-clock with 5 parallel agents) | estimated | -| Remaining to full parity | **~262–519 h** one agent (~83–145 h wall-clock with 5 parallel agents) | estimated | -| Stage | **alpha**: the alpha gate passes ([roadmap](roadmap.md#alpha-gate)); multiplayer and checking the best-effort `.fig` import on real files are beta gaps (see [ROADMAP](../ROADMAP.md#stage)) | | - -The previous figures (ROADMAP of 2026-10-10, "feature depth ~79%", "full parity 115–195 h") were -self-graded against a 16-area rubric that left out, by design, multiplayer, plugins, cloud -libraries and AI features, and that counted no file-format blocker. Figma users rely on all of -those every day, so this re-measure weights them in. The core-editor depth figure drops from ~79% -to ~67% because nothing has yet been checked against real Figma files or side by side tool by -tool; presence was being graded as behaviour in places. - -## Readiness by audience - -| Audience | Ready % | Opus 5.5 agent wall-clock hours to ~95% | Work that dominates | -|---|---|---|---| -| Full target (ready for real work) | ~53% | ~268–522 h | multiplayer (42–84), localization (29–51), plugins (8–26), the interaction pass (40–60), GPU canvas (30–60), AI (30–60), the `.fig` import's tail (5–10) | -| Mainstream practitioner | ~50% | ~120–229 h | core editor depth incl. the `.fig` import's tail (49–98), the interaction pass (40–60), real-file corpus and platform QA (25–50), checking the experimental `.fig` export opens elsewhere (5–15) | -| Essentials user | ~59% | ~55–100 h | the `.fig` import's tail so the files people send open well (5–10), core tool polish (25–45), interaction pass on the core tools (15–25), stability QA (10–20) | - -Hours are one agent working sequentially, calibrated from this repo's history (about 1 agent-hour -per 1,000 added lines, 1–2.5 h per feature PR, ~7 h per honest depth point in the polish tail; see -[how the hours are calibrated](#how-the-hours-are-calibrated)). Each audience's work is a subset of -the next. About 70–80% parallelizes across 4–6 agents on disjoint crates (roughly ÷4 in -wall-clock); the `.fig` reader, the multiplayer core and a plugin runtime each need one owner, and -native-speaker review, real-file licensing and screen-reader QA need people. - -The full number is an additive weighted sum over the dimension table; mainstream and essentials use -multiplicative discounts, as the standard says. - -## What was measured, and how - -- **Target and version.** Figma desktop for macOS, `CFBundleShortVersionString` 126.10.7, - installed at `/Applications/Figma.app`. FigCraft is measured as of `origin/main` d452da3 - (2026-10-10, PR #99). -- **The installed app, names and public listings only.** FigCraft is clean-room - ([`AGENTS.md`](../AGENTS.md)): nothing inside the bundle's code or web assets was read. From - `Info.plist`: one document type, `com.figma.document`, extensions `fig`, `jam`, `deck` - (MIME `application/x-figma`), and the `figma://` URL scheme. The `.lproj` folders are - Electron/Chromium locale packs (60+), not Figma's UI languages; the UI languages come from - Figma's help center (see [`localization-parity.md`](localization-parity.md)). The bundle ships - GPU, Plugin and Renderer helper processes (GPU rendering; a sandboxed plugin runtime). -- **The menu catalog.** `crates/engine/src/catalog.rs` holds Figma's main menu tree (202 items in - File, Edit, View, Object, Text, Arrange, Vector, Plugins, Preferences, Libraries, Help), written - from public documentation and black-box observation (local notes in the gitignored - `plan/figma/03-menu-tree.md`). It leaves out the Widgets menu and dynamic plugin lists, so the - 100% overstates breadth against the full menu a little. -- **From source** (no build; 2026-10-10): 449 command ids registered with `spec!`, 963 `#[test]` - functions in 177 files, ~108,800 lines of Rust in 13 crates + 3 apps + xtask (before #101–#103, which added the `plugin` and `collab` crates and ~7k lines), 140 key bindings - and 13 gesture rows in `figcraft_engine::keymap`, 21 files in the SVG import corpus, 0 - translated UI strings (no localization layer exists). -- **Figma's feature set**: help center, release notes and Config 2025 announcements (Figma Draw, - grid auto layout, slots, glass/noise/texture effects, progressive blur), known to the assessor up - to mid-2026, plus web checks on 2026-10-10 (supported image/video types, UI languages, pen - pressure in Figma Draw). -- **Side by side.** Look & feel was compared with Figma UI3 on the owner's Mac (screenshots kept - local, never committed). No real Figma file has been opened in FigCraft (no `.fig` reader), and - no rendered frame has been compared pixel for pixel against Figma's own export. - -## Weights - -Weights are the share of a typical product designer's working time in Figma Design, written down -so they can be argued with. Areas 1–16 keep the original rubric's weights (they sum to 94, not the 100 the rubric claimed); -areas 17–21 are added (sum 19) because Figma users depend on them. Percentages below divide by the -total, 113. -Out of scope as separate products: FigJam, Figma Slides, Sites, Make, Buzz, Weave (the `.jam` and -`.deck` document kinds belong to them). - -## By feature area - -Breadth: does it exist. Ready: depth, correctness and fidelity for real projects. Hours: one Opus -5.5 agent, sequential, tests included. - -| # | Area | Weight | Breadth | Ready | Remaining | Hours | -|---|---|---|---|---|---|---| -| 1 | Canvas & navigation | 6 | 98% | 74% | Outline mode's Include hidden layers and Include object bounds, Display P3 on screen, QA at extreme zooms (behaviour suite for navigation, rulers, guides, layout grids and outline mode landed: guide selection, Auto grid counts, outline-mode clicks) | 1–4 | -| 2 | Selection & transform | 8 | 97% | 72% | Handles, measuring and snapping checked tool by tool against Figma and real files (snap distance, handle reach, cursors); children turned off a right angle scale without skew, approximately (behaviour suite from the documentation, rotated groups and mixed selections, equal spacing snapping, Cmd/Ctrl to ignore constraints and marquees inside frames landed) | 3–5 | -| 3 | Layers panel | 5 | 99% | 85% | QA with long real files (behaviour suite from the documentation, Rename layers dialog, sweeping eyes and padlocks, page dragging and dividers, several layers dropped together, keyboard box selection, F2 rename, Shift+F10 context menu and 10k-layer timings landed) | 1–2 | -| 4 | Shapes & vector | 8 | 99% | 73% | Corner smoothing on polygons, stars and vectors; the transform box around points of several layers; per-point caps and joins on dashed, variable-width and brush strokes; vector QA with real files (behaviour suites from the documentation, corner radius and star ratio handles, rounded vector points drawn, also beside curved segments, a cap and join per point, several handles dragged together, cut and paint bucket drags, delete and heal along the curve, a boolean group's look by operation and the miter angle landed) | 1–3 | -| 5 | Paint | 7 | 90% | 71% | Video fills, wide-gamut canvas, HEIC images; plus darker in PDF (behaviour suite from the documentation, pass-through and isolating groups, clipping frames that don't isolate, plus darker, animated GIF fills, TIFF images and embedded image colour profiles landed) | 2–5 | -| 6 | Strokes | 3 | 95% | 75% | Stroke QA with real files (colour styles on strokes, stroke weight and per-side weight variables, brush strokes landed, see 20) | 1–2 | -| 7 | Effects | 4 | 100% | 75% | Glass and progressive blur tuned side by side (behaviour suite from the documentation, inner shadows and background blur on groups, shadows hidden behind translucent layers on the canvas and in every export landed) | 2–5 | -| 8 | Frames, groups, sections, masks | 5 | 97% | 80% | Visual QA with real files (a one-step frame/group convert, and a behaviour suite from the documentation landed: masked-out areas don't take clicks, mask group bounds and exports follow the mask, sections kept out of frames and groups, frames and sections drawn around layers take them, section resizing leaves contents in place, X/Y measured in the frame) | 1–3 | -| 9 | Auto layout & constraints | 9 | 98% | 74% | Behaviour against real Figma files (behaviour suite from the documentation, `fr` tracks like CSS, vertical wrap, auto gaps evenly/around, cell alignment and wrapped-line alignment landed) ([auto-layout-parity](auto-layout-parity.md)) | 3–6 | -| 10 | Text | 8 | 90% | 70% | Spell check, text metrics on real files and in other fonts (text box sizes measured against documents re-saved by other software: all 230 self-sizing boxes in 7 documents come out to the pixel, one font family; behaviour suite with CJK line breaking, vertical trim and carets by grapheme cluster landed; lists per paragraph with nesting and list spacing, hanging punctuation and lists, decoration style, thickness, offset, skip ink and colour landed) | 3–7 | -| 11 | Components & variants | 9 | 96% | 72% | Real (not synthetic) libraries, `.fig` libraries (a component playground for variant, boolean and text properties, a behaviour suite from the documentation, overrides of padding, gap, grids, exports and interactions, refused geometry overrides, variant-switch and Assets-swap override rules, top-left default variant landed) ([components-parity](components-parity.md)) | 4–9 | -| 12 | Styles & variables | 5 | 95% | 76% | Real-library QA (variables inside styles, behaviour suite from the documentation, style folders and order, mode order, code syntax, extended collections with their tail (new library variables as updates, chains through libraries) and variables import/export landed) ([components-parity](components-parity.md)) | 2–4 | -| 13 | Prototyping | 6 | 92% | 74% | Video triggers and media actions, controllers on macOS, Linux and FreeBSD, real prototype QA (behaviour suite from the documentation, delays on mouse triggers, manual overlay offsets, key combinations, controller buttons on key/gamepad triggers (Windows and the browser), widgets reacting in prototypes, interactive components playing their variants, drag-driven animations, physical springs landed) ([prototyping-parity](prototyping-parity.md)) | 2–5 | -| 14 | Import/export | 6 | 86% | 58% | `.fig` import's tail (best-effort import, editable vector networks, stand-ins for non-design layers and variable prototype actions landed; no real-file checks yet), checking the rest of the experimental `.fig` export (landed; written files open and draw in other software) on effects, images, prototypes and real files, HEIC/video import, CMYK output intent ([file-format-parity](file-format-parity.md)) | 5–10 | -| 15 | Dev Mode / inspect | 2 | 96% | 72% | Import lines for connected instances inside generated layer trees; an editor extension is left to the MCP server (inspect behaviour suite with colour formats, rem and icon and image assets, code connect, plugins in Dev Mode and dev resources links landed) ([dev-mode-parity](dev-mode-parity.md)) | 0–1 | -| 16 | Comments, versions, find, misc | 3 | 87% | 63% | Comments are local only; comparing two versions; branches in live sessions; text runs and other lists besides fills, strokes and effects merge whole (branches with review and merge, their own version history and per-item list merges, and a comments and version history behaviour suite from the documentation landed) | 1–3 | -| 17 | Multiplayer & sharing | 8 | 17% | 5% | Core landed (`crates/collab`: ids, change sets, server ordering, convergence tested); live sessions (preview): WebSocket transport, hosting from the desktop app, joining from the app or the browser, invite links that open the app, cursors, selections and follow, cursor chat and spotlight, per-person undo, offline resume; a self-hosted `figcraft-server` with persistence and share links to edit or view, which can be open to the public (owner tokens, limits per address, expiry, moderation). Left: accounts and teams, audio ([collaboration-parity](collaboration-parity.md)) | 42–84 | -| 18 | Plugins & widgets | 4 | 60% | 35% | Own sandboxed WASM plugins landed (#101: manifest, permissions, menu, Run last, Manage, SDK), with log byte/count limits checked before allocation and host forwarding ([logging limits](plugins.md#logging-limits)), plugin panels called back per button or changed value and per-plugin settings (ABI v2), then widgets (API 3: plugin-drawn layers on the canvas with state kept in the document, redrawn on clicks; a Counter example), panels side by side and a colour picker for plugin colour fields, then a plugin directory on `figcraft-server` (publish with the server key, browse, install after a permissions confirmation, hash-checked downloads, updates by version). Left: a public catalogue, publisher accounts and signatures, ratings, background update checks, the directory in the browser; widgets take clicks only (no text input, property menus, hover or timers) and draw frames, text, shapes and images only; Figma's plugins are JavaScript and don't run (a JS engine would be an owner decision) ([plugins.md](plugins.md)) | 8–26 | -| 19 | Team libraries & organization | 2 | 70% | 40% | Libraries kept on `figcraft-server` under their file and enabled by a share link (per-asset versions, background update checks, overrides kept), usage counts per component and file, swapping a library by names. Left: team-wide library lists, accounts and permissions, analytics over time, pushed update notices | 4–8 | -| 20 | Figma Draw extras | 2 | 88% | 60% | Have: variable width, lasso, shape builder, noise/texture, text on a path, pattern fills, stretch and scatter brushes, pen pressure (Windows pens, browser styluses), multi-edit (several vector layers in edit mode at once). Missing: pressure from macOS/Linux tablets | 1–3 | -| 21 | AI features | 3 | 0% | 0% | First draft, rename layers, image edit/remove background, visual search, Make: needs local models (owner decision) | 30–60 | -| | **Weighted (÷113)** | 113 | **~85%** | **~63%** | | **117–255** | -| | Core editor only (1–16, ÷94) | 94 | ~94% | ~72% | | 32–74 | - -## By dimension (ready for real work) - -| Dimension | Weight | Ready | Hours | Evidence | Doc | -|---|---|---|---|---|---| -| Features (depth) | 35 | 63% | 28–67 (areas 1–13, 15, 16, 20) | The table above; core editor ~72% | this file | -| UI/UX fidelity | 15 | 58% | 40–60 | Look & feel compared side by side with UI3 (~65% by the builders); tool-by-tool interaction (modifiers while dragging, hover states, popovers) not yet compared (a cursor for every tool and modifier, number fields and panel widths built from the documentation, not compared); accessibility names and keyboard navigation landed | [ui-parity](ui-parity.md) | -| File formats | 12 | 60% | 5–10 | `.fig` read, best effort, with editable vector networks (tested on generated fixtures, not real files), `.fig` write experimental (round trips through our own importer; written files open and draw in other software); REST JSON import, SVG in/out, PNG/JPG/WebP/PDF export solid, with a behaviour suite from the documentation | [file-format-parity](file-format-parity.md) | -| Collaboration | 10 | 13% | 52–104 | Local comments, version history and branches with review and merge; the multiplayer core (sync model, server ordering, client reconciliation, convergence tests) landed in #103, and live sessions (preview) co-edit a file hosted by a desktop app or kept on a self-hosted `figcraft-server` with edit and view links, joined from the app or the browser, with cursor chat and spotlight (`docs/collab-protocol.md`) | [collaboration-parity](collaboration-parity.md) | -| Stability | 8 | 55% | 20–40 | Never-crash lints, command guard with rollback, recovery copies, 963 tests incl. junk-parameter sweeps and a seeded retained-render walk; but two days old, one release (v0.2.0), no field reports, QA mostly on macOS | [gaps](gaps.md) | -| Performance | 6 | 60% | 30–60 | `cargo xtask bench`: 50k-layer page ~8 ms zoomed out, 10k-layer edits ~0.2 ms, 2,000-instance design system edits 4–64 ms; CPU (tiny-skia) canvas where Figma renders on the GPU; nothing timed against Figma on the same file | [hardware-parity](hardware-parity.md) | -| Hardware | 3 | 60% | 10–21 | Trackpad pinch and HiDPI yes; pen pressure on Windows and from browser styluses (none from macOS/Linux tablets); several windows (screen-reader tree in the main one only); no GPU canvas, no P3 on screen | [hardware-parity](hardware-parity.md) | -| Localization | 3 | 39% | 29–51 | The layer, English, Russian reviewed by VanTan, and machine drafts of Spanish (with a Latin American overlay), French, German, Brazilian Portuguese, Japanese, Korean and Simplified Chinese (none reviewed by a native speaker; every draft covers all current strings), CJK UI text in installed faces loaded on demand (web: only the craft-fonts CJK build); engine messages stay English | [localization-parity](localization-parity.md) | -| Platforms | 3 | 75% | 10–20 | macOS, Windows (x64/x86/arm64), Linux, FreeBSD, web all packaged (v0.2.0); Figma also has iOS/Android apps; non-macOS QA thin | [hardware-parity](hardware-parity.md) | -| Ecosystem/plugins | 3 | 38% | 8–26 | Own WASM plugins with a Rust SDK and three examples (#101), plugin panels and settings (ABI v2), widgets that take clicks (API 3), a self-hosted plugin directory on `figcraft-server`; no Figma plugin compatibility, no public catalogue or community files; the CLI, control channel and MCP server are ahead of Figma for agents | [gaps](gaps.md) | -| AI features | 2 | 0% | 30–60 | None | [gaps](gaps.md) | -| **Ready for real work** | 100 | **~53%** | **~262–519** | | | - -Dimension hours add up to the full-parity total without double counting: the feature-area rows -for import/export (14), multiplayer and team libraries (17, 19), plugins (18) and AI (21) are -counted under their dimensions. - -## Essentials user - -Someone who uses only the core of Figma: a casual or beginner user making a few screens, a social -graphic or a simple mockup. Same method, with the core features a casual user touches. - -**1. Core features, weights and depth** (depth from the area table and checklists above): - -| Feature | Weight | Depth | Notes | -|---|---|---|---| -| Create, open, save and reopen a file; pages | 10 | 85% | home screen, `.figcraft` round trips, Open recent | -| Pan and zoom | 10 | 80% | wheel, pinch, Space-drag, zoom shortcuts | -| Select, move, resize, rotate, align, group, duplicate | 15 | 75% | not yet compared tool by tool | -| Frames with device presets | 8 | 80% | | -| Rectangle, ellipse, line, arrow, corner radius | 8 | 80% | | -| Text: type, font, size, weight, colour, alignment | 12 | 70% | no spell check | -| Colour picker, fills and strokes; place and crop images | 10 | 72% | no HEIC images | -| Layers panel: rename, hide, lock, reorder | 7 | 80% | | -| Basic auto layout (add, gap, padding) | 6 | 70% | | -| Undo / redo | 5 | 90% | unlimited, one step per gesture | -| Export PNG, JPG, SVG, PDF | 6 | 85% | | -| Copy and paste, incl. images from other apps | 3 | 75% | | -| **Weighted** | 100 | **77.8%** | | - -Left out: components and variables beyond using one, prototyping, Dev Mode, advanced vector and -type, and everything the mainstream number leaves out. - -**2. Discounts a casual user actually hits:** - -| Discount | Factor | Evidence | -|---|---|---| -| Launch and stability | ×0.92 | signed and notarized macOS build, installers for every platform (v0.2.0), never-crash rules and recovery copies; no field record yet | -| Discoverability and UI clarity | ×0.94 | the UI3 layout Figma users know, Actions palette, keyboard shortcuts panel; a first run opens on the home screen with a Getting started card (six tips with their shortcuts, closable, back from the Help menu) and eight starter templates to open (also File ▸ New from template…); no Community files, no guided tour, and nobody new to the app has been watched using it | -| Opening files people send them | ×0.88 | what people send a Figma user is a Figma link or a `.fig`; a `.fig` opens (best effort, not yet checked on real files), a link doesn't. Images and SVG do | - -77.8% × 0.92 × 0.94 × 0.88 = **~59%** (estimated). User evidence: none yet (see above). - -## Mainstream practitioner - -The same question for the typical professional Figma Design user: a product designer who designs -screens, maintains or uses a design system, prototypes and hands off to developers every week. -Computed per craftrules `standards/progress-docs.md`. - -**1. Areas used weekly, with weights.** Feature areas 1–16 of the table above, with their weights -(sum 94). Left out as the standard says: plugins and widgets (18), AI (21), team and organization -libraries (19, cloud/team admin), Figma Draw extras (20, not weekly for a product designer), -localization beyond English, specialist hardware. Real-time multiplayer (17) is cloud collaboration -and is left out as an area, but its effect on working with Figma users is counted in discount 3. - -**2. Average depth** ("Ready" column, weighted): **72.7%**. - -**3. Discounts for what still stops real work:** - -| Discount | Factor | Evidence | -|---|---|---| -| Interaction fidelity | ×0.90 | UI3 look compared side by side (~65% by the builders); no tool-by-tool comparison of cursors, modifier keys while dragging, snapping thresholds or popovers yet; Ctrl to suspend snapping on macOS missing ([ui-parity](ui-parity.md)) | -| Stability on real machines | ×0.90 | Never-crash lints, command guard with rollback, recovery copies and 963 tests, but no field record at all: the repository is private, v0.2.0 has had no outside users or issue reports, CI tests run on Linux only and hands-on QA is on macOS. Harsher than VectorCraft's ×0.92 because there is no outside evidence either way | -| File exchange with Figma users | ×0.85 | A Figma designer's work lives in shared Figma files. FigCraft opens a `.fig` (best effort, not yet checked on real files) and writes an experimental one (written files open and draw in other software; effects, images and prototypes unchecked there); incoming work can come through the REST API JSON (one way, needs an access token), outgoing only as SVG, PDF, PNG or code. Teammates, reviewers and developers would all need FigCraft ([file-format-parity](file-format-parity.md), [collaboration-parity](collaboration-parity.md)) | - -72.7% × 0.90 × 0.90 × 0.85 = **~50%** (estimated; 50.03%, rounded down). - -**4. User evidence (2026-10-10):** 0 issues, 0 praise or "switched from Figma" reports, 0 comments -from anyone but the maintainers (`gh issue list`, `gh api .../issues/comments`; the repository is -private, 1 star, PRs only from maintainers). No outside evidence either way; nothing raises the -number. - -**Why it is lower than ready for real work (~53%), unusually.** The full number averages -dimensions additively, so the missing `.fig` exchange costs only the file-format dimension's 12% -weight, and FigCraft earns points the mainstream number doesn't count (platforms 75%: Linux, -FreeBSD and the web, where Figma has no desktop app). The mainstream number applies file exchange -as a multiplier on everything, because a Figma practitioner's whole working week is shared through -Figma files. Both are computed from written weights; neither was moved to match the other. The -mainstream number rises fastest with `.fig` import and the experimental `.fig` export checked on real files (the ×0.85 toward ×0.95: ~53%) and the -interaction pass. - -## How the hours are calibrated - -From this repository's own history (`git log`, `gh pr list`): - -- The whole app was built in ~25 wall-clock hours: first commit 2026-10-09 15:38 UTC, PR #99 - merged 2026-10-10 16:53 UTC; ~49k lines on day 1 before PRs, then 99 PRs adding ~81k lines. The - ROADMAP of that period records 4–6 agents in parallel, so ~110–150 agent-hours in all: about - **1 agent-hour per ~1,000 added lines**, and a typical feature PR (1,000–2,500 lines, e.g. - #84 arc handles, #92 PDF print options, #96 accessibility) took **1–2.5 agent-hours**. -- Between the ROADMAP revisions of 2026-10-09 22:19 UTC and 2026-10-10 16:53 UTC (~18.5 h wall, - 78 PRs, ~75–110 agent-hours) the self-graded depth went 58% → 79%: **~4–5 agent-hours per - self-graded point**. This re-measure grades about 0.6 points for each of those, which puts an - honest point nearer 7 agent-hours in the polish tail; new subsystems (a `.fig` reader, - multiplayer, a plugin runtime) are estimated bottom-up instead, since nothing comparable has been - built here yet. -- Parallelism: most of the work splits across 4–6 agents on disjoint crates (the history shows it - working); multiplayer, `.fig` import and the plugin runtime each need one owner for their core. - Needs a human: native-speaker review for every language, the plugin-runtime and AI-model - decisions, real-world Figma files to test with (licensing), Windows/Linux/screen-reader QA. - -## Area detail: what has landed and what is left - -Carried over from the ROADMAP's "Where we're lacking" list (2026-10-10, before this re-measure). -Hours in this narrative are the builders' estimates; the tables above supersede them. - -1. **Vector edit mode**: *landed:* edit-mode toolbar (move, pen, bend, paint bucket, lasso, cut), - pen that extends, branches and closes paths (vector networks), paint bucket fill/unfill of any - closed area (faces with holes), point/segment selection, handle mirroring (none/angle/angle and - length) with a Point panel (position, corner radius, mirroring), smooth/sharp points, join and - merge, delete vs delete-and-heal, nudging, one undo step per gesture, eraser (⇧E), shape builder - (⇧M: the regions of overlapping shapes, click or drag to merge, Alt to remove, curves kept), - variable width strokes (width profile presets with previews in the stroke settings, the Variable - width tool ⇧W: drag the path to add a width point, slide points, drag their handles, Delete; - smooth interpolation per subpath, dashes, caps and joins, canvas/PNG/SVG/PDF and outline stroke), - a transform box around several selected points (corners and edges scale them, Shift keeps - proportions, Alt from the centre; just outside a corner rotates, Shift in 15° steps; drag inside - to move; the Design panel's X/Y/W/H and flip/rotate act on the points), point snapping while - dragging points or handles and with both pens (to the points and segment midpoints of this and - other vectors, the usual layer targets and the pixel grid, with guides and a marker on the point - landed on; Ctrl/Cmd turns it off), arc handles on the canvas (sweep, start and ratio with a - live readout, Shift snapping, closing at the start, the other part from the gap; rotated and - flipped ellipses, any zoom), Design panel Arc fields (Start, Sweep, Ratio; bindable), live - boolean editing (operands selected by double-click or Cmd/Ctrl-click; moving, resizing or - editing an operand's points redraws the result during the drag with every operand outlined; - per-operand operations from the boolean controls; only the operands from the edited one up are - recombined; clicks hit a boolean group where its result is), the shape builder on strokes and open paths (strokes as their outlined areas), and - one-sided variable widths (Alt-drag a width handle; canvas, exports and outline stroke), and - filled regions with the Move tool (the area under the pointer hatched, clear of points and - segments, its spacing by octave folding fitted to measurements at several zoom levels and fixed - to the shape; click, Shift-click and drag to select and move areas with their points). - *Left:* vector QA with real files. 1–2 h. -2. **Text editing depth**: *landed:* OpenType features panel, small caps, lists, title case, - truncation/max lines, font picker with previews and recents, word/line selection, IME commit, - inline IME composition (underlined at the caret, reflowing, candidate window at the caret; the - document only gets the committed text), variable fonts (axes and named instances read from the - font, named instances as styles, Type settings ▸ Variable sliders, per-range axis values applied - to shaping, outlines and PDF), range-aware Typography panel with "Mixed". Range fills in the Fill - section. Right-to-left and mixed-direction text (Unicode bidi: Arabic shaping, mirrored brackets, - caret, clicks and selection follow the runs; arrow keys move visually). Right-to-left and - mixed-direction names in the panels (layers, pages, Assets, tabs, the file header, canvas frame - titles: visual order, joined Arabic, the ellipsis at a right-to-left name's left end; rename fields - whose caret, clicks, drags and arrow keys follow the shown text; system fonts for scripts the UI - fonts lack). The text style browser in the Typography section (list and grid views in each - style's 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, "Mixed" when the - chars use several styles); text styles link chars, not just layers (editing what a style sets - unlinks the chars edited). Live previews of a hovered named instance or font style and of a - dragged axis slider (one undo step on release, Escape cancels). Visual QA: list markers keep the - paragraph's font, renames start with the name selected and centred, Max lines shows while - truncating. Right-to-left paragraphs mirror lists and indents: bullets and numbers (".1") on - the right, wrapped lines hanging from the right, the paragraph indent on the right, trailing - spaces hanging outside the line (canvas, PNG, PDF and SVG). A behaviour suite from the public - documentation (`crates/text/src/behaviour_tests.rs`, `crates/engine/src/text_behaviour_tests.rs`): - Auto / % / px line heights with the leading split above and below every line, letter spacing in - % and px, paragraph spacing and indent (start-aligned text only), resizing and truncation, - decorations from the font's metrics; CJK line breaking (between ideographs, kana and Hangul, no - break inside Latin words, kinsoku for closing and opening punctuation, small kana and the - prolonged sound mark); a style's metrics from its own font, not a fallback; carets, Delete and - Backspace by grapheme cluster; double-click and word moves by script run; IME composition with - CJK; vertical trim (cap height to baseline). The tail suite (`crates/text/src/tail_tests.rs`, - `crates/engine/src/text_tail_tests.rs`): lists per paragraph, nested up to 8 levels with Tab and - Shift+Tab (1. a. i. and • ◦ ▪ per level, counts restarting after plain paragraphs), Enter on an - empty item and Backspace at an item's start stepping out, `- `/`* `/`1. ` starting a list; list - spacing between items; hanging lists and hanging punctuation; decoration style (solid, dotted, - wavy), thickness, underline offset, skip ink and colour, with inspect, CSS and the REST import. - *Left:* visual QA; hanging punctuation in right-to-left paragraphs and centred text, list - numbering that continues across a plain paragraph and a list's own start number (documentation - silent); decoration colour in SVG exports (drawn with the text's fill, like run fills); Korean - keeps breaking by syllable and Han runs aren't split by a dictionary (documentation silent). 0.5 h. -3. **Auto layout and sections depth**: *landed:* on-canvas padding/gap handles (also wrapped-row - and grid column/row gaps, grid tracks outlined), min/max width/height (UI, Hug/Fill/Fixed, - hugging text wraps at its max), ignore auto layout (absolute position) with constraints, text - baseline alignment, grid tracks (fixed/hug/`fr`), grid cells and spans, wrap row gap UI, - wrap-aware drag-reorder (by row) and dragging into a grid cell, nested hug/fill settling in one - pass (wrapping fill text, three levels deep, grid rows), min/max redistribution, coherent - Fill/Hug rules, drag across nested auto layout frames (deepest one wins, insertion line, out to - the canvas or a plain frame). Arrow keys move auto layout children one place (along the flow - past hidden and absolute siblings, Up/Down between wrapped rows, to the neighbouring grid cell - (automatically placed children swap, anchored ones take or swap cells), and at the end of a - flow or across a row's axis into the neighbouring nested auto layout frame when the frame - around flows that way; one undo step, none when nothing can move). Visual QA of nested - hug/fill/fixed/min/max/wrap/grid samples against the documented rules (Fill turns a hugging - parent Fixed, min/max redistribution, wrap inside fill, absolute children following - constraints as hug frames grow). Sections: hide contents (name, outline and "Contents hidden" - on the canvas; contents skipped by rendering, hit testing and labels; dimmed in the layers - panel; Design panel checkbox and context menu), Wrap in new section, frames in sections get - canvas labels and are top-level frames (flows start there, navigation, Export frames to PDF), - "Navigate to" a section shows its frame shown last, else its first; ready for dev turns - "Changed" when the contents are edited, resolved with a reason; Compare changes shows what a - changed frame or section looked like when it was marked next to how it is now, with the - changed layers and their property changes. *Left:* whether `fr` rows of a fixed-height grid - should make room for fixed-size children (now `minmax(0, 1fr)`-like). 1–2 h. -4. **Components depth**: *landed:* component properties (edit/delete/bind) with the properties - panel, add variant, instance swap UI, per-layer overrides with reset, restore main, instance - structure protection; Assets panel with live thumbnails, search and drag-to-canvas (drops into - the frame or auto layout slot under the pointer); exposed nested instances (their properties - show on the main's instances); component descriptions (edited on the main, shown on - instances); local libraries (publish a file's components, sets and styles with per-asset - versions, enable library files, place library components and apply library styles as - read-only linked copies, "Library updates available" with overrides kept on update); slots - (a frame of a main made a slot property with preferred components; instances fill it with any - content of their own, kept across main updates, variant switches, copy/paste and detach, with - the main's default content until edited; reset slot; slot badges in the layers panel and on the - canvas; fill menu in the properties panel; drops from the Assets panel and the canvas land in - slots); nested overrides kept when the main moves or swaps a nested instance, and on nested - instances copied out of an instance (re-linked to their own main); canvas drags can no longer - pull layers out of an instance; library variables (publishing includes the file's variable - collections with their modes, aliases and scopes, versioned per variable and collection, without - variables hidden from publishing; files using the library see its collections read-only under - the library's name in the Variables editor and the variable pickers; binding, aliasing or setting - a library collection's mode on a frame copies the variables in as read-only linked copies, so the - file renders without the library; library components keep their variable bindings and modes; - "Library updates available" covers changed variables and bound layers follow). Explicit variable - modes in the Design panel: a frame's mode per collection, Auto (inherited) or its own, with - enabled libraries' collections listed under the library's name (picking a mode copies the - collection in). Instance boolean and text properties bound to boolean and string variables - (following modes, renames, reset and detach); layout grid settings in the Design panel whose - count, size, margin/offset and gutter bind to number variables; fills, strokes, effects and - layout grids reordered by dragging their rows, their variable bindings following. Components at - scale, QA'd with a synthetic design system (about 300 components: buttons, inputs, cards, navs - and badges as sets of size × state × type × theme variants, boolean/text/instance swap/slot - properties, instances nested four deep, text and paint styles, light/dark variables bound across - it) used 2,000 times with overrides at every level: main edits keep every override and match a - full rebuild exactly; values from variables (a theme switch, a bound corner radius) are no longer - recorded as overrides; detaching a variable on an instance layer is an override that survives main - edits; exposed nested instances stay exposed and swapped nested instances keep their layer names - through main edits; layers a swapped nested instance's main adds keep their ids and overrides. - Instance swap properties have preferred values (offered first). Alt-drop from the Assets panel - onto an instance swaps it (same-set instance preferred, highlighted while dragging); instances - pasted into other files follow their mains (library copies shared, published components used - from their library, other mains left behind as a restorable missing main). The Assets panel lays - out only the tiles in view and renders thumbnails as they show (cached, re-rendered only for - changed components). A behaviour suite from the public documentation checks overrides (what an - instance takes, what it refuses: geometry, constraints and resizing of its layers, its auto - layout direction), variant switches (overrides stay where both variants agreed; text layers keep - theirs), swaps (by layer name; from the Assets panel only text overrides), the top-left default - variant, detach, reset and push. *Left:* QA with real-world (not synthetic) libraries. 1 h. -5. **Prototyping depth**: *landed:* on-canvas connections with drag-to-connect, interaction editor, - flows, smart animate, overlays, scroll/fixed/sticky, change-to and set-variable in the presenter. - Custom bezier and custom spring easings with editor fields; device frames (phone, tablet, - laptop, desktop bodies drawn in code) around the presented frame; overlays animate out with - their opening transition reversed; several selected layers edit their interactions together - (or replace mixed ones); scrolling inside device frames (the frame sized to the device's screen, - clipped or scrolling by wheel and drag, fixed layers pinned to the screen by their constraints, - nested "Scroll to", preserve scroll position); a resizable preview pane over the canvas that - plays the selected frame and follows edits live; device rotation (phones and tablets turn to - landscape: saved in the file, imported from REST files, drawn turned in the presenter and the - pane); presentation options (fit, fit width, fill, 100%, with Z to cycle them and R to - restart, the device frame shown or hidden, kept between presentations; what overflows the - window pans); scroll momentum (drags fling and slow down the same at any frame rate, the wheel - coasts, a drag past the ends stretches and springs back); responsive presentation (the presented - frame resized to the device's screen, or the window without a device, its constraints and auto - layout responding live; in the Options menu and the Prototype panel, saved in the file); device - colours (each model's colour variants, bodies drawn in code) and custom device sizes (a bare - screen with width and height fields, imported from REST files); flow descriptions in the - presenter's flow picker and under each flow; conditional actions (if / else if / else over - variable expressions) and set-variable-mode actions run in the presenter (from REST files or - commands). Conditionals are built in the interaction editor: an interaction's actions are a list - (add, reorder by dragging a row's grip, also inside conditional blocks, or from its menu, remove, - open one to edit it; one undo step per move), "Add action ▸ Conditional" adds an if whose - condition comes from a builder (a variable from the variable picker, an operator, a value or - another variable) or is typed as an expression (`count >= 3 and not done`, checked when entered, - what's wrong shown under the field), each block with its own actions (the same editors, - nested), plus "Else if" and "Else"; set variable values can be expressions (`count + 1`, - `"Hi " + name`); expressions read a variable's value in a given mode of its collection - (`accent[Dark]`, also from REST files' variable mode lookups) and the mode a collection is in - where the interaction runs (`mode(Theme) == "Dark"`, following set variable mode actions); every - edit is an undo step (`proto.action.*`, `proto.conditional.*`). Hotspot hints: a click where - nothing reacts flashes the screen's hotspots (an Options menu toggle, on by default); the - presenter's Inspect panel shows the hovered layer's properties. Overlays opened through commands - close on a click outside unless told not to, as in the editor. A behaviour suite written from - the public documentation covers triggers, actions, overlays, animations, easing, scrolling and - interactive components ([prototyping-parity](prototyping-parity.md)); its fixes: drag-driven - animations go their way and follow the pointer, key combinations, overlay timers from when they - open, instances play their variant's interactions, physical springs, smart animate overshoot, - fixed layers on top; scroll to takes an offset. *Left:* QA against larger real-world - prototypes. 1–2 h. -6. **Interop**: *landed:* image crop handles; REST API file JSON import (`file.open` / `file.import` - / CLI: pages, layer tree, geometry, paints, strokes, effects, text runs, vectors from geometry - paths, auto layout, constraints, grids, components, sets, instances with overrides, component - properties, styles; image fills from an `images` folder or placeholders; warnings for the rest), - now with prototyping (interactions with their triggers, actions, transitions and easing, flows, - scrolling, the prototype device; conditional actions (if / else if / else over expressions), - set variable with variable references and expressions, set variable mode, all run by the - presenter) and variables (the local variables JSON passed along, next to the file or embedded: - collections, modes, aliases, scopes, layer bindings and explicit modes). - Export panel: a row per export setting (typed or preset sizes like `2x` / `512w` / `512h`, PNG, - JPG, WebP, SVG and PDF, suffix, advanced options: contents only, include bounding box, and for - SVG outline text, id attributes and simplified strokes), a preview, and "Export N layers" (`file.exportLayers`) in the - panel and the Export dialog. Paste from other apps: images, SVG and text (the newest clipboard - content wins over our own copy; pasted SVG lands centred in the view). SVG import is editable and - checked against a corpus of synthetic files (expected pixels worked out by hand, layer structure, - export and re-import): groups named from their ids with transforms baked into the layers; - rectangles (corner radius), ellipses and lines kept as those shapes; gradients with exact - handles, `reflect`/`repeat` and focal points; patterns as image fills; clip paths and masks as - vector, luminance and alpha masks; shadow and blur filter chains as drop shadows, inner shadows - and layer blurs; blend modes; text as text layers with runs, anchors and stacked lines; images - and nested SVG images; import warnings for what's left out; linked files never read; hostile - input fuzzed. SVG export keeps alpha and luminance masks. PDF export: one page per frame in points (Export frames to - PDF, the Export panel, `figcraft-cli export -o x.pdf`), vectors, gradients and strokes as - vectors, real selectable text in subset fonts with clickable hyperlinks, images embedded once, - opacity and blend modes, clip content, alpha/luminance/vector masks, shadows and layer and - background blurs as images; checked by rendering the PDFs back against PNG exports. PDF print - options in the Export panel (per PDF export setting), the Export dialog, `file.export`, - `file.exportLayers`, `file.exportFramesPdf` and `figcraft-cli export`: a bleed showing what lies - past the frame (its fills reach out, its children are clipped at the bleed), crop and bleed - marks outside it, MediaBox / BleedBox / TrimBox, and CMYK colour (a documented, deterministic - naive conversion; images and rasterised effects too). - *Left:* .fig import (best effort), a CMYK output intent (no CMYK ICC profile can ship in the - repository), SVG filters other than shadows and blurs (left out with a warning). 8–16 h. -7. **Look & feel and interaction-fidelity pass**: *landed:* keyboard shortcuts audited against the - public shortcut reference: one keymap (`figcraft_engine::keymap`) drives the menus, the canvas, - the control channel and MCP, with macOS and Windows/Linux keys where they differ (mask, bring to - front/back, distribute, tidy up, layout grids, pixel preview), Text-only and canvas-only bindings - (Cmd/Ctrl+K: link while typing, Actions on the canvas), Cmd/Ctrl+/ for Actions, `+`/`-` zoom, - Shift+0/1/2 from any layout, Cmd/Ctrl+0, hold Z for the zoom tool (Alt zooms out, drag an area), - two quick digits for an exact opacity, boolean keys (Alt+Shift+U/S/I/E), remove fill/stroke - (Alt+/, Shift+/), outlines on Shift+Cmd+O, pixel grid on Shift+' (and Cmd+'), Shift+E / Shift+D and the other panel keys, Ctrl+Tab between files, Shift+B (and B) for the paint bucket in vector edit mode; the Keyboard - shortcuts panel docks at the bottom with a tab per group and search, keycaps per platform. - A shortcut parity suite (`shortcut_parity_tests.rs`) checks every documented shortcut on both - platforms (129 rows: 128 bound, including moving the rotation origin with Alt+R and placing an - object with Enter from the toolbar; 1 moved off a documented conflict) and pins the 48 undocumented - bindings; others' cursors (`Alt+Cmd+\`), list levels while editing (Cmd/Ctrl+] and [), the - keyboard box selection on Alt+Space / Ctrl+Space, arrows that pan with nothing selected, - Control to suspend snapping on macOS and Space/N/C/F while presenting landed with it - ([ui-parity](ui-parity.md#keyboard-shortcuts)). - *Left:* tool by tool, panel by panel against the incumbent (cursors, modifier keys while - dragging, hover states, menus, popovers); per-keyboard-layout shortcut tables. 40–60 h. -8. **Collaboration (local):** comments landed: pins pinned to frames and layers that move with them - (to other pages too) and stay where they were when the layer is deleted, your pins dragged onto - another frame, crowded pins clustered when zoomed out (a click zooms them apart), Shift+C to show - or hide them; threads where you edit and delete your own messages, emoji reactions, @mentions - with a picker of the names used in the file (your name from Preferences), read and unread state, - resolve, and "Copy link" (`figcraft://comment/`: pasting or `comment.open` goes to the page, - zooms to the frame and opens the thread); the Comments panel with search (text, names, frame), - filters (open, resolved, yours, unread), sorting by date or page, each thread's frame and page, - and Up/Down to step through threads; comments on the presented frame (Comments button or - Shift+C: read and reply). All of it through `comment.*` commands. Version history - landed (named versions saved in the `.figcraft` file; restore, rename, duplicate, delete; panel; - view a version on the canvas read-only before restoring it; autosave checkpoints every 30 - minutes, folded in the panel). A behaviour suite written from the public documentation covers - both: restoring keeps the comments and first keeps the replaced design as an autosave, a titled - autosave becomes a named version, merging a branch records the main file before and after, and - threads made in a live session can be replied to and resolved. Real-time collaboration is designed (`docs/collab-protocol.md`: - WebSocket sessions the app can host itself, server-ordered per-property changes, live cursors) - and its core landed (`crates/collab`, no networking yet): client-scoped node ids, every saved - layer field and the document-level data (styles, variables, comments, images…) as synced - properties, diffs after a command that cost the size of the edit, fractional sibling positions, - a session server that orders and validates batches (cycles, deleted layers, id spaces, sizes) - and a client that shows its own edits at once and replays them over others'; derived data - (auto layout, instances, bindings) is repaired by the author after concurrent edits; binary - messages (a moved layer is 43 bytes, a cursor 23) with a JSON form; a randomized multi-client - test over real commands converges. Live sessions run over the network (hosting from the app, - invite links, document sync, live cursors, selections, avatars, follow). Undo is per person in - a session (`edit.undo` takes back only your own steps, leaving what others changed since; - creations and deletions invert; a gesture or command group stays one step); refused edits roll - back with a toast; a dropped connection keeps editing and resumes as the same client with what - it missed (or a snapshot), then sends what was made offline. Randomized sessions with undo, - redo and dropped connections converge, and a checker confirms no undo reverts someone else's - change. - Plugins landed: sandboxed WebAssembly plugins (a metered wasmi - interpreter with a memory cap and no WASI, also in the browser) whose API is the command - registry; each run is one undo step and is rolled back when it fails (error, trap, endless loop, - memory); a `figcraft-plugin.json` manifest declares menu commands with parameters and read or - write access with an optional command allowlist, and files, other documents, the undo history, - dialogs and other plugins are always off limits. Plugins ▸ Development ▸ Import plugin from - manifest… (kept in the preferences, the module re-read on every run), the installed plugins in - the Plugins menu with a parameters form drawn from the manifest, Run last plugin (⌥⌘P), Manage - plugins, and `plugin.*` commands for scripts, the CLI and MCP; a Rust SDK and two example - plugins (Tidy names, Swatch grid) in `plugins/`, built by `cargo xtask plugins`. Then plugin - panels (ABI v2: fields and buttons drawn with FigCraft's widgets, the plugin called back for - each button or changed value as one undo step, `plugin.ui` / `plugin.event` / `plugin.close` - for scripts and MCP) and per-plugin settings kept with the preferences. Then widgets (API 3: - a plugin describes a layer of its own, validated and built as a frame with locked layers; clicks - on its named parts call the plugin back in a fresh sandbox with the state kept in the layer, one - undo step each; `widget.insert` / `widget.click` / `widget.list`; a Counter example), panels of - several plugins side by side and the colour picker on plugin colour fields. Then a plugin - directory on `figcraft-server`: bundles published with the server key and checked in full, - browsed and installed from Plugins ▸ Manage plugins after a permissions confirmation, downloads - checked against their SHA-256, updates by version. *Left:* a public catalogue, publisher - accounts and signatures; richer widget events; the directory in the browser. 8–26 h. -9. **Hardening and performance**: *landed:* `cargo xtask bench` (synthetic 1k/10k/50k-layer - pages: engine, render and headless UI-frame timings). The canvas keeps its last image and - re-renders only what a pan uncovers or an edit touches: moving one layer at 10k layers re-renders - in ~0.2 ms instead of ~8 ms (0.8 vs 24 ms at 50k zoomed out), a 40 px pan in ~0.7 ms instead of - ~7.5 ms. The layers panel lays out only the rows in view (every screen expanded at 50k: 39 → - 0.6 ms a frame); text layouts are shared instead of re-formatted per lookup; overflowing text is - culled by its glyphs. At 10k: full 1440×900 renders 7–15 ms, single-layer edits ~10 µs, hit - tests ~26 µs. Zoomed out (50% and below) the canvas draws levels of detail: layers under 2 px - land as their averaged colour, text under 4 px is greeked into bars, small rectangles and clip - layers skip the path pipeline (exports and closer zooms stay exact): a whole 50k-layer page - renders in ~8 ms instead of ~50 ms. `.figcraft` at 50k layers saves in ~0.09 s instead of ~1 s - and loads in ~0.16 s instead of ~0.86 s (JSON streamed to and from the document without a - value tree, layer fields at their defaults left out: 63 → 22 MB of JSON, old files still - open). The retained canvas is held to a full render: a seeded random walk (edits, undo/redo, - pans and zooms over nested, rotated and clipping frames, groups, masks, booleans, auto layout, - effects, text, instances, variables and styles) checks every step, and found clip edges on a - pixel boundary letting a column through in some renders. Drags show live on the canvas (every - finished render is shown, not only the newest one asked for), and a button held still no - longer rebuilds the preview and re-renders every frame. - Components at scale (`cargo xtask bench`, synthetic design system of about 300 - components used by 2,000 instances): edits of mains reach their copies directly instead of - rebuilding every instance (value, size and layer added/removed edits; other edits rebuild each - affected instance once, inner mains first), bound variables no longer touch unchanged layers on - every edit, auto layout re-flows only where sizes or places changed: recolouring or resizing a - main's layer used ~900 times 188/226 → 4/18 ms, adding a layer to it 430 → 21 ms, a label style - in all 72 button variants (1,800 copies) 450 → 57 ms, switching a variant, reset, detach, push - and paste ~450 → under 3 ms, accepting a library update 460 → 64 ms; the Assets panel with 2,000 - assets 3.4 → 1.3 ms a frame. Accessibility: the custom-painted - controls name themselves for screen readers (AccessKit on desktop: role, name, value and state - for tools, layer rows with their type, level, selection and hidden/locked state, fields, - dropdowns, tabs, menu items, and the canvas, which describes the selection); keyboard navigation: - F6 / Shift+F6 move between the toolbar, layers, canvas and properties, Tab / Shift+Tab move - within a region with a focus ring, Enter or Space activate, the arrows go along the toolbar and - through the layer rows, menus and tool flyouts opened from the keyboard take it and give it back, - Escape returns to the canvas (where Tab selects the next layer again instead of wandering into - the tab strip); reduced motion follows the system's setting or a preference; secondary and - accent text meet WCAG AA in both themes. - An idle window draws no frames: it redrew ~240 times a second - (the window title was re-sent every frame, and each viewport command asks for another), ~33% of - a core in a debug build, and now sits at 0 frames and ~0% CPU; the toast, the autosave - checkpoint and the presenter's "after delay" timers wake it at their deadline, the interaction - editor's curve preview plays when it shows, changes or is hovered, and a screenshot keeps frames - coming only until it arrives (`ui.inspect` `framesDrawn` and `repaintCauses` show what asked for - frames; headless tests hold it). *Left:* a real-world file corpus, Windows/Linux/browser - QA, runs with real screen readers (NVDA, Narrator, VoiceOver, Orca), the AccessKit tree in the - browser (eframe's web backend doesn't send it yet). Landed since: F2 rename from the keyboard, - Shift+F10 context menus at the focused layer row or the canvas selection, keyboard box selection - (F8). 27–44 h. - -## Inventory: what exists today - -Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count there was stale: 963 `#[test]` functions as of this re-measure. - -- **Architecture:** 15 crates with enforced layering (`cargo xtask layers`): geom, color, doc, - layout, text, pathops, plugin, collab, render, format, svg, engine, ui-egui (+ mcp, pdf). Every action - is a command (449 command ids); undo is unlimited via structural sharing (`Arc` node snapshots); gestures are - one undo step; `command.batch` runs several commands as one step. -- **Document model:** pages, frames, groups, sections, rectangles, ellipses (arcs/rings), polygons, - stars, lines/arrows, vector networks, text with styled runs, boolean groups, components, component - sets (variants), instances with per-layer overrides, slices; fills (solid, linear/radial/angular/ - diamond gradients, images with fill/fit/crop/tile + adjustments), multiple strokes with - inside/center/outside alignment, per-side weights, dashes, caps/arrowheads, joins; effects (drop - and inner shadow with spread that offsets the outline exactly: ellipses, stars, vectors, text; - inner shadows over the fills and under the children and strokes; drop shadows hidden behind what - the layer covers, translucent fills included, unless shown behind transparent areas; layer blur; background blur - that sees the whole backdrop, inside clipping frames, faded groups and masks too; both blurs - uniform or progressive (the radius ramps between two points, dragged on the canvas); noise in - mono, duo and multi colour with grain size, density and blend mode; texture with clip to shape; - glass with light angle and intensity, refraction, depth, dispersion, frost and splay; the same in - PNG, PDF and SVG exports, SVG embedding renders of noise grains, texture, progressive and - background blur and glass); corner radii with - smoothing; constraints; auto layout (horizontal, vertical, wrap, grid; hug/fill/fixed; min/max; - absolute children); layout grids; export settings; prototype interactions, flows and transitions; - paint/text/effect/grid styles; variables with collections, modes and aliases. -- **Renderer:** CPU (tiny-skia) scene renderer used for canvas, export and the web: isolated layers - for opacity/blend/effects, clip content, masks, booleans (linesweeper), text (harfrust shaping, - skrifa outlines, font fallback), aligned strokes with caching. -- **Engine:** pointer tools (move/marquee/deep select/resize/rotate/Alt-duplicate with smart-guide - snapping, frame/section/slice/shapes/line/arrow with Shift/Alt, text, pen, pencil, hand, - eyedropper), Cmd/Ctrl-click and Cmd/Ctrl-marquee into nested layers, Shift-marquee toggling, - Select layer (every layer under the click), measuring (Alt-hover distances from the selection to - layers, parents and ruler guides, Cmd+Alt for nested layers, from a guide being dragged to the layers of its frame, extension lines, one box inside the - other, overlaps, live distances to the neighbours while Alt-dragging, Shift+Alt for auto layout - padding and gaps; labels that never overlap at any zoom), text editing (caret, selection, words, - lines), auto layout reorder by dragging, drop into frames, align/distribute/tidy, z-order, - flip/rotate, the scale tool (panel, and canvas handles that scale everything about its anchor - point or from the centre with Alt, with a live factor, one undo step per drag), booleans, flatten, - outline stroke, components (create, instances, sync with overrides, reset, detach, push changes, - variants, properties, slots), styles and variables, prototype commands, find/replace, batch rename, - copy/paste (internal + SVG/text), file new/open/save/import/export (native `.figcraft`, SVG - in/out, PNG/JPG/WebP), version snapshots. -- **UI (egui, swappable):** the incumbent's 2026 layout: tab strip that is also the window's title - bar (frameless on Windows/Linux with drawn window buttons, drag, double-click maximise and edge - resizing; macOS window buttons inset into it), icon rail, file header, pages and - layers tree (rename, visibility/lock, drag to reorder/reparent), Assets panel, floating bottom - toolbar with flyouts and mode switcher, Design / Prototype / Inspect panels (Dev Mode: properties with their styles and variables, annotations - with pinned properties and categories drawn as coloured callouts on the canvas (filtered by category), - a ready-for-dev list, a focus view on one frame with previous / next, Compare changes since a frame - was marked ready (before / after side by side or overlaid, property-level diffs), CSS/HTML/React/SwiftUI/Compose - code where components take their properties as parameters and instances are calls, components - connected to their code components showing the real snippet, read-only plugins), colour picker - (HSB square, hue/alpha, hex, gradient stops, image fills), main menu generated from the catalog, - context menu, Actions palette (⌘K), keyboard shortcuts from one keymap per platform (a tabbed, - searchable Keyboard shortcuts panel), keyboard navigation (F6 between regions, Tab within them, a - focus ring) and screen-reader names for the custom controls (AccessKit), presentation player with - transitions, light and dark themes, home screen, About/Preferences/shortcuts/variables dialogs. -- **Automation:** desktop control channel (`--control `: execute commands, inspect document/UI, - synthetic pointer/keys/typing, menu invocation, screenshots, renders). -- **Tests:** 963 `#[test]` functions (2026-10-10) including junk-parameter sweeps over every command (and every vector-edit command while editing), importer fuzzing and render regressions. - -## Revision history - -| Date | Change | Summary | -|---|---|---| -| 2026-10-14 | minor | Russian catalog (M9), reviewed and approved by VanTan: all 2,165 current entries, Русский in Preferences and regional ru tags. Russian integer plurals use one/few/many; contributor display name VanTan added. Existing estimates unchanged. | -| 2026-10-13 | minor | Vector edit mode regions: the filled area under the pointer is hatched (clear of points and segments, holes left out, spacing by octave folding fitted to measurements at several zoom levels, fixed to the shape); click, Shift-click and drag select and move areas with their points (drawn unselected); a click on an empty spot clears them and stays in edit mode (see [ui-parity](ui-parity.md)) | -| 2026-10-13 | minor | Plugin log limits (#191): cap each UTF-8 line at 8 KiB, total log bytes at 1 MiB and lines at 1,000 per run/callback before copying guest memory or forwarding to the host. Over-budget runs fail and roll back document edits; callbacks get fresh budgets. Added sandbox boundary/forwarding tests and an engine rollback regression. Percentages and hours unchanged. | -| 2026-10-13 | minor | Component playground (M3): with an instance, a component or a component set selected, the Inspect tab shows a Playground: a preview of the component and a control for each variant, boolean and text property. What is tried there changes the preview and the code in the Code section, never the file (no undo step, the canvas instance stays as it is); Reset goes back to the component's or the instance's own values. `component.playground {id?, properties?, language?, path?}` gives the same to scripts: the properties with their values, the code, and a PNG on request (`playground_tests`). Left: instance-swap and slot properties in the playground (the command takes an instance swap; the panel has no control for it), a code connect template's snippet with the tried values. Numbers unchanged. | -| 2026-10-13 | minor | Auto layout, on-canvas handles of automatic gaps (M2): the handle between spread rows or columns (and between items with an auto gap) shows the distance the layout gives the gap instead of the stored value, and dragging it makes the gap a fixed one from there, as typing a gap in the panel does; one undo step (`gap_handle_tests`). Before, the drag changed a value that had no effect. The documentation is silent on dragging an automatic gap. Numbers unchanged. | -| 2026-10-13 | minor | Variables inside styles (M3): a shared style can bind variables to fields of its value (`style.bind {style, field, variable?}`: a paint style's colours and gradient stops, an effect style's colour, radius, spread and offsets, a grid style's size, count, gutter and offset, a text style's font size, line height, letter spacing, family and style). Every layer using the style shows those fields resolved in its own modes and stays linked when a variable or a mode changes; the style keeps them resolved in the default modes. A style made from a layer keeps that layer's bindings; a new value drops bindings to what is gone. The style editor binds from its property rows (paint colour, shadow colour, blur radius, grid size) and detaches from the variable's chip; the style picker marks styles that use variables. `.fig` export and import carry the bindings on the style, REST layers hand theirs to the style they use. Left: binding text style fields and the other effect and grid fields from the editor (commands only), library variables in the picker, a style's library copy. Numbers unchanged. | -| 2026-10-13 | minor | Effects on groups, inner shadows and background blur (M3): a group is no outline of its own, so its inner shadows are cast inwards from around what its layers draw together (their pixels, over them: no shade at a seam between two layers) and its background blur and glass show the backdrop through the outlines of its layers (under them). Canvas and PNG; SVG embeds a group with an inner shadow as a render and its background blur as an image under the layers; PDF places both as images, the blur matching the PNG. Where the group holds one shape the pixels match that shape, or a frame filled like it, carrying the effect (`group_effects_tests` in render, svg and pdf). Offsets and spreads of a group's inner shadow go by whole pixels. Numbers unchanged. | -| 2026-10-13 | minor | Text made by commands on a dark page (M1): in the app, `text.create` and `node.create {type: text}` that name no fill give white text on a dark page, by the same rule as the Text tool (the page's colour decides, also inside a frame); a named `fill`, `fills` or `fillStyle` is kept. Sessions without an interface (CLI, MCP, exports) still make black text everywhere. Numbers unchanged. | -| 2026-10-13 | minor | Frames and groups, a one-step convert (M1): `object.convertToGroup` and `object.convertToFrame` (the Design panel's type menu on a frame or a group) change the layer in place, its layers staying where they are on the page. A frame turned into a group loses what only a frame has (auto layout, with the positions it gave kept and fill children fixed; fills, strokes, corner radius, clipping, layout grids, guides, scrolling) and takes its layers' box; a group turned into a frame keeps its box, with no fill and no clipping. Instances, main components, layers inside instances and empty frames are refused with the reason, all or nothing; one undo step. Numbers unchanged. | -| 2026-10-13 | minor | Prototyping, delays on mouse triggers and manual overlay offsets (M5): mouse enter, leave, down and up wait `delayMs` before their actions run (once while waiting; dropped when the screen or overlay they were on goes away; a pointer that leaves early doesn't cancel, the documentation is silent), and a manual overlay sits at `offset: [x, y]` from the top-left of the layer that opens it (without one, right below it, as before). Delay and X/Y fields in the interaction editor, the player on its own clock, REST import (`delay`, `overlayRelativePosition`), `.fig` import and export (the export's field for mouse delays is unchecked against other software). Numbers unchanged. | -| 2026-10-13 | minor | Auto layout, wrapped-line alignment (M2): a wrapping layout's lines (rows of a horizontal one, columns of a vertical one) are packed where the cross alignment says (start, centre, end; before, they always sat at the start), stretch when every child fills across the flow, or take the free space between them (`contentAlign: spaceBetween`, the Design panel's Row/Column gap mode, the alignment pad lighting the whole cross axis). Read from REST JSON and `.fig`, written to `.fig`, `align-content` in CSS, an instance override with the other alignment settings. 13 hand-computed layout cases and command tests. Numbers unchanged. | -| 2026-10-13 | minor | Starter templates and a first run that explains itself: eight templates built in code through the command layer (mobile app, landing page, dashboard, design system starter, wireframe kit, social post sizes, presentation slides, icon set starter; original and synthetic, deterministic, about 10–190 ms each to build), `template.list` and `file.newFromTemplate` for the app, CLI and MCP, File ▸ New from template…, a Templates tab on the home screen (cards with thumbnails rendered from the documents, search, kind filter, keyboard and screen-reader names) in place of the Community placeholder, a row of templates on Recents while there are no recent files, a Getting started card that can be closed and comes back from Help ▸ Getting started (`help.gettingStarted`), and a first run that opens on the home screen. The Drafts placeholder is gone (the app has no drafts folder). Essentials: discoverability ×0.92 → ×0.94 (templates and first-run tips exist; no Community files, no tour, no user evidence), 77.8% × 0.92 × 0.94 × 0.88 = 59.2%, ~57% → ~59%; essentials hours ~60–110 h → ~55–100 h (the onboarding and templates item is done); menu breadth stays 211/211 (the two new menu items are FigCraft's own and not counted); every other number unchanged | -| 2026-10-12 | minor | Everyday editing partials closed (gap 4): number fields scrub a step a point (×10 with Shift, ×0.1 with Alt, on past the screen edge where the platform reports it), step with the arrow keys by the nudge preferences, take brackets, units and relative edits (`+10`, `*2`, `50%`), commit on Tab and nothing when left untouched, and on Mixed apply relative edits to each layer in one undo step; outline mode's Include hidden layers and Include object bounds (`view.outlinesHiddenLayers`, `view.outlinesObjectBounds`), drawn and clickable; Hide when publishing from the Assets panel (`component.hideWhenPublishing`); a cursor for every tool, modifier and hover target (rotate turned to its corner, pen states, eyedropper, comment, copy, not-allowed), with guides and handles keeping theirs; panel widths with limits, double-click reset and the preferences (`view.panelWidths`); a file or page made in the dark theme has a #1E1E1E page (sessions without an interface keep #F5F5F5) and text drawn on a dark page is white. Menu breadth 211/211. UI/UX fidelity 58% and the interaction pass 40–60 h unchanged at this rounding: the side-by-side comparison is what remains | -| 2026-10-12 | minor | Text metrics, first measured fidelity data (gap 3): our example documents, exported and re-saved by other software, were read back and every text layer's stored box, line heights, baselines and glyph positions compared with ours. Three rules explained every row and were applied: a line's height is rounded to whole pixels (halves up; Auto, % and px alike; the baseline keeps its fraction inside the rounded line); glyph advances and kerning are each rounded to 1/1024 em before scaling by the font size; letter spacing is not added after a line's last visible glyph. Before: 24 of 131 text boxes of the design-system example matched (118 in width, 24 in height); after: 131 of 131, and all 230 self-sizing text boxes across the 7 documents (Inter only, 11–120 px, letter spacing −6% to 8%, one wrapped paragraph). Glyph positions within 0.001 px: 223 of 1,832 → 1,781; the rest are kerning pairs at weights where the installed font files differ slightly, at most 0.05 px. Frames and other layers matching in size and position: 115 of 231 → 208; the other 23 hang on one grid frame whose row positions came back as written. Not measured: other font families, fractional px line heights, centred or right-aligned text with letter spacing, lists, mixed sizes on a line. Wording only; no number moves | -| 2026-10-12 | minor | `.fig` export (gap 1): written files were checked by opening them in other software: layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft; effects such as glass and noise, images and prototype interactions are still unchecked there, and variables are not written. Wording only; no number moves | -| 2026-10-12 | minor | A public `figcraft-server` (open hosting): with `open_publish = true` anyone publishes a file without the server key and gets its edit and view links plus an owner token (128 bits, shown once, stored hashed) that lists, makes and revokes its links, renames it or removes it (`collab.manageFile`); limits per address (connections at once and a minute, messages a second, publishes an hour, files and MiB kept; IPv6 by /64; `X-Forwarded-For` believed only from `trusted_proxy`; tables capped at 10,000 addresses) and for everyone (`max_total_mb`), each refusal saying why; files published without the key expire after `idle_days` unopened (the date shown to everyone in the file); moderation (`files list --sort size|age|idle`, `block-ip`, `reports`, a Report button and `collab.report`); the operator's contact and terms shown in the Share dialog; `GET /healthz` on the same port; `packaging/server/install.sh` for a Debian/Ubuntu machine behind Caddy and a nightly backup timer. Protocol additions within version 2, gated like signals (older apps get clear refusals). Hosting infrastructure rather than a Figma feature: percentages and hours unchanged | -| 2026-10-12 | minor | Experimental `.fig` export (gap 1): the whole document written from the same public format notes as the import, verified only by round trips through our own importer. Import/export breadth 84% → 86%, ready 57% → 58%; file formats 59% → 60%; file exchange with Figma users stays ×0.85 (nothing shows another app opens the files); mainstream hours ~130–244 h → ~120–229 h (`.fig` export 15–30 h → checking it elsewhere 5–15 h); feature breadth (~84%), ready for real work (~53%), essentials (~57%), mainstream (~49%), beta and full parity hours unchanged at this rounding | -| 2026-10-12 | minor | Keyboard shortcut parity suite and bindings (gap 4): a table test with one row per shortcut in the public documentation, macOS and Windows/Linux keys each (129 rows: 126 bound, 110 before; 1 moved off a documented conflict; 2 missing; 2 kept by a platform), plus 48 pinned undocumented bindings so every default binding is in the table; newly bound: A (Frame), Shift+' (pixel grid), Alt+Cmd+\ (others' cursors, new `view.multiplayerCursors`), Ctrl+F6, Shift+Cmd+U (link), Cmd/Ctrl+] and [ (list levels while editing, new `text.listIndent` / `text.listOutdent`), Cmd/Ctrl+Backspace (ungroup), Alt+Space / Ctrl+Space (keyboard box selection, Cmd/Ctrl+arrows draw it out), suggest auto layout, go to main component, arrows that pan with nothing selected (new `view.pan`), Control suspending snapping on macOS, Space/N/C/F while presenting; symbols another layout types with Shift count without it; no default binding uses a system-reserved chord (tested). Menu breadth 207/207 (View ▸ Multiplayer cursors). UI/UX fidelity 58% and the interaction pass 40–60 h unchanged at this rounding | -| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Localization 36% → 39%, 31–54 h → 29–51 h; ready for real work unchanged (~53%); full parity ~267–527 h → ~265–524 h | -| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Localization 35% → 36%, 32–55 h → 31–54 h; ready for real work unchanged (~53%); full parity ~268–528 h → ~267–527 h | -| 2026-10-12 | minor | Prototyping tail (gap 15, the part that doesn't need video): key/gamepad triggers take controller buttons (`Gamepad A` … `Gamepad Home`, the standard layout's 17 buttons, from a Controller menu beside the key field; the REST import maps controller triggers instead of skipping them); the player reads controllers on Windows (Windows.Gaming.Input through the `windows` crate the app already used; no new dependency) and in the browser (the Gamepad API), polling only while the prototype has such a trigger and the window has the focus, and the control channel presses buttons (`ui.input {gamepad}`); macOS, Linux and FreeBSD read no controllers yet. Widgets react to clicks in a presented prototype (the canvas's callback and sandbox limits, on the prototype's copy: the file and its history don't change, restarting starts over). `proto.evaluate` computes an expression where a layer's interaction would run (MCP); the mode lookup and set-variable-mode rules are cases in a behaviour suite (they already held). Prototyping breadth 90% → 92%, ready unchanged at 74%, 3–6 h → 2–5 h; area hours 120–260 → 119–259 (core 33–75 → 32–74), Features (depth) 29–68 h → 28–67 h; percentages unchanged at this rounding; full parity ~276–538 h → ~275–537 h | -| 2026-10-12 | minor | Plugin directory on `figcraft-server` (gap 6): the server keeps plugin bundles (manifest, module, up to 32 assets; an uncompressed length-prefixed format named by its SHA-256) published with the server key (`figcraft-cli plugin publish`, `plugin.directory.publish`), checked in full before they are kept (size cap, manifest and plugin API version, a version newer than the one kept, the module compiled but never run, plain asset names) and capped (`max_plugins`, `max_plugin_mb`, `max_plugins_mb`); anyone who reaches the server lists, searches and fetches them (hash-checked again on read, a shared download budget); the operator moderates with `figcraft-server plugins [remove]`. The app's Plugins ▸ Manage plugins gains a Browse tab: a server's plugins with Install / Update / Remove, an install confirmation that shows the permissions asked for, the source and the SHA-256, the download checked against the listed hash before anything is read, plugins kept per user in hash-named folders; `plugin.directory.list` / `install` (needs `confirm: true`) / `publish` / `unpublish` for scripts and MCP. Not in the browser build yet; no public catalogue, publisher accounts, signatures, ratings or background update checks. Plugins & widgets 55%/32% → 60%/35%, 10–30 h → 8–26 h; Ecosystem dimension 34% → 38%; weighted totals unchanged at this rounding; full parity ~281–548 h → ~279–544 h | -| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Localization 30% → 35% (drafts, not reviewed: graded low), 40–65 h → 32–55 h; ready for real work ~52% → ~53% (52.9 → 53.1 by the dimension table); full parity ~284–554 h → ~276–544 h; beta ~162–292 h → ~155–280 h (Japanese and Korean leave the beta list; their native reviews need people) | -| 2026-10-12 | minor | Plugin widgets and the panel tail (gap 6, plugin API 3; v1 and v2 plugins unchanged): a plugin marked as a widget puts a layer of its own on the canvas from Plugins ▸ Widgets or `widget.insert`: it describes the layer (frames with auto layout, text, rectangles, ellipses, images it carries) and FigCraft validates the description (size, layer count, depth, numbers, colours, text and image sizes) and builds it as a frame with locked layers inside; a click on a part the plugin named (on the canvas with the Move tool, a pointing hand over it, or `widget.click`) calls the plugin back in a fresh, metered sandbox with the widget's state, as one undo step, and the widget is redrawn; the state lives in the widget's own layer data, so widgets survive save and reload, copying, undo, export and live sessions; no plugin can write that data or call `widget.*`. `widget.list` lists them. A Counter example widget. Plugin panels of different plugins open side by side, and a colour field's swatch opens the colour picker (the plugin hears of the colour when a drag ends). Plugins & widgets 45%/28% → 55%/32%, 15–40 h → 10–30 h; Ecosystem dimension 28% → 34%; weighted totals unchanged at this rounding; full parity ~286–558 h → ~281–548 h | -| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): every UI string, command label and menu wording (1,899 entries) machine-drafted in each and marked as drafts awaiting native review, falling back to English per key (new strings need only English and Spanish); 0 counts as singular in French and Brazilian Portuguese; System picks the first OS language with a catalog (pt-PT and pt read pt-BR); long translations elide with a tooltip in dropdowns, two-column labels and the home cards. Localization 20% → 30%, 45–75 h → 40–65 h; ready for real work unchanged (~52%; 52.6 → 52.9); full parity ~289–564 h → ~284–554 h; beta ~170–305 h → ~162–292 h (Figma's languages left on the way to beta: Japanese and Korean, 7–12 h) | -| 2026-10-12 | minor | Cursor chat and spotlight in live sessions: `/` opens a chat bubble at your pointer that everyone sees as you type and that fades a few seconds after the last change; a spotlight (your avatar's menu, `collab.spotlight`) makes everyone follow your view and page, latecomers included, until they move their own. Both are ephemeral presence the host validates and rate-limits, passed only to clients that read them. Multiplayer & sharing breadth 15% → 17%, ready unchanged (5%), 45–90 h → 42–84 h; the Collaboration dimension's hours 55–110 → 52–104 h (the dimension hours still add up to the total); feature breadth (~84%) and ready for real work (~51%) unchanged at this rounding; full parity ~301–587 h → ~298–581 h | -| 2026-10-12 | minor | Layers and Assets panels behaviour suite written from the public documentation (6 engine tests with about 50 table rows and 5 panel tests driven through the UI; about 30 rows that the code before the fixes contradicts) and its fixes: several layers dropped together in the layers panel land together (layers from apart split around the drop point); Collapse layers keeps the selection's rows open (it closed everything); the eye and padlock stay under the pointer (they vanished as it reached them, so clicks could miss) and dragging across them sets several layers in one undo step; several layers selected open a Rename layers dialog (match as text or a regular expression, Rename to, current name and number buttons, a start value, preview) instead of renaming the top one; `object.batchRename` counts `$n` up and `$N` down (they were swapped), pads with `$nnn`, takes `start`, regular expressions with `$1`…`$9`, `` $` `` / `$'` and codes in the replacement; pages are dragged to reorder, a blank page named with a hyphen is a divider (never opened, holds no layers), duplicates are named "Copy of …", the last page that isn't a divider can't be deleted; the Assets panel files components by page, frame and slash names in folders that fold away (`component.list` gives each `folder`); a leading `.` or `_` now keeps a component out of the library even after it was published, and removing it brings the component back. Pinned where the documentation is silent: Alt-click on an arrow, numbering in stacking order, hidden layers and locked frames taking drops, locking deselects. Layers panel 98%/82% → 99%/85%; percentages and full parity hours unchanged at this rounding | -| 2026-10-12 | minor | Vector multi-edit and the vector edit tail, with a behaviour suite written from the public documentation (5 tests, about 30 table rows and steps, and a junk-parameter sweep of every vector command in multi-edit mode): several vector layers are edited at once (Enter, double-click or `vector.edit.begin {ids}`; a press on another layer's point, handle or segment makes it the active layer; Shift-click, marquee and lasso select across layers; dragging, the arrows, Delete, delete and heal, mirroring, smoothing, corner radius and point caps and joins reach every layer's selected points in one undo step; the pen, bend, paint bucket, cut and eraser work in the active layer); several Bezier handles are selected with Shift-click and dragged together (`vector.edit.moveHandles`); a sharp point next to a curved segment rounds (the curve is cut where the arc meets it and keeps its shape); a point can have its own stroke cap and join (`vector.edit.strokeCap`, `vector.edit.strokeJoin`, the Point section of the Design panel; canvas, PNG, SVG and PDF, outline stroke); the Pen outside edit mode connects to one of its own points and ends the path, and continues a selected open path from an end point. Left: corner smoothing beyond rectangles, the transform box around points of several layers, per-point caps and joins on dashed, variable-width and brush strokes. Shapes & vector 98%/72% → 99%/73%, 2–4 h → 1–3 h; Figma Draw extras 80%/55% → 88%/60%, 2–5 h → 1–3 h (gap 9: pen pressure on macOS and Linux tablets left). Totals recomputed by the doc's method, which rows since the last recompute had left stale: the area table's weighted breadth is 83.7% (~83%; core 94.1%, ~94%) and ready 63.1% (~63%; core 72.1%, ~72%), hours 133–284 (core 38–83); Features (depth) 61% → 63%, 46–94 h → 34–76 h; ready for real work 52.6% (~52%); mainstream weekly areas 71.0% → 72.1% (× 0.9 × 0.9 × 0.85 = 49.7%, ~49%, rounded down); essentials unchanged (~57%); full parity ~306–595 h → ~294–572 h (on top of the rows merged since: ~301–587 h → ~289–564 h; area-table totals recomputed on the merged table and rounded down: breadth ~84%, ready ~63%, core ~94%/~72%, mainstream weekly areas 72.4%, ~49%) | -| 2026-10-12 | minor | Dev resources (gap 16): links for developers (a URL and a name) on any layer: `devResource.add` / `edit` / `remove` / `list`, undoable, saved and synced; an instance also lists its main component's (and that component's set's) links read-only with where they come from, and layers inside an instance show their main layer's; URLs are checked as web links (http or https, a host, no spaces, at most 2,048 characters) and never fetched (a link added without a name is named after its URL), opened only when clicked; the Inspect tab lists, adds, edits and removes them; `get_node` and `inspect_document` return them as `devResources`. The editor extension stays out of scope (the MCP server serves agents and editors). Dev Mode breadth 93% → 96%, ready 70% → 72%, 1–3 h → 0–1 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~304–593 h → ~303–591 h | -| 2026-10-12 | minor | Extended collections' tail (gap 14): variables a library adds to a collection this file extends now count as a library update of their own (listed by name without a copy yet) and arrive inheriting when accepted, instead of waiting for the library's next other update; a library collection that is itself an extended collection can be extended again, values flowing down the chain as each file accepts updates with every level's overrides kept (checked end to end). Styles & variables breadth 94% → 95%, ready unchanged (76%), 3–5 h → 2–4 h; gap 14 1–2 h → 0–1 h (real-library checks left); feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~305–594 h → ~304–593 h | -| 2026-10-12 | minor | A branch's own version history and finer list merges (gap 17): versions saved or autosaved on a branch tab go into the branch (viewed, restored, renamed, deleted and duplicated there, saved in the file with it); merging adds a version to the main file's history and the merged or archived branch keeps its own; fills, strokes and effects merge item by item, conflicting only when the same item changed both ways, an item was removed on one side or both sides added different items. Comments, versions, find, misc breadth 85% → 87%, ready 60% → 61%, 2–5 h → 1–4 h; gap 17 1–3 h → 1–2 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~306–595 h → ~305–594 h | -| 2026-10-12 | minor | Headline percentages recomputed from the area table by its own method (weights × written scores): feature breadth ~82% → ~84% (core editor ~93% → ~94%), weighted ready ~61% → ~63%, core ready ~70% → ~72%, mainstream weekly areas 71.3% → 72.3% and mainstream ~49% → ~50%; several earlier rows had moved the area scores without carrying them into the totals. Hours are unchanged (left for the next full re-measure) | -| 2026-10-12 | minor | Text lists, hanging punctuation and decoration styles, with a behaviour suite written from the public documentation (7 layout tests and 7 command tests, about 60 table rows): lists are set per paragraph (the list buttons, shortcuts and Type settings act on the paragraphs the selection touches, the whole layer when it isn't being edited), nest up to 8 levels (Tab and Shift+Tab; numbers go 1. a. i., bullets • ◦ ▪, counted per level), Enter on an empty item and Backspace at an item's start step out a level and then out of the list, typing `- `, `* ` or `1. ` at a paragraph's start starts one; list spacing (between items, in place of paragraph spacing), hanging lists (markers outside the box) and hanging punctuation (opening punctuation outside a start-aligned edge, closing punctuation outside an end-aligned one, taking no room when lines wrap); underline and strikethrough style (solid, dotted, wavy), thickness, underline offset, skip ink and colour. Canvas, PNG, SVG and PDF; `node.set` keys (`indentation`, `listSpacing`, `hangingPunctuation`, `hangingList`, `textDecorationStyle`/`Thickness`/`Offset`/`SkipInk`/`Color`), inspect (`lineTypes`, `lineIndentations`), CSS (`text-decoration-style`/`-thickness`/`-color`, `text-underline-offset`, `text-decoration-skip-ink`) and the REST import (`lineTypes` with `lineIndentations` per paragraph, which collapsed to one list style before). Text 87%/68% → 90%/70%, 5–10 h → 3–7 h; mainstream weekly areas 71.0% → 71.2%; other percentages unchanged at this rounding; full parity ~306–595 h → ~304–592 h | -| 2026-10-12 | minor | Comments and version history behaviour suite written from the public documentation (15 tests, about 80 table rows plus stepwise checks; 4 tests failed before the fixes) and its fixes: in a live session, threads made in the session couldn't be replied to, edited, reacted to, resolved, moved, deleted or opened (their ids, from their creator's id space, were refused as out of range); restoring a version brought back that version's comments, losing newer threads and replies (comments now stay, those on layers the version lacks staying where they were); restoring now keeps the replaced design in the history as an autosave first, unless a version of it was just saved or restored; giving an autosave a title makes it a named version (it could be dropped to make room); merging a branch adds the main file as it was and then a `Merged “”` version to its history. Pinned where the documentation is silent: pins go to the innermost frame under them, copies, duplicates and duplicated pages come without comments, replying reopens a resolved thread, versions stay with whoever saved them in a live session (a shared history needs accounts). Left: a branch's own version history, comparing two versions. Comments, versions, find, misc 85%/60% → 85%/62%, 2–5 h → 2–4 h; mainstream weekly areas 71.0% → 71.1%; percentages unchanged at this rounding; full parity ~306–595 h → ~306–594 h | -| 2026-10-11 | minor | Canvas navigation, rulers, guides, layout grids, outline mode and measuring behaviour suite written from the public documentation (18 tests, about 120 table rows and checks) and its fixes: the pixel grid shows from 400% (it waited for 800%); guides belong to the page or a top-level frame (a guide dropped over a nested frame goes to the frame holding it, and a frame moved into another keeps its guides but they stop showing, snapping and measuring); a guide can be selected, deleted with Delete, nudged with the arrows and removed from the context menu (`guide.select`, `guide.remove` without an index); Alt-dragging a guide drops a copy and holding Alt while dragging one measures to the nearest edges in the frame it crosses; columns and rows count Auto (as many fixed-size ones as fit); layout grids don't show or snap on a rotated frame; in outline mode shapes are clicked on their outlines, reaching layers behind. Left: outline mode's Include hidden layers and Include object bounds. Canvas & navigation 97%/72% → 98%/74%, 2–5 h → 1–4 h; feature breadth (~80%), ready for real work (~49%), core editor (~68%), essentials (~57%) and mainstream (~46%) unchanged at this rounding; full parity ~320–615 h → ~319–614 h | -| 2026-10-11 | minor | Export and inspect behaviour suite written from the public documentation (139 cases in 19 tests; 47 failed before the fixes) and its fixes: an export covers what the layer paints, so a drop shadow adds room only where it falls, strokes on boxes and ellipses reach their real width (they added up to four times as much), hidden layers take no room and a mask group exports the mask's area; a slice exports everything under it; slashes in layer names export into folders; Copy as PNG copies the whole selection. Added: the export settings' contents only and include bounding box, and for SVG outline text (off writes ``), unique id attributes and simplified strokes (stroke attributes where exact); Inspect colour formats (hex, RGB, CSS, HSL, HSB), CSS in rem, a Blend row, positions measured from the frame around a layer (groups aren't frames), CSS for rotation, blend modes, gradient directions and kinds, text alignment, case and decoration; icons and images in the asset list (`file.exportLayers {assets}`). Import/export 80%/50% → 82%/53%; Dev Mode 90%/64% → 93%/70%; file formats 55% → 56%; mainstream weekly areas 68.1% → 68.4%; feature breadth (~80%), ready for real work (~49%), essentials (~57%), mainstream (~46%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | `.fig` import's tail (gap 1): vectors open as editable vector networks (points, curved segments, filled areas with their winding, per-point corner radius and handle mirroring, end caps from the end points), falling back to outlines when the network is missing or damaged; sticky notes, shapes with text, connectors, tables, code blocks, slides and similar layers import as the closest frames, text layers and vectors with a warning instead of being dropped; set-variable, set-variable-mode and conditional prototype actions come across. New parsers fuzzed on fixtures generated in code. File formats 55% → 58%, 8–15 h → 5–10 h; import/export breadth 80% → 82%, ready 50% → 54%; feature and area totals and ready for real work (~49%) unchanged; beta ~173–310 h → ~170–305 h; full parity ~321–618 h → ~318–613 h | -| 2026-10-11 | minor | Shapes and vector behaviour suite written from the public documentation (10 shape tests, 6 vector tests and 3 handle tests: about 70 table rows plus stepwise checks) and its fixes: rounded vector points are drawn and exported (the radius was stored only; a vector's corner radius now rounds every point); a boolean group looks like its top layer, a subtraction like the bottom one (always the bottom); Alt draws a line from its centre; stars keep to 3–60 points; delete and heal fits the curve through the deleted point; Shift-clicking a selected point at a corner of the points' box takes it out (it scaled); outline stroke turns the stroke's colour style into the fill's and keeps opacity and effects; the miter field shows the miter angle (it showed the ratio under that name); individual corners only for rectangles and frames; Option/Alt+Shift+F flattens; edit-mode keys reach the edit tools through MCP `key` (Delete removed the layer). Added: corner radius and star ratio handles on the canvas, Count and Ratio fields for polygons and stars, cut tool drags that divide a shape into layers, paint bucket drags across areas. Shapes & vector 97%/68% → 98%/72%, 1–2 h → 2–4 h (the suite found gaps the estimate didn't count: corner smoothing beyond rectangles, a join or cap per point). Totals recomputed by the doc's method, which earlier rows had left stale: the area table's weighted figures are breadth ~82% (core ~93%) and ready ~61% (core ~69%), not the ~80%/~58% (~91%/~68%) carried since #130; Features (depth) follows the weighted ready, 57% → 61%; ready for real work ~49% → ~51%; mainstream ~46% → ~48% (weekly areas 67.8% → 69.4%); the selection suite's area 2 hours (−3/−5) hadn't reached the totals, and neither it nor the text suite had reached the Features (depth) row: Features 60–115 h → 56–109 h, full parity ~321–618 h → ~319–615 h (−3/−5 for area 2, +1/+2 for area 4) | -| 2026-10-11 | minor | Paint and effects behaviour suite written from the public documentation (14 paint and 9 effects tests with table rows for fills, opacity, every blend mode by its formula, pass-through and Normal containers, gradient handles and stops, image scale modes and adjustments, colour models, the colour profile, shadows, blurs, effect order and exports; 3 command tests) and its fixes: groups and frames set to Normal blend their children among themselves first, and clipping frames no longer isolate their children (blend modes inside a clipping frame met only the frame's children, not its fill or what is behind it) — canvas, SVG (`isolation`; clips put on what blends, since a clipped group isolates) and PDF (isolated groups); plus darker is drawn (it was multiply) on the canvas and in PNG exports; drop shadows hide behind what the layer covers, translucent fills included, unless shown behind transparent areas, in SVG too (it never cut them out) and the SVG filter region reaches as far as the effects; Fill images cover the box to its edges (upscaled ones faded out at the edge); "Normal" names Normal, not pass-through, in commands and the blend menu (the menu's Normal set pass-through). Added: a blend menu for each paint in the colour picker. Paint 66% → 70%, 4–7 h → 3–6 h; effects 72% → 75%, 3–6 h → 2–5 h; percentages unchanged at this rounding; full parity ~321–618 h → ~319–616 h | -| 2026-10-11 | minor | Frames, groups, sections and masks behaviour suite written from the public documentation (15 frame, group and section tests, 6 mask tests) and its fixes: a layer a mask hides can't be clicked there (clicks reach what's behind; Select layer agrees), a mask group's box and exports are the mask's (plus any layers under it), sections stay on the page or in sections (moves, drags, pastes, grouping and new sections in frames refused or kept on the page), drawing a frame or section around layers puts those fully inside it in it, resizing a section leaves its contents where they are (and a frame resized with Cmd/Ctrl held keeps its layers' sizes too), and X/Y of layers in groups are measured in the frame (groups have no coordinate space). Frames, groups, sections, masks 95%/75% → 97%/80%, 2–4 h → 1–3 h; percentages unchanged at this rounding; full parity ~323–621 h → ~322–620 h | -| 2026-10-11 | minor | Styles and variables behaviour suite written from the public documentation (18 command-level tests) and its fixes: editing a styled fill, stroke, effect or layout grid list (directly, a paint row, an added fill or a bound variable) unlinks that style instead of being overwritten by the next style edit; applying a style replaces variables bound to what it sets; text styles go on text layers only; deleting a variable (or its collection) unbinds the layers using it, which keep their last value, and aliases to it take its values; bindings are type-checked (a colour variable can't drive a width) and number variables fill text content; aliases are checked when a variable is created. Added: duplicate, move (reorder, into a folder) and rename folder for styles, with a Local styles list grouped by type and folder (click to edit, right-click, drag to reorder); duplicate mode, set as default and move column for modes; per-platform code syntax (Web, iOS, Android) used by generated CSS and the variables JSON; font weight, paragraph spacing/indent and text content scopes for number variables. Found missing: variables inside styles. Styles & variables 92%/72% → 94%/76%, 1–2 h → 3–5 h; percentages unchanged at this rounding; full parity ~323–621 h → ~325–624 h | -| 2026-10-11 | minor | Prototyping behaviour suite written from the public documentation (7 model tests, 12 player tests with 21 gestures) and its fixes: on drag starts only for a drag its animation's way and the animation follows the pointer, key combinations (`Shift+K`) fire (and are recorded and imported), an overlay's after-delay timer starts when it opens, instances of interactive components play their variant's interactions and "Change to" changes the right instance, spring presets are physical springs that play until they settle, smart animate overshoots with spring and back easings, fixed layers draw above scrolling content; added scroll to offsets. Prototyping 88%/70% → 90%/74%, 5–9 h → 3–6 h; mainstream weekly areas 68.1% → 68.4%, mainstream ~46% → ~47%; other percentages unchanged at this rounding; full parity ~320–615 h → ~318–612 h | -| 2026-10-11 | minor | Components behaviour suite written from the public documentation (10 tests, 45 table cases) and its fixes: instances now keep overrides of auto layout padding, gap and alignment, layout grids, export settings, prototype interactions and every text property (resizing, paragraph spacing, lists…; these were lost when the main changed); moving, rotating, resizing or re-constraining layers inside an instance, and its auto layout direction, are refused (the instance itself resizes); switching variants keeps an override only where both variants had the same value (text layers keep theirs); swapping from the Assets panel keeps only text overrides; the default variant is the top-left one (instances, Assets thumbnails, code); hidden exposed instances hide their properties; pushing changes from a nested layer goes to the outer main. Added: variant property reordering and duplicate combination warnings. Components 95%/68% → 96%/72%, 6–12 h → 4–9 h; with #133's selection and transform change, core editor ready ~68% → ~69% and mainstream ~46% → ~47% (weekly areas 68.1% → 68.8%); full parity ~320–615 h → ~315–607 h (the totals now also carry #133's area 2 hours) | -| 2026-10-11 | minor | Embedded image colour profiles: placed and opened images are converted once, when decoded, from their own colour space into the file's (sRGB or Display P3): ICC profiles in PNG (`iCCP`), JPG (APP2), WebP and TIFF, matrix/curve and table profiles in RGB or grey (moxcms, already built for the `image` crate), and a PNG's `sRGB`, `gAMA` and `cHRM` chunks; images without a profile, or with one that can't be used (CMYK, damaged, over 4 MB), are taken as sRGB as before. The original file is kept (PDF exports still embed it); pixel exports carry the converted pixels with the file's profile. The image fill's details show the source profile (Unknown — treated as sRGB when there's none) and `image.place` reports it as `imageProfile`. Paint breadth 87% → 90%, ready 66% → 67%, 4–7 h → 3–6 h; feature breadth (~80%), ready for real work (~49%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | Layers panel keyboard and stroke variables: F2 renames the selected layer (also from a layer row with the keyboard, which gets it back; Escape there cancels), Shift+F10 (`view.contextMenu`) opens the context menu at the focused row or below the canvas selection and the menu takes the keyboard, F8 (`select.area`) draws a selection box for keyboard-only users (arrows move it, Shift+arrows size it, Enter selects like a marquee; agents pass a box), long lists checked at 10k layers (revealing a layer, expanding or collapsing everything and arrow keys take at most 1.3 ms a frame in the release UI bench; the revealed row takes the keyboard); colour styles on strokes were already complete, and each side's stroke weight now binds to a number variable (`strokeTopWeight`…, bindable fields in the Design panel and REST import). Layers panel 95%/78% → 98%/82%, 2–4 h → 1–2 h; Strokes 90%/72% → 95%/75%, 1–3 h → 1–2 h; mainstream depth 67.8% → 68.1%; feature breadth (~80%), ready for real work (~49%), essentials (~57%) and mainstream (~46%) unchanged at this rounding; full parity ~321–618 h → ~320–615 h | -| 2026-10-11 | minor | Text behaviour suite written from the public documentation (13 text cases, 5 command tests) and its fixes: CJK lines break between ideographs, kana and Hangul and before and after Latin words without breaking inside them, with the line-start and line-end rules (closing punctuation, small kana and the prolonged sound mark never start a line, opening brackets never end one); a style's line height, baseline and decoration come from its own font, so lines with CJK or emoji drawn by fallback fonts keep the spacing of the others; paragraph indent applies to start-aligned text only; carets, Delete and Backspace step over whole grapheme clusters (emoji sequences, flags, combining marks), which also shape with one font; double-click and word moves take one script's run in text without spaces. Added: vertical trim (cap height to baseline; `leadingTrim`, Type settings, REST import, inspect, CSS `text-box`). Text 85%/64% → 87%/68%, 7–13 h → 5–10 h; percentages unchanged at this rounding; full parity ~323–621 h → ~321–618 h | -| 2026-10-11 | minor | Selection and transform behaviour suite written from the public documentation (5 selection tests with 38 table rows, 11 transform tests) and its fixes: Alt-resizing shrinks about the centre too (it could only grow); groups, frames with Scale or stretch constraints and several layers with different rotations scale each layer along its own axes, keeping its rotation and flip (children turned 90° grew along the wrong axis, nested turned groups too); several layers dragged past an edge mirror and flip; a flipped layer reads back the rotation typed; resizes land on whole pixels with the pixel grid on; a press inside a layer smaller than its handles moves it and the nearest handle wins (the first in reach did), with its handles hidden under 16 px; locked layers selected in the layers panel get no handles and aren't dragged with the rest; the background of a top-level frame with layers starts a marquee inside it (it moved the frame); Select inverse skips hidden layers like Select all. Added: equal spacing snapping with labelled gaps, Cmd/Ctrl while resizing a frame ignores its constraints. Selection & transform 95%/68% → 97%/72%, 6–10 h → 3–5 h; core editor ready ~67% → ~68%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~345–666 h → ~342–661 h | -| 2026-10-11 | minor | `.fig` import, best effort (gap 1): `.fig` files open (best effort): the zip, its deflate/zstd chunks and the kiwi schema are decoded generically and mapped by field name onto the REST importer, so pages, the layer tree, geometry, paints (styles resolved), strokes, effects, text with styled runs, vectors (as outlines), auto layout (grid included), constraints, components, sets, instances (expanded from their main with overrides, derived layout and property values), styles, variables with modes and bindings, prototyping and image fills from the archive come across; anything else is a warning. Import/export breadth 65% → 80%, ready 35% → 50%, 30–60 h → 8–15 h; file formats 40% → 55%, 30–60 h → 8–15 h; feature breadth ~79% → ~80% (core ~90% → ~91%), areas ready ~57% → ~58% (core ~67% → ~68%); ready for real work ~47% → ~49%; essentials ~53% → ~57% (opening files people send ×0.80 → ×0.88); mainstream ~43% → ~46% (weekly areas 66.8% → 67.8%, file exchange ×0.80 → ×0.85); beta ~195–355 h → ~173–310 h; full parity ~345–666 h → ~322–620 h | -| 2026-10-11 | minor | Auto layout behaviour suite written from the public documentation (64 layout cases, 8 command tests) and its fixes: a frame is never smaller than its padding, only the frame's inside stroke counts when strokes are included (and new auto layout includes them), auto gaps stop at 0, grid `fr` tracks behave like the CSS `fr` unit (a track never gets less than its content), canvas stacking is drawn, exported and hit (it was stored only). Added: auto gaps evenly and around, vertical wrap, grid cell alignment, strokes and canvas stacking controls. Auto layout 95%/68% → 98%/74%, 6–12 h → 3–6 h; percentages unchanged at this rounding; full parity ~348–672 h → ~345–666 h | -| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | -| 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | -| 2026-10-11 | minor | Localization layer landed (gap 5): per-language catalogs baked in at build time with English text as keys and fallback, placeholders and plurals, command labels by id, Preferences ▸ Language (System follows the OS or the browser) applied live and `pref.language`; English catalog and a complete Spanish draft awaiting native review. Localization 10% → 20%, 60–100 h → 45–75 h; ready for real work unchanged (~47%); full parity ~363–697 h → ~348–672 h; beta ~200–365 h → ~195–355 h | -| 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | -| 2026-10-11 | minor | Plugin panels and settings landed (gap 6, ABI v2; v1 plugins unchanged): a plugin shows a panel of notes, fields (text, number, checkbox, menu, list, colour) and buttons drawn with FigCraft's widgets; a button or a changed value calls it back with every value (same limits per callback, one undo step per callback that edits, rolled back on failure), its module staying loaded while the panel is open; `plugin.ui` / `plugin.event` / `plugin.close` drive panels headless; per-plugin settings (64 KB) kept with the preferences; the Swatch grid example gained a palette panel that remembers the last palette. Plugins & widgets breadth 35% → 45%, ready 20% → 28%, 25–55 h → 15–40 h; ecosystem 20% → 28%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~373–712 h → ~363–697 h | -| 2026-10-11 | minor | Canvas fixes (gap 13 closed): boolean groups are clicked where their result is (a click in a subtracted hole reaches what's behind; with the group or an operand selected its operands are hit, so each can still be picked), a layer dragged inside a group stays in the group (frames and auto layout still take other drops), View ▸ Frame outlines and View ▸ Slices draw on the canvas, and the idle tests wait for the render thread instead of counting frames. Shapes & vector 1–3 h → 1–2 h (vector QA with real files left); percentages, feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~378–722 h → ~375–717 h | -| 2026-10-11 | minor | Stretch and scatter brushes and pen pressure landed (gap 9): eight brushes drawn in code (four stretched along the path, four scattered with spacing, size, rotation and scatter jitter from a seed) in the stroke settings, drawn everywhere strokes are and flattened by outline stroke; the Pencil records pen pressure as the stroke's width profile (Windows pens, browser styluses; macOS and Linux tablets report none) and can draw with a brush; Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–9 h; Strokes 2–4 h → 1–3 h; Hardware 50% → 55%, 15–30 h → 13–27 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~378–722 h → ~358–687 h | -| 2026-10-11 | minor | Text on a path and pattern fills landed (gap 9): text follows a path or shape (start, side, alignment; edited in place; canvas, PNG, SVG and PDF; SVG `` imports as text on a path) and pattern fills tile a layer (grid or half-shifted rows or columns, scale, spacing, alignment; live as the source changes; SVG ``, PDF tiling patterns); Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–10 h; Text 8–14 h → 7–13 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~375–715 h | -| 2026-10-11 | minor | Team libraries on `figcraft-server` landed (gap 10): a file's library is published to the server under the file and enabled in other files by a share link (per-asset versions; updates noticed in the background and accepted with overrides kept), the server counts which files use which components, and a whole library can be swapped for another by names; Team libraries breadth 40% → 70%, ready 25% → 40%, 10–20 h → 4–8 h; collaboration 12% → 13%, 55–110 h → 49–98 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~380–725 h → ~375–715 h | -| 2026-10-11 | minor | Branches landed (gap 17): File ▸ Create branch… copies the file into a branch tab that remembers its base; Review and merge shows both sides' changes (layers with Inspect-style property changes, styles, variables, comments) and the conflicts (the same property changed on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merging is one undo step and archives the branch; Update from main; branches are saved in the `.figcraft` file. Comments, versions, find, misc breadth 80% → 85%, ready 58% → 60%, 4–8 h → 2–5 h; collaboration 12% → 13%; menu breadth 204/204 (the catalog gained File ▸ Create branch…); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~378–722 h | -| 2026-10-11 | minor | Animated GIF fills and TIFF images (gap 12, without video and HEIC): GIF fills keep the original file, show the first frame on the canvas and in exports and play in the presenter and the preview (frames decoded on first play, at most 500 frames and 128 MB; always looping; the window wakes only for a GIF's next frame); TIFF images open, import, place and paste. Paint breadth 85% → 87%, 5–9 h → 4–7 h; gap 12 6–12 h → 5–10 h (video playback and HEIC left, both needing non-Rust decoders); feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | The last three catalogued menu items got commands (gap 21): File ▸ Open recent (recent files kept in the preferences, at most 20, a file that's gone greyed in the menu and dropped when picked, Clear recent), View ▸ Outlines ▸ Show outlines for selection and View ▸ Mask outlines: menu breadth 98.5% → 100% (203/203); Canvas & navigation breadth 95% → 97%, 3–6 h → 2–5 h; feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | Extended collections, variables import/export and gradient stop and variant property bindings landed (gap 14): Styles & variables breadth 85% → 92%, ready 70% → 72%, 4–8 h → 1–2 h; feature breadth unchanged (~79%); ready for real work unchanged (~47%); full parity ~385–730 h → ~380–725 h | -| 2026-10-11 | minor | Collaboration wording after the standalone server: `figcraft-server` keeps many files, persisted as a snapshot plus a log of what changed (compacted, checksummed, damaged files skipped and reported), restarts lose nothing confirmed and apps resume where they were; the app publishes its file to it; share links per file let people edit or only view (the server refuses a viewer's edits), are revocable and stored as hashes; TLS comes from a reverse proxy (the desktop app has none of its own yet); percentages and hours unchanged until the next re-measure | -| 2026-10-11 | minor | Live boolean editing, Design panel arc fields, the shape builder on strokes and open paths and one-sided variable widths landed (gap 13): Shapes & vector breadth 92% → 97%, ready 66% → 68%, 8–14 h → 1–3 h; feature breadth ~78% → ~79%; ready for real work unchanged (~47%); full parity ~390–745 h → ~385–730 h | -| 2026-10-11 | minor | Wording only: live sessions (#104), then the browser joining them and `figcraft://` invite links opening the desktop app; percentages and hours unchanged until the next re-measure | -| 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 | minor | Readiness-by-audience table: hours to ~95% for full (~400–750 h), mainstream (~180–330 h) and essentials (~85–160 h). Full number confirmed as an additive weighted sum; unchanged | -| 2026-10-10 | minor | Added the essentials user number (~53%: core features 77.8% × 0.92 launch/stability × 0.92 discoverability × 0.8 opening files people send) | -| 2026-10-10 | minor | Added the mainstream practitioner number (~43%: weekly areas 66.5% × 0.9 interaction × 0.9 stability × 0.8 file exchange) and user-evidence count (none). Checked that ready for real work (~46%) is built from the written dimension weights and scores; unchanged | -| 2026-10-10 | minor | Stage checked against the alpha gate: stays alpha | -| 2026-10-10 | major | Created from the ROADMAP's parity rubric, honest assessment and area narrative. Full re-measure against Figma desktop 126.10.7: added multiplayer, plugins, team libraries, Figma Draw extras and AI as weighted areas; split breadth from ready-for-real-work; core depth re-graded ~79% → ~67%; hours calibrated from git history; full parity 115–195 h → 420–790 h | +# FigCraft parity with Figma + +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (Outline stroke preserves stroke holes; reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS) + +The authoritative assessment of how close FigCraft is to Figma Design. [`ROADMAP.md`](../ROADMAP.md) +summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu checklist is +[`parity-checklist.md`](parity-checklist.md). Deeper checklists: +[`ui-parity.md`](ui-parity.md), [`file-format-parity.md`](file-format-parity.md), +[`hardware-parity.md`](hardware-parity.md), [`localization-parity.md`](localization-parity.md), +[`auto-layout-parity.md`](auto-layout-parity.md), [`components-parity.md`](components-parity.md), +[`prototyping-parity.md`](prototyping-parity.md), [`dev-mode-parity.md`](dev-mode-parity.md), +[`collaboration-parity.md`](collaboration-parity.md). + +## Headline + +| Number | Value | Kind | +|---|---|---| +| Menu/command breadth | **100%** (211 / 211 catalogued menu items have a live command) | **measured** (`cargo xtask parity`, 2026-10-12) | +| Feature breadth (does each Figma Design feature exist, weighted) | **~85%** (core editor areas 1–16: ~94%) | estimated from the area table below | +| Ready for real work | **~53%** | estimated (dimension table below: written weights × written per-dimension scores) | +| Essentials user | **~59%** | estimated ([below](#essentials-user): core features 77.8% × discounts 0.92 × 0.94 × 0.88) | +| Mainstream practitioner | **~50%** | estimated ([below](#mainstream-practitioner): weekly areas 72.7% × discounts 0.9 × 0.9 × 0.85) | +| Remaining to beta | **~155–280 h** one Opus 5.5 agent (~32–64 h wall-clock with 5 parallel agents) | estimated | +| Remaining to full parity | **~262–519 h** one agent (~83–145 h wall-clock with 5 parallel agents) | estimated | +| Stage | **alpha**: the alpha gate passes ([roadmap](roadmap.md#alpha-gate)); multiplayer and checking the best-effort `.fig` import on real files are beta gaps (see [ROADMAP](../ROADMAP.md#stage)) | | + +The previous figures (ROADMAP of 2026-10-10, "feature depth ~79%", "full parity 115–195 h") were +self-graded against a 16-area rubric that left out, by design, multiplayer, plugins, cloud +libraries and AI features, and that counted no file-format blocker. Figma users rely on all of +those every day, so this re-measure weights them in. The core-editor depth figure drops from ~79% +to ~67% because nothing has yet been checked against real Figma files or side by side tool by +tool; presence was being graded as behaviour in places. + +## Readiness by audience + +| Audience | Ready % | Opus 5.5 agent wall-clock hours to ~95% | Work that dominates | +|---|---|---|---| +| Full target (ready for real work) | ~53% | ~268–522 h | multiplayer (42–84), localization (29–51), plugins (8–26), the interaction pass (40–60), GPU canvas (30–60), AI (30–60), the `.fig` import's tail (5–10) | +| Mainstream practitioner | ~50% | ~120–229 h | core editor depth incl. the `.fig` import's tail (49–98), the interaction pass (40–60), real-file corpus and platform QA (25–50), checking the experimental `.fig` export opens elsewhere (5–15) | +| Essentials user | ~59% | ~55–100 h | the `.fig` import's tail so the files people send open well (5–10), core tool polish (25–45), interaction pass on the core tools (15–25), stability QA (10–20) | + +Hours are one agent working sequentially, calibrated from this repo's history (about 1 agent-hour +per 1,000 added lines, 1–2.5 h per feature PR, ~7 h per honest depth point in the polish tail; see +[how the hours are calibrated](#how-the-hours-are-calibrated)). Each audience's work is a subset of +the next. About 70–80% parallelizes across 4–6 agents on disjoint crates (roughly ÷4 in +wall-clock); the `.fig` reader, the multiplayer core and a plugin runtime each need one owner, and +native-speaker review, real-file licensing and screen-reader QA need people. + +The full number is an additive weighted sum over the dimension table; mainstream and essentials use +multiplicative discounts, as the standard says. + +## What was measured, and how + +- **Target and version.** Figma desktop for macOS, `CFBundleShortVersionString` 126.10.7, + installed at `/Applications/Figma.app`. FigCraft is measured as of `origin/main` d452da3 + (2026-10-10, PR #99). +- **The installed app, names and public listings only.** FigCraft is clean-room + ([`AGENTS.md`](../AGENTS.md)): nothing inside the bundle's code or web assets was read. From + `Info.plist`: one document type, `com.figma.document`, extensions `fig`, `jam`, `deck` + (MIME `application/x-figma`), and the `figma://` URL scheme. The `.lproj` folders are + Electron/Chromium locale packs (60+), not Figma's UI languages; the UI languages come from + Figma's help center (see [`localization-parity.md`](localization-parity.md)). The bundle ships + GPU, Plugin and Renderer helper processes (GPU rendering; a sandboxed plugin runtime). +- **The menu catalog.** `crates/engine/src/catalog.rs` holds Figma's main menu tree (202 items in + File, Edit, View, Object, Text, Arrange, Vector, Plugins, Preferences, Libraries, Help), written + from public documentation and black-box observation (local notes in the gitignored + `plan/figma/03-menu-tree.md`). It leaves out the Widgets menu and dynamic plugin lists, so the + 100% overstates breadth against the full menu a little. +- **From source** (no build; 2026-10-10): 449 command ids registered with `spec!`, 963 `#[test]` + functions in 177 files, ~108,800 lines of Rust in 13 crates + 3 apps + xtask (before #101–#103, which added the `plugin` and `collab` crates and ~7k lines), 140 key bindings + and 13 gesture rows in `figcraft_engine::keymap`, 21 files in the SVG import corpus, 0 + translated UI strings (no localization layer exists). +- **Figma's feature set**: help center, release notes and Config 2025 announcements (Figma Draw, + grid auto layout, slots, glass/noise/texture effects, progressive blur), known to the assessor up + to mid-2026, plus web checks on 2026-10-10 (supported image/video types, UI languages, pen + pressure in Figma Draw). +- **Side by side.** Look & feel was compared with Figma UI3 on the owner's Mac (screenshots kept + local, never committed). No real Figma file has been opened in FigCraft (no `.fig` reader), and + no rendered frame has been compared pixel for pixel against Figma's own export. + +## Weights + +Weights are the share of a typical product designer's working time in Figma Design, written down +so they can be argued with. Areas 1–16 keep the original rubric's weights (they sum to 94, not the 100 the rubric claimed); +areas 17–21 are added (sum 19) because Figma users depend on them. Percentages below divide by the +total, 113. +Out of scope as separate products: FigJam, Figma Slides, Sites, Make, Buzz, Weave (the `.jam` and +`.deck` document kinds belong to them). + +## By feature area + +Breadth: does it exist. Ready: depth, correctness and fidelity for real projects. Hours: one Opus +5.5 agent, sequential, tests included. + +| # | Area | Weight | Breadth | Ready | Remaining | Hours | +|---|---|---|---|---|---|---| +| 1 | Canvas & navigation | 6 | 98% | 74% | Outline mode's Include hidden layers and Include object bounds, Display P3 on screen, QA at extreme zooms (behaviour suite for navigation, rulers, guides, layout grids and outline mode landed: guide selection, Auto grid counts, outline-mode clicks) | 1–4 | +| 2 | Selection & transform | 8 | 97% | 72% | Handles, measuring and snapping checked tool by tool against Figma and real files (snap distance, handle reach, cursors); children turned off a right angle scale without skew, approximately (behaviour suite from the documentation, rotated groups and mixed selections, equal spacing snapping, Cmd/Ctrl to ignore constraints and marquees inside frames landed) | 3–5 | +| 3 | Layers panel | 5 | 99% | 85% | QA with long real files (behaviour suite from the documentation, Rename layers dialog, sweeping eyes and padlocks, page dragging and dividers, several layers dropped together, keyboard box selection, F2 rename, Shift+F10 context menu and 10k-layer timings landed) | 1–2 | +| 4 | Shapes & vector | 8 | 99% | 73% | Outline-stroke compositing; corner smoothing on polygons, stars and vectors; the transform box around points of several layers; per-point caps and joins on dashed, variable-width and brush strokes; vector QA with real files (outline stroke rings covered by `outline_stroke`; behaviour suites from the documentation, corner radius and star ratio handles, rounded vector points drawn, also beside curved segments, a cap and join per point, several handles dragged together, cut and paint bucket drags, delete and heal along the curve, a boolean group's look by operation and the miter angle landed) | 1–3 | +| 5 | Paint | 7 | 90% | 71% | Video fills, wide-gamut canvas, HEIC images; plus darker in PDF (behaviour suite from the documentation, pass-through and isolating groups, clipping frames that don't isolate, plus darker, animated GIF fills, TIFF images and embedded image colour profiles landed) | 2–5 | +| 6 | Strokes | 3 | 95% | 75% | Stroke QA with real files (colour styles on strokes, stroke weight and per-side weight variables, brush strokes landed, see 20) | 1–2 | +| 7 | Effects | 4 | 100% | 75% | Glass and progressive blur tuned side by side (behaviour suite from the documentation, inner shadows and background blur on groups, shadows hidden behind translucent layers on the canvas and in every export landed) | 2–5 | +| 8 | Frames, groups, sections, masks | 5 | 97% | 80% | Visual QA with real files (a one-step frame/group convert, and a behaviour suite from the documentation landed: masked-out areas don't take clicks, mask group bounds and exports follow the mask, sections kept out of frames and groups, frames and sections drawn around layers take them, section resizing leaves contents in place, X/Y measured in the frame) | 1–3 | +| 9 | Auto layout & constraints | 9 | 98% | 74% | Behaviour against real Figma files (behaviour suite from the documentation, `fr` tracks like CSS, vertical wrap, auto gaps evenly/around, cell alignment and wrapped-line alignment landed) ([auto-layout-parity](auto-layout-parity.md)) | 3–6 | +| 10 | Text | 8 | 90% | 70% | Spell check, text metrics on real files and in other fonts (text box sizes measured against documents re-saved by other software: all 230 self-sizing boxes in 7 documents come out to the pixel, one font family; behaviour suite with CJK line breaking, vertical trim and carets by grapheme cluster landed; lists per paragraph with nesting and list spacing, hanging punctuation and lists, decoration style, thickness, offset, skip ink and colour landed) | 3–7 | +| 11 | Components & variants | 9 | 96% | 72% | Real (not synthetic) libraries, `.fig` libraries (a component playground for variant, boolean and text properties, a behaviour suite from the documentation, overrides of padding, gap, grids, exports and interactions, refused geometry overrides, variant-switch and Assets-swap override rules, top-left default variant landed) ([components-parity](components-parity.md)) | 4–9 | +| 12 | Styles & variables | 5 | 95% | 76% | Real-library QA (variables inside styles, behaviour suite from the documentation, style folders and order, mode order, code syntax, extended collections with their tail (new library variables as updates, chains through libraries) and variables import/export landed) ([components-parity](components-parity.md)) | 2–4 | +| 13 | Prototyping | 6 | 92% | 74% | Video triggers and media actions, controllers on macOS, Linux and FreeBSD, real prototype QA (behaviour suite from the documentation, delays on mouse triggers, manual overlay offsets, key combinations, controller buttons on key/gamepad triggers (Windows and the browser), widgets reacting in prototypes, interactive components playing their variants, drag-driven animations, physical springs landed) ([prototyping-parity](prototyping-parity.md)) | 2–5 | +| 14 | Import/export | 6 | 86% | 58% | `.fig` import's tail (best-effort import, editable vector networks, stand-ins for non-design layers and variable prototype actions landed; no real-file checks yet), checking the rest of the experimental `.fig` export (landed; written files open and draw in other software) on effects, images, prototypes and real files, HEIC/video import, CMYK output intent ([file-format-parity](file-format-parity.md)) | 5–10 | +| 15 | Dev Mode / inspect | 2 | 96% | 72% | Import lines for connected instances inside generated layer trees; an editor extension is left to the MCP server (inspect behaviour suite with colour formats, rem and icon and image assets, code connect, plugins in Dev Mode and dev resources links landed) ([dev-mode-parity](dev-mode-parity.md)) | 0–1 | +| 16 | Comments, versions, find, misc | 3 | 87% | 63% | Comments are local only; comparing two versions; branches in live sessions; text runs and other lists besides fills, strokes and effects merge whole (branches with review and merge, their own version history and per-item list merges, and a comments and version history behaviour suite from the documentation landed) | 1–3 | +| 17 | Multiplayer & sharing | 8 | 17% | 5% | Core landed (`crates/collab`: ids, change sets, server ordering, convergence tested); live sessions (preview): WebSocket transport, hosting from the desktop app, joining from the app or the browser, invite links that open the app, cursors, selections and follow, cursor chat and spotlight, per-person undo, offline resume; a self-hosted `figcraft-server` with persistence and share links to edit or view, which can be open to the public (owner tokens, limits per address, expiry, moderation). Left: accounts and teams, audio ([collaboration-parity](collaboration-parity.md)) | 42–84 | +| 18 | Plugins & widgets | 4 | 60% | 35% | Own sandboxed WASM plugins landed (#101: manifest, permissions, menu, Run last, Manage, SDK), with log byte/count limits checked before allocation and host forwarding ([logging limits](plugins.md#logging-limits)), plugin panels called back per button or changed value and per-plugin settings (ABI v2), then widgets (API 3: plugin-drawn layers on the canvas with state kept in the document, redrawn on clicks; a Counter example), panels side by side and a colour picker for plugin colour fields, then a plugin directory on `figcraft-server` (publish with the server key, browse, install after a permissions confirmation, hash-checked downloads, updates by version). Left: a public catalogue, publisher accounts and signatures, ratings, background update checks, the directory in the browser; widgets take clicks only (no text input, property menus, hover or timers) and draw frames, text, shapes and images only; Figma's plugins are JavaScript and don't run (a JS engine would be an owner decision) ([plugins.md](plugins.md)) | 8–26 | +| 19 | Team libraries & organization | 2 | 70% | 40% | Libraries kept on `figcraft-server` under their file and enabled by a share link (per-asset versions, background update checks, overrides kept), usage counts per component and file, swapping a library by names. Left: team-wide library lists, accounts and permissions, analytics over time, pushed update notices | 4–8 | +| 20 | Figma Draw extras | 2 | 88% | 60% | Have: variable width, lasso, shape builder, noise/texture, text on a path, pattern fills, stretch and scatter brushes, pen pressure (Windows pens, browser styluses), multi-edit (several vector layers in edit mode at once). Missing: pressure from macOS/Linux tablets | 1–3 | +| 21 | AI features | 3 | 0% | 0% | First draft, rename layers, image edit/remove background, visual search, Make: needs local models (owner decision) | 30–60 | +| | **Weighted (÷113)** | 113 | **~85%** | **~63%** | | **117–255** | +| | Core editor only (1–16, ÷94) | 94 | ~94% | ~72% | | 32–74 | + +## By dimension (ready for real work) + +| Dimension | Weight | Ready | Hours | Evidence | Doc | +|---|---|---|---|---|---| +| Features (depth) | 35 | 63% | 28–67 (areas 1–13, 15, 16, 20) | The table above; core editor ~72% | this file | +| UI/UX fidelity | 15 | 58% | 40–60 | Look & feel compared side by side with UI3 (~65% by the builders); tool-by-tool interaction (modifiers while dragging, hover states, popovers) not yet compared (a cursor for every tool and modifier, number fields and panel widths built from the documentation, not compared); accessibility names and keyboard navigation landed | [ui-parity](ui-parity.md) | +| File formats | 12 | 60% | 5–10 | `.fig` read, best effort, with editable vector networks (tested on generated fixtures, not real files), `.fig` write experimental (round trips through our own importer; written files open and draw in other software); REST JSON import, SVG in/out, PNG/JPG/WebP/PDF export solid, with a behaviour suite from the documentation | [file-format-parity](file-format-parity.md) | +| Collaboration | 10 | 13% | 52–104 | Local comments, version history and branches with review and merge; the multiplayer core (sync model, server ordering, client reconciliation, convergence tests) landed in #103, and live sessions (preview) co-edit a file hosted by a desktop app or kept on a self-hosted `figcraft-server` with edit and view links, joined from the app or the browser, with cursor chat and spotlight (`docs/collab-protocol.md`) | [collaboration-parity](collaboration-parity.md) | +| Stability | 8 | 55% | 20–40 | Never-crash lints, command guard with rollback, recovery copies, 963 tests incl. junk-parameter sweeps and a seeded retained-render walk; but two days old, one release (v0.2.0), no field reports, QA mostly on macOS | [gaps](gaps.md) | +| Performance | 6 | 60% | 30–60 | `cargo xtask bench`: 50k-layer page ~8 ms zoomed out, 10k-layer edits ~0.2 ms, 2,000-instance design system edits 4–64 ms; CPU (tiny-skia) canvas where Figma renders on the GPU; nothing timed against Figma on the same file | [hardware-parity](hardware-parity.md) | +| Hardware | 3 | 60% | 10–21 | Trackpad pinch and HiDPI yes; pen pressure on Windows and from browser styluses (none from macOS/Linux tablets); several windows (screen-reader tree in the main one only); no GPU canvas, no P3 on screen | [hardware-parity](hardware-parity.md) | +| Localization | 3 | 39% | 29–51 | The layer, English, Russian reviewed by VanTan, and machine drafts of Spanish (with a Latin American overlay), French, German, Brazilian Portuguese, Japanese, Korean and Simplified Chinese (none reviewed by a native speaker; every draft covers all current strings), CJK UI text in installed faces loaded on demand (web: only the craft-fonts CJK build); engine messages stay English | [localization-parity](localization-parity.md) | +| Platforms | 3 | 75% | 10–20 | macOS, Windows (x64/x86/arm64), Linux, FreeBSD, web all packaged (v0.2.0); Figma also has iOS/Android apps; non-macOS QA thin | [hardware-parity](hardware-parity.md) | +| Ecosystem/plugins | 3 | 38% | 8–26 | Own WASM plugins with a Rust SDK and three examples (#101), plugin panels and settings (ABI v2), widgets that take clicks (API 3), a self-hosted plugin directory on `figcraft-server`; no Figma plugin compatibility, no public catalogue or community files; the CLI, control channel and MCP server are ahead of Figma for agents | [gaps](gaps.md) | +| AI features | 2 | 0% | 30–60 | None | [gaps](gaps.md) | +| **Ready for real work** | 100 | **~53%** | **~262–519** | | | + +Dimension hours add up to the full-parity total without double counting: the feature-area rows +for import/export (14), multiplayer and team libraries (17, 19), plugins (18) and AI (21) are +counted under their dimensions. + +## Essentials user + +Someone who uses only the core of Figma: a casual or beginner user making a few screens, a social +graphic or a simple mockup. Same method, with the core features a casual user touches. + +**1. Core features, weights and depth** (depth from the area table and checklists above): + +| Feature | Weight | Depth | Notes | +|---|---|---|---| +| Create, open, save and reopen a file; pages | 10 | 85% | home screen, `.figcraft` round trips, Open recent | +| Pan and zoom | 10 | 80% | wheel, pinch, Space-drag, zoom shortcuts | +| Select, move, resize, rotate, align, group, duplicate | 15 | 75% | not yet compared tool by tool | +| Frames with device presets | 8 | 80% | | +| Rectangle, ellipse, line, arrow, corner radius | 8 | 80% | | +| Text: type, font, size, weight, colour, alignment | 12 | 70% | no spell check | +| Colour picker, fills and strokes; place and crop images | 10 | 72% | no HEIC images | +| Layers panel: rename, hide, lock, reorder | 7 | 80% | | +| Basic auto layout (add, gap, padding) | 6 | 70% | | +| Undo / redo | 5 | 90% | unlimited, one step per gesture | +| Export PNG, JPG, SVG, PDF | 6 | 85% | | +| Copy and paste, incl. images from other apps | 3 | 75% | | +| **Weighted** | 100 | **77.8%** | | + +Left out: components and variables beyond using one, prototyping, Dev Mode, advanced vector and +type, and everything the mainstream number leaves out. + +**2. Discounts a casual user actually hits:** + +| Discount | Factor | Evidence | +|---|---|---| +| Launch and stability | ×0.92 | signed and notarized macOS build, installers for every platform (v0.2.0), never-crash rules and recovery copies; no field record yet | +| Discoverability and UI clarity | ×0.94 | the UI3 layout Figma users know, Actions palette, keyboard shortcuts panel; a first run opens on the home screen with a Getting started card (six tips with their shortcuts, closable, back from the Help menu) and eight starter templates to open (also File ▸ New from template…); no Community files, no guided tour, and nobody new to the app has been watched using it | +| Opening files people send them | ×0.88 | what people send a Figma user is a Figma link or a `.fig`; a `.fig` opens (best effort, not yet checked on real files), a link doesn't. Images and SVG do | + +77.8% × 0.92 × 0.94 × 0.88 = **~59%** (estimated). User evidence: none yet (see above). + +## Mainstream practitioner + +The same question for the typical professional Figma Design user: a product designer who designs +screens, maintains or uses a design system, prototypes and hands off to developers every week. +Computed per craftrules `standards/progress-docs.md`. + +**1. Areas used weekly, with weights.** Feature areas 1–16 of the table above, with their weights +(sum 94). Left out as the standard says: plugins and widgets (18), AI (21), team and organization +libraries (19, cloud/team admin), Figma Draw extras (20, not weekly for a product designer), +localization beyond English, specialist hardware. Real-time multiplayer (17) is cloud collaboration +and is left out as an area, but its effect on working with Figma users is counted in discount 3. + +**2. Average depth** ("Ready" column, weighted): **72.7%**. + +**3. Discounts for what still stops real work:** + +| Discount | Factor | Evidence | +|---|---|---| +| Interaction fidelity | ×0.90 | UI3 look compared side by side (~65% by the builders); no tool-by-tool comparison of cursors, modifier keys while dragging, snapping thresholds or popovers yet; Ctrl to suspend snapping on macOS missing ([ui-parity](ui-parity.md)) | +| Stability on real machines | ×0.90 | Never-crash lints, command guard with rollback, recovery copies and 963 tests, but no field record at all: the repository is private, v0.2.0 has had no outside users or issue reports, CI tests run on Linux only and hands-on QA is on macOS. Harsher than VectorCraft's ×0.92 because there is no outside evidence either way | +| File exchange with Figma users | ×0.85 | A Figma designer's work lives in shared Figma files. FigCraft opens a `.fig` (best effort, not yet checked on real files) and writes an experimental one (written files open and draw in other software; effects, images and prototypes unchecked there); incoming work can come through the REST API JSON (one way, needs an access token), outgoing only as SVG, PDF, PNG or code. Teammates, reviewers and developers would all need FigCraft ([file-format-parity](file-format-parity.md), [collaboration-parity](collaboration-parity.md)) | + +72.7% × 0.90 × 0.90 × 0.85 = **~50%** (estimated; 50.03%, rounded down). + +**4. User evidence (2026-10-10):** 0 issues, 0 praise or "switched from Figma" reports, 0 comments +from anyone but the maintainers (`gh issue list`, `gh api .../issues/comments`; the repository is +private, 1 star, PRs only from maintainers). No outside evidence either way; nothing raises the +number. + +**Why it is lower than ready for real work (~53%), unusually.** The full number averages +dimensions additively, so the missing `.fig` exchange costs only the file-format dimension's 12% +weight, and FigCraft earns points the mainstream number doesn't count (platforms 75%: Linux, +FreeBSD and the web, where Figma has no desktop app). The mainstream number applies file exchange +as a multiplier on everything, because a Figma practitioner's whole working week is shared through +Figma files. Both are computed from written weights; neither was moved to match the other. The +mainstream number rises fastest with `.fig` import and the experimental `.fig` export checked on real files (the ×0.85 toward ×0.95: ~53%) and the +interaction pass. + +## How the hours are calibrated + +From this repository's own history (`git log`, `gh pr list`): + +- The whole app was built in ~25 wall-clock hours: first commit 2026-10-09 15:38 UTC, PR #99 + merged 2026-10-10 16:53 UTC; ~49k lines on day 1 before PRs, then 99 PRs adding ~81k lines. The + ROADMAP of that period records 4–6 agents in parallel, so ~110–150 agent-hours in all: about + **1 agent-hour per ~1,000 added lines**, and a typical feature PR (1,000–2,500 lines, e.g. + #84 arc handles, #92 PDF print options, #96 accessibility) took **1–2.5 agent-hours**. +- Between the ROADMAP revisions of 2026-10-09 22:19 UTC and 2026-10-10 16:53 UTC (~18.5 h wall, + 78 PRs, ~75–110 agent-hours) the self-graded depth went 58% → 79%: **~4–5 agent-hours per + self-graded point**. This re-measure grades about 0.6 points for each of those, which puts an + honest point nearer 7 agent-hours in the polish tail; new subsystems (a `.fig` reader, + multiplayer, a plugin runtime) are estimated bottom-up instead, since nothing comparable has been + built here yet. +- Parallelism: most of the work splits across 4–6 agents on disjoint crates (the history shows it + working); multiplayer, `.fig` import and the plugin runtime each need one owner for their core. + Needs a human: native-speaker review for every language, the plugin-runtime and AI-model + decisions, real-world Figma files to test with (licensing), Windows/Linux/screen-reader QA. + +## Area detail: what has landed and what is left + +Carried over from the ROADMAP's "Where we're lacking" list (2026-10-10, before this re-measure). +Hours in this narrative are the builders' estimates; the tables above supersede them. + +1. **Vector edit mode**: *landed:* edit-mode toolbar (move, pen, bend, paint bucket, lasso, cut), + pen that extends, branches and closes paths (vector networks), paint bucket fill/unfill of any + closed area (faces with holes), point/segment selection, handle mirroring (none/angle/angle and + length) with a Point panel (position, corner radius, mirroring), smooth/sharp points, join and + merge, delete vs delete-and-heal, nudging, one undo step per gesture, eraser (⇧E), shape builder + (⇧M: the regions of overlapping shapes, click or drag to merge, Alt to remove, curves kept), + variable width strokes (width profile presets with previews in the stroke settings, the Variable + width tool ⇧W: drag the path to add a width point, slide points, drag their handles, Delete; + smooth interpolation per subpath, dashes, caps and joins, canvas/PNG/SVG/PDF and outline stroke), + a transform box around several selected points (corners and edges scale them, Shift keeps + proportions, Alt from the centre; just outside a corner rotates, Shift in 15° steps; drag inside + to move; the Design panel's X/Y/W/H and flip/rotate act on the points), point snapping while + dragging points or handles and with both pens (to the points and segment midpoints of this and + other vectors, the usual layer targets and the pixel grid, with guides and a marker on the point + landed on; Ctrl/Cmd turns it off), arc handles on the canvas (sweep, start and ratio with a + live readout, Shift snapping, closing at the start, the other part from the gap; rotated and + flipped ellipses, any zoom), Design panel Arc fields (Start, Sweep, Ratio; bindable), live + boolean editing (operands selected by double-click or Cmd/Ctrl-click; moving, resizing or + editing an operand's points redraws the result during the drag with every operand outlined; + per-operand operations from the boolean controls; only the operands from the edited one up are + recombined; clicks hit a boolean group where its result is), the shape builder on strokes and open paths (strokes as their outlined areas), and + one-sided variable widths (Alt-drag a width handle; canvas, exports and outline stroke), and + filled regions with the Move tool (the area under the pointer hatched, clear of points and + segments, its spacing by octave folding fitted to measurements at several zoom levels and fixed + to the shape; click, Shift-click and drag to select and move areas with their points). + *Left:* vector QA with real files. 1–2 h. +2. **Text editing depth**: *landed:* OpenType features panel, small caps, lists, title case, + truncation/max lines, font picker with previews and recents, word/line selection, IME commit, + inline IME composition (underlined at the caret, reflowing, candidate window at the caret; the + document only gets the committed text), variable fonts (axes and named instances read from the + font, named instances as styles, Type settings ▸ Variable sliders, per-range axis values applied + to shaping, outlines and PDF), range-aware Typography panel with "Mixed". Range fills in the Fill + section. Right-to-left and mixed-direction text (Unicode bidi: Arabic shaping, mirrored brackets, + caret, clicks and selection follow the runs; arrow keys move visually). Right-to-left and + mixed-direction names in the panels (layers, pages, Assets, tabs, the file header, canvas frame + titles: visual order, joined Arabic, the ellipsis at a right-to-left name's left end; rename fields + whose caret, clicks, drags and arrow keys follow the shown text; system fonts for scripts the UI + fonts lack). The text style browser in the Typography section (list and grid views in each + style's 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, "Mixed" when the + chars use several styles); text styles link chars, not just layers (editing what a style sets + unlinks the chars edited). Live previews of a hovered named instance or font style and of a + dragged axis slider (one undo step on release, Escape cancels). Visual QA: list markers keep the + paragraph's font, renames start with the name selected and centred, Max lines shows while + truncating. Right-to-left paragraphs mirror lists and indents: bullets and numbers (".1") on + the right, wrapped lines hanging from the right, the paragraph indent on the right, trailing + spaces hanging outside the line (canvas, PNG, PDF and SVG). A behaviour suite from the public + documentation (`crates/text/src/behaviour_tests.rs`, `crates/engine/src/text_behaviour_tests.rs`): + Auto / % / px line heights with the leading split above and below every line, letter spacing in + % and px, paragraph spacing and indent (start-aligned text only), resizing and truncation, + decorations from the font's metrics; CJK line breaking (between ideographs, kana and Hangul, no + break inside Latin words, kinsoku for closing and opening punctuation, small kana and the + prolonged sound mark); a style's metrics from its own font, not a fallback; carets, Delete and + Backspace by grapheme cluster; double-click and word moves by script run; IME composition with + CJK; vertical trim (cap height to baseline). The tail suite (`crates/text/src/tail_tests.rs`, + `crates/engine/src/text_tail_tests.rs`): lists per paragraph, nested up to 8 levels with Tab and + Shift+Tab (1. a. i. and • ◦ ▪ per level, counts restarting after plain paragraphs), Enter on an + empty item and Backspace at an item's start stepping out, `- `/`* `/`1. ` starting a list; list + spacing between items; hanging lists and hanging punctuation; decoration style (solid, dotted, + wavy), thickness, underline offset, skip ink and colour, with inspect, CSS and the REST import. + *Left:* visual QA; hanging punctuation in right-to-left paragraphs and centred text, list + numbering that continues across a plain paragraph and a list's own start number (documentation + silent); decoration colour in SVG exports (drawn with the text's fill, like run fills); Korean + keeps breaking by syllable and Han runs aren't split by a dictionary (documentation silent). 0.5 h. +3. **Auto layout and sections depth**: *landed:* on-canvas padding/gap handles (also wrapped-row + and grid column/row gaps, grid tracks outlined), min/max width/height (UI, Hug/Fill/Fixed, + hugging text wraps at its max), ignore auto layout (absolute position) with constraints, text + baseline alignment, grid tracks (fixed/hug/`fr`), grid cells and spans, wrap row gap UI, + wrap-aware drag-reorder (by row) and dragging into a grid cell, nested hug/fill settling in one + pass (wrapping fill text, three levels deep, grid rows), min/max redistribution, coherent + Fill/Hug rules, drag across nested auto layout frames (deepest one wins, insertion line, out to + the canvas or a plain frame). Arrow keys move auto layout children one place (along the flow + past hidden and absolute siblings, Up/Down between wrapped rows, to the neighbouring grid cell + (automatically placed children swap, anchored ones take or swap cells), and at the end of a + flow or across a row's axis into the neighbouring nested auto layout frame when the frame + around flows that way; one undo step, none when nothing can move). Visual QA of nested + hug/fill/fixed/min/max/wrap/grid samples against the documented rules (Fill turns a hugging + parent Fixed, min/max redistribution, wrap inside fill, absolute children following + constraints as hug frames grow). Sections: hide contents (name, outline and "Contents hidden" + on the canvas; contents skipped by rendering, hit testing and labels; dimmed in the layers + panel; Design panel checkbox and context menu), Wrap in new section, frames in sections get + canvas labels and are top-level frames (flows start there, navigation, Export frames to PDF), + "Navigate to" a section shows its frame shown last, else its first; ready for dev turns + "Changed" when the contents are edited, resolved with a reason; Compare changes shows what a + changed frame or section looked like when it was marked next to how it is now, with the + changed layers and their property changes. *Left:* whether `fr` rows of a fixed-height grid + should make room for fixed-size children (now `minmax(0, 1fr)`-like). 1–2 h. +4. **Components depth**: *landed:* component properties (edit/delete/bind) with the properties + panel, add variant, instance swap UI, per-layer overrides with reset, restore main, instance + structure protection; Assets panel with live thumbnails, search and drag-to-canvas (drops into + the frame or auto layout slot under the pointer); exposed nested instances (their properties + show on the main's instances); component descriptions (edited on the main, shown on + instances); local libraries (publish a file's components, sets and styles with per-asset + versions, enable library files, place library components and apply library styles as + read-only linked copies, "Library updates available" with overrides kept on update); slots + (a frame of a main made a slot property with preferred components; instances fill it with any + content of their own, kept across main updates, variant switches, copy/paste and detach, with + the main's default content until edited; reset slot; slot badges in the layers panel and on the + canvas; fill menu in the properties panel; drops from the Assets panel and the canvas land in + slots); nested overrides kept when the main moves or swaps a nested instance, and on nested + instances copied out of an instance (re-linked to their own main); canvas drags can no longer + pull layers out of an instance; library variables (publishing includes the file's variable + collections with their modes, aliases and scopes, versioned per variable and collection, without + variables hidden from publishing; files using the library see its collections read-only under + the library's name in the Variables editor and the variable pickers; binding, aliasing or setting + a library collection's mode on a frame copies the variables in as read-only linked copies, so the + file renders without the library; library components keep their variable bindings and modes; + "Library updates available" covers changed variables and bound layers follow). Explicit variable + modes in the Design panel: a frame's mode per collection, Auto (inherited) or its own, with + enabled libraries' collections listed under the library's name (picking a mode copies the + collection in). Instance boolean and text properties bound to boolean and string variables + (following modes, renames, reset and detach); layout grid settings in the Design panel whose + count, size, margin/offset and gutter bind to number variables; fills, strokes, effects and + layout grids reordered by dragging their rows, their variable bindings following. Components at + scale, QA'd with a synthetic design system (about 300 components: buttons, inputs, cards, navs + and badges as sets of size × state × type × theme variants, boolean/text/instance swap/slot + properties, instances nested four deep, text and paint styles, light/dark variables bound across + it) used 2,000 times with overrides at every level: main edits keep every override and match a + full rebuild exactly; values from variables (a theme switch, a bound corner radius) are no longer + recorded as overrides; detaching a variable on an instance layer is an override that survives main + edits; exposed nested instances stay exposed and swapped nested instances keep their layer names + through main edits; layers a swapped nested instance's main adds keep their ids and overrides. + Instance swap properties have preferred values (offered first). Alt-drop from the Assets panel + onto an instance swaps it (same-set instance preferred, highlighted while dragging); instances + pasted into other files follow their mains (library copies shared, published components used + from their library, other mains left behind as a restorable missing main). The Assets panel lays + out only the tiles in view and renders thumbnails as they show (cached, re-rendered only for + changed components). A behaviour suite from the public documentation checks overrides (what an + instance takes, what it refuses: geometry, constraints and resizing of its layers, its auto + layout direction), variant switches (overrides stay where both variants agreed; text layers keep + theirs), swaps (by layer name; from the Assets panel only text overrides), the top-left default + variant, detach, reset and push. *Left:* QA with real-world (not synthetic) libraries. 1 h. +5. **Prototyping depth**: *landed:* on-canvas connections with drag-to-connect, interaction editor, + flows, smart animate, overlays, scroll/fixed/sticky, change-to and set-variable in the presenter. + Custom bezier and custom spring easings with editor fields; device frames (phone, tablet, + laptop, desktop bodies drawn in code) around the presented frame; overlays animate out with + their opening transition reversed; several selected layers edit their interactions together + (or replace mixed ones); scrolling inside device frames (the frame sized to the device's screen, + clipped or scrolling by wheel and drag, fixed layers pinned to the screen by their constraints, + nested "Scroll to", preserve scroll position); a resizable preview pane over the canvas that + plays the selected frame and follows edits live; device rotation (phones and tablets turn to + landscape: saved in the file, imported from REST files, drawn turned in the presenter and the + pane); presentation options (fit, fit width, fill, 100%, with Z to cycle them and R to + restart, the device frame shown or hidden, kept between presentations; what overflows the + window pans); scroll momentum (drags fling and slow down the same at any frame rate, the wheel + coasts, a drag past the ends stretches and springs back); responsive presentation (the presented + frame resized to the device's screen, or the window without a device, its constraints and auto + layout responding live; in the Options menu and the Prototype panel, saved in the file); device + colours (each model's colour variants, bodies drawn in code) and custom device sizes (a bare + screen with width and height fields, imported from REST files); flow descriptions in the + presenter's flow picker and under each flow; conditional actions (if / else if / else over + variable expressions) and set-variable-mode actions run in the presenter (from REST files or + commands). Conditionals are built in the interaction editor: an interaction's actions are a list + (add, reorder by dragging a row's grip, also inside conditional blocks, or from its menu, remove, + open one to edit it; one undo step per move), "Add action ▸ Conditional" adds an if whose + condition comes from a builder (a variable from the variable picker, an operator, a value or + another variable) or is typed as an expression (`count >= 3 and not done`, checked when entered, + what's wrong shown under the field), each block with its own actions (the same editors, + nested), plus "Else if" and "Else"; set variable values can be expressions (`count + 1`, + `"Hi " + name`); expressions read a variable's value in a given mode of its collection + (`accent[Dark]`, also from REST files' variable mode lookups) and the mode a collection is in + where the interaction runs (`mode(Theme) == "Dark"`, following set variable mode actions); every + edit is an undo step (`proto.action.*`, `proto.conditional.*`). Hotspot hints: a click where + nothing reacts flashes the screen's hotspots (an Options menu toggle, on by default); the + presenter's Inspect panel shows the hovered layer's properties. Overlays opened through commands + close on a click outside unless told not to, as in the editor. A behaviour suite written from + the public documentation covers triggers, actions, overlays, animations, easing, scrolling and + interactive components ([prototyping-parity](prototyping-parity.md)); its fixes: drag-driven + animations go their way and follow the pointer, key combinations, overlay timers from when they + open, instances play their variant's interactions, physical springs, smart animate overshoot, + fixed layers on top; scroll to takes an offset. *Left:* QA against larger real-world + prototypes. 1–2 h. +6. **Interop**: *landed:* image crop handles; REST API file JSON import (`file.open` / `file.import` + / CLI: pages, layer tree, geometry, paints, strokes, effects, text runs, vectors from geometry + paths, auto layout, constraints, grids, components, sets, instances with overrides, component + properties, styles; image fills from an `images` folder or placeholders; warnings for the rest), + now with prototyping (interactions with their triggers, actions, transitions and easing, flows, + scrolling, the prototype device; conditional actions (if / else if / else over expressions), + set variable with variable references and expressions, set variable mode, all run by the + presenter) and variables (the local variables JSON passed along, next to the file or embedded: + collections, modes, aliases, scopes, layer bindings and explicit modes). + Export panel: a row per export setting (typed or preset sizes like `2x` / `512w` / `512h`, PNG, + JPG, WebP, SVG and PDF, suffix, advanced options: contents only, include bounding box, and for + SVG outline text, id attributes and simplified strokes), a preview, and "Export N layers" (`file.exportLayers`) in the + panel and the Export dialog. Paste from other apps: images, SVG and text (the newest clipboard + content wins over our own copy; pasted SVG lands centred in the view). SVG import is editable and + checked against a corpus of synthetic files (expected pixels worked out by hand, layer structure, + export and re-import): groups named from their ids with transforms baked into the layers; + rectangles (corner radius), ellipses and lines kept as those shapes; gradients with exact + handles, `reflect`/`repeat` and focal points; patterns as image fills; clip paths and masks as + vector, luminance and alpha masks; shadow and blur filter chains as drop shadows, inner shadows + and layer blurs; blend modes; text as text layers with runs, anchors and stacked lines; images + and nested SVG images; import warnings for what's left out; linked files never read; hostile + input fuzzed. SVG export keeps alpha and luminance masks. PDF export: one page per frame in points (Export frames to + PDF, the Export panel, `figcraft-cli export -o x.pdf`), vectors, gradients and strokes as + vectors, real selectable text in subset fonts with clickable hyperlinks, images embedded once, + opacity and blend modes, clip content, alpha/luminance/vector masks, shadows and layer and + background blurs as images; checked by rendering the PDFs back against PNG exports. PDF print + options in the Export panel (per PDF export setting), the Export dialog, `file.export`, + `file.exportLayers`, `file.exportFramesPdf` and `figcraft-cli export`: a bleed showing what lies + past the frame (its fills reach out, its children are clipped at the bleed), crop and bleed + marks outside it, MediaBox / BleedBox / TrimBox, and CMYK colour (a documented, deterministic + naive conversion; images and rasterised effects too). + *Left:* .fig import (best effort), a CMYK output intent (no CMYK ICC profile can ship in the + repository), SVG filters other than shadows and blurs (left out with a warning). 8–16 h. +7. **Look & feel and interaction-fidelity pass**: *landed:* keyboard shortcuts audited against the + public shortcut reference: one keymap (`figcraft_engine::keymap`) drives the menus, the canvas, + the control channel and MCP, with macOS and Windows/Linux keys where they differ (mask, bring to + front/back, distribute, tidy up, layout grids, pixel preview), Text-only and canvas-only bindings + (Cmd/Ctrl+K: link while typing, Actions on the canvas), Cmd/Ctrl+/ for Actions, `+`/`-` zoom, + Shift+0/1/2 from any layout, Cmd/Ctrl+0, hold Z for the zoom tool (Alt zooms out, drag an area), + two quick digits for an exact opacity, boolean keys (Alt+Shift+U/S/I/E), remove fill/stroke + (Alt+/, Shift+/), outlines on Shift+Cmd+O, pixel grid on Shift+' (and Cmd+'), Shift+E / Shift+D and the other panel keys, Ctrl+Tab between files, Shift+B (and B) for the paint bucket in vector edit mode; the Keyboard + shortcuts panel docks at the bottom with a tab per group and search, keycaps per platform. + A shortcut parity suite (`shortcut_parity_tests.rs`) checks every documented shortcut on both + platforms (129 rows: 128 bound, including moving the rotation origin with Alt+R and placing an + object with Enter from the toolbar; 1 moved off a documented conflict) and pins the 48 undocumented + bindings; others' cursors (`Alt+Cmd+\`), list levels while editing (Cmd/Ctrl+] and [), the + keyboard box selection on Alt+Space / Ctrl+Space, arrows that pan with nothing selected, + Control to suspend snapping on macOS and Space/N/C/F while presenting landed with it + ([ui-parity](ui-parity.md#keyboard-shortcuts)). + *Left:* tool by tool, panel by panel against the incumbent (cursors, modifier keys while + dragging, hover states, menus, popovers); per-keyboard-layout shortcut tables. 40–60 h. +8. **Collaboration (local):** comments landed: pins pinned to frames and layers that move with them + (to other pages too) and stay where they were when the layer is deleted, your pins dragged onto + another frame, crowded pins clustered when zoomed out (a click zooms them apart), Shift+C to show + or hide them; threads where you edit and delete your own messages, emoji reactions, @mentions + with a picker of the names used in the file (your name from Preferences), read and unread state, + resolve, and "Copy link" (`figcraft://comment/`: pasting or `comment.open` goes to the page, + zooms to the frame and opens the thread); the Comments panel with search (text, names, frame), + filters (open, resolved, yours, unread), sorting by date or page, each thread's frame and page, + and Up/Down to step through threads; comments on the presented frame (Comments button or + Shift+C: read and reply). All of it through `comment.*` commands. Version history + landed (named versions saved in the `.figcraft` file; restore, rename, duplicate, delete; panel; + view a version on the canvas read-only before restoring it; autosave checkpoints every 30 + minutes, folded in the panel). A behaviour suite written from the public documentation covers + both: restoring keeps the comments and first keeps the replaced design as an autosave, a titled + autosave becomes a named version, merging a branch records the main file before and after, and + threads made in a live session can be replied to and resolved. Real-time collaboration is designed (`docs/collab-protocol.md`: + WebSocket sessions the app can host itself, server-ordered per-property changes, live cursors) + and its core landed (`crates/collab`, no networking yet): client-scoped node ids, every saved + layer field and the document-level data (styles, variables, comments, images…) as synced + properties, diffs after a command that cost the size of the edit, fractional sibling positions, + a session server that orders and validates batches (cycles, deleted layers, id spaces, sizes) + and a client that shows its own edits at once and replays them over others'; derived data + (auto layout, instances, bindings) is repaired by the author after concurrent edits; binary + messages (a moved layer is 43 bytes, a cursor 23) with a JSON form; a randomized multi-client + test over real commands converges. Live sessions run over the network (hosting from the app, + invite links, document sync, live cursors, selections, avatars, follow). Undo is per person in + a session (`edit.undo` takes back only your own steps, leaving what others changed since; + creations and deletions invert; a gesture or command group stays one step); refused edits roll + back with a toast; a dropped connection keeps editing and resumes as the same client with what + it missed (or a snapshot), then sends what was made offline. Randomized sessions with undo, + redo and dropped connections converge, and a checker confirms no undo reverts someone else's + change. + Plugins landed: sandboxed WebAssembly plugins (a metered wasmi + interpreter with a memory cap and no WASI, also in the browser) whose API is the command + registry; each run is one undo step and is rolled back when it fails (error, trap, endless loop, + memory); a `figcraft-plugin.json` manifest declares menu commands with parameters and read or + write access with an optional command allowlist, and files, other documents, the undo history, + dialogs and other plugins are always off limits. Plugins ▸ Development ▸ Import plugin from + manifest… (kept in the preferences, the module re-read on every run), the installed plugins in + the Plugins menu with a parameters form drawn from the manifest, Run last plugin (⌥⌘P), Manage + plugins, and `plugin.*` commands for scripts, the CLI and MCP; a Rust SDK and two example + plugins (Tidy names, Swatch grid) in `plugins/`, built by `cargo xtask plugins`. Then plugin + panels (ABI v2: fields and buttons drawn with FigCraft's widgets, the plugin called back for + each button or changed value as one undo step, `plugin.ui` / `plugin.event` / `plugin.close` + for scripts and MCP) and per-plugin settings kept with the preferences. Then widgets (API 3: + a plugin describes a layer of its own, validated and built as a frame with locked layers; clicks + on its named parts call the plugin back in a fresh sandbox with the state kept in the layer, one + undo step each; `widget.insert` / `widget.click` / `widget.list`; a Counter example), panels of + several plugins side by side and the colour picker on plugin colour fields. Then a plugin + directory on `figcraft-server`: bundles published with the server key and checked in full, + browsed and installed from Plugins ▸ Manage plugins after a permissions confirmation, downloads + checked against their SHA-256, updates by version. *Left:* a public catalogue, publisher + accounts and signatures; richer widget events; the directory in the browser. 8–26 h. +9. **Hardening and performance**: *landed:* `cargo xtask bench` (synthetic 1k/10k/50k-layer + pages: engine, render and headless UI-frame timings). The canvas keeps its last image and + re-renders only what a pan uncovers or an edit touches: moving one layer at 10k layers re-renders + in ~0.2 ms instead of ~8 ms (0.8 vs 24 ms at 50k zoomed out), a 40 px pan in ~0.7 ms instead of + ~7.5 ms. The layers panel lays out only the rows in view (every screen expanded at 50k: 39 → + 0.6 ms a frame); text layouts are shared instead of re-formatted per lookup; overflowing text is + culled by its glyphs. At 10k: full 1440×900 renders 7–15 ms, single-layer edits ~10 µs, hit + tests ~26 µs. Zoomed out (50% and below) the canvas draws levels of detail: layers under 2 px + land as their averaged colour, text under 4 px is greeked into bars, small rectangles and clip + layers skip the path pipeline (exports and closer zooms stay exact): a whole 50k-layer page + renders in ~8 ms instead of ~50 ms. `.figcraft` at 50k layers saves in ~0.09 s instead of ~1 s + and loads in ~0.16 s instead of ~0.86 s (JSON streamed to and from the document without a + value tree, layer fields at their defaults left out: 63 → 22 MB of JSON, old files still + open). The retained canvas is held to a full render: a seeded random walk (edits, undo/redo, + pans and zooms over nested, rotated and clipping frames, groups, masks, booleans, auto layout, + effects, text, instances, variables and styles) checks every step, and found clip edges on a + pixel boundary letting a column through in some renders. Drags show live on the canvas (every + finished render is shown, not only the newest one asked for), and a button held still no + longer rebuilds the preview and re-renders every frame. + Components at scale (`cargo xtask bench`, synthetic design system of about 300 + components used by 2,000 instances): edits of mains reach their copies directly instead of + rebuilding every instance (value, size and layer added/removed edits; other edits rebuild each + affected instance once, inner mains first), bound variables no longer touch unchanged layers on + every edit, auto layout re-flows only where sizes or places changed: recolouring or resizing a + main's layer used ~900 times 188/226 → 4/18 ms, adding a layer to it 430 → 21 ms, a label style + in all 72 button variants (1,800 copies) 450 → 57 ms, switching a variant, reset, detach, push + and paste ~450 → under 3 ms, accepting a library update 460 → 64 ms; the Assets panel with 2,000 + assets 3.4 → 1.3 ms a frame. Accessibility: the custom-painted + controls name themselves for screen readers (AccessKit on desktop: role, name, value and state + for tools, layer rows with their type, level, selection and hidden/locked state, fields, + dropdowns, tabs, menu items, and the canvas, which describes the selection); keyboard navigation: + F6 / Shift+F6 move between the toolbar, layers, canvas and properties, Tab / Shift+Tab move + within a region with a focus ring, Enter or Space activate, the arrows go along the toolbar and + through the layer rows, menus and tool flyouts opened from the keyboard take it and give it back, + Escape returns to the canvas (where Tab selects the next layer again instead of wandering into + the tab strip); reduced motion follows the system's setting or a preference; secondary and + accent text meet WCAG AA in both themes. + An idle window draws no frames: it redrew ~240 times a second + (the window title was re-sent every frame, and each viewport command asks for another), ~33% of + a core in a debug build, and now sits at 0 frames and ~0% CPU; the toast, the autosave + checkpoint and the presenter's "after delay" timers wake it at their deadline, the interaction + editor's curve preview plays when it shows, changes or is hovered, and a screenshot keeps frames + coming only until it arrives (`ui.inspect` `framesDrawn` and `repaintCauses` show what asked for + frames; headless tests hold it). *Left:* a real-world file corpus, Windows/Linux/browser + QA, runs with real screen readers (NVDA, Narrator, VoiceOver, Orca), the AccessKit tree in the + browser (eframe's web backend doesn't send it yet). Landed since: F2 rename from the keyboard, + Shift+F10 context menus at the focused layer row or the canvas selection, keyboard box selection + (F8). 27–44 h. + +## Inventory: what exists today + +Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count there was stale: 963 `#[test]` functions as of this re-measure. + +- **Architecture:** 15 crates with enforced layering (`cargo xtask layers`): geom, color, doc, + layout, text, pathops, plugin, collab, render, format, svg, engine, ui-egui (+ mcp, pdf). Every action + is a command (449 command ids); undo is unlimited via structural sharing (`Arc` node snapshots); gestures are + one undo step; `command.batch` runs several commands as one step. +- **Document model:** pages, frames, groups, sections, rectangles, ellipses (arcs/rings), polygons, + stars, lines/arrows, vector networks, text with styled runs, boolean groups, components, component + sets (variants), instances with per-layer overrides, slices; fills (solid, linear/radial/angular/ + diamond gradients, images with fill/fit/crop/tile + adjustments), multiple strokes with + inside/center/outside alignment, per-side weights, dashes, caps/arrowheads, joins; effects (drop + and inner shadow with spread that offsets the outline exactly: ellipses, stars, vectors, text; + inner shadows over the fills and under the children and strokes; drop shadows hidden behind what + the layer covers, translucent fills included, unless shown behind transparent areas; layer blur; background blur + that sees the whole backdrop, inside clipping frames, faded groups and masks too; both blurs + uniform or progressive (the radius ramps between two points, dragged on the canvas); noise in + mono, duo and multi colour with grain size, density and blend mode; texture with clip to shape; + glass with light angle and intensity, refraction, depth, dispersion, frost and splay; the same in + PNG, PDF and SVG exports, SVG embedding renders of noise grains, texture, progressive and + background blur and glass); corner radii with + smoothing; constraints; auto layout (horizontal, vertical, wrap, grid; hug/fill/fixed; min/max; + absolute children); layout grids; export settings; prototype interactions, flows and transitions; + paint/text/effect/grid styles; variables with collections, modes and aliases. +- **Renderer:** CPU (tiny-skia) scene renderer used for canvas, export and the web: isolated layers + for opacity/blend/effects, clip content, masks, booleans (linesweeper), text (harfrust shaping, + skrifa outlines, font fallback), aligned strokes with caching. +- **Engine:** pointer tools (move/marquee/deep select/resize/rotate/Alt-duplicate with smart-guide + snapping, frame/section/slice/shapes/line/arrow with Shift/Alt, text, pen, pencil, hand, + eyedropper), Cmd/Ctrl-click and Cmd/Ctrl-marquee into nested layers, Shift-marquee toggling, + Select layer (every layer under the click), measuring (Alt-hover distances from the selection to + layers, parents and ruler guides, Cmd+Alt for nested layers, from a guide being dragged to the layers of its frame, extension lines, one box inside the + other, overlaps, live distances to the neighbours while Alt-dragging, Shift+Alt for auto layout + padding and gaps; labels that never overlap at any zoom), text editing (caret, selection, words, + lines), auto layout reorder by dragging, drop into frames, align/distribute/tidy, z-order, + flip/rotate, the scale tool (panel, and canvas handles that scale everything about its anchor + point or from the centre with Alt, with a live factor, one undo step per drag), booleans, flatten, + outline stroke, components (create, instances, sync with overrides, reset, detach, push changes, + variants, properties, slots), styles and variables, prototype commands, find/replace, batch rename, + copy/paste (internal + SVG/text), file new/open/save/import/export (native `.figcraft`, SVG + in/out, PNG/JPG/WebP), version snapshots. +- **UI (egui, swappable):** the incumbent's 2026 layout: tab strip that is also the window's title + bar (frameless on Windows/Linux with drawn window buttons, drag, double-click maximise and edge + resizing; macOS window buttons inset into it), icon rail, file header, pages and + layers tree (rename, visibility/lock, drag to reorder/reparent), Assets panel, floating bottom + toolbar with flyouts and mode switcher, Design / Prototype / Inspect panels (Dev Mode: properties with their styles and variables, annotations + with pinned properties and categories drawn as coloured callouts on the canvas (filtered by category), + a ready-for-dev list, a focus view on one frame with previous / next, Compare changes since a frame + was marked ready (before / after side by side or overlaid, property-level diffs), CSS/HTML/React/SwiftUI/Compose + code where components take their properties as parameters and instances are calls, components + connected to their code components showing the real snippet, read-only plugins), colour picker + (HSB square, hue/alpha, hex, gradient stops, image fills), main menu generated from the catalog, + context menu, Actions palette (⌘K), keyboard shortcuts from one keymap per platform (a tabbed, + searchable Keyboard shortcuts panel), keyboard navigation (F6 between regions, Tab within them, a + focus ring) and screen-reader names for the custom controls (AccessKit), presentation player with + transitions, light and dark themes, home screen, About/Preferences/shortcuts/variables dialogs. +- **Automation:** desktop control channel (`--control `: execute commands, inspect document/UI, + synthetic pointer/keys/typing, menu invocation, screenshots, renders). +- **Tests:** 963 `#[test]` functions (2026-10-10) including junk-parameter sweeps over every command (and every vector-edit command while editing), importer fuzzing and render regressions. + +## Revision history + +| Date | Change | Summary | +|---|---|---| +| 2026-10-15 | minor | M4: Outline stroke preserves even-odd stroke holes. Pixel comparisons cover alignment, dashes, individual side weights, vector paths and stroke-only opacity; native save/reopen, Undo/Redo and instance opacity inheritance/overrides are covered by `outline_stroke`. Existing estimates unchanged; filled-shape compositing needs separate work. | +| 2026-10-14 | minor | Russian catalog (M9), reviewed and approved by VanTan: all 2,165 current entries, Русский in Preferences and regional ru tags. Russian integer plurals use one/few/many; contributor display name VanTan added. Existing estimates unchanged. | +| 2026-10-13 | minor | Vector edit mode regions: the filled area under the pointer is hatched (clear of points and segments, holes left out, spacing by octave folding fitted to measurements at several zoom levels, fixed to the shape); click, Shift-click and drag select and move areas with their points (drawn unselected); a click on an empty spot clears them and stays in edit mode (see [ui-parity](ui-parity.md)) | +| 2026-10-13 | minor | Plugin log limits (#191): cap each UTF-8 line at 8 KiB, total log bytes at 1 MiB and lines at 1,000 per run/callback before copying guest memory or forwarding to the host. Over-budget runs fail and roll back document edits; callbacks get fresh budgets. Added sandbox boundary/forwarding tests and an engine rollback regression. Percentages and hours unchanged. | +| 2026-10-13 | minor | Component playground (M3): with an instance, a component or a component set selected, the Inspect tab shows a Playground: a preview of the component and a control for each variant, boolean and text property. What is tried there changes the preview and the code in the Code section, never the file (no undo step, the canvas instance stays as it is); Reset goes back to the component's or the instance's own values. `component.playground {id?, properties?, language?, path?}` gives the same to scripts: the properties with their values, the code, and a PNG on request (`playground_tests`). Left: instance-swap and slot properties in the playground (the command takes an instance swap; the panel has no control for it), a code connect template's snippet with the tried values. Numbers unchanged. | +| 2026-10-13 | minor | Auto layout, on-canvas handles of automatic gaps (M2): the handle between spread rows or columns (and between items with an auto gap) shows the distance the layout gives the gap instead of the stored value, and dragging it makes the gap a fixed one from there, as typing a gap in the panel does; one undo step (`gap_handle_tests`). Before, the drag changed a value that had no effect. The documentation is silent on dragging an automatic gap. Numbers unchanged. | +| 2026-10-13 | minor | Variables inside styles (M3): a shared style can bind variables to fields of its value (`style.bind {style, field, variable?}`: a paint style's colours and gradient stops, an effect style's colour, radius, spread and offsets, a grid style's size, count, gutter and offset, a text style's font size, line height, letter spacing, family and style). Every layer using the style shows those fields resolved in its own modes and stays linked when a variable or a mode changes; the style keeps them resolved in the default modes. A style made from a layer keeps that layer's bindings; a new value drops bindings to what is gone. The style editor binds from its property rows (paint colour, shadow colour, blur radius, grid size) and detaches from the variable's chip; the style picker marks styles that use variables. `.fig` export and import carry the bindings on the style, REST layers hand theirs to the style they use. Left: binding text style fields and the other effect and grid fields from the editor (commands only), library variables in the picker, a style's library copy. Numbers unchanged. | +| 2026-10-13 | minor | Effects on groups, inner shadows and background blur (M3): a group is no outline of its own, so its inner shadows are cast inwards from around what its layers draw together (their pixels, over them: no shade at a seam between two layers) and its background blur and glass show the backdrop through the outlines of its layers (under them). Canvas and PNG; SVG embeds a group with an inner shadow as a render and its background blur as an image under the layers; PDF places both as images, the blur matching the PNG. Where the group holds one shape the pixels match that shape, or a frame filled like it, carrying the effect (`group_effects_tests` in render, svg and pdf). Offsets and spreads of a group's inner shadow go by whole pixels. Numbers unchanged. | +| 2026-10-13 | minor | Text made by commands on a dark page (M1): in the app, `text.create` and `node.create {type: text}` that name no fill give white text on a dark page, by the same rule as the Text tool (the page's colour decides, also inside a frame); a named `fill`, `fills` or `fillStyle` is kept. Sessions without an interface (CLI, MCP, exports) still make black text everywhere. Numbers unchanged. | +| 2026-10-13 | minor | Frames and groups, a one-step convert (M1): `object.convertToGroup` and `object.convertToFrame` (the Design panel's type menu on a frame or a group) change the layer in place, its layers staying where they are on the page. A frame turned into a group loses what only a frame has (auto layout, with the positions it gave kept and fill children fixed; fills, strokes, corner radius, clipping, layout grids, guides, scrolling) and takes its layers' box; a group turned into a frame keeps its box, with no fill and no clipping. Instances, main components, layers inside instances and empty frames are refused with the reason, all or nothing; one undo step. Numbers unchanged. | +| 2026-10-13 | minor | Prototyping, delays on mouse triggers and manual overlay offsets (M5): mouse enter, leave, down and up wait `delayMs` before their actions run (once while waiting; dropped when the screen or overlay they were on goes away; a pointer that leaves early doesn't cancel, the documentation is silent), and a manual overlay sits at `offset: [x, y]` from the top-left of the layer that opens it (without one, right below it, as before). Delay and X/Y fields in the interaction editor, the player on its own clock, REST import (`delay`, `overlayRelativePosition`), `.fig` import and export (the export's field for mouse delays is unchecked against other software). Numbers unchanged. | +| 2026-10-13 | minor | Auto layout, wrapped-line alignment (M2): a wrapping layout's lines (rows of a horizontal one, columns of a vertical one) are packed where the cross alignment says (start, centre, end; before, they always sat at the start), stretch when every child fills across the flow, or take the free space between them (`contentAlign: spaceBetween`, the Design panel's Row/Column gap mode, the alignment pad lighting the whole cross axis). Read from REST JSON and `.fig`, written to `.fig`, `align-content` in CSS, an instance override with the other alignment settings. 13 hand-computed layout cases and command tests. Numbers unchanged. | +| 2026-10-13 | minor | Starter templates and a first run that explains itself: eight templates built in code through the command layer (mobile app, landing page, dashboard, design system starter, wireframe kit, social post sizes, presentation slides, icon set starter; original and synthetic, deterministic, about 10–190 ms each to build), `template.list` and `file.newFromTemplate` for the app, CLI and MCP, File ▸ New from template…, a Templates tab on the home screen (cards with thumbnails rendered from the documents, search, kind filter, keyboard and screen-reader names) in place of the Community placeholder, a row of templates on Recents while there are no recent files, a Getting started card that can be closed and comes back from Help ▸ Getting started (`help.gettingStarted`), and a first run that opens on the home screen. The Drafts placeholder is gone (the app has no drafts folder). Essentials: discoverability ×0.92 → ×0.94 (templates and first-run tips exist; no Community files, no tour, no user evidence), 77.8% × 0.92 × 0.94 × 0.88 = 59.2%, ~57% → ~59%; essentials hours ~60–110 h → ~55–100 h (the onboarding and templates item is done); menu breadth stays 211/211 (the two new menu items are FigCraft's own and not counted); every other number unchanged | +| 2026-10-12 | minor | Everyday editing partials closed (gap 4): number fields scrub a step a point (×10 with Shift, ×0.1 with Alt, on past the screen edge where the platform reports it), step with the arrow keys by the nudge preferences, take brackets, units and relative edits (`+10`, `*2`, `50%`), commit on Tab and nothing when left untouched, and on Mixed apply relative edits to each layer in one undo step; outline mode's Include hidden layers and Include object bounds (`view.outlinesHiddenLayers`, `view.outlinesObjectBounds`), drawn and clickable; Hide when publishing from the Assets panel (`component.hideWhenPublishing`); a cursor for every tool, modifier and hover target (rotate turned to its corner, pen states, eyedropper, comment, copy, not-allowed), with guides and handles keeping theirs; panel widths with limits, double-click reset and the preferences (`view.panelWidths`); a file or page made in the dark theme has a #1E1E1E page (sessions without an interface keep #F5F5F5) and text drawn on a dark page is white. Menu breadth 211/211. UI/UX fidelity 58% and the interaction pass 40–60 h unchanged at this rounding: the side-by-side comparison is what remains | +| 2026-10-12 | minor | Text metrics, first measured fidelity data (gap 3): our example documents, exported and re-saved by other software, were read back and every text layer's stored box, line heights, baselines and glyph positions compared with ours. Three rules explained every row and were applied: a line's height is rounded to whole pixels (halves up; Auto, % and px alike; the baseline keeps its fraction inside the rounded line); glyph advances and kerning are each rounded to 1/1024 em before scaling by the font size; letter spacing is not added after a line's last visible glyph. Before: 24 of 131 text boxes of the design-system example matched (118 in width, 24 in height); after: 131 of 131, and all 230 self-sizing text boxes across the 7 documents (Inter only, 11–120 px, letter spacing −6% to 8%, one wrapped paragraph). Glyph positions within 0.001 px: 223 of 1,832 → 1,781; the rest are kerning pairs at weights where the installed font files differ slightly, at most 0.05 px. Frames and other layers matching in size and position: 115 of 231 → 208; the other 23 hang on one grid frame whose row positions came back as written. Not measured: other font families, fractional px line heights, centred or right-aligned text with letter spacing, lists, mixed sizes on a line. Wording only; no number moves | +| 2026-10-12 | minor | `.fig` export (gap 1): written files were checked by opening them in other software: layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft; effects such as glass and noise, images and prototype interactions are still unchecked there, and variables are not written. Wording only; no number moves | +| 2026-10-12 | minor | A public `figcraft-server` (open hosting): with `open_publish = true` anyone publishes a file without the server key and gets its edit and view links plus an owner token (128 bits, shown once, stored hashed) that lists, makes and revokes its links, renames it or removes it (`collab.manageFile`); limits per address (connections at once and a minute, messages a second, publishes an hour, files and MiB kept; IPv6 by /64; `X-Forwarded-For` believed only from `trusted_proxy`; tables capped at 10,000 addresses) and for everyone (`max_total_mb`), each refusal saying why; files published without the key expire after `idle_days` unopened (the date shown to everyone in the file); moderation (`files list --sort size|age|idle`, `block-ip`, `reports`, a Report button and `collab.report`); the operator's contact and terms shown in the Share dialog; `GET /healthz` on the same port; `packaging/server/install.sh` for a Debian/Ubuntu machine behind Caddy and a nightly backup timer. Protocol additions within version 2, gated like signals (older apps get clear refusals). Hosting infrastructure rather than a Figma feature: percentages and hours unchanged | +| 2026-10-12 | minor | Experimental `.fig` export (gap 1): the whole document written from the same public format notes as the import, verified only by round trips through our own importer. Import/export breadth 84% → 86%, ready 57% → 58%; file formats 59% → 60%; file exchange with Figma users stays ×0.85 (nothing shows another app opens the files); mainstream hours ~130–244 h → ~120–229 h (`.fig` export 15–30 h → checking it elsewhere 5–15 h); feature breadth (~84%), ready for real work (~53%), essentials (~57%), mainstream (~49%), beta and full parity hours unchanged at this rounding | +| 2026-10-12 | minor | Keyboard shortcut parity suite and bindings (gap 4): a table test with one row per shortcut in the public documentation, macOS and Windows/Linux keys each (129 rows: 126 bound, 110 before; 1 moved off a documented conflict; 2 missing; 2 kept by a platform), plus 48 pinned undocumented bindings so every default binding is in the table; newly bound: A (Frame), Shift+' (pixel grid), Alt+Cmd+\ (others' cursors, new `view.multiplayerCursors`), Ctrl+F6, Shift+Cmd+U (link), Cmd/Ctrl+] and [ (list levels while editing, new `text.listIndent` / `text.listOutdent`), Cmd/Ctrl+Backspace (ungroup), Alt+Space / Ctrl+Space (keyboard box selection, Cmd/Ctrl+arrows draw it out), suggest auto layout, go to main component, arrows that pan with nothing selected (new `view.pan`), Control suspending snapping on macOS, Space/N/C/F while presenting; symbols another layout types with Shift count without it; no default binding uses a system-reserved chord (tested). Menu breadth 207/207 (View ▸ Multiplayer cursors). UI/UX fidelity 58% and the interaction pass 40–60 h unchanged at this rounding | +| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Localization 36% → 39%, 31–54 h → 29–51 h; ready for real work unchanged (~53%); full parity ~267–527 h → ~265–524 h | +| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Localization 35% → 36%, 32–55 h → 31–54 h; ready for real work unchanged (~53%); full parity ~268–528 h → ~267–527 h | +| 2026-10-12 | minor | Prototyping tail (gap 15, the part that doesn't need video): key/gamepad triggers take controller buttons (`Gamepad A` … `Gamepad Home`, the standard layout's 17 buttons, from a Controller menu beside the key field; the REST import maps controller triggers instead of skipping them); the player reads controllers on Windows (Windows.Gaming.Input through the `windows` crate the app already used; no new dependency) and in the browser (the Gamepad API), polling only while the prototype has such a trigger and the window has the focus, and the control channel presses buttons (`ui.input {gamepad}`); macOS, Linux and FreeBSD read no controllers yet. Widgets react to clicks in a presented prototype (the canvas's callback and sandbox limits, on the prototype's copy: the file and its history don't change, restarting starts over). `proto.evaluate` computes an expression where a layer's interaction would run (MCP); the mode lookup and set-variable-mode rules are cases in a behaviour suite (they already held). Prototyping breadth 90% → 92%, ready unchanged at 74%, 3–6 h → 2–5 h; area hours 120–260 → 119–259 (core 33–75 → 32–74), Features (depth) 29–68 h → 28–67 h; percentages unchanged at this rounding; full parity ~276–538 h → ~275–537 h | +| 2026-10-12 | minor | Plugin directory on `figcraft-server` (gap 6): the server keeps plugin bundles (manifest, module, up to 32 assets; an uncompressed length-prefixed format named by its SHA-256) published with the server key (`figcraft-cli plugin publish`, `plugin.directory.publish`), checked in full before they are kept (size cap, manifest and plugin API version, a version newer than the one kept, the module compiled but never run, plain asset names) and capped (`max_plugins`, `max_plugin_mb`, `max_plugins_mb`); anyone who reaches the server lists, searches and fetches them (hash-checked again on read, a shared download budget); the operator moderates with `figcraft-server plugins [remove]`. The app's Plugins ▸ Manage plugins gains a Browse tab: a server's plugins with Install / Update / Remove, an install confirmation that shows the permissions asked for, the source and the SHA-256, the download checked against the listed hash before anything is read, plugins kept per user in hash-named folders; `plugin.directory.list` / `install` (needs `confirm: true`) / `publish` / `unpublish` for scripts and MCP. Not in the browser build yet; no public catalogue, publisher accounts, signatures, ratings or background update checks. Plugins & widgets 55%/32% → 60%/35%, 10–30 h → 8–26 h; Ecosystem dimension 34% → 38%; weighted totals unchanged at this rounding; full parity ~281–548 h → ~279–544 h | +| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Localization 30% → 35% (drafts, not reviewed: graded low), 40–65 h → 32–55 h; ready for real work ~52% → ~53% (52.9 → 53.1 by the dimension table); full parity ~284–554 h → ~276–544 h; beta ~162–292 h → ~155–280 h (Japanese and Korean leave the beta list; their native reviews need people) | +| 2026-10-12 | minor | Plugin widgets and the panel tail (gap 6, plugin API 3; v1 and v2 plugins unchanged): a plugin marked as a widget puts a layer of its own on the canvas from Plugins ▸ Widgets or `widget.insert`: it describes the layer (frames with auto layout, text, rectangles, ellipses, images it carries) and FigCraft validates the description (size, layer count, depth, numbers, colours, text and image sizes) and builds it as a frame with locked layers inside; a click on a part the plugin named (on the canvas with the Move tool, a pointing hand over it, or `widget.click`) calls the plugin back in a fresh, metered sandbox with the widget's state, as one undo step, and the widget is redrawn; the state lives in the widget's own layer data, so widgets survive save and reload, copying, undo, export and live sessions; no plugin can write that data or call `widget.*`. `widget.list` lists them. A Counter example widget. Plugin panels of different plugins open side by side, and a colour field's swatch opens the colour picker (the plugin hears of the colour when a drag ends). Plugins & widgets 45%/28% → 55%/32%, 15–40 h → 10–30 h; Ecosystem dimension 28% → 34%; weighted totals unchanged at this rounding; full parity ~286–558 h → ~281–548 h | +| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): every UI string, command label and menu wording (1,899 entries) machine-drafted in each and marked as drafts awaiting native review, falling back to English per key (new strings need only English and Spanish); 0 counts as singular in French and Brazilian Portuguese; System picks the first OS language with a catalog (pt-PT and pt read pt-BR); long translations elide with a tooltip in dropdowns, two-column labels and the home cards. Localization 20% → 30%, 45–75 h → 40–65 h; ready for real work unchanged (~52%; 52.6 → 52.9); full parity ~289–564 h → ~284–554 h; beta ~170–305 h → ~162–292 h (Figma's languages left on the way to beta: Japanese and Korean, 7–12 h) | +| 2026-10-12 | minor | Cursor chat and spotlight in live sessions: `/` opens a chat bubble at your pointer that everyone sees as you type and that fades a few seconds after the last change; a spotlight (your avatar's menu, `collab.spotlight`) makes everyone follow your view and page, latecomers included, until they move their own. Both are ephemeral presence the host validates and rate-limits, passed only to clients that read them. Multiplayer & sharing breadth 15% → 17%, ready unchanged (5%), 45–90 h → 42–84 h; the Collaboration dimension's hours 55–110 → 52–104 h (the dimension hours still add up to the total); feature breadth (~84%) and ready for real work (~51%) unchanged at this rounding; full parity ~301–587 h → ~298–581 h | +| 2026-10-12 | minor | Layers and Assets panels behaviour suite written from the public documentation (6 engine tests with about 50 table rows and 5 panel tests driven through the UI; about 30 rows that the code before the fixes contradicts) and its fixes: several layers dropped together in the layers panel land together (layers from apart split around the drop point); Collapse layers keeps the selection's rows open (it closed everything); the eye and padlock stay under the pointer (they vanished as it reached them, so clicks could miss) and dragging across them sets several layers in one undo step; several layers selected open a Rename layers dialog (match as text or a regular expression, Rename to, current name and number buttons, a start value, preview) instead of renaming the top one; `object.batchRename` counts `$n` up and `$N` down (they were swapped), pads with `$nnn`, takes `start`, regular expressions with `$1`…`$9`, `` $` `` / `$'` and codes in the replacement; pages are dragged to reorder, a blank page named with a hyphen is a divider (never opened, holds no layers), duplicates are named "Copy of …", the last page that isn't a divider can't be deleted; the Assets panel files components by page, frame and slash names in folders that fold away (`component.list` gives each `folder`); a leading `.` or `_` now keeps a component out of the library even after it was published, and removing it brings the component back. Pinned where the documentation is silent: Alt-click on an arrow, numbering in stacking order, hidden layers and locked frames taking drops, locking deselects. Layers panel 98%/82% → 99%/85%; percentages and full parity hours unchanged at this rounding | +| 2026-10-12 | minor | Vector multi-edit and the vector edit tail, with a behaviour suite written from the public documentation (5 tests, about 30 table rows and steps, and a junk-parameter sweep of every vector command in multi-edit mode): several vector layers are edited at once (Enter, double-click or `vector.edit.begin {ids}`; a press on another layer's point, handle or segment makes it the active layer; Shift-click, marquee and lasso select across layers; dragging, the arrows, Delete, delete and heal, mirroring, smoothing, corner radius and point caps and joins reach every layer's selected points in one undo step; the pen, bend, paint bucket, cut and eraser work in the active layer); several Bezier handles are selected with Shift-click and dragged together (`vector.edit.moveHandles`); a sharp point next to a curved segment rounds (the curve is cut where the arc meets it and keeps its shape); a point can have its own stroke cap and join (`vector.edit.strokeCap`, `vector.edit.strokeJoin`, the Point section of the Design panel; canvas, PNG, SVG and PDF, outline stroke); the Pen outside edit mode connects to one of its own points and ends the path, and continues a selected open path from an end point. Left: corner smoothing beyond rectangles, the transform box around points of several layers, per-point caps and joins on dashed, variable-width and brush strokes. Shapes & vector 98%/72% → 99%/73%, 2–4 h → 1–3 h; Figma Draw extras 80%/55% → 88%/60%, 2–5 h → 1–3 h (gap 9: pen pressure on macOS and Linux tablets left). Totals recomputed by the doc's method, which rows since the last recompute had left stale: the area table's weighted breadth is 83.7% (~83%; core 94.1%, ~94%) and ready 63.1% (~63%; core 72.1%, ~72%), hours 133–284 (core 38–83); Features (depth) 61% → 63%, 46–94 h → 34–76 h; ready for real work 52.6% (~52%); mainstream weekly areas 71.0% → 72.1% (× 0.9 × 0.9 × 0.85 = 49.7%, ~49%, rounded down); essentials unchanged (~57%); full parity ~306–595 h → ~294–572 h (on top of the rows merged since: ~301–587 h → ~289–564 h; area-table totals recomputed on the merged table and rounded down: breadth ~84%, ready ~63%, core ~94%/~72%, mainstream weekly areas 72.4%, ~49%) | +| 2026-10-12 | minor | Dev resources (gap 16): links for developers (a URL and a name) on any layer: `devResource.add` / `edit` / `remove` / `list`, undoable, saved and synced; an instance also lists its main component's (and that component's set's) links read-only with where they come from, and layers inside an instance show their main layer's; URLs are checked as web links (http or https, a host, no spaces, at most 2,048 characters) and never fetched (a link added without a name is named after its URL), opened only when clicked; the Inspect tab lists, adds, edits and removes them; `get_node` and `inspect_document` return them as `devResources`. The editor extension stays out of scope (the MCP server serves agents and editors). Dev Mode breadth 93% → 96%, ready 70% → 72%, 1–3 h → 0–1 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~304–593 h → ~303–591 h | +| 2026-10-12 | minor | Extended collections' tail (gap 14): variables a library adds to a collection this file extends now count as a library update of their own (listed by name without a copy yet) and arrive inheriting when accepted, instead of waiting for the library's next other update; a library collection that is itself an extended collection can be extended again, values flowing down the chain as each file accepts updates with every level's overrides kept (checked end to end). Styles & variables breadth 94% → 95%, ready unchanged (76%), 3–5 h → 2–4 h; gap 14 1–2 h → 0–1 h (real-library checks left); feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~305–594 h → ~304–593 h | +| 2026-10-12 | minor | A branch's own version history and finer list merges (gap 17): versions saved or autosaved on a branch tab go into the branch (viewed, restored, renamed, deleted and duplicated there, saved in the file with it); merging adds a version to the main file's history and the merged or archived branch keeps its own; fills, strokes and effects merge item by item, conflicting only when the same item changed both ways, an item was removed on one side or both sides added different items. Comments, versions, find, misc breadth 85% → 87%, ready 60% → 61%, 2–5 h → 1–4 h; gap 17 1–3 h → 1–2 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~306–595 h → ~305–594 h | +| 2026-10-12 | minor | Headline percentages recomputed from the area table by its own method (weights × written scores): feature breadth ~82% → ~84% (core editor ~93% → ~94%), weighted ready ~61% → ~63%, core ready ~70% → ~72%, mainstream weekly areas 71.3% → 72.3% and mainstream ~49% → ~50%; several earlier rows had moved the area scores without carrying them into the totals. Hours are unchanged (left for the next full re-measure) | +| 2026-10-12 | minor | Text lists, hanging punctuation and decoration styles, with a behaviour suite written from the public documentation (7 layout tests and 7 command tests, about 60 table rows): lists are set per paragraph (the list buttons, shortcuts and Type settings act on the paragraphs the selection touches, the whole layer when it isn't being edited), nest up to 8 levels (Tab and Shift+Tab; numbers go 1. a. i., bullets • ◦ ▪, counted per level), Enter on an empty item and Backspace at an item's start step out a level and then out of the list, typing `- `, `* ` or `1. ` at a paragraph's start starts one; list spacing (between items, in place of paragraph spacing), hanging lists (markers outside the box) and hanging punctuation (opening punctuation outside a start-aligned edge, closing punctuation outside an end-aligned one, taking no room when lines wrap); underline and strikethrough style (solid, dotted, wavy), thickness, underline offset, skip ink and colour. Canvas, PNG, SVG and PDF; `node.set` keys (`indentation`, `listSpacing`, `hangingPunctuation`, `hangingList`, `textDecorationStyle`/`Thickness`/`Offset`/`SkipInk`/`Color`), inspect (`lineTypes`, `lineIndentations`), CSS (`text-decoration-style`/`-thickness`/`-color`, `text-underline-offset`, `text-decoration-skip-ink`) and the REST import (`lineTypes` with `lineIndentations` per paragraph, which collapsed to one list style before). Text 87%/68% → 90%/70%, 5–10 h → 3–7 h; mainstream weekly areas 71.0% → 71.2%; other percentages unchanged at this rounding; full parity ~306–595 h → ~304–592 h | +| 2026-10-12 | minor | Comments and version history behaviour suite written from the public documentation (15 tests, about 80 table rows plus stepwise checks; 4 tests failed before the fixes) and its fixes: in a live session, threads made in the session couldn't be replied to, edited, reacted to, resolved, moved, deleted or opened (their ids, from their creator's id space, were refused as out of range); restoring a version brought back that version's comments, losing newer threads and replies (comments now stay, those on layers the version lacks staying where they were); restoring now keeps the replaced design in the history as an autosave first, unless a version of it was just saved or restored; giving an autosave a title makes it a named version (it could be dropped to make room); merging a branch adds the main file as it was and then a `Merged “”` version to its history. Pinned where the documentation is silent: pins go to the innermost frame under them, copies, duplicates and duplicated pages come without comments, replying reopens a resolved thread, versions stay with whoever saved them in a live session (a shared history needs accounts). Left: a branch's own version history, comparing two versions. Comments, versions, find, misc 85%/60% → 85%/62%, 2–5 h → 2–4 h; mainstream weekly areas 71.0% → 71.1%; percentages unchanged at this rounding; full parity ~306–595 h → ~306–594 h | +| 2026-10-11 | minor | Canvas navigation, rulers, guides, layout grids, outline mode and measuring behaviour suite written from the public documentation (18 tests, about 120 table rows and checks) and its fixes: the pixel grid shows from 400% (it waited for 800%); guides belong to the page or a top-level frame (a guide dropped over a nested frame goes to the frame holding it, and a frame moved into another keeps its guides but they stop showing, snapping and measuring); a guide can be selected, deleted with Delete, nudged with the arrows and removed from the context menu (`guide.select`, `guide.remove` without an index); Alt-dragging a guide drops a copy and holding Alt while dragging one measures to the nearest edges in the frame it crosses; columns and rows count Auto (as many fixed-size ones as fit); layout grids don't show or snap on a rotated frame; in outline mode shapes are clicked on their outlines, reaching layers behind. Left: outline mode's Include hidden layers and Include object bounds. Canvas & navigation 97%/72% → 98%/74%, 2–5 h → 1–4 h; feature breadth (~80%), ready for real work (~49%), core editor (~68%), essentials (~57%) and mainstream (~46%) unchanged at this rounding; full parity ~320–615 h → ~319–614 h | +| 2026-10-11 | minor | Export and inspect behaviour suite written from the public documentation (139 cases in 19 tests; 47 failed before the fixes) and its fixes: an export covers what the layer paints, so a drop shadow adds room only where it falls, strokes on boxes and ellipses reach their real width (they added up to four times as much), hidden layers take no room and a mask group exports the mask's area; a slice exports everything under it; slashes in layer names export into folders; Copy as PNG copies the whole selection. Added: the export settings' contents only and include bounding box, and for SVG outline text (off writes ``), unique id attributes and simplified strokes (stroke attributes where exact); Inspect colour formats (hex, RGB, CSS, HSL, HSB), CSS in rem, a Blend row, positions measured from the frame around a layer (groups aren't frames), CSS for rotation, blend modes, gradient directions and kinds, text alignment, case and decoration; icons and images in the asset list (`file.exportLayers {assets}`). Import/export 80%/50% → 82%/53%; Dev Mode 90%/64% → 93%/70%; file formats 55% → 56%; mainstream weekly areas 68.1% → 68.4%; feature breadth (~80%), ready for real work (~49%), essentials (~57%), mainstream (~46%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | `.fig` import's tail (gap 1): vectors open as editable vector networks (points, curved segments, filled areas with their winding, per-point corner radius and handle mirroring, end caps from the end points), falling back to outlines when the network is missing or damaged; sticky notes, shapes with text, connectors, tables, code blocks, slides and similar layers import as the closest frames, text layers and vectors with a warning instead of being dropped; set-variable, set-variable-mode and conditional prototype actions come across. New parsers fuzzed on fixtures generated in code. File formats 55% → 58%, 8–15 h → 5–10 h; import/export breadth 80% → 82%, ready 50% → 54%; feature and area totals and ready for real work (~49%) unchanged; beta ~173–310 h → ~170–305 h; full parity ~321–618 h → ~318–613 h | +| 2026-10-11 | minor | Shapes and vector behaviour suite written from the public documentation (10 shape tests, 6 vector tests and 3 handle tests: about 70 table rows plus stepwise checks) and its fixes: rounded vector points are drawn and exported (the radius was stored only; a vector's corner radius now rounds every point); a boolean group looks like its top layer, a subtraction like the bottom one (always the bottom); Alt draws a line from its centre; stars keep to 3–60 points; delete and heal fits the curve through the deleted point; Shift-clicking a selected point at a corner of the points' box takes it out (it scaled); outline stroke turns the stroke's colour style into the fill's and keeps opacity and effects; the miter field shows the miter angle (it showed the ratio under that name); individual corners only for rectangles and frames; Option/Alt+Shift+F flattens; edit-mode keys reach the edit tools through MCP `key` (Delete removed the layer). Added: corner radius and star ratio handles on the canvas, Count and Ratio fields for polygons and stars, cut tool drags that divide a shape into layers, paint bucket drags across areas. Shapes & vector 97%/68% → 98%/72%, 1–2 h → 2–4 h (the suite found gaps the estimate didn't count: corner smoothing beyond rectangles, a join or cap per point). Totals recomputed by the doc's method, which earlier rows had left stale: the area table's weighted figures are breadth ~82% (core ~93%) and ready ~61% (core ~69%), not the ~80%/~58% (~91%/~68%) carried since #130; Features (depth) follows the weighted ready, 57% → 61%; ready for real work ~49% → ~51%; mainstream ~46% → ~48% (weekly areas 67.8% → 69.4%); the selection suite's area 2 hours (−3/−5) hadn't reached the totals, and neither it nor the text suite had reached the Features (depth) row: Features 60–115 h → 56–109 h, full parity ~321–618 h → ~319–615 h (−3/−5 for area 2, +1/+2 for area 4) | +| 2026-10-11 | minor | Paint and effects behaviour suite written from the public documentation (14 paint and 9 effects tests with table rows for fills, opacity, every blend mode by its formula, pass-through and Normal containers, gradient handles and stops, image scale modes and adjustments, colour models, the colour profile, shadows, blurs, effect order and exports; 3 command tests) and its fixes: groups and frames set to Normal blend their children among themselves first, and clipping frames no longer isolate their children (blend modes inside a clipping frame met only the frame's children, not its fill or what is behind it) — canvas, SVG (`isolation`; clips put on what blends, since a clipped group isolates) and PDF (isolated groups); plus darker is drawn (it was multiply) on the canvas and in PNG exports; drop shadows hide behind what the layer covers, translucent fills included, unless shown behind transparent areas, in SVG too (it never cut them out) and the SVG filter region reaches as far as the effects; Fill images cover the box to its edges (upscaled ones faded out at the edge); "Normal" names Normal, not pass-through, in commands and the blend menu (the menu's Normal set pass-through). Added: a blend menu for each paint in the colour picker. Paint 66% → 70%, 4–7 h → 3–6 h; effects 72% → 75%, 3–6 h → 2–5 h; percentages unchanged at this rounding; full parity ~321–618 h → ~319–616 h | +| 2026-10-11 | minor | Frames, groups, sections and masks behaviour suite written from the public documentation (15 frame, group and section tests, 6 mask tests) and its fixes: a layer a mask hides can't be clicked there (clicks reach what's behind; Select layer agrees), a mask group's box and exports are the mask's (plus any layers under it), sections stay on the page or in sections (moves, drags, pastes, grouping and new sections in frames refused or kept on the page), drawing a frame or section around layers puts those fully inside it in it, resizing a section leaves its contents where they are (and a frame resized with Cmd/Ctrl held keeps its layers' sizes too), and X/Y of layers in groups are measured in the frame (groups have no coordinate space). Frames, groups, sections, masks 95%/75% → 97%/80%, 2–4 h → 1–3 h; percentages unchanged at this rounding; full parity ~323–621 h → ~322–620 h | +| 2026-10-11 | minor | Styles and variables behaviour suite written from the public documentation (18 command-level tests) and its fixes: editing a styled fill, stroke, effect or layout grid list (directly, a paint row, an added fill or a bound variable) unlinks that style instead of being overwritten by the next style edit; applying a style replaces variables bound to what it sets; text styles go on text layers only; deleting a variable (or its collection) unbinds the layers using it, which keep their last value, and aliases to it take its values; bindings are type-checked (a colour variable can't drive a width) and number variables fill text content; aliases are checked when a variable is created. Added: duplicate, move (reorder, into a folder) and rename folder for styles, with a Local styles list grouped by type and folder (click to edit, right-click, drag to reorder); duplicate mode, set as default and move column for modes; per-platform code syntax (Web, iOS, Android) used by generated CSS and the variables JSON; font weight, paragraph spacing/indent and text content scopes for number variables. Found missing: variables inside styles. Styles & variables 92%/72% → 94%/76%, 1–2 h → 3–5 h; percentages unchanged at this rounding; full parity ~323–621 h → ~325–624 h | +| 2026-10-11 | minor | Prototyping behaviour suite written from the public documentation (7 model tests, 12 player tests with 21 gestures) and its fixes: on drag starts only for a drag its animation's way and the animation follows the pointer, key combinations (`Shift+K`) fire (and are recorded and imported), an overlay's after-delay timer starts when it opens, instances of interactive components play their variant's interactions and "Change to" changes the right instance, spring presets are physical springs that play until they settle, smart animate overshoots with spring and back easings, fixed layers draw above scrolling content; added scroll to offsets. Prototyping 88%/70% → 90%/74%, 5–9 h → 3–6 h; mainstream weekly areas 68.1% → 68.4%, mainstream ~46% → ~47%; other percentages unchanged at this rounding; full parity ~320–615 h → ~318–612 h | +| 2026-10-11 | minor | Components behaviour suite written from the public documentation (10 tests, 45 table cases) and its fixes: instances now keep overrides of auto layout padding, gap and alignment, layout grids, export settings, prototype interactions and every text property (resizing, paragraph spacing, lists…; these were lost when the main changed); moving, rotating, resizing or re-constraining layers inside an instance, and its auto layout direction, are refused (the instance itself resizes); switching variants keeps an override only where both variants had the same value (text layers keep theirs); swapping from the Assets panel keeps only text overrides; the default variant is the top-left one (instances, Assets thumbnails, code); hidden exposed instances hide their properties; pushing changes from a nested layer goes to the outer main. Added: variant property reordering and duplicate combination warnings. Components 95%/68% → 96%/72%, 6–12 h → 4–9 h; with #133's selection and transform change, core editor ready ~68% → ~69% and mainstream ~46% → ~47% (weekly areas 68.1% → 68.8%); full parity ~320–615 h → ~315–607 h (the totals now also carry #133's area 2 hours) | +| 2026-10-11 | minor | Embedded image colour profiles: placed and opened images are converted once, when decoded, from their own colour space into the file's (sRGB or Display P3): ICC profiles in PNG (`iCCP`), JPG (APP2), WebP and TIFF, matrix/curve and table profiles in RGB or grey (moxcms, already built for the `image` crate), and a PNG's `sRGB`, `gAMA` and `cHRM` chunks; images without a profile, or with one that can't be used (CMYK, damaged, over 4 MB), are taken as sRGB as before. The original file is kept (PDF exports still embed it); pixel exports carry the converted pixels with the file's profile. The image fill's details show the source profile (Unknown — treated as sRGB when there's none) and `image.place` reports it as `imageProfile`. Paint breadth 87% → 90%, ready 66% → 67%, 4–7 h → 3–6 h; feature breadth (~80%), ready for real work (~49%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | Layers panel keyboard and stroke variables: F2 renames the selected layer (also from a layer row with the keyboard, which gets it back; Escape there cancels), Shift+F10 (`view.contextMenu`) opens the context menu at the focused row or below the canvas selection and the menu takes the keyboard, F8 (`select.area`) draws a selection box for keyboard-only users (arrows move it, Shift+arrows size it, Enter selects like a marquee; agents pass a box), long lists checked at 10k layers (revealing a layer, expanding or collapsing everything and arrow keys take at most 1.3 ms a frame in the release UI bench; the revealed row takes the keyboard); colour styles on strokes were already complete, and each side's stroke weight now binds to a number variable (`strokeTopWeight`…, bindable fields in the Design panel and REST import). Layers panel 95%/78% → 98%/82%, 2–4 h → 1–2 h; Strokes 90%/72% → 95%/75%, 1–3 h → 1–2 h; mainstream depth 67.8% → 68.1%; feature breadth (~80%), ready for real work (~49%), essentials (~57%) and mainstream (~46%) unchanged at this rounding; full parity ~321–618 h → ~320–615 h | +| 2026-10-11 | minor | Text behaviour suite written from the public documentation (13 text cases, 5 command tests) and its fixes: CJK lines break between ideographs, kana and Hangul and before and after Latin words without breaking inside them, with the line-start and line-end rules (closing punctuation, small kana and the prolonged sound mark never start a line, opening brackets never end one); a style's line height, baseline and decoration come from its own font, so lines with CJK or emoji drawn by fallback fonts keep the spacing of the others; paragraph indent applies to start-aligned text only; carets, Delete and Backspace step over whole grapheme clusters (emoji sequences, flags, combining marks), which also shape with one font; double-click and word moves take one script's run in text without spaces. Added: vertical trim (cap height to baseline; `leadingTrim`, Type settings, REST import, inspect, CSS `text-box`). Text 85%/64% → 87%/68%, 7–13 h → 5–10 h; percentages unchanged at this rounding; full parity ~323–621 h → ~321–618 h | +| 2026-10-11 | minor | Selection and transform behaviour suite written from the public documentation (5 selection tests with 38 table rows, 11 transform tests) and its fixes: Alt-resizing shrinks about the centre too (it could only grow); groups, frames with Scale or stretch constraints and several layers with different rotations scale each layer along its own axes, keeping its rotation and flip (children turned 90° grew along the wrong axis, nested turned groups too); several layers dragged past an edge mirror and flip; a flipped layer reads back the rotation typed; resizes land on whole pixels with the pixel grid on; a press inside a layer smaller than its handles moves it and the nearest handle wins (the first in reach did), with its handles hidden under 16 px; locked layers selected in the layers panel get no handles and aren't dragged with the rest; the background of a top-level frame with layers starts a marquee inside it (it moved the frame); Select inverse skips hidden layers like Select all. Added: equal spacing snapping with labelled gaps, Cmd/Ctrl while resizing a frame ignores its constraints. Selection & transform 95%/68% → 97%/72%, 6–10 h → 3–5 h; core editor ready ~67% → ~68%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~345–666 h → ~342–661 h | +| 2026-10-11 | minor | `.fig` import, best effort (gap 1): `.fig` files open (best effort): the zip, its deflate/zstd chunks and the kiwi schema are decoded generically and mapped by field name onto the REST importer, so pages, the layer tree, geometry, paints (styles resolved), strokes, effects, text with styled runs, vectors (as outlines), auto layout (grid included), constraints, components, sets, instances (expanded from their main with overrides, derived layout and property values), styles, variables with modes and bindings, prototyping and image fills from the archive come across; anything else is a warning. Import/export breadth 65% → 80%, ready 35% → 50%, 30–60 h → 8–15 h; file formats 40% → 55%, 30–60 h → 8–15 h; feature breadth ~79% → ~80% (core ~90% → ~91%), areas ready ~57% → ~58% (core ~67% → ~68%); ready for real work ~47% → ~49%; essentials ~53% → ~57% (opening files people send ×0.80 → ×0.88); mainstream ~43% → ~46% (weekly areas 66.8% → 67.8%, file exchange ×0.80 → ×0.85); beta ~195–355 h → ~173–310 h; full parity ~345–666 h → ~322–620 h | +| 2026-10-11 | minor | Auto layout behaviour suite written from the public documentation (64 layout cases, 8 command tests) and its fixes: a frame is never smaller than its padding, only the frame's inside stroke counts when strokes are included (and new auto layout includes them), auto gaps stop at 0, grid `fr` tracks behave like the CSS `fr` unit (a track never gets less than its content), canvas stacking is drawn, exported and hit (it was stored only). Added: auto gaps evenly and around, vertical wrap, grid cell alignment, strokes and canvas stacking controls. Auto layout 95%/68% → 98%/74%, 6–12 h → 3–6 h; percentages unchanged at this rounding; full parity ~348–672 h → ~345–666 h | +| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | +| 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | +| 2026-10-11 | minor | Localization layer landed (gap 5): per-language catalogs baked in at build time with English text as keys and fallback, placeholders and plurals, command labels by id, Preferences ▸ Language (System follows the OS or the browser) applied live and `pref.language`; English catalog and a complete Spanish draft awaiting native review. Localization 10% → 20%, 60–100 h → 45–75 h; ready for real work unchanged (~47%); full parity ~363–697 h → ~348–672 h; beta ~200–365 h → ~195–355 h | +| 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | +| 2026-10-11 | minor | Plugin panels and settings landed (gap 6, ABI v2; v1 plugins unchanged): a plugin shows a panel of notes, fields (text, number, checkbox, menu, list, colour) and buttons drawn with FigCraft's widgets; a button or a changed value calls it back with every value (same limits per callback, one undo step per callback that edits, rolled back on failure), its module staying loaded while the panel is open; `plugin.ui` / `plugin.event` / `plugin.close` drive panels headless; per-plugin settings (64 KB) kept with the preferences; the Swatch grid example gained a palette panel that remembers the last palette. Plugins & widgets breadth 35% → 45%, ready 20% → 28%, 25–55 h → 15–40 h; ecosystem 20% → 28%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~373–712 h → ~363–697 h | +| 2026-10-11 | minor | Canvas fixes (gap 13 closed): boolean groups are clicked where their result is (a click in a subtracted hole reaches what's behind; with the group or an operand selected its operands are hit, so each can still be picked), a layer dragged inside a group stays in the group (frames and auto layout still take other drops), View ▸ Frame outlines and View ▸ Slices draw on the canvas, and the idle tests wait for the render thread instead of counting frames. Shapes & vector 1–3 h → 1–2 h (vector QA with real files left); percentages, feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~378–722 h → ~375–717 h | +| 2026-10-11 | minor | Stretch and scatter brushes and pen pressure landed (gap 9): eight brushes drawn in code (four stretched along the path, four scattered with spacing, size, rotation and scatter jitter from a seed) in the stroke settings, drawn everywhere strokes are and flattened by outline stroke; the Pencil records pen pressure as the stroke's width profile (Windows pens, browser styluses; macOS and Linux tablets report none) and can draw with a brush; Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–9 h; Strokes 2–4 h → 1–3 h; Hardware 50% → 55%, 15–30 h → 13–27 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~378–722 h → ~358–687 h | +| 2026-10-11 | minor | Text on a path and pattern fills landed (gap 9): text follows a path or shape (start, side, alignment; edited in place; canvas, PNG, SVG and PDF; SVG `` imports as text on a path) and pattern fills tile a layer (grid or half-shifted rows or columns, scale, spacing, alignment; live as the source changes; SVG ``, PDF tiling patterns); Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–10 h; Text 8–14 h → 7–13 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~375–715 h | +| 2026-10-11 | minor | Team libraries on `figcraft-server` landed (gap 10): a file's library is published to the server under the file and enabled in other files by a share link (per-asset versions; updates noticed in the background and accepted with overrides kept), the server counts which files use which components, and a whole library can be swapped for another by names; Team libraries breadth 40% → 70%, ready 25% → 40%, 10–20 h → 4–8 h; collaboration 12% → 13%, 55–110 h → 49–98 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~380–725 h → ~375–715 h | +| 2026-10-11 | minor | Branches landed (gap 17): File ▸ Create branch… copies the file into a branch tab that remembers its base; Review and merge shows both sides' changes (layers with Inspect-style property changes, styles, variables, comments) and the conflicts (the same property changed on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merging is one undo step and archives the branch; Update from main; branches are saved in the `.figcraft` file. Comments, versions, find, misc breadth 80% → 85%, ready 58% → 60%, 4–8 h → 2–5 h; collaboration 12% → 13%; menu breadth 204/204 (the catalog gained File ▸ Create branch…); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~378–722 h | +| 2026-10-11 | minor | Animated GIF fills and TIFF images (gap 12, without video and HEIC): GIF fills keep the original file, show the first frame on the canvas and in exports and play in the presenter and the preview (frames decoded on first play, at most 500 frames and 128 MB; always looping; the window wakes only for a GIF's next frame); TIFF images open, import, place and paste. Paint breadth 85% → 87%, 5–9 h → 4–7 h; gap 12 6–12 h → 5–10 h (video playback and HEIC left, both needing non-Rust decoders); feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | The last three catalogued menu items got commands (gap 21): File ▸ Open recent (recent files kept in the preferences, at most 20, a file that's gone greyed in the menu and dropped when picked, Clear recent), View ▸ Outlines ▸ Show outlines for selection and View ▸ Mask outlines: menu breadth 98.5% → 100% (203/203); Canvas & navigation breadth 95% → 97%, 3–6 h → 2–5 h; feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | Extended collections, variables import/export and gradient stop and variant property bindings landed (gap 14): Styles & variables breadth 85% → 92%, ready 70% → 72%, 4–8 h → 1–2 h; feature breadth unchanged (~79%); ready for real work unchanged (~47%); full parity ~385–730 h → ~380–725 h | +| 2026-10-11 | minor | Collaboration wording after the standalone server: `figcraft-server` keeps many files, persisted as a snapshot plus a log of what changed (compacted, checksummed, damaged files skipped and reported), restarts lose nothing confirmed and apps resume where they were; the app publishes its file to it; share links per file let people edit or only view (the server refuses a viewer's edits), are revocable and stored as hashes; TLS comes from a reverse proxy (the desktop app has none of its own yet); percentages and hours unchanged until the next re-measure | +| 2026-10-11 | minor | Live boolean editing, Design panel arc fields, the shape builder on strokes and open paths and one-sided variable widths landed (gap 13): Shapes & vector breadth 92% → 97%, ready 66% → 68%, 8–14 h → 1–3 h; feature breadth ~78% → ~79%; ready for real work unchanged (~47%); full parity ~390–745 h → ~385–730 h | +| 2026-10-11 | minor | Wording only: live sessions (#104), then the browser joining them and `figcraft://` invite links opening the desktop app; percentages and hours unchanged until the next re-measure | +| 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 | minor | Readiness-by-audience table: hours to ~95% for full (~400–750 h), mainstream (~180–330 h) and essentials (~85–160 h). Full number confirmed as an additive weighted sum; unchanged | +| 2026-10-10 | minor | Added the essentials user number (~53%: core features 77.8% × 0.92 launch/stability × 0.92 discoverability × 0.8 opening files people send) | +| 2026-10-10 | minor | Added the mainstream practitioner number (~43%: weekly areas 66.5% × 0.9 interaction × 0.9 stability × 0.8 file exchange) and user-evidence count (none). Checked that ready for real work (~46%) is built from the written dimension weights and scores; unchanged | +| 2026-10-10 | minor | Stage checked against the alpha gate: stays alpha | +| 2026-10-10 | major | Created from the ROADMAP's parity rubric, honest assessment and area narrative. Full re-measure against Figma desktop 126.10.7: added multiplayer, plugins, team libraries, Figma Draw extras and AI as weighted areas; split breadth from ready-for-real-work; core depth re-graded ~79% → ~67%; hours calibrated from git history; full parity 115–195 h → 420–790 h | From 9cb1dba1e27339a541858e80f30d81f9d2db0211 Mon Sep 17 00:00:00 2001 From: Mex <44814932+MMexicann@users.noreply.github.com> Date: Fri, 16 Oct 2026 00:07:13 +0200 Subject: [PATCH 02/38] M4: normalize contribution files to LF --- ROADMAP.md | 834 +++++++-------- crates/engine/src/cmd/structure.rs | 1306 +++++++++++------------ crates/engine/tests/outline_stroke.rs | 348 +++--- docs/gaps.md | 896 ++++++++-------- docs/roadmap.md | 250 ++--- docs/target-app-parity.md | 1406 ++++++++++++------------- 6 files changed, 2520 insertions(+), 2520 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 29de0e90..10b2d411 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,417 +1,417 @@ -# FigCraft Roadmap - -**Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents) - -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (Outline stroke preserves stroke holes; Typography spacing icons support scrubbing (#373); font scans skip named pipes; system theme on first run; recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading (#207), SVG import budgets (#190), Leptos code (#223), full screen (#227), server snapshots readable again (#188), owner-only preferences (#200), ruzstd 0.9.1 (#182), layer cycle repair (#205), batch rollback, Direct3D 12 on Windows, plugin metering (#192); vector edit mode regions: hover hatch, selection and drag; SVG/PDF exports refuse over-depth documents (#208); bounded MCP/control framing, connections and queues (#201); plugin log limits (#191); live-session revert guard (#198); attached MCP no longer replays after a lost reply (#194); repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7) - -FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow, -native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through -the CLI, a JSON control channel and MCP. This page is the one-page summary; agents choosing work -start with [`docs/gaps.md`](docs/gaps.md). - -## Headline numbers - -| | Value | Kind | -|---|---|---| -| Menu/command breadth | **100%** (211 / 211) | measured, [`docs/parity-checklist.md`](docs/parity-checklist.md) (`cargo xtask parity`) | -| Feature breadth | **~85%** (core editor ~94%) | estimated, [`docs/target-app-parity.md`](docs/target-app-parity.md) | -| Ready for real work | **~53%** | estimated, same doc (written dimension weights and scores) | -| Essentials user | **~59%** | estimated: core features 77.8% × 0.92 launch/stability × 0.94 discoverability × 0.88 opening files people send ([method](docs/target-app-parity.md#essentials-user)) | -| Mainstream practitioner | **~50%** | estimated: weekly areas 72.7% × 0.9 interaction × 0.9 stability × 0.85 file exchange with Figma users ([method](docs/target-app-parity.md#mainstream-practitioner)) | -| Remaining to beta | **~155–280 h** one agent | estimated | -| Remaining to full parity | **~262–519 h** one agent (~83–145 h wall-clock with 5 agents) | estimated | - -### Readiness by audience - -| Audience | Ready % | Opus 5.5 agent wall-clock hours to ~95% | Work that dominates | -|---|---|---|---| -| Full target (ready for real work) | ~53% | ~268–522 h | multiplayer (42–84), localization (29–51), plugins (8–26), the interaction pass (40–60), GPU canvas (30–60), AI (30–60), the `.fig` import's tail (5–10) | -| Mainstream practitioner | ~50% | ~120–229 h | core editor depth incl. the `.fig` import's tail (49–98), the interaction pass (40–60), real-file corpus and platform QA (25–50), checking the rest of the experimental `.fig` export elsewhere: effects, images, prototypes, real files (5–15) | -| Essentials user | ~59% | ~55–100 h | the `.fig` import's tail so the files people send open well (5–10), core tool polish (25–45), interaction pass on the core tools (15–25), stability QA (10–20) | - -Hours are one agent working sequentially, calibrated from this repo's history (about 1 agent-hour -per 1,000 added lines, 1–2.5 h per feature PR, ~7 h per honest depth point in the polish tail; see -[how the hours are calibrated](docs/target-app-parity.md#how-the-hours-are-calibrated)). Each audience's work is a subset of -the next. About 70–80% parallelizes across 4–6 agents on disjoint crates (roughly ÷4 in -wall-clock); the `.fig` reader, the multiplayer core and a plugin runtime each need one owner, and -native-speaker review, real-file licensing and screen-reader QA need people. - -Hours are Opus 5.5 agent wall-clock hours, calibrated from this repo's history: the app was built in -~25 wall-clock hours by 4–6 parallel agents (~110–150 agent-hours, ~1 agent-hour per 1,000 added -lines, a typical feature PR 1–2.5 h). Details and what needs a human: -[`docs/target-app-parity.md`](docs/target-app-parity.md#how-the-hours-are-calibrated). - -## Stage - -**Alpha.** Core workflows exist end to end (frames, shapes, vectors, text, auto layout, components -with variants and overrides, styles, variables, prototyping, Dev Mode, SVG/PNG/JPG/PDF), but -FigCraft opens `.fig` files only best effort, has only a preview of multiplayer, and nothing has been checked -against real Figma documents. Ready-for-real-work is ~53%, inside alpha's ~40–75% band, and the alpha gate passes: every daily core workflow (design screens, build a design system, prototype, hand off to developers) works end to end on FigCraft's own files, which save and reopen; review works only asynchronously and `.fig` import is unproven on real files, both beta gaps ([alpha gate](docs/roadmap.md#alpha-gate)). Beta needs -~75% **and** reliable `.fig` import: the `.fig` import's tail (5–10 h), a real-file corpus with measured -fidelity (15–30 h), the interaction pass (25–40 h of the 40–60), core depth to ~80% (40–75 h), -basic multiplayer on the landed core (30–55 h), stability and platform QA (20–40 h), a partial GPU path (15–30 h); -native speakers' reviews of the six language drafts need people. - -## By dimension - -| Dimension | Ready | Hours | Doc | -|---|---|---|---| -| Features (depth) | 63% (core editor 72%) | 28–67 | [target-app-parity](docs/target-app-parity.md) | -| UI/UX fidelity | 58% | 40–60 | [ui-parity](docs/ui-parity.md) | -| File formats | 60% (`.fig` read best effort with editable vectors; experimental write, round trips through our own importer and written files opened in other software) | 5–10 | [file-format-parity](docs/file-format-parity.md) | -| Collaboration | 13% (core in; live sessions with document sync, cursors and text carets, cursor chat and spotlight, the browser joins, invite links open the app; a self-hosted server with edit and view links and team libraries; preview) | 46–92 | [collaboration-parity](docs/collaboration-parity.md) | -| Stability | 55% | 20–40 | [gaps](docs/gaps.md) | -| Performance | 60% | 30–60 | [hardware-parity](docs/hardware-parity.md) | -| Hardware | 60% | 10–21 | [hardware-parity](docs/hardware-parity.md) | -| Localization | 39% (layer; English; Spanish with a Latin American overlay, French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese drafts; CJK UI fonts) | 29–51 | [localization-parity](docs/localization-parity.md) | -| Platforms | 75% | 10–20 | [hardware-parity](docs/hardware-parity.md) | -| Ecosystem / plugins | 38% (own WASM plugins with panels, settings and widgets; a self-hosted plugin directory) | 8–26 | [gaps](docs/gaps.md) | -| AI features | 0% | 30–60 | [gaps](docs/gaps.md) | - -## Features - -| Area | Breadth | Ready | Hours | Doc | -|---|---|---|---|---| -| Canvas & navigation | 98% | 74% | 1–4 | [ui-parity](docs/ui-parity.md) | -| Selection & transform | 97% | 72% | 3–5 | [ui-parity](docs/ui-parity.md) | -| Layers panel | 99% | 85% | 1–2 | [ui-parity](docs/ui-parity.md) | -| Shapes & vector | 99% | 73% | 1–3 | [target-app-parity](docs/target-app-parity.md) | -| Paint | 90% | 71% | 2–5 | [target-app-parity](docs/target-app-parity.md) | -| Strokes | 95% | 75% | 1–2 | [target-app-parity](docs/target-app-parity.md) | -| Effects | 100% | 75% | 2–5 | [target-app-parity](docs/target-app-parity.md) | -| Frames, groups, sections, masks | 97% | 80% | 1–3 | [target-app-parity](docs/target-app-parity.md) | -| Auto layout & constraints | 98% | 74% | 3–6 | [auto-layout-parity](docs/auto-layout-parity.md) | -| Text | 90% | 70% | 3–7 | [target-app-parity](docs/target-app-parity.md) | -| Components & variants | 96% | 72% | 4–9 | [components-parity](docs/components-parity.md) | -| Styles & variables | 95% | 76% | 2–4 | [components-parity](docs/components-parity.md) | -| Prototyping | 92% | 74% | 2–5 | [prototyping-parity](docs/prototyping-parity.md) | -| Import/export | 86% | 58% | 5–10 | [file-format-parity](docs/file-format-parity.md) | -| Dev Mode / inspect | 96% | 72% | 0–1 | [dev-mode-parity](docs/dev-mode-parity.md) | -| Comments, versions, misc | 87% | 63% | 1–3 | [collaboration-parity](docs/collaboration-parity.md) | -| Multiplayer & sharing | 17% | 5% | 42–84 | [collaboration-parity](docs/collaboration-parity.md) | -| Plugins & widgets | 60% | 35% | 8–26 | [plugins](docs/plugins.md) | -| Team libraries | 70% | 40% | 4–8 | [components-parity](docs/components-parity.md) | -| Figma Draw extras | 88% | 60% | 1–3 | [gaps](docs/gaps.md) | -| AI features | 0% | 0% | 30–60 | [gaps](docs/gaps.md) | - -## Languages - -| Language | Code | Strings | Status | -|---|---|---|---| -| English | en | 100% | full | -| Simplified Chinese | zh-Hans | 100% | draft (needs native review) | -| Spanish | es, es-419 | 100% (es-419: an overlay of 98 entries) | draft (needs native review) | -| Hindi | hi | 0% | none | -| Arabic | ar | 0% | none | -| French | fr | 100% | draft (needs native review) | -| Portuguese (Brazil) | pt-BR | 100% | draft (needs native review) | -| Indonesian | id | 0% | none | -| Japanese | ja | 100% | draft (needs native review) | -| German | de | 100% | draft (needs native review) | -| Korean | ko | 100% | draft (needs native review) | -| Vietnamese | vi | 0% | none | - -Also shipped: Swedish (`sv`: every UI string, 120 of 588 command labels and menu wordings; native review welcome). Figma ships English, Japanese, Korean, French, German, Spanish and Portuguese -(Brazil); FigCraft has the localization layer, English and Spanish (Spain and Latin America), -French, German, Brazilian Portuguese, Japanese, Korean and Simplified Chinese drafts (Preferences ▸ -Language; Traditional Chinese has no catalog; CJK UI -text uses an installed CJK font, loaded only when needed; adding a language: -[`docs/translating.md`](docs/translating.md)). Document text in any script (bidi, Arabic shaping, -IME) already works. Detail: [`docs/localization-parity.md`](docs/localization-parity.md). - -## Upcoming - -Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). - -1. `.fig` import's tail: fixes from real files, connector labels, table cells, filled areas with their own fills (the best-effort import, editable vector networks, the other layer kinds and variable prototype actions landed 2026-10-11): 5–10 h; then checking the rest of the experimental `.fig` export (landed 2026-10-12; written files open and draw in other software) on effects, images, prototypes and real files: 5–15 h. -2. Real-file corpus and measured fidelity: 15–30 h. -3. Tool-by-tool interaction pass against Figma: 40–60 h. -4. Languages: native reviews of the es, fr, de, pt-BR, ja and ko drafts (people), Latin American Spanish (1–2 h), then Simplified Chinese (3–5 h). -5. Multiplayer (M9): accounts and teams on top of the live sessions and the standalone server (transport, hosting, invite links that open the app, document sync, presence with remote text carets, cursor chat and spotlight, per-user undo, reconnect with offline edits, collision-free ids for styles, variables and comments, the browser client, `figcraft-server` with persistence and share links to edit or view, team libraries on the server landed): 42–84 h. - -## Progress log - -- **2026-10-15** · M4: Outline stroke preserves even-odd stroke holes. Pixel comparisons cover alignment, dashes, individual side weights, vector paths and stroke-only opacity; native save/reopen, Undo/Redo and instance opacity inheritance/overrides are covered by `outline_stroke`. Existing estimates unchanged; filled-shape compositing needs separate work. - -- **2026-10-14** · Corner handles win over the corner-radius handles (M1, #292): a press on the inner half of a corner handle's square picked the radius handle 10 px in, so the drag neither resized nor (dragging outward) rounded anything. Inside the drawn square the corner now resizes both sides (in ratio with Shift or the link); further in the radius handle still answers. Regression test in the transform behaviour suite. Numbers unchanged. -- **2026-10-14** · Home screen recent files as cards (M7): each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged. -- **2026-10-13** · Vector edit mode regions (M4): with the Move tool, the filled area under the pointer is hatched with "/" lines in the selection colour, clipped by the area's own fill rule so holes stay clear; the hatch goes near points (3.5 px) and segments (2 px), and its spacing follows octave folding (8 px at every power-of-two zoom), fixed to the shape. A click selects the area with its points (they move with it but keep the unselected look), Shift adds or removes one, a press on a selected area drags the whole selection as one undo step, and a click on an empty spot clears the areas and stays in edit mode. `vector.edit.select {regions}` and `vector.edit.state` (`regions`, `regionVertices`, `regionHover`); `vector_regions_tests` and `regions_tests`. Numbers unchanged. -- **2026-10-14** · SVG/PDF export depth (M0.9, #208): visible descendants deeper than 256 levels below an exported root are refused before bounds or drawing, including masks and raster fallbacks. SVG has a checked API; the engine and CLI report the limit and suggest selecting an inner layer. Existing destination files survive refusal. Boundary, inner-root, hidden-subtree, multi-root/page and command/CLI regressions cover the change. Numbers unchanged. -- **2026-10-13** · MCP/control transport hardening (#201): 32 MiB incoming frames are bounded while reading; oversized frames return an error and close without draining. The opt-in loopback control listener caps active workers at 8, applies 60 s socket read/write inactivity timeouts and caps both request queues at 8, discarding timed-out requests including those deferred to hidden windows. Reader, queue, idle-client and blocked-writer regressions cover the limits. Numbers unchanged. -- **2026-10-13** · Attached MCP (M0.9, #194): a transport failure no longer replays a request that may already have edited the document. The caller gets an outcome-unknown error and must inspect before retrying; the next explicit call reconnects. Loopback tests cover a lost mutation reply, a single applied edit, reconnect and a connection failure before sending. Numbers unchanged. -- **2026-10-13** · Constrain proportions in the Layout panel (M1, #220): with the W/H link on, typing a width or height scales the other side by the layer's own ratio; the property edit dropped the ratio, so the link only worked while dragging resize handles. Both sides in one edit stay as typed, the `w`/`h` aliases work the same, each layer of a multi-selection keeps its own ratio, and a zero side has no ratio to keep. Regression test in the transform behaviour suite. Numbers unchanged. -- **2026-10-13** · Repeat last command (M1, #181): repeated edits keep the underlying command and parameters, so repeated nudges accumulate with one undo step each. A stored self-reference is refused instead of recursing. Regression tests cover three repeats, their undo steps, and missing or self-referencing commands. Numbers unchanged. -- **2026-10-13** · Component playground (M3): with an instance, a component or a component set selected, the Inspect tab shows a Playground: a preview of the component and a control for each variant, boolean and text property. What is tried there changes the preview and the code in the Code section, never the file (no undo step, the canvas instance stays as it is); Reset goes back to the component's or the instance's own values. `component.playground {id?, properties?, language?, path?}` gives the same to scripts: the properties with their values, the code, and a PNG on request (`playground_tests`). Left: instance-swap and slot properties in the playground (the command takes an instance swap; the panel has no control for it), a code connect template's snippet with the tried values. Numbers unchanged. -- **2026-10-13** · Auto layout, on-canvas handles of automatic gaps (M2): the handle between spread rows or columns (and between items with an auto gap) shows the distance the layout gives the gap instead of the stored value, and dragging it makes the gap a fixed one from there, as typing a gap in the panel does; one undo step (`gap_handle_tests`). Before, the drag changed a value that had no effect. The documentation is silent on dragging an automatic gap. Numbers unchanged. -- **2026-10-13** · Variables inside styles (M3): a shared style can bind variables to fields of its value (`style.bind {style, field, variable?}`: a paint style's colours and gradient stops, an effect style's colour, radius, spread and offsets, a grid style's size, count, gutter and offset, a text style's font size, line height, letter spacing, family and style). Every layer using the style shows those fields resolved in its own modes and stays linked when a variable or a mode changes; the style keeps them resolved in the default modes. A style made from a layer keeps that layer's bindings; a new value drops bindings to what is gone. The style editor binds from its property rows (paint colour, shadow colour, blur radius, grid size) and detaches from the variable's chip; the style picker marks styles that use variables. `.fig` export and import carry the bindings on the style, REST layers hand theirs to the style they use. Left: binding text style fields and the other effect and grid fields from the editor (commands only), library variables in the picker, a style's library copy. Numbers unchanged. -- **2026-10-13** · Effects on groups, inner shadows and background blur (M3): a group is no outline of its own, so its inner shadows are cast inwards from around what its layers draw together (their pixels, over them: no shade at a seam between two layers) and its background blur and glass show the backdrop through the outlines of its layers (under them). Canvas and PNG; SVG embeds a group with an inner shadow as a render and its background blur as an image under the layers; PDF places both as images, the blur matching the PNG. Where the group holds one shape the pixels match that shape, or a frame filled like it, carrying the effect (`group_effects_tests` in render, svg and pdf). Offsets and spreads of a group's inner shadow go by whole pixels. Numbers unchanged. -- **2026-10-13** · Text made by commands on a dark page (M1): in the app, `text.create` and `node.create {type: text}` that name no fill give white text on a dark page, by the same rule as the Text tool (the page's colour decides, also inside a frame); a named `fill`, `fills` or `fillStyle` is kept. Sessions without an interface (CLI, MCP, exports) still make black text everywhere. Numbers unchanged. -- **2026-10-13** · Frames and groups, a one-step convert (M1): `object.convertToGroup` and `object.convertToFrame` (the Design panel's type menu on a frame or a group) change the layer in place, its layers staying where they are on the page. A frame turned into a group loses what only a frame has (auto layout, with the positions it gave kept and fill children fixed; fills, strokes, corner radius, clipping, layout grids, guides, scrolling) and takes its layers' box; a group turned into a frame keeps its box, with no fill and no clipping. Instances, main components, layers inside instances and empty frames are refused with the reason, all or nothing; one undo step. Numbers unchanged. -- **2026-10-13** · Prototyping, delays on mouse triggers and manual overlay offsets (M5): mouse enter, leave, down and up wait `delayMs` before their actions run (once while waiting; dropped when the screen or overlay they were on goes away; a pointer that leaves early doesn't cancel, the documentation is silent), and a manual overlay sits at `offset: [x, y]` from the top-left of the layer that opens it (without one, right below it, as before). Delay and X/Y fields in the interaction editor, the player on its own clock, REST import (`delay`, `overlayRelativePosition`), `.fig` import and export (the export's field for mouse delays is unchecked against other software). Numbers unchanged. -- **2026-10-13** · Auto layout, wrapped-line alignment (M2): a wrapping layout's lines (rows of a horizontal one, columns of a vertical one) are packed where the cross alignment says (start, centre, end; before, they always sat at the start), stretch when every child fills across the flow, or take the free space between them (`contentAlign: spaceBetween`, the Design panel's Row/Column gap mode, the alignment pad lighting the whole cross axis). Read from REST JSON and `.fig`, written to `.fig`, `align-content` in CSS, an instance override with the other alignment settings. 13 hand-computed layout cases and command tests. Numbers unchanged. -- **2026-10-13** · `.fig` export: instance sizes, variables, styles (gap 1): instances write what they change from - their main (size, name, paints) as an override addressed by the main's id, what follows for their layers as - derived data and a scale factor when scaled as a whole; variable collections, modes, variables and bindings, - the modes frames and pages set, shared styles with their links and variable-width strokes are written, and the - import reads all of it back. Checked by opening written files in other software: resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width; - number, string and boolean bindings and bindings on effects, layout guides and gradient stops come across as the values they give but did not show as bound there (their entries now also name the layer field they set, not yet checked there), and strokes wider on one side of the path are written as filled outlines, no longer editable strokes there (the stroke itself is kept as plugin data and our own import puts it back; not yet checked there). Numbers unchanged. - text, effect and grid styles, number, string and boolean bindings, bindings on effects, layout guides and gradient stops, and variable-width strokes are not yet checked there. Numbers unchanged. -- **2026-10-13** · Starter templates and first-run help: eight templates built in code (mobile app, landing page, - dashboard, design system starter, wireframe kit, social post sizes, presentation slides, icon set starter), a - Templates tab on the home screen, `template.list` and `file.newFromTemplate` for scripts and agents, and a Getting - started card that can be closed and comes back from the Help menu -- **2026-10-12** · Your online files: a file shared or published online stays yours after its tab closes or the app - quits. The app remembers it (with its owner token, kept with the preferences and never shown) and Home ▸ **Online** - lists what you own: open a file again as its owner, start or stop sharing, copy its code, rename it, remove it from - the server or forget it here. Servers are asked once, when the list opens; tabs mark files in a live session; a - crash reopens such a file from its server and puts back the changes that hadn't reached it. `collab.files`, `collab.openFile`, `collab.forgetFile`; servers need an - upgrade only to tell which files are still there. The six draft catalogs translate every string again -- **2026-10-12** · Everyday editing partials: number fields (scrub rates with Shift and Alt, arrow keys by the nudge - preferences, arithmetic with brackets, relative edits of Mixed values per layer), outline mode's hidden layers and - object bounds, Hide when publishing in the Assets panel, a cursor for every tool and modifier, panel widths that are - kept, and dark pages (white text on them) for files made in the dark theme -- **2026-10-12** · Sharing with a join code: Share ▸ **Online** (through the server in Preferences, built in) or - **Local network**, one Start sharing button and a short code to read out (`K7M-2QX-9TD`; a longer one locally that - carries the host's address); others type it under Join with a code (the popover, the home screen), where a link - works too. The server names links with codes, keeps them hashed and limits wrong ones; `collab.share`, - `collab.join {code}`; servers must be upgraded to hand codes out (older ones keep working with links) -- **2026-10-12** · Text metrics measured: our example documents, re-saved by other software, were read back and - compared layer by layer. Line heights are now whole pixels, advances sit on a 1024-per-em grid and letter spacing - goes between glyphs only; all 230 self-sizing text boxes in the 7 documents match to the pixel (24 of 131 before - in the design-system example). One font family; real files still unchecked (gap 3). No number moves -- **2026-10-12** · Live sessions are per document: every open file has its own session or none, several run at - once (host one, join another, publish a third) and keep syncing behind the tab in front, the Share popover, - people, cursors, chat, spotlight and follow are the file in front's, `collab.peers` lists them all (`sessions`), - and closing a file ends its session only (before, one session followed every tab and starting another replaced it) -- **2026-10-12** · A public `figcraft-server`: `open_publish = true` lets anyone publish without the server key - (Share ▸ Publish to server asks the server first and shows who hosts it, its terms and how long unopened files are - kept), with an owner token shown once that manages the file later (`collab.manageFile`); limits per address, - idle expiry, reports (Share ▸ Report this file…), `block-ip` and `files list --sort`, `GET /healthz`, and - `packaging/server/install.sh` that sets up a Debian/Ubuntu machine with Caddy, ufw and backups - ([server.md](docs/server.md#running-a-public-server)). Numbers unchanged (hosting, not a Figma feature). -- **2026-10-12** · `.fig` export opens elsewhere (gap 1): written files were checked by opening them in other - software: layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft. The - export now says that the document and its pages are visible and opaque, numbers its schema as the format does, - and writes outlines, text settings, standard font style names and line height units; effects such as glass and - noise, images and prototype interactions are still unchecked there, and variables are not written. Numbers - unchanged. -- **2026-10-12** · Experimental `.fig` export (gap 1): `file.export {format: "fig"}`, File ▸ Export… ▸ .fig - (experimental), `figcraft-cli export --format fig` and MCP's `export` write the whole document: layers, - geometry, paints with their images, strokes, effects, styled text, vector networks, auto layout, components and - instances with their overrides, and prototype interactions; what's left out (style links, variables, comments, - Dev Mode data…) comes back as warnings. Checked only by reading the files back with our own importer. File - formats 60%. -- **2026-10-12** · Keyboard shortcut parity (gap 4): a table test with a row for each of the 129 - shortcuts in the public documentation, macOS and Windows/Linux keys each, and the 48 bindings it - is silent on pinned; 126 bound (110 before), Run last plugin moved off a documented conflict on - Windows, 2 missing. New: others' cursors toggle, list levels on Cmd/Ctrl+] and [ while editing, - keyboard box selection on Alt+Space / Ctrl+Space, arrows that pan with nothing selected, Control - suspending snapping on macOS, Space/N/C/F while presenting, the Frame tool on A, pixel grid on - Shift+', link on Shift+Cmd+U, ungroup on Cmd/Ctrl+Backspace, suggest auto layout and go to main - component keys; shifted symbols on other keyboard layouts. -- **2026-10-12** · Plugin directory on `figcraft-server` (gap 6): publish a plugin folder with the server key - (`figcraft-cli plugin publish`), browse a server's plugins in Plugins ▸ Manage plugins ▸ Browse, install after a - confirmation that shows the permissions it asks for, update when a newer version appears, remove; bundles are - checked in full on the server and again in the app, downloads must match their SHA-256, installed plugins live - per user in the same sandbox; `plugin.directory.*` for scripts and MCP; the operator moderates with - `figcraft-server plugins`. Plugins & widgets 60%. -- **2026-10-12** · Plugin widgets and the panel tail (gap 6, plugin API 3; older plugins run unchanged): a widget - plugin puts a layer of its own on the canvas (Plugins ▸ Widgets, `widget.insert`), described by the plugin, - validated and built with locked layers inside; clicking a part it named calls it back with the widget's state - (kept in the layer, so it saves, copies, undoes, exports and syncs) as one undo step, and it redraws; - `widget.click` and `widget.list` for agents; a Counter example. Panels of several plugins open side by side and - plugin colour fields open the colour picker. Plugins & widgets 55%. -- **2026-10-12** · Layers and Assets panels behaviour suite (gap 4): several layers dropped together land - together, Collapse layers keeps the selection's rows open, dragging across eyes or padlocks sets several layers - at once, a Rename layers dialog for several layers (number codes as documented, regular expressions, a start - value), pages dragged to reorder, dividers, "Copy of" duplicates, Assets folders by page, frame and slash names, - and hidden-from-publishing names that decide both ways. -- **2026-10-12** · Vector multi-edit and the vector edit tail (gap 9): several vector layers in edit - mode at once (select, marquee, drag, nudge, delete, mirroring and radius across them), several - Bezier handles dragged together, rounded points beside curved segments, a stroke cap and join per - point, and the Pen connecting to its own points or continuing a selected open path. -- **2026-10-11** · Canvas navigation, guides and grids behaviour suite (gap 4): zoom, panning, the pixel - grid (now from 400%), rulers, guides (top-level frames only; select, Delete, nudge, Remove guide, - Alt-drag copies, Alt measures while dragging), layout grids (Auto counts; hidden on rotated frames), - outline mode (clicks reach layers behind) and measuring, each rule from the documentation. -- **2026-10-11** · Export and inspect behaviour suite (gap 4): exports cover what a layer paints (shadows - only where they fall, true stroke widths, no room for hidden layers, a mask group's mask), slices export what's under - them, layer names with slashes export into folders; export settings gained contents only, include bounding box and - the SVG options (outline text, ids, simplified strokes); Inspect shows colours in hex, RGB, CSS, HSL or HSB, CSS in - rem, blend modes and positions from the frame, and lists icons and images as assets. Dev Mode 70%. -- **2026-10-11** · `.fig` import's tail (gap 1): vectors open as editable vector networks with their - corner radii, handle mirroring and end caps (outlines when the network is missing or damaged); sticky notes, shapes - with text, connectors, tables and similar layers import as frames, text and vectors instead of being dropped; - set-variable and conditional prototype actions come across. File formats 58%, ready ~49%. -- **2026-10-11** · Paint and effects behaviour suite: every documented rule for fills, blend modes, - gradients, images, colour models, shadows, blurs and effect exports is a table-driven case. Fixes: Normal - groups isolate and clipping frames don't, plus darker is drawn, drop shadows hide behind translucent - layers in every export, Fill images reach the box's edges, "Normal" no longer means pass-through. - Paint 70%, effects 75%. -- **2026-10-11** · `.fig` import, best effort (gap 1): `file.open`, `file.import`, drag and drop, the CLI and - MCP open `.fig` files. A generic kiwi decoder reads each file's own schema, and the decoded nodes map by field name - onto the REST importer: layers, paints and styles, text runs, vectors as outlines, auto layout, components and - instances (expanded with their overrides and property values), variables, prototyping and images from the archive; - the rest is a warning. File formats 55%, ready ~49%. -- **2026-10-11** · Gamut warnings (gap 18): the colour picker of a Display P3 file flags colours - outside sRGB and View ▸ Soft-proof sRGB stripes them on the canvas. Showing P3 itself waits on - eframe/egui-wgpu exposing the surface colour space wgpu 30 already supports. -- **2026-10-11** · Several windows (gap 18): File ▸ New window, Open in new window on a tab or a - tab dragged off the strip; each window has its own tabs, panels and canvas, draws only when it - needs to and asks about unsaved changes when it closes (the main window too); the control channel - and MCP open, list, focus and capture windows. Hardware 60%. -- **2026-10-11** · Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls, - certificates always verified; `FIGCRAFT_CA_FILE` for a private authority), releases ship the server for Linux - x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` walks through deploying it on - a rented Linux server (systemd unit, Caddy or nginx, firewall, Docker, upgrades, backups). -- **2026-10-11** · Plugin panels and settings (gap 6, ABI v2; v1 plugins run unchanged): a plugin shows a panel of - fields and buttons drawn with FigCraft's widgets and is called back for each button or changed value (one undo step - per callback, same sandbox limits), `plugin.ui` / `plugin.event` / `plugin.close` drive panels from scripts and MCP, - and per-plugin settings are kept with the preferences; Swatch grid gained a palette panel. Plugins & widgets 45%. -- **2026-10-11** · Canvas fixes: clicks hit boolean groups where their result is (a click in a - subtracted hole reaches what's behind), a layer dragged inside a group stays in it, View ▸ Frame - outlines and View ▸ Slices draw, and a flaky idle test now waits for the render thread (gap 13). -- **2026-10-11** · Team libraries on `figcraft-server` (gap 10): Assets ▸ Libraries ▸ Publish to server keeps - the file's library on the server under the file (per-asset versions; the server's version is followed); other files - enable it by pasting a share link (a view link is enough), get "Library updates available" from a background check - and accept with overrides kept; the server counts which files use which components (Show usage, `library.usage`); - `library.swap` re-points instances, styles and variables to another library by names. Team libraries 70%. -- **2026-10-11** · Animated GIF fills (first frame on the canvas and in exports, played in the - presenter and the preview, waking the window only for the next frame) and TIFF images (gap 12). -- **2026-10-11** · The last three menu items got commands: File ▸ Open recent (kept in the - preferences, missing files greyed, Clear recent), outlines for the selection and mask outlines - (gap 21). Menu breadth 203/203. -- **2026-10-11** · Extended variable collections (a library collection's modes and variables in a - collection of the file, overridden per mode with reset, following library updates), variables - import/export (the REST local variables JSON, merged by name; CSS custom properties), gradient - stops and variant properties bound to variables (gap 14). -- **2026-10-11** · Live boolean editing: operands stay editable layers (double-click or Cmd/Ctrl-click - selects one, moving, resizing or editing its points redraws the result during the drag with every - operand outlined, per-operand operations from the boolean controls, prefix-cached recompute); - Design panel arc fields; the shape builder on strokes and open paths; one-sided variable widths - (gap 13). Feature breadth ~79%. -- **2026-10-10** · WASM plugins (#101: sandboxed wasmi runtime, manifest permissions, Plugins menu, - Run last, Manage, Rust SDK and two examples), docs refresh (#102), the multiplayer core without - networking (#103: client-scoped ids, change sets, server ordering and validation, client - reconciliation, convergence tests). Menu breadth 200/203; ready for real work ~47%. -- **2026-10-10** · Full re-measure against Figma desktop 126.10.7 under the craftrules progress-docs - standard: ready for real work ~46%, feature breadth ~76%, stage alpha; progress docs split out - (`docs/target-app-parity.md`, `gaps.md`, `roadmap.md`, `architecture.md`, the per-area parity - docs); `docs/parity.md` renamed `docs/parity-checklist.md`. -- **2026-10-10** · Day 2 (PRs #1–#99): vector edit mode depth (transform box, point snapping, arc - handles, shape builder, variable width), text depth (variable fonts, bidi, RTL names, style - browser), auto layout nesting and keyboard reordering, slots and library variables, components - at scale (2,000 instances), effects fidelity (glass, noise, texture, progressive blur), - prototyping (conditionals, expressions, device frames, responsive presentation), REST import of - prototypes and variables, SVG import fidelity, PDF print options, Dev Mode (code, annotations, - ready for dev, compare changes), comments polish, version history, keyboard shortcut parity, - accessibility, retained canvas and 50k-layer performance, no redraws while idle; v0.2.0 - released for every platform; multiplayer protocol designed. -- **2026-10-09** · Day 1: foundation (13 crates, layering, command registry, renderer, engine, - UI3 frontend, control channel), CLI and MCP, web build, PDF export, packaging, first depth - passes on text, components, prototyping and comments. - -## Revision history - -| Date | Change | Summary | -|---|---|---| -| 2026-10-15 | minor | M4: Outline stroke preserves even-odd stroke holes. Pixel comparisons cover alignment, dashes, individual side weights, vector paths and stroke-only opacity; native save/reopen, Undo/Redo and instance opacity inheritance/overrides are covered by `outline_stroke`. Existing estimates unchanged; filled-shape compositing needs separate work. | -| 2026-10-15 | minor | M7: system font discovery skips entries observed as non-regular files before opening them, avoiding the reproduced named-pipe stall. Regular-file symlinks remain supported; directory depth, font limits and priorities are unchanged. Numbers unchanged. | -| 2026-10-15 | minor | Typography spacing icons scrub with the shared number-field gesture, modifier rates and one Undo step per drag (#373). Units and selected-character targeting are preserved. Numbers unchanged. | -| 2026-10-14 | minor | System theme on first run (M7): preferences without a theme default to System; the first frame reads the OS appearance directly, including when returning from a manual theme. Saved Light/Dark choices stay explicit; unavailable OS appearance falls back to Light. Numbers unchanged. | -| 2026-10-14 | minor | Home screen recent files as cards (M7): each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged. | -| 2026-10-14 | minor | Lost images reported on loading (#207): a `.figcraft` file whose image member is damaged, too large or missing no longer loads in silence; each lost image (up to 20, then a count) is listed in `repairs` on `file.open` with its hash and the reason, and the app shows it as a toast. Saving still writes the file without the lost images. Numbers unchanged | -| 2026-10-14 | minor | SVG import budgets (#190): gzipped SVG is unpacked by FigCraft within 64 MiB before parsing (it was unpacked without a limit), so the nesting check sees the real document; SVG documents embedded in `` data URIs share one 32 MiB budget and at most 4 levels, and one past a budget is left out. Linked files stay unread. Numbers unchanged | -| 2026-10-14 | minor | Leptos code (#223): `inspect.code`, `inspect.componentCode` and `codeConnect.set` take `language: leptos`: a `#[component]` with `#[prop]` defaults, variant enums and `view!` markup, instances as calls, Code Connect `use` lines; the Dev Mode language list and rem units include it. Numbers unchanged | -| 2026-10-14 | minor | Full screen (#227): View ▸ Full screen (`view.fullscreen {on?}`, F11 on Windows and Linux, Ctrl+Cmd+F on macOS) makes the window fill the screen and leaves it again; FigCraft's own item. Numbers unchanged | -| 2026-10-14 | minor | Colour picker: wrapped rows of page and recent swatches stay inset on both sides instead of running past the picker's right edge. Numbers unchanged | -| 2026-10-14 | minor | Server snapshots (#188): figcraft-server never replaces a readable snapshot with one too large to be read back, and reads snapshots up to twice the file cap (recent batches kept for reconnecting clients may take a file over it). Numbers unchanged | -| 2026-10-14 | minor | Preferences owner-only on Unix (#200): `state.json`, which holds online owner tokens, is written with mode 0600 from the first byte, and an older world-readable file is narrowed. Numbers unchanged | -| 2026-10-14 | minor | ruzstd 0.9.1 (#182): `.fig` import requires the release that fixes RUSTSEC-2026-0344 (a zstd decompression bomb). Numbers unchanged | -| 2026-10-14 | minor | Layer cycles (#205): `validate` reports children listed twice, pages with a parent and layers not reachable from a page (cycles, detached subtrees); `repair` drops them in one linear pass. Numbers unchanged | -| 2026-10-14 | minor | AppImage CLI: `figcraft.AppImage cli ` runs the bundled `figcraft-cli` (MCP server included) on Linux without a second download. Numbers unchanged | -| 2026-10-14 | minor | Boolean outline depth (#193): nested boolean groups share one outline depth budget instead of each level getting its own. Numbers unchanged | -| 2026-10-14 | minor | Command batches roll back as one: a failed `command.batch` (MCP `batch`) restores every open document and its state, and puts the tab in front back. Numbers unchanged | -| 2026-10-14 | minor | Desktop builds no longer rebuild the text crate on every build when the craft-fonts manifest is missing. Numbers unchanged | -| 2026-10-14 | minor | Direct3D 12 on Windows: the desktop app asks wgpu for Direct3D 12 there instead of Vulkan, so it starts without Vulkan loader errors on machines with stray overlay layers. Numbers unchanged | -| 2026-10-14 | minor | Plugin metering (#192): every host call and every byte copied between a plugin and the host costs fuel, so a plugin can't stall the app through cheap-looking host calls. Numbers unchanged | -| 2026-10-14 | minor | SVG/PDF export depth (#208): explicit preflight refusal beyond 256 descendant levels, propagated through public APIs, commands and CLI, with boundary/content and destination-preservation regressions. Numbers unchanged. | -| 2026-10-13 | minor | MCP/control transport hardening (#201): frame limits during reads, bounded connections and both UI request queues, socket inactivity timeouts; percentages and estimates unchanged | -| 2026-10-13 | minor | Plugin log limits (#191): cap each UTF-8 line at 8 KiB, total log bytes at 1 MiB and lines at 1,000 per run/callback before copying guest memory or forwarding to the host. Over-budget runs fail and roll back document edits; callbacks get fresh budgets. Added sandbox boundary/forwarding tests and an engine rollback regression. Percentages and hours unchanged. | -| 2026-10-13 | minor | Live-session revert guard (M9, #198): `file.revert` is refused while its document belongs to a live session, including publishing, reconnecting and hosting before the welcome. The document, previews, branch tabs and session stay intact; leave the session before reverting a local save. Offline revert is unchanged. Scripted access and sync regressions in `live_access_tests`. Numbers unchanged. | -| 2026-10-13 | minor | Repeat last command (M1, #181): keep the underlying command and parameters across repeats, reject self-reference, and retain one undo step per repeated edit. Numbers unchanged. | -| 2026-10-13 | minor | Component playground (M3): with an instance, a component or a component set selected, the Inspect tab shows a Playground: a preview of the component and a control for each variant, boolean and text property. What is tried there changes the preview and the code in the Code section, never the file (no undo step, the canvas instance stays as it is); Reset goes back to the component's or the instance's own values. `component.playground {id?, properties?, language?, path?}` gives the same to scripts: the properties with their values, the code, and a PNG on request (`playground_tests`). Left: instance-swap and slot properties in the playground (the command takes an instance swap; the panel has no control for it), a code connect template's snippet with the tried values. Numbers unchanged. | -| 2026-10-13 | minor | Auto layout, on-canvas handles of automatic gaps (M2): the handle between spread rows or columns (and between items with an auto gap) shows the distance the layout gives the gap instead of the stored value, and dragging it makes the gap a fixed one from there, as typing a gap in the panel does; one undo step (`gap_handle_tests`). Before, the drag changed a value that had no effect. The documentation is silent on dragging an automatic gap. Numbers unchanged. | -| 2026-10-13 | minor | Variables inside styles (M3): a shared style can bind variables to fields of its value (`style.bind {style, field, variable?}`: a paint style's colours and gradient stops, an effect style's colour, radius, spread and offsets, a grid style's size, count, gutter and offset, a text style's font size, line height, letter spacing, family and style). Every layer using the style shows those fields resolved in its own modes and stays linked when a variable or a mode changes; the style keeps them resolved in the default modes. A style made from a layer keeps that layer's bindings; a new value drops bindings to what is gone. The style editor binds from its property rows (paint colour, shadow colour, blur radius, grid size) and detaches from the variable's chip; the style picker marks styles that use variables. `.fig` export and import carry the bindings on the style, REST layers hand theirs to the style they use. Left: binding text style fields and the other effect and grid fields from the editor (commands only), library variables in the picker, a style's library copy. Numbers unchanged. | -| 2026-10-13 | minor | Effects on groups, inner shadows and background blur (M3): a group is no outline of its own, so its inner shadows are cast inwards from around what its layers draw together (their pixels, over them: no shade at a seam between two layers) and its background blur and glass show the backdrop through the outlines of its layers (under them). Canvas and PNG; SVG embeds a group with an inner shadow as a render and its background blur as an image under the layers; PDF places both as images, the blur matching the PNG. Where the group holds one shape the pixels match that shape, or a frame filled like it, carrying the effect (`group_effects_tests` in render, svg and pdf). Offsets and spreads of a group's inner shadow go by whole pixels. Numbers unchanged. | -| 2026-10-13 | minor | Text made by commands on a dark page (M1): in the app, `text.create` and `node.create {type: text}` that name no fill give white text on a dark page, by the same rule as the Text tool (the page's colour decides, also inside a frame); a named `fill`, `fills` or `fillStyle` is kept. Sessions without an interface (CLI, MCP, exports) still make black text everywhere. Numbers unchanged. | -| 2026-10-13 | minor | Frames and groups, a one-step convert (M1): `object.convertToGroup` and `object.convertToFrame` (the Design panel's type menu on a frame or a group) change the layer in place, its layers staying where they are on the page. A frame turned into a group loses what only a frame has (auto layout, with the positions it gave kept and fill children fixed; fills, strokes, corner radius, clipping, layout grids, guides, scrolling) and takes its layers' box; a group turned into a frame keeps its box, with no fill and no clipping. Instances, main components, layers inside instances and empty frames are refused with the reason, all or nothing; one undo step. Numbers unchanged. | -| 2026-10-13 | minor | Prototyping, delays on mouse triggers and manual overlay offsets (M5): mouse enter, leave, down and up wait `delayMs` before their actions run (once while waiting; dropped when the screen or overlay they were on goes away; a pointer that leaves early doesn't cancel, the documentation is silent), and a manual overlay sits at `offset: [x, y]` from the top-left of the layer that opens it (without one, right below it, as before). Delay and X/Y fields in the interaction editor, the player on its own clock, REST import (`delay`, `overlayRelativePosition`), `.fig` import and export (the export's field for mouse delays is unchecked against other software). Numbers unchanged. | -| 2026-10-13 | minor | Auto layout, wrapped-line alignment (M2): a wrapping layout's lines (rows of a horizontal one, columns of a vertical one) are packed where the cross alignment says (start, centre, end; before, they always sat at the start), stretch when every child fills across the flow, or take the free space between them (`contentAlign: spaceBetween`, the Design panel's Row/Column gap mode, the alignment pad lighting the whole cross axis). Read from REST JSON and `.fig`, written to `.fig`, `align-content` in CSS, an instance override with the other alignment settings. 13 hand-computed layout cases and command tests. Numbers unchanged. | -| 2026-10-13 | minor | `.fig` export (gap 1): instance sizes (resized, filling an auto layout, scaled), variables with modes and bindings, shared styles and variable-width strokes are written and read back. Checked by opening written files in other software: resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width; number, string and boolean bindings and bindings on effects, layout guides and gradient stops come across as the values they give but did not show as bound there (their entries now also name the layer field they set, not yet checked there), and strokes wider on one side of the path are written as filled outlines, no longer editable strokes there (the stroke itself is kept as plugin data and our own import puts it back; not yet checked there). Numbers unchanged | -| 2026-10-13 | minor | Starter templates and a first run that explains itself: eight templates built in code through the command layer (mobile app, landing page, dashboard, design system starter, wireframe kit, social post sizes, presentation slides, icon set starter; original and synthetic, deterministic, about 10–190 ms each to build), `template.list` and `file.newFromTemplate` for the app, CLI and MCP, File ▸ New from template…, a Templates tab on the home screen (cards with thumbnails rendered from the documents, search, kind filter, keyboard and screen-reader names) in place of the Community placeholder, a row of templates on Recents while there are no recent files, a Getting started card that can be closed and comes back from Help ▸ Getting started (`help.gettingStarted`), and a first run that opens on the home screen. The Drafts placeholder is gone (the app has no drafts folder). Essentials ~57% → ~59% (discoverability ×0.92 → ×0.94), its hours ~60–110 h → ~55–100 h; other numbers unchanged | -| 2026-10-12 | minor | Your online files (M9): a file put on a server without its key stays its owner's after its tab closes or the app quits. The app records each one with the preferences (server, id, name, owner token, the owner's links, the codes others join with; at most 200; the token never in a command result, `ui.inspect`, a log or debug output) and Home ▸ **Online** lists them: open one as its owner (a new tab, or its tab comes to the front), Start / Stop sharing, Copy code / Copy link, Rename on the server, Remove from server (confirmed), Forget (the file stays on the server). Nothing is asked of a server until the section opens; then one request per server (`Admin owned`, a compatible addition within protocol version 2) says which files are still there, and a server that is offline or older leaves the list as recorded. Tabs of files in a live session carry a mark (shared, or open only to you), toasts about a file that isn't in front name it, and after a crash a recorded file reopens from its server as its owner, its recovery copy kept until its unsent changes are back on the server (or it is opened beside the file). `collab.files`, `collab.openFile`, `collab.forgetFile`; `collab.manageFile` takes its server and token from the record and gains `share` / `unshare`. The draft catalogs (French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese) translate the 71 strings and 5 command labels they lacked and the new ones. Numbers unchanged | -| 2026-10-12 | minor | Everyday editing partials closed (gap 4): number fields scrub a step a point (×10 with Shift, ×0.1 with Alt, on past the screen edge where the platform reports it), step with the arrow keys by the nudge preferences, take brackets, units and relative edits (`+10`, `*2`, `50%`), commit on Tab and nothing when left untouched, and on Mixed apply relative edits to each layer in one undo step; outline mode's Include hidden layers and Include object bounds (`view.outlinesHiddenLayers`, `view.outlinesObjectBounds`), drawn and clickable; Hide when publishing from the Assets panel (`component.hideWhenPublishing`); a cursor for every tool, modifier and hover target (rotate turned to its corner, pen states, eyedropper, comment, copy, not-allowed), with guides and handles keeping theirs; panel widths with limits, double-click reset and the preferences (`view.panelWidths`); a file or page made in the dark theme has a #1E1E1E page (sessions without an interface keep #F5F5F5) and text drawn on a dark page is white. Menu breadth 211/211. UI/UX fidelity 58% and the interaction pass 40–60 h unchanged at this rounding: the side-by-side comparison is what remains | -| 2026-10-12 | minor | Sharing with a join code (M9, from the owner's use: sharing wasn't intuitive): the Share popover is one flow, **Online** (the file is published to the server in Preferences, built in `wss://collab.craftapps.dev/`, and the server names its edit and view links with nine-symbol codes like `K7M-2QX-9TD`) or **Local network** (the app hosts; a 20-symbol code carries its IPv4 address, port and a 44-bit secret, with check bits), one Start sharing button, the code large with Copy code and Copy link, who is here, can edit / can view online, and Stop sharing (online: the others' links and codes are revoked and they are disconnected, while the file stays on the server with its owner in it and can be shared again with new codes; removing it is a separate, confirmed step); **Join with a code** is one field (popover, home screen, Join dialog) for a code of either kind or a link; addresses, keys and ports moved under Advanced and Preferences ▸ Server for online sharing (reset, or empty to turn online sharing off). Protocol additions within version 2 (`Resolve` / `Resolved`, `Admin code`): the server keeps a stretched hash and the wrapped token, never the code, and limits wrong codes (10 a minute per address, a 15-minute lockout after 10 wrong, 1,000 wrong an hour for everyone); codes go with their link and file. Older servers leave the links in place of a code; older apps join by link. `collab.share`, `collab.join {code}`, `code` and `share` in `collab.peers`; the browser joins by code. Numbers unchanged | -| 2026-10-12 | minor | Live sessions per document (M9, a fix from real use): the engine keeps one session per open document instead of one for the app, the link carries several under their own ids (desktop, browser, tests), the window and the `collab.*` commands follow the active document, `collab.peers` gains `session` and `sessions`, `ui.inspect`'s `live` gains `session` and `sessions`; joining opens the session's file in its own tab without touching other sessions and closing a file ends its session only. Numbers unchanged | -| 2026-10-12 | minor | `.fig` export checked by opening written files in other software (gap 1): layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft, after fixes to what the export writes (visible document and pages, the format's schema numbering, outlines, text settings, font style names, line height units); effects such as glass and noise, images and prototype interactions still unchecked there. Numbers unchanged | -| 2026-10-12 | minor | Public `figcraft-server` hardened against resource exhaustion after a security review: messages capped by what a connection is for (a file's size when open), byte-bounded send queues and a bound on bytes waiting for the hub, files checked as they grow (refused batches roll back with the reason; per-address and total caps count growth), the core's recent-batch memory bounded in bytes, no confirmation for changes that couldn't be written, team libraries only in key-published files on public servers with unpacking bounded by the file cap, usage reports kept in memory, layered IPv6 limits (/64, /56, /48), a global publish rate, connections reserved for sessions, per-address download shares, library connections cut on revoke and removal, a private data directory and key, and safer install.sh, systemd and backups. Percentages and hours unchanged | -| 2026-10-12 | minor | A public `figcraft-server` (open hosting): with `open_publish = true` anyone publishes a file without the server key and gets its edit and view links plus an owner token (128 bits, shown once, stored hashed) that lists, makes and revokes its links, renames it or removes it (`collab.manageFile`); limits per address (connections at once and a minute, messages a second, publishes an hour, files and MiB kept; IPv6 by /64; `X-Forwarded-For` believed only from `trusted_proxy`; tables capped at 10,000 addresses) and for everyone (`max_total_mb`), each refusal saying why; files published without the key expire after `idle_days` unopened (the date shown to everyone in the file); moderation (`files list --sort size|age|idle`, `block-ip`, `reports`, a Report button and `collab.report`); the operator's contact and terms shown in the Share dialog; `GET /healthz` on the same port; `packaging/server/install.sh` for a Debian/Ubuntu machine behind Caddy and a nightly backup timer. Protocol additions within version 2, gated like signals (older apps get clear refusals). Hosting infrastructure rather than a Figma feature: percentages and hours unchanged | -| 2026-10-12 | minor | Experimental `.fig` export (gap 1): `file.export {format: "fig"}`, File ▸ Export… ▸ .fig (experimental), the CLI and MCP write the whole document (layer tree, geometry, paints with image bytes, strokes, effects, text with styled runs, vector networks, auto layout, constraints, components, sets, instances with their overrides and property values, prototype interactions) with a kiwi encoder mirroring the decoder and the schema of the fields written; what's left out is a warning; verified only by round trips through our own importer. File formats 59% → 60%; import/export 84%/57% → 86%/58%; file exchange stays ×0.85; mainstream hours ~130–244 h → ~120–229 h; feature breadth (~84%), ready for real work (~53%), essentials (~57%), mainstream (~49%), beta (~155–280 h) and full parity (~265–523 h) unchanged at this rounding | -| 2026-10-12 | minor | Keyboard shortcut parity suite and bindings (gap 4): a table test with one row per shortcut in the public documentation, macOS and Windows/Linux keys each (129 rows: 126 bound, 110 before; 1 moved off a documented conflict; 2 missing; 2 kept by a platform), plus 48 pinned undocumented bindings so every default binding is in the table; newly bound: A (Frame), Shift+' (pixel grid), Alt+Cmd+\ (others' cursors, new `view.multiplayerCursors`), Ctrl+F6, Shift+Cmd+U (link), Cmd/Ctrl+] and [ (list levels while editing, new `text.listIndent` / `text.listOutdent`), Cmd/Ctrl+Backspace (ungroup), Alt+Space / Ctrl+Space (keyboard box selection, Cmd/Ctrl+arrows draw it out), suggest auto layout, go to main component, arrows that pan with nothing selected (new `view.pan`), Control suspending snapping on macOS, Space/N/C/F while presenting; symbols another layout types with Shift count without it; no default binding uses a system-reserved chord (tested). Menu breadth 207/207 (View ▸ Multiplayer cursors). UI/UX fidelity 58% and the interaction pass 40–60 h unchanged at this rounding | -| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Localization 36% → 39%, 31–54 h → 29–51 h; ready for real work unchanged (~53%); full parity ~267–527 h → ~265–524 h | -| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Localization 35% → 36%, 32–55 h → 31–54 h; ready for real work unchanged (~53%); full parity ~268–528 h → ~267–527 h | -| 2026-10-12 | minor | Prototyping tail (gap 15, the part that doesn't need video): key/gamepad triggers take controller buttons (`Gamepad A` … `Gamepad Home`, the standard layout's 17 buttons, from a Controller menu beside the key field; the REST import maps controller triggers instead of skipping them); the player reads controllers on Windows (Windows.Gaming.Input through the `windows` crate the app already used; no new dependency) and in the browser (the Gamepad API), polling only while the prototype has such a trigger and the window has the focus, and the control channel presses buttons (`ui.input {gamepad}`); macOS, Linux and FreeBSD read no controllers yet. Widgets react to clicks in a presented prototype (the canvas's callback and sandbox limits, on the prototype's copy: the file and its history don't change, restarting starts over). `proto.evaluate` computes an expression where a layer's interaction would run (MCP); the mode lookup and set-variable-mode rules are cases in a behaviour suite (they already held). Prototyping breadth 90% → 92%, ready unchanged at 74%, 3–6 h → 2–5 h; area hours 120–260 → 119–259 (core 33–75 → 32–74), Features (depth) 29–68 h → 28–67 h; percentages unchanged at this rounding; full parity ~276–538 h → ~275–537 h | -| 2026-10-12 | minor | Plugin directory on `figcraft-server` (gap 6): the server keeps plugin bundles (manifest, module, up to 32 assets; an uncompressed length-prefixed format named by its SHA-256) published with the server key (`figcraft-cli plugin publish`, `plugin.directory.publish`), checked in full before they are kept (size cap, manifest and plugin API version, a version newer than the one kept, the module compiled but never run, plain asset names) and capped (`max_plugins`, `max_plugin_mb`, `max_plugins_mb`); anyone who reaches the server lists, searches and fetches them (hash-checked again on read, a shared download budget); the operator moderates with `figcraft-server plugins [remove]`. The app's Plugins ▸ Manage plugins gains a Browse tab: a server's plugins with Install / Update / Remove, an install confirmation that shows the permissions asked for, the source and the SHA-256, the download checked against the listed hash before anything is read, plugins kept per user in hash-named folders; `plugin.directory.list` / `install` (needs `confirm: true`) / `publish` / `unpublish` for scripts and MCP. Not in the browser build yet; no public catalogue, publisher accounts, signatures, ratings or background update checks. Plugins & widgets 55%/32% → 60%/35%, 10–30 h → 8–26 h; Ecosystem dimension 34% → 38%; weighted totals unchanged at this rounding; full parity ~281–548 h → ~279–544 h | -| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Localization 30% → 35%, 40–65 h → 32–55 h; ready for real work ~52% → ~53% (52.9 → 53.1); full parity ~284–554 h → ~276–544 h; beta ~162–292 h → ~155–280 h | -| 2026-10-12 | minor | Plugin widgets and the panel tail (gap 6, plugin API 3; v1 and v2 plugins unchanged): a plugin marked as a widget puts a layer of its own on the canvas from Plugins ▸ Widgets or `widget.insert`: it describes the layer (frames with auto layout, text, rectangles, ellipses, images it carries) and FigCraft validates the description (size, layer count, depth, numbers, colours, text and image sizes) and builds it as a frame with locked layers inside; a click on a part the plugin named (on the canvas with the Move tool, a pointing hand over it, or `widget.click`) calls the plugin back in a fresh, metered sandbox with the widget's state, as one undo step, and the widget is redrawn; the state lives in the widget's own layer data, so widgets survive save and reload, copying, undo, export and live sessions; no plugin can write that data or call `widget.*`. `widget.list` lists them. A Counter example widget. Plugin panels of different plugins open side by side, and a colour field's swatch opens the colour picker (the plugin hears of the colour when a drag ends). Plugins & widgets 45%/28% → 55%/32%, 15–40 h → 10–30 h; Ecosystem dimension 28% → 34%; weighted totals unchanged at this rounding; full parity ~286–558 h → ~281–548 h | -| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): every UI string, command label and menu wording (1,899 entries) machine-drafted in each and marked as drafts awaiting native review, falling back to English per key (new strings need only English and Spanish); 0 counts as singular in French and Brazilian Portuguese; System picks the first OS language with a catalog (pt-PT and pt read pt-BR); long translations elide with a tooltip in dropdowns, two-column labels and the home cards. Localization 20% → 30%, 45–75 h → 40–65 h; ready for real work unchanged (~52%); full parity ~289–564 h → ~284–554 h; beta ~170–305 h → ~162–292 h | -| 2026-10-12 | minor | Cursor chat and spotlight in live sessions: `/` opens a chat bubble at your pointer that everyone sees as you type and that fades a few seconds after the last change; a spotlight (your avatar's menu, `collab.spotlight`) makes everyone follow your view and page until they move their own. Ephemeral presence the host validates and rate-limits, passed only to clients that read it (older clients keep working). Multiplayer & sharing breadth 15% → 17%, ready unchanged (5%), 45–90 h → 42–84 h; collaboration 13% unchanged, 49–98 h → 46–92 h; feature breadth (~84%) and ready for real work (~51%) unchanged at this rounding; full parity ~301–587 h → ~298–581 h | -| 2026-10-12 | minor | Layers and Assets panels behaviour suite written from the public documentation (6 engine tests with about 50 table rows and 5 panel tests driven through the UI; about 30 rows that the code before the fixes contradicts) and its fixes (several layers dropped together, Collapse layers, eye and padlock sweeps, Rename layers dialog with the documented number codes, page dragging, dividers, "Copy of" duplicates, Assets folders, hidden-from-publishing names both ways). Layers panel 98%/82% → 99%/85%; percentages and full parity hours unchanged at this rounding | -| 2026-10-12 | minor | Vector multi-edit and the vector edit tail, with a behaviour suite written from the public documentation (5 tests, about 30 table rows and steps, and a junk-parameter sweep of every vector command in multi-edit mode): several vector layers are edited at once (Enter, double-click or `vector.edit.begin {ids}`; a press on another layer's point, handle or segment makes it the active layer; Shift-click, marquee and lasso select across layers; dragging, the arrows, Delete, delete and heal, mirroring, smoothing, corner radius and point caps and joins reach every layer's selected points in one undo step; the pen, bend, paint bucket, cut and eraser work in the active layer); several Bezier handles are selected with Shift-click and dragged together (`vector.edit.moveHandles`); a sharp point next to a curved segment rounds (the curve is cut where the arc meets it and keeps its shape); a point can have its own stroke cap and join (`vector.edit.strokeCap`, `vector.edit.strokeJoin`, the Point section of the Design panel; canvas, PNG, SVG and PDF, outline stroke); the Pen outside edit mode connects to one of its own points and ends the path, and continues a selected open path from an end point. Left: corner smoothing beyond rectangles, the transform box around points of several layers, per-point caps and joins on dashed, variable-width and brush strokes. Shapes & vector 98%/72% → 99%/73%, 2–4 h → 1–3 h; Figma Draw extras 80%/55% → 88%/60%, 2–5 h → 1–3 h (gap 9: pen pressure on macOS and Linux tablets left). Totals recomputed by the doc's method, which rows since the last recompute had left stale: the area table's weighted breadth is 83.7% (~83%; core 94.1%, ~94%) and ready 63.1% (~63%; core 72.1%, ~72%), hours 133–284 (core 38–83); Features (depth) 61% → 63%, 46–94 h → 34–76 h; ready for real work 52.6% (~52%); mainstream weekly areas 71.0% → 72.1% (× 0.9 × 0.9 × 0.85 = 49.7%, ~49%, rounded down); essentials unchanged (~57%); full parity ~306–595 h → ~294–572 h (on top of the rows merged since: ~301–587 h → ~289–564 h; area-table totals recomputed on the merged table and rounded down: breadth ~84%, ready ~63%, core ~94%/~72%, mainstream weekly areas 72.4%, ~49%) | -| 2026-10-12 | minor | Dev resources (gap 16): links for developers (a URL and a name) on any layer: `devResource.add` / `edit` / `remove` / `list`, undoable, saved and synced; an instance also lists its main component's (and that component's set's) links read-only with where they come from, and layers inside an instance show their main layer's; URLs are checked as web links (http or https, a host, no spaces, at most 2,048 characters) and never fetched (a link added without a name is named after its URL), opened only when clicked; the Inspect tab lists, adds, edits and removes them; `get_node` and `inspect_document` return them as `devResources`. The editor extension stays out of scope (the MCP server serves agents and editors). Dev Mode breadth 93% → 96%, ready 70% → 72%, 1–3 h → 0–1 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~304–593 h → ~303–591 h | -| 2026-10-12 | minor | Extended collections' tail (gap 14): variables a library adds to a collection this file extends now count as a library update of their own (listed by name without a copy yet) and arrive inheriting when accepted, instead of waiting for the library's next other update; a library collection that is itself an extended collection can be extended again, values flowing down the chain as each file accepts updates with every level's overrides kept (checked end to end). Styles & variables breadth 94% → 95%, ready unchanged (76%), 3–5 h → 2–4 h; gap 14 1–2 h → 0–1 h (real-library checks left); feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~305–594 h → ~304–593 h | -| 2026-10-12 | minor | A branch's own version history and finer list merges (gap 17): versions saved or autosaved on a branch tab go into the branch (viewed, restored, renamed, deleted and duplicated there, saved in the file with it); merging adds a version to the main file's history and the merged or archived branch keeps its own; fills, strokes and effects merge item by item, conflicting only when the same item changed both ways, an item was removed on one side or both sides added different items. Comments, versions, find, misc breadth 85% → 87%, ready 60% → 61%, 2–5 h → 1–4 h; gap 17 1–3 h → 1–2 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~306–595 h → ~305–594 h (the Features table here still showed 80%/58%/4–8 h from before branches landed) | -| 2026-10-12 | minor | Headline percentages recomputed from the area table by its own method (weights × written scores): feature breadth ~82% → ~84% (core editor ~93% → ~94%), weighted ready ~61% → ~63%, core ready ~70% → ~72%, mainstream weekly areas 71.3% → 72.3% and mainstream ~49% → ~50%; several earlier rows had moved the area scores without carrying them into the totals. Hours are unchanged (left for the next full re-measure) | -| 2026-10-12 | minor | Text lists, hanging punctuation and decoration styles, with a behaviour suite written from the public documentation (7 layout tests and 7 command tests, about 60 table rows): lists are set per paragraph (the list buttons, shortcuts and Type settings act on the paragraphs the selection touches, the whole layer when it isn't being edited), nest up to 8 levels (Tab and Shift+Tab; numbers go 1. a. i., bullets • ◦ ▪, counted per level), Enter on an empty item and Backspace at an item's start step out a level and then out of the list, typing `- `, `* ` or `1. ` at a paragraph's start starts one; list spacing (between items, in place of paragraph spacing), hanging lists (markers outside the box) and hanging punctuation (opening punctuation outside a start-aligned edge, closing punctuation outside an end-aligned one, taking no room when lines wrap); underline and strikethrough style (solid, dotted, wavy), thickness, underline offset, skip ink and colour. Canvas, PNG, SVG and PDF; `node.set` keys (`indentation`, `listSpacing`, `hangingPunctuation`, `hangingList`, `textDecorationStyle`/`Thickness`/`Offset`/`SkipInk`/`Color`), inspect (`lineTypes`, `lineIndentations`), CSS (`text-decoration-style`/`-thickness`/`-color`, `text-underline-offset`, `text-decoration-skip-ink`) and the REST import (`lineTypes` with `lineIndentations` per paragraph, which collapsed to one list style before). Text 87%/68% → 90%/70%, 5–10 h → 3–7 h; mainstream weekly areas 71.0% → 71.2%; other percentages unchanged at this rounding; full parity ~306–595 h → ~304–592 h | -| 2026-10-12 | minor | Comments and version history behaviour suite written from the public documentation (15 tests, about 80 table rows plus stepwise checks; 4 tests failed before the fixes) and its fixes: in a live session, threads made in the session couldn't be replied to, edited, reacted to, resolved, moved, deleted or opened (their ids, from their creator's id space, were refused as out of range); restoring a version brought back that version's comments, losing newer threads and replies (comments now stay, those on layers the version lacks staying where they were); restoring now keeps the replaced design in the history as an autosave first, unless a version of it was just saved or restored; giving an autosave a title makes it a named version (it could be dropped to make room); merging a branch adds the main file as it was and then a `Merged “”` version to its history. Pinned where the documentation is silent: pins go to the innermost frame under them, copies, duplicates and duplicated pages come without comments, replying reopens a resolved thread, versions stay with whoever saved them in a live session (a shared history needs accounts). Left: a branch's own version history, comparing two versions. Comments, versions, misc 85%/60% → 85%/62%, 2–5 h → 2–4 h (this table still showed 80%/58%/4–8 h from before branches landed); percentages unchanged at this rounding; full parity ~306–595 h → ~306–594 h | -| 2026-10-11 | minor | Canvas navigation, rulers, guides, layout grids, outline mode and measuring behaviour suite written from the public documentation (18 tests, about 120 table rows and checks) and its fixes: the pixel grid shows from 400% (it waited for 800%); guides belong to the page or a top-level frame (a guide dropped over a nested frame goes to the frame holding it, and a frame moved into another keeps its guides but they stop showing, snapping and measuring); a guide can be selected, deleted with Delete, nudged with the arrows and removed from the context menu (`guide.select`, `guide.remove` without an index); Alt-dragging a guide drops a copy and holding Alt while dragging one measures to the nearest edges in the frame it crosses; columns and rows count Auto (as many fixed-size ones as fit); layout grids don't show or snap on a rotated frame; in outline mode shapes are clicked on their outlines, reaching layers behind. Left: outline mode's Include hidden layers and Include object bounds. Canvas & navigation 97%/72% → 98%/74%, 2–5 h → 1–4 h (this table still showed 95%/72%/3–6 h from before the outlines landed); percentages unchanged at this rounding; full parity ~320–615 h → ~319–614 h | -| 2026-10-11 | minor | Export and inspect behaviour suite written from the public documentation (139 cases in 19 tests; 47 failed before the fixes) and its fixes: an export covers what the layer paints, so a drop shadow adds room only where it falls, strokes on boxes and ellipses reach their real width (they added up to four times as much), hidden layers take no room and a mask group exports the mask's area; a slice exports everything under it; slashes in layer names export into folders; Copy as PNG copies the whole selection. Added: the export settings' contents only and include bounding box, and for SVG outline text (off writes ``), unique id attributes and simplified strokes (stroke attributes where exact); Inspect colour formats (hex, RGB, CSS, HSL, HSB), CSS in rem, a Blend row, positions measured from the frame around a layer (groups aren't frames), CSS for rotation, blend modes, gradient directions and kinds, text alignment, case and decoration; icons and images in the asset list (`file.exportLayers {assets}`). Import/export 80%/50% → 82%/53% (the row still showed the figures from before `.fig` import); Dev Mode 90%/64% → 93%/70%; mainstream weekly areas 68.1% → 68.4%; percentages and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | `.fig` import's tail (gap 1): vectors open as editable vector networks (points, curved segments, filled areas with their winding, per-point corner radius and handle mirroring, end caps from the end points), falling back to outlines when the network is missing or damaged; sticky notes, shapes with text, connectors, tables, code blocks, slides and similar layers import as the closest frames, text layers and vectors with a warning instead of being dropped; set-variable, set-variable-mode and conditional prototype actions come across. New parsers fuzzed on fixtures generated in code. File formats 55% → 58%, 8–15 h → 5–10 h; import/export breadth 80% → 82%, ready 50% → 54%; feature and area totals and ready for real work (~49%) unchanged; beta ~173–310 h → ~170–305 h; full parity ~321–618 h → ~318–613 h | -| 2026-10-11 | minor | Shapes and vector behaviour suite written from the public documentation and its fixes: rounded vector points are drawn and exported, a boolean group looks like its top layer (a subtraction like the bottom one), Alt draws lines from the centre, stars keep to 3–60 points, delete and heal follows the curve, Shift-click on a point at a corner of the points' box deselects it, outline stroke keeps the stroke's colour style, opacity and effects, the miter field shows degrees, individual corners only for rectangles and frames, Option/Alt+Shift+F flattens, MCP `key` reaches the edit tools. Added: corner radius and star ratio handles, Count and Ratio fields, cut drags that divide shapes into layers, paint bucket drags. Shapes & vector 97%/68% → 98%/72%, 1–3 h → 2–4 h. Totals recomputed from the area table, which earlier rows had left stale: feature breadth ~80% → ~82% (core ~91% → ~93%), Features (depth) 57% → 61% (core 68% → 69%), 60–115 h → 56–109 h, ready for real work ~49% → ~51%, mainstream ~46% → ~48%; full parity ~321–618 h → ~319–615 h (the selection suite's −3/−5 h hadn't reached the totals) | -| 2026-10-11 | minor | Paint and effects behaviour suite written from the public documentation and its fixes: Normal groups and frames isolate their children and clipping frames don't (canvas, SVG, PDF); plus darker drawn on the canvas and in PNG exports; drop shadows hidden behind what the layer covers unless shown behind transparent areas, SVG included, with filter regions that reach as far as the effects; Fill images cover the box to its edges; "Normal" names Normal in commands and the blend menu. Paint 85%/66% → 87%/70% (breadth from the GIF/TIFF row), 5–9 h → 3–6 h; effects 72% → 75%, 3–6 h → 2–5 h; percentages unchanged at this rounding; full parity ~321–618 h → ~319–616 h | -| 2026-10-11 | minor | Frames, groups, sections and masks behaviour suite written from the public documentation (15 frame, group and section tests, 6 mask tests) and its fixes: a layer a mask hides can't be clicked there (clicks reach what's behind; Select layer agrees), a mask group's box and exports are the mask's (plus any layers under it), sections stay on the page or in sections (moves, drags, pastes, grouping and new sections in frames refused or kept on the page), drawing a frame or section around layers puts those fully inside it in it, resizing a section leaves its contents where they are (and a frame resized with Cmd/Ctrl held keeps its layers' sizes too), and X/Y of layers in groups are measured in the frame (groups have no coordinate space). Frames, groups, sections, masks 95%/75% → 97%/80%, 2–4 h → 1–3 h; percentages unchanged at this rounding; full parity ~323–621 h → ~322–620 h | -| 2026-10-11 | minor | Styles and variables behaviour suite written from the public documentation (18 command-level tests) and its fixes: editing a styled fill, stroke, effect or layout grid list (directly, a paint row, an added fill or a bound variable) unlinks that style instead of being overwritten by the next style edit; applying a style replaces variables bound to what it sets; text styles go on text layers only; deleting a variable (or its collection) unbinds the layers using it, which keep their last value, and aliases to it take its values; bindings are type-checked (a colour variable can't drive a width) and number variables fill text content; aliases are checked when a variable is created. Added: duplicate, move (reorder, into a folder) and rename folder for styles, with a Local styles list grouped by type and folder (click to edit, right-click, drag to reorder); duplicate mode, set as default and move column for modes; per-platform code syntax (Web, iOS, Android) used by generated CSS and the variables JSON; font weight, paragraph spacing/indent and text content scopes for number variables. Found missing: variables inside styles. Styles & variables 92%/72% → 94%/76%, 1–2 h → 3–5 h; percentages unchanged at this rounding; full parity ~323–621 h → ~325–624 h | -| 2026-10-11 | minor | Prototyping behaviour suite written from the public documentation (7 model tests, 12 player tests with 21 gestures) and its fixes: on drag starts only for a drag its animation's way and the animation follows the pointer, key combinations (`Shift+K`) fire (and are recorded and imported), an overlay's after-delay timer starts when it opens, instances of interactive components play their variant's interactions and "Change to" changes the right instance, spring presets are physical springs that play until they settle, smart animate overshoots with spring and back easings, fixed layers draw above scrolling content; added scroll to offsets. Prototyping 88%/70% → 90%/74%, 5–9 h → 3–6 h; mainstream weekly areas 68.1% → 68.4%, mainstream ~46% → ~47%; other percentages unchanged at this rounding; full parity ~320–615 h → ~318–612 h | -| 2026-10-11 | minor | Components behaviour suite written from the public documentation (10 tests, 45 table cases) and its fixes: instances now keep overrides of auto layout padding, gap and alignment, layout grids, export settings, prototype interactions and every text property (resizing, paragraph spacing, lists…; these were lost when the main changed); moving, rotating, resizing or re-constraining layers inside an instance, and its auto layout direction, are refused (the instance itself resizes); switching variants keeps an override only where both variants had the same value (text layers keep theirs); swapping from the Assets panel keeps only text overrides; the default variant is the top-left one (instances, Assets thumbnails, code); hidden exposed instances hide their properties; pushing changes from a nested layer goes to the outer main. Added: variant property reordering and duplicate combination warnings. Components 95%/68% → 96%/72%, 6–12 h → 4–9 h; with #133's selection and transform change, core editor ready ~68% → ~69% and mainstream ~46% → ~47% (weekly areas 68.1% → 68.8%); full parity ~320–615 h → ~315–607 h (the totals now also carry #133's area 2 hours) | -| 2026-10-11 | minor | Embedded image colour profiles (ICC in PNG/JPG/WebP/TIFF, PNG sRGB/gAMA/cHRM) converted into the file's colour space when decoded. Paint 85%/66% → 90%/67% (the row had lagged the parity doc), 5–9 h → 3–6 h; totals unchanged at this rounding | -| 2026-10-11 | minor | Layers panel keyboard and stroke variables: F2 renames the selected layer (also from a layer row with the keyboard, which gets it back; Escape there cancels), Shift+F10 (`view.contextMenu`) opens the context menu at the focused row or below the canvas selection and the menu takes the keyboard, F8 (`select.area`) draws a selection box for keyboard-only users (arrows move it, Shift+arrows size it, Enter selects like a marquee; agents pass a box), long lists checked at 10k layers (revealing a layer, expanding or collapsing everything and arrow keys take at most 1.3 ms a frame in the release UI bench; the revealed row takes the keyboard); colour styles on strokes were already complete, and each side's stroke weight now binds to a number variable (`strokeTopWeight`…, bindable fields in the Design panel and REST import). Layers panel 95%/78% → 98%/82%, 2–4 h → 1–2 h; Strokes 90%/72% → 95%/75%, 1–3 h → 1–2 h; mainstream depth 67.8% → 68.1%; feature breadth (~80%), ready for real work (~49%), essentials (~57%) and mainstream (~46%) unchanged at this rounding; full parity ~321–618 h → ~320–615 h | -| 2026-10-11 | minor | Text behaviour suite written from the public documentation (13 text cases, 5 command tests) and its fixes: CJK lines break between ideographs, kana and Hangul and before and after Latin words without breaking inside them, with the line-start and line-end rules (closing punctuation, small kana and the prolonged sound mark never start a line, opening brackets never end one); a style's line height, baseline and decoration come from its own font, so lines with CJK or emoji drawn by fallback fonts keep the spacing of the others; paragraph indent applies to start-aligned text only; carets, Delete and Backspace step over whole grapheme clusters (emoji sequences, flags, combining marks), which also shape with one font; double-click and word moves take one script's run in text without spaces. Added: vertical trim (cap height to baseline; `leadingTrim`, Type settings, REST import, inspect, CSS `text-box`). Text 85%/64% → 87%/68%, 7–13 h → 5–10 h; percentages unchanged at this rounding; full parity ~323–621 h → ~321–618 h | -| 2026-10-11 | minor | Selection and transform behaviour suite written from the public documentation (5 selection tests with 38 table rows, 11 transform tests) and its fixes: Alt-resizing shrinks about the centre too (it could only grow); groups, frames with Scale or stretch constraints and several layers with different rotations scale each layer along its own axes, keeping its rotation and flip (children turned 90° grew along the wrong axis, nested turned groups too); several layers dragged past an edge mirror and flip; a flipped layer reads back the rotation typed; resizes land on whole pixels with the pixel grid on; a press inside a layer smaller than its handles moves it and the nearest handle wins (the first in reach did), with its handles hidden under 16 px; locked layers selected in the layers panel get no handles and aren't dragged with the rest; the background of a top-level frame with layers starts a marquee inside it (it moved the frame); Select inverse skips hidden layers like Select all. Added: equal spacing snapping with labelled gaps, Cmd/Ctrl while resizing a frame ignores its constraints. Selection & transform 95%/68% → 97%/72%, 6–10 h → 3–5 h; core editor ready ~67% → ~68%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~345–666 h → ~342–661 h | -| 2026-10-11 | minor | Windows QA pass (gap 8): everyday workflows on Windows 11 in the desktop app and through MCP; eight fixes: recovery copies of unsaved documents reopen after a crash or a forced quit (`file.recover`), Ctrl moves and deletes by word in text on Windows and Linux, scrubbed fields keep their value, typing on a field Tab reached edits it, Escape closes dialogs, linear gradients keep square bands, panel hints wrap, the window-close question and "Open in new window" are translated. Percentages and hours unchanged | -| 2026-10-11 | minor | Auto layout behaviour suite written from the public documentation (64 layout cases, 8 command tests) and its fixes: a frame is never smaller than its padding, only the frame's inside stroke counts when strokes are included (and new auto layout includes them), auto gaps stop at 0, grid `fr` tracks behave like the CSS `fr` unit (a track never gets less than its content), canvas stacking is drawn, exported and hit (it was stored only). Added: auto gaps evenly and around, vertical wrap, grid cell alignment, strokes and canvas stacking controls. Auto layout 95%/68% → 98%/74%, 6–12 h → 3–6 h; percentages unchanged at this rounding; full parity ~348–672 h → ~345–666 h | -| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | -| 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | -| 2026-10-11 | minor | Localization layer landed (gap 5): per-language catalogs baked in at build time with English text as keys and fallback, placeholders and plurals, command labels by id, Preferences ▸ Language (System follows the OS or the browser) applied live and `pref.language`; English catalog and a complete Spanish draft awaiting native review. Localization 10% → 20%, 60–100 h → 45–75 h; ready for real work unchanged (~47%); full parity ~363–697 h → ~348–672 h; beta ~200–365 h → ~195–355 h | -| 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | -| 2026-10-11 | minor | Plugin panels and settings landed (gap 6, ABI v2; v1 plugins unchanged): a plugin shows a panel of notes, fields (text, number, checkbox, menu, list, colour) and buttons drawn with FigCraft's widgets; a button or a changed value calls it back with every value (same limits per callback, one undo step per callback that edits, rolled back on failure), its module staying loaded while the panel is open; `plugin.ui` / `plugin.event` / `plugin.close` drive panels headless; per-plugin settings (64 KB) kept with the preferences; the Swatch grid example gained a palette panel that remembers the last palette. Plugins & widgets breadth 35% → 45%, ready 20% → 28%, 25–55 h → 15–40 h; ecosystem 20% → 28%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~373–712 h → ~363–697 h | -| 2026-10-11 | minor | Canvas fixes (gap 13 closed): boolean groups are clicked where their result is (a click in a subtracted hole reaches what's behind; with the group or an operand selected its operands are hit, so each can still be picked), a layer dragged inside a group stays in the group (frames and auto layout still take other drops), View ▸ Frame outlines and View ▸ Slices draw on the canvas, and the idle tests wait for the render thread instead of counting frames. Shapes & vector 1–3 h → 1–2 h (vector QA with real files left); percentages, feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~378–722 h → ~375–717 h | -| 2026-10-11 | minor | Stretch and scatter brushes and pen pressure landed (gap 9): eight brushes drawn in code (four stretched along the path, four scattered with spacing, size, rotation and scatter jitter from a seed) in the stroke settings, drawn everywhere strokes are and flattened by outline stroke; the Pencil records pen pressure as the stroke's width profile (Windows pens, browser styluses; macOS and Linux tablets report none) and can draw with a brush; Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–9 h; Strokes 2–4 h → 1–3 h; Hardware 50% → 55%, 15–30 h → 13–27 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~378–722 h → ~358–687 h | -| 2026-10-11 | minor | Text on a path and pattern fills landed (gap 9): text follows a path or shape (start, side, alignment; edited in place; canvas, PNG, SVG and PDF; SVG `` imports as text on a path) and pattern fills tile a layer (grid or half-shifted rows or columns, scale, spacing, alignment; live as the source changes; SVG ``, PDF tiling patterns); Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–10 h; Text 8–14 h → 7–13 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~375–715 h | -| 2026-10-11 | minor | Team libraries on `figcraft-server` landed (gap 10): a file's library is published to the server under the file and enabled in other files by a share link (per-asset versions; updates noticed in the background and accepted with overrides kept), the server counts which files use which components, and a whole library can be swapped for another by names; Team libraries breadth 40% → 70%, ready 25% → 40%, 10–20 h → 4–8 h; collaboration 12% → 13%, 55–110 h → 49–98 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~380–725 h → ~375–715 h | -| 2026-10-11 | minor | Branches landed (gap 17): File ▸ Create branch… copies the file into a branch tab that remembers its base; Review and merge shows both sides' changes (layers with Inspect-style property changes, styles, variables, comments) and the conflicts (the same property changed on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merging is one undo step and archives the branch; Update from main; branches are saved in the `.figcraft` file. Comments, versions, find, misc breadth 80% → 85%, ready 58% → 60%, 4–8 h → 2–5 h; collaboration 12% → 13%; menu breadth 204/204 (the catalog gained File ▸ Create branch…); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~378–722 h | -| 2026-10-11 | minor | Animated GIF fills and TIFF images (gap 12, without video and HEIC): GIF fills keep the original file, show the first frame on the canvas and in exports and play in the presenter and the preview (frames decoded on first play, at most 500 frames and 128 MB; always looping; the window wakes only for a GIF's next frame); TIFF images open, import, place and paste. Paint breadth 85% → 87%, 5–9 h → 4–7 h; gap 12 6–12 h → 5–10 h (video playback and HEIC left, both needing non-Rust decoders); feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | The last three catalogued menu items got commands (gap 21): File ▸ Open recent (recent files kept in the preferences, at most 20, a file that's gone greyed in the menu and dropped when picked, Clear recent), View ▸ Outlines ▸ Show outlines for selection and View ▸ Mask outlines: menu breadth 98.5% → 100% (203/203); Canvas & navigation breadth 95% → 97%, 3–6 h → 2–5 h; feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | Extended collections, variables import/export and gradient stop and variant property bindings landed (gap 14): Styles & variables breadth 85% → 92%, ready 70% → 72%, 4–8 h → 1–2 h; feature breadth unchanged (~79%); ready for real work unchanged (~47%); full parity ~385–730 h → ~380–725 h | -| 2026-10-11 | minor | Collaboration wording after the standalone server: `figcraft-server` keeps many files, persisted as a snapshot plus a log of what changed (compacted, checksummed, damaged files skipped and reported), restarts lose nothing confirmed and apps resume where they were; the app publishes its file to it; share links per file let people edit or only view (the server refuses a viewer's edits), are revocable and stored as hashes; TLS comes from a reverse proxy (the desktop app has none of its own yet); percentages and hours unchanged until the next re-measure | -| 2026-10-11 | minor | Live boolean editing, Design panel arc fields, the shape builder on strokes and open paths and one-sided variable widths landed (gap 13): Shapes & vector breadth 92% → 97%, ready 66% → 68%, 8–14 h → 1–3 h; feature breadth ~78% → ~79%; ready for real work unchanged (~47%); full parity ~390–745 h → ~385–730 h | -| 2026-10-11 | minor | Collaboration wording after the collaboration polish: styles, variables, collections, modes and comments created in a live session take ids from their creator's id space (concurrent creations no longer overwrite each other; the server checks it), others' text carets and selections show on the canvas, and typing in a layer someone else is typing in says so; percentages unchanged | -| 2026-10-11 | minor | Wording only: live sessions (#104), then the browser joining them and `figcraft://` invite links opening the desktop app; percentages and hours unchanged until the next re-measure | -| 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 | minor | Readiness by audience with hours to ~95% (full ~400–750, mainstream ~180–330, essentials ~85–160) | -| 2026-10-10 | minor | Added essentials user (~53%) and mainstream practitioner (~43%); ready for real work unchanged at ~46% | -| 2026-10-10 | minor | Alpha gate (core workflows) checked under the revised stage rules: passes, stays alpha | -| 2026-10-10 | major | Rewritten to the craftrules progress-docs shape after a full re-measure; rubric, area narrative and inventory moved to docs/target-app-parity.md, gaps to docs/gaps.md, milestones to docs/roadmap.md. Depth ~79% (self-graded, 16 areas) → ready for real work ~46% (all dimensions); full parity 115–195 h → 420–790 h | -| 2026-10-10 | minor | Day-2 refreshes after each batch of PRs (depth 58% → 79%) | -| 2026-10-09 | major | First roadmap | +# FigCraft Roadmap + +**Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents) + +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (Outline stroke preserves stroke holes; Typography spacing icons support scrubbing (#373); font scans skip named pipes; system theme on first run; recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading (#207), SVG import budgets (#190), Leptos code (#223), full screen (#227), server snapshots readable again (#188), owner-only preferences (#200), ruzstd 0.9.1 (#182), layer cycle repair (#205), batch rollback, Direct3D 12 on Windows, plugin metering (#192); vector edit mode regions: hover hatch, selection and drag; SVG/PDF exports refuse over-depth documents (#208); bounded MCP/control framing, connections and queues (#201); plugin log limits (#191); live-session revert guard (#198); attached MCP no longer replays after a lost reply (#194); repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7) + +FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow, +native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through +the CLI, a JSON control channel and MCP. This page is the one-page summary; agents choosing work +start with [`docs/gaps.md`](docs/gaps.md). + +## Headline numbers + +| | Value | Kind | +|---|---|---| +| Menu/command breadth | **100%** (211 / 211) | measured, [`docs/parity-checklist.md`](docs/parity-checklist.md) (`cargo xtask parity`) | +| Feature breadth | **~85%** (core editor ~94%) | estimated, [`docs/target-app-parity.md`](docs/target-app-parity.md) | +| Ready for real work | **~53%** | estimated, same doc (written dimension weights and scores) | +| Essentials user | **~59%** | estimated: core features 77.8% × 0.92 launch/stability × 0.94 discoverability × 0.88 opening files people send ([method](docs/target-app-parity.md#essentials-user)) | +| Mainstream practitioner | **~50%** | estimated: weekly areas 72.7% × 0.9 interaction × 0.9 stability × 0.85 file exchange with Figma users ([method](docs/target-app-parity.md#mainstream-practitioner)) | +| Remaining to beta | **~155–280 h** one agent | estimated | +| Remaining to full parity | **~262–519 h** one agent (~83–145 h wall-clock with 5 agents) | estimated | + +### Readiness by audience + +| Audience | Ready % | Opus 5.5 agent wall-clock hours to ~95% | Work that dominates | +|---|---|---|---| +| Full target (ready for real work) | ~53% | ~268–522 h | multiplayer (42–84), localization (29–51), plugins (8–26), the interaction pass (40–60), GPU canvas (30–60), AI (30–60), the `.fig` import's tail (5–10) | +| Mainstream practitioner | ~50% | ~120–229 h | core editor depth incl. the `.fig` import's tail (49–98), the interaction pass (40–60), real-file corpus and platform QA (25–50), checking the rest of the experimental `.fig` export elsewhere: effects, images, prototypes, real files (5–15) | +| Essentials user | ~59% | ~55–100 h | the `.fig` import's tail so the files people send open well (5–10), core tool polish (25–45), interaction pass on the core tools (15–25), stability QA (10–20) | + +Hours are one agent working sequentially, calibrated from this repo's history (about 1 agent-hour +per 1,000 added lines, 1–2.5 h per feature PR, ~7 h per honest depth point in the polish tail; see +[how the hours are calibrated](docs/target-app-parity.md#how-the-hours-are-calibrated)). Each audience's work is a subset of +the next. About 70–80% parallelizes across 4–6 agents on disjoint crates (roughly ÷4 in +wall-clock); the `.fig` reader, the multiplayer core and a plugin runtime each need one owner, and +native-speaker review, real-file licensing and screen-reader QA need people. + +Hours are Opus 5.5 agent wall-clock hours, calibrated from this repo's history: the app was built in +~25 wall-clock hours by 4–6 parallel agents (~110–150 agent-hours, ~1 agent-hour per 1,000 added +lines, a typical feature PR 1–2.5 h). Details and what needs a human: +[`docs/target-app-parity.md`](docs/target-app-parity.md#how-the-hours-are-calibrated). + +## Stage + +**Alpha.** Core workflows exist end to end (frames, shapes, vectors, text, auto layout, components +with variants and overrides, styles, variables, prototyping, Dev Mode, SVG/PNG/JPG/PDF), but +FigCraft opens `.fig` files only best effort, has only a preview of multiplayer, and nothing has been checked +against real Figma documents. Ready-for-real-work is ~53%, inside alpha's ~40–75% band, and the alpha gate passes: every daily core workflow (design screens, build a design system, prototype, hand off to developers) works end to end on FigCraft's own files, which save and reopen; review works only asynchronously and `.fig` import is unproven on real files, both beta gaps ([alpha gate](docs/roadmap.md#alpha-gate)). Beta needs +~75% **and** reliable `.fig` import: the `.fig` import's tail (5–10 h), a real-file corpus with measured +fidelity (15–30 h), the interaction pass (25–40 h of the 40–60), core depth to ~80% (40–75 h), +basic multiplayer on the landed core (30–55 h), stability and platform QA (20–40 h), a partial GPU path (15–30 h); +native speakers' reviews of the six language drafts need people. + +## By dimension + +| Dimension | Ready | Hours | Doc | +|---|---|---|---| +| Features (depth) | 63% (core editor 72%) | 28–67 | [target-app-parity](docs/target-app-parity.md) | +| UI/UX fidelity | 58% | 40–60 | [ui-parity](docs/ui-parity.md) | +| File formats | 60% (`.fig` read best effort with editable vectors; experimental write, round trips through our own importer and written files opened in other software) | 5–10 | [file-format-parity](docs/file-format-parity.md) | +| Collaboration | 13% (core in; live sessions with document sync, cursors and text carets, cursor chat and spotlight, the browser joins, invite links open the app; a self-hosted server with edit and view links and team libraries; preview) | 46–92 | [collaboration-parity](docs/collaboration-parity.md) | +| Stability | 55% | 20–40 | [gaps](docs/gaps.md) | +| Performance | 60% | 30–60 | [hardware-parity](docs/hardware-parity.md) | +| Hardware | 60% | 10–21 | [hardware-parity](docs/hardware-parity.md) | +| Localization | 39% (layer; English; Spanish with a Latin American overlay, French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese drafts; CJK UI fonts) | 29–51 | [localization-parity](docs/localization-parity.md) | +| Platforms | 75% | 10–20 | [hardware-parity](docs/hardware-parity.md) | +| Ecosystem / plugins | 38% (own WASM plugins with panels, settings and widgets; a self-hosted plugin directory) | 8–26 | [gaps](docs/gaps.md) | +| AI features | 0% | 30–60 | [gaps](docs/gaps.md) | + +## Features + +| Area | Breadth | Ready | Hours | Doc | +|---|---|---|---|---| +| Canvas & navigation | 98% | 74% | 1–4 | [ui-parity](docs/ui-parity.md) | +| Selection & transform | 97% | 72% | 3–5 | [ui-parity](docs/ui-parity.md) | +| Layers panel | 99% | 85% | 1–2 | [ui-parity](docs/ui-parity.md) | +| Shapes & vector | 99% | 73% | 1–3 | [target-app-parity](docs/target-app-parity.md) | +| Paint | 90% | 71% | 2–5 | [target-app-parity](docs/target-app-parity.md) | +| Strokes | 95% | 75% | 1–2 | [target-app-parity](docs/target-app-parity.md) | +| Effects | 100% | 75% | 2–5 | [target-app-parity](docs/target-app-parity.md) | +| Frames, groups, sections, masks | 97% | 80% | 1–3 | [target-app-parity](docs/target-app-parity.md) | +| Auto layout & constraints | 98% | 74% | 3–6 | [auto-layout-parity](docs/auto-layout-parity.md) | +| Text | 90% | 70% | 3–7 | [target-app-parity](docs/target-app-parity.md) | +| Components & variants | 96% | 72% | 4–9 | [components-parity](docs/components-parity.md) | +| Styles & variables | 95% | 76% | 2–4 | [components-parity](docs/components-parity.md) | +| Prototyping | 92% | 74% | 2–5 | [prototyping-parity](docs/prototyping-parity.md) | +| Import/export | 86% | 58% | 5–10 | [file-format-parity](docs/file-format-parity.md) | +| Dev Mode / inspect | 96% | 72% | 0–1 | [dev-mode-parity](docs/dev-mode-parity.md) | +| Comments, versions, misc | 87% | 63% | 1–3 | [collaboration-parity](docs/collaboration-parity.md) | +| Multiplayer & sharing | 17% | 5% | 42–84 | [collaboration-parity](docs/collaboration-parity.md) | +| Plugins & widgets | 60% | 35% | 8–26 | [plugins](docs/plugins.md) | +| Team libraries | 70% | 40% | 4–8 | [components-parity](docs/components-parity.md) | +| Figma Draw extras | 88% | 60% | 1–3 | [gaps](docs/gaps.md) | +| AI features | 0% | 0% | 30–60 | [gaps](docs/gaps.md) | + +## Languages + +| Language | Code | Strings | Status | +|---|---|---|---| +| English | en | 100% | full | +| Simplified Chinese | zh-Hans | 100% | draft (needs native review) | +| Spanish | es, es-419 | 100% (es-419: an overlay of 98 entries) | draft (needs native review) | +| Hindi | hi | 0% | none | +| Arabic | ar | 0% | none | +| French | fr | 100% | draft (needs native review) | +| Portuguese (Brazil) | pt-BR | 100% | draft (needs native review) | +| Indonesian | id | 0% | none | +| Japanese | ja | 100% | draft (needs native review) | +| German | de | 100% | draft (needs native review) | +| Korean | ko | 100% | draft (needs native review) | +| Vietnamese | vi | 0% | none | + +Also shipped: Swedish (`sv`: every UI string, 120 of 588 command labels and menu wordings; native review welcome). Figma ships English, Japanese, Korean, French, German, Spanish and Portuguese +(Brazil); FigCraft has the localization layer, English and Spanish (Spain and Latin America), +French, German, Brazilian Portuguese, Japanese, Korean and Simplified Chinese drafts (Preferences ▸ +Language; Traditional Chinese has no catalog; CJK UI +text uses an installed CJK font, loaded only when needed; adding a language: +[`docs/translating.md`](docs/translating.md)). Document text in any script (bidi, Arabic shaping, +IME) already works. Detail: [`docs/localization-parity.md`](docs/localization-parity.md). + +## Upcoming + +Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). + +1. `.fig` import's tail: fixes from real files, connector labels, table cells, filled areas with their own fills (the best-effort import, editable vector networks, the other layer kinds and variable prototype actions landed 2026-10-11): 5–10 h; then checking the rest of the experimental `.fig` export (landed 2026-10-12; written files open and draw in other software) on effects, images, prototypes and real files: 5–15 h. +2. Real-file corpus and measured fidelity: 15–30 h. +3. Tool-by-tool interaction pass against Figma: 40–60 h. +4. Languages: native reviews of the es, fr, de, pt-BR, ja and ko drafts (people), Latin American Spanish (1–2 h), then Simplified Chinese (3–5 h). +5. Multiplayer (M9): accounts and teams on top of the live sessions and the standalone server (transport, hosting, invite links that open the app, document sync, presence with remote text carets, cursor chat and spotlight, per-user undo, reconnect with offline edits, collision-free ids for styles, variables and comments, the browser client, `figcraft-server` with persistence and share links to edit or view, team libraries on the server landed): 42–84 h. + +## Progress log + +- **2026-10-15** · M4: Outline stroke preserves even-odd stroke holes. Pixel comparisons cover alignment, dashes, individual side weights, vector paths and stroke-only opacity; native save/reopen, Undo/Redo and instance opacity inheritance/overrides are covered by `outline_stroke`. Existing estimates unchanged; filled-shape compositing needs separate work. + +- **2026-10-14** · Corner handles win over the corner-radius handles (M1, #292): a press on the inner half of a corner handle's square picked the radius handle 10 px in, so the drag neither resized nor (dragging outward) rounded anything. Inside the drawn square the corner now resizes both sides (in ratio with Shift or the link); further in the radius handle still answers. Regression test in the transform behaviour suite. Numbers unchanged. +- **2026-10-14** · Home screen recent files as cards (M7): each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged. +- **2026-10-13** · Vector edit mode regions (M4): with the Move tool, the filled area under the pointer is hatched with "/" lines in the selection colour, clipped by the area's own fill rule so holes stay clear; the hatch goes near points (3.5 px) and segments (2 px), and its spacing follows octave folding (8 px at every power-of-two zoom), fixed to the shape. A click selects the area with its points (they move with it but keep the unselected look), Shift adds or removes one, a press on a selected area drags the whole selection as one undo step, and a click on an empty spot clears the areas and stays in edit mode. `vector.edit.select {regions}` and `vector.edit.state` (`regions`, `regionVertices`, `regionHover`); `vector_regions_tests` and `regions_tests`. Numbers unchanged. +- **2026-10-14** · SVG/PDF export depth (M0.9, #208): visible descendants deeper than 256 levels below an exported root are refused before bounds or drawing, including masks and raster fallbacks. SVG has a checked API; the engine and CLI report the limit and suggest selecting an inner layer. Existing destination files survive refusal. Boundary, inner-root, hidden-subtree, multi-root/page and command/CLI regressions cover the change. Numbers unchanged. +- **2026-10-13** · MCP/control transport hardening (#201): 32 MiB incoming frames are bounded while reading; oversized frames return an error and close without draining. The opt-in loopback control listener caps active workers at 8, applies 60 s socket read/write inactivity timeouts and caps both request queues at 8, discarding timed-out requests including those deferred to hidden windows. Reader, queue, idle-client and blocked-writer regressions cover the limits. Numbers unchanged. +- **2026-10-13** · Attached MCP (M0.9, #194): a transport failure no longer replays a request that may already have edited the document. The caller gets an outcome-unknown error and must inspect before retrying; the next explicit call reconnects. Loopback tests cover a lost mutation reply, a single applied edit, reconnect and a connection failure before sending. Numbers unchanged. +- **2026-10-13** · Constrain proportions in the Layout panel (M1, #220): with the W/H link on, typing a width or height scales the other side by the layer's own ratio; the property edit dropped the ratio, so the link only worked while dragging resize handles. Both sides in one edit stay as typed, the `w`/`h` aliases work the same, each layer of a multi-selection keeps its own ratio, and a zero side has no ratio to keep. Regression test in the transform behaviour suite. Numbers unchanged. +- **2026-10-13** · Repeat last command (M1, #181): repeated edits keep the underlying command and parameters, so repeated nudges accumulate with one undo step each. A stored self-reference is refused instead of recursing. Regression tests cover three repeats, their undo steps, and missing or self-referencing commands. Numbers unchanged. +- **2026-10-13** · Component playground (M3): with an instance, a component or a component set selected, the Inspect tab shows a Playground: a preview of the component and a control for each variant, boolean and text property. What is tried there changes the preview and the code in the Code section, never the file (no undo step, the canvas instance stays as it is); Reset goes back to the component's or the instance's own values. `component.playground {id?, properties?, language?, path?}` gives the same to scripts: the properties with their values, the code, and a PNG on request (`playground_tests`). Left: instance-swap and slot properties in the playground (the command takes an instance swap; the panel has no control for it), a code connect template's snippet with the tried values. Numbers unchanged. +- **2026-10-13** · Auto layout, on-canvas handles of automatic gaps (M2): the handle between spread rows or columns (and between items with an auto gap) shows the distance the layout gives the gap instead of the stored value, and dragging it makes the gap a fixed one from there, as typing a gap in the panel does; one undo step (`gap_handle_tests`). Before, the drag changed a value that had no effect. The documentation is silent on dragging an automatic gap. Numbers unchanged. +- **2026-10-13** · Variables inside styles (M3): a shared style can bind variables to fields of its value (`style.bind {style, field, variable?}`: a paint style's colours and gradient stops, an effect style's colour, radius, spread and offsets, a grid style's size, count, gutter and offset, a text style's font size, line height, letter spacing, family and style). Every layer using the style shows those fields resolved in its own modes and stays linked when a variable or a mode changes; the style keeps them resolved in the default modes. A style made from a layer keeps that layer's bindings; a new value drops bindings to what is gone. The style editor binds from its property rows (paint colour, shadow colour, blur radius, grid size) and detaches from the variable's chip; the style picker marks styles that use variables. `.fig` export and import carry the bindings on the style, REST layers hand theirs to the style they use. Left: binding text style fields and the other effect and grid fields from the editor (commands only), library variables in the picker, a style's library copy. Numbers unchanged. +- **2026-10-13** · Effects on groups, inner shadows and background blur (M3): a group is no outline of its own, so its inner shadows are cast inwards from around what its layers draw together (their pixels, over them: no shade at a seam between two layers) and its background blur and glass show the backdrop through the outlines of its layers (under them). Canvas and PNG; SVG embeds a group with an inner shadow as a render and its background blur as an image under the layers; PDF places both as images, the blur matching the PNG. Where the group holds one shape the pixels match that shape, or a frame filled like it, carrying the effect (`group_effects_tests` in render, svg and pdf). Offsets and spreads of a group's inner shadow go by whole pixels. Numbers unchanged. +- **2026-10-13** · Text made by commands on a dark page (M1): in the app, `text.create` and `node.create {type: text}` that name no fill give white text on a dark page, by the same rule as the Text tool (the page's colour decides, also inside a frame); a named `fill`, `fills` or `fillStyle` is kept. Sessions without an interface (CLI, MCP, exports) still make black text everywhere. Numbers unchanged. +- **2026-10-13** · Frames and groups, a one-step convert (M1): `object.convertToGroup` and `object.convertToFrame` (the Design panel's type menu on a frame or a group) change the layer in place, its layers staying where they are on the page. A frame turned into a group loses what only a frame has (auto layout, with the positions it gave kept and fill children fixed; fills, strokes, corner radius, clipping, layout grids, guides, scrolling) and takes its layers' box; a group turned into a frame keeps its box, with no fill and no clipping. Instances, main components, layers inside instances and empty frames are refused with the reason, all or nothing; one undo step. Numbers unchanged. +- **2026-10-13** · Prototyping, delays on mouse triggers and manual overlay offsets (M5): mouse enter, leave, down and up wait `delayMs` before their actions run (once while waiting; dropped when the screen or overlay they were on goes away; a pointer that leaves early doesn't cancel, the documentation is silent), and a manual overlay sits at `offset: [x, y]` from the top-left of the layer that opens it (without one, right below it, as before). Delay and X/Y fields in the interaction editor, the player on its own clock, REST import (`delay`, `overlayRelativePosition`), `.fig` import and export (the export's field for mouse delays is unchecked against other software). Numbers unchanged. +- **2026-10-13** · Auto layout, wrapped-line alignment (M2): a wrapping layout's lines (rows of a horizontal one, columns of a vertical one) are packed where the cross alignment says (start, centre, end; before, they always sat at the start), stretch when every child fills across the flow, or take the free space between them (`contentAlign: spaceBetween`, the Design panel's Row/Column gap mode, the alignment pad lighting the whole cross axis). Read from REST JSON and `.fig`, written to `.fig`, `align-content` in CSS, an instance override with the other alignment settings. 13 hand-computed layout cases and command tests. Numbers unchanged. +- **2026-10-13** · `.fig` export: instance sizes, variables, styles (gap 1): instances write what they change from + their main (size, name, paints) as an override addressed by the main's id, what follows for their layers as + derived data and a scale factor when scaled as a whole; variable collections, modes, variables and bindings, + the modes frames and pages set, shared styles with their links and variable-width strokes are written, and the + import reads all of it back. Checked by opening written files in other software: resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width; + number, string and boolean bindings and bindings on effects, layout guides and gradient stops come across as the values they give but did not show as bound there (their entries now also name the layer field they set, not yet checked there), and strokes wider on one side of the path are written as filled outlines, no longer editable strokes there (the stroke itself is kept as plugin data and our own import puts it back; not yet checked there). Numbers unchanged. + text, effect and grid styles, number, string and boolean bindings, bindings on effects, layout guides and gradient stops, and variable-width strokes are not yet checked there. Numbers unchanged. +- **2026-10-13** · Starter templates and first-run help: eight templates built in code (mobile app, landing page, + dashboard, design system starter, wireframe kit, social post sizes, presentation slides, icon set starter), a + Templates tab on the home screen, `template.list` and `file.newFromTemplate` for scripts and agents, and a Getting + started card that can be closed and comes back from the Help menu +- **2026-10-12** · Your online files: a file shared or published online stays yours after its tab closes or the app + quits. The app remembers it (with its owner token, kept with the preferences and never shown) and Home ▸ **Online** + lists what you own: open a file again as its owner, start or stop sharing, copy its code, rename it, remove it from + the server or forget it here. Servers are asked once, when the list opens; tabs mark files in a live session; a + crash reopens such a file from its server and puts back the changes that hadn't reached it. `collab.files`, `collab.openFile`, `collab.forgetFile`; servers need an + upgrade only to tell which files are still there. The six draft catalogs translate every string again +- **2026-10-12** · Everyday editing partials: number fields (scrub rates with Shift and Alt, arrow keys by the nudge + preferences, arithmetic with brackets, relative edits of Mixed values per layer), outline mode's hidden layers and + object bounds, Hide when publishing in the Assets panel, a cursor for every tool and modifier, panel widths that are + kept, and dark pages (white text on them) for files made in the dark theme +- **2026-10-12** · Sharing with a join code: Share ▸ **Online** (through the server in Preferences, built in) or + **Local network**, one Start sharing button and a short code to read out (`K7M-2QX-9TD`; a longer one locally that + carries the host's address); others type it under Join with a code (the popover, the home screen), where a link + works too. The server names links with codes, keeps them hashed and limits wrong ones; `collab.share`, + `collab.join {code}`; servers must be upgraded to hand codes out (older ones keep working with links) +- **2026-10-12** · Text metrics measured: our example documents, re-saved by other software, were read back and + compared layer by layer. Line heights are now whole pixels, advances sit on a 1024-per-em grid and letter spacing + goes between glyphs only; all 230 self-sizing text boxes in the 7 documents match to the pixel (24 of 131 before + in the design-system example). One font family; real files still unchecked (gap 3). No number moves +- **2026-10-12** · Live sessions are per document: every open file has its own session or none, several run at + once (host one, join another, publish a third) and keep syncing behind the tab in front, the Share popover, + people, cursors, chat, spotlight and follow are the file in front's, `collab.peers` lists them all (`sessions`), + and closing a file ends its session only (before, one session followed every tab and starting another replaced it) +- **2026-10-12** · A public `figcraft-server`: `open_publish = true` lets anyone publish without the server key + (Share ▸ Publish to server asks the server first and shows who hosts it, its terms and how long unopened files are + kept), with an owner token shown once that manages the file later (`collab.manageFile`); limits per address, + idle expiry, reports (Share ▸ Report this file…), `block-ip` and `files list --sort`, `GET /healthz`, and + `packaging/server/install.sh` that sets up a Debian/Ubuntu machine with Caddy, ufw and backups + ([server.md](docs/server.md#running-a-public-server)). Numbers unchanged (hosting, not a Figma feature). +- **2026-10-12** · `.fig` export opens elsewhere (gap 1): written files were checked by opening them in other + software: layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft. The + export now says that the document and its pages are visible and opaque, numbers its schema as the format does, + and writes outlines, text settings, standard font style names and line height units; effects such as glass and + noise, images and prototype interactions are still unchecked there, and variables are not written. Numbers + unchanged. +- **2026-10-12** · Experimental `.fig` export (gap 1): `file.export {format: "fig"}`, File ▸ Export… ▸ .fig + (experimental), `figcraft-cli export --format fig` and MCP's `export` write the whole document: layers, + geometry, paints with their images, strokes, effects, styled text, vector networks, auto layout, components and + instances with their overrides, and prototype interactions; what's left out (style links, variables, comments, + Dev Mode data…) comes back as warnings. Checked only by reading the files back with our own importer. File + formats 60%. +- **2026-10-12** · Keyboard shortcut parity (gap 4): a table test with a row for each of the 129 + shortcuts in the public documentation, macOS and Windows/Linux keys each, and the 48 bindings it + is silent on pinned; 126 bound (110 before), Run last plugin moved off a documented conflict on + Windows, 2 missing. New: others' cursors toggle, list levels on Cmd/Ctrl+] and [ while editing, + keyboard box selection on Alt+Space / Ctrl+Space, arrows that pan with nothing selected, Control + suspending snapping on macOS, Space/N/C/F while presenting, the Frame tool on A, pixel grid on + Shift+', link on Shift+Cmd+U, ungroup on Cmd/Ctrl+Backspace, suggest auto layout and go to main + component keys; shifted symbols on other keyboard layouts. +- **2026-10-12** · Plugin directory on `figcraft-server` (gap 6): publish a plugin folder with the server key + (`figcraft-cli plugin publish`), browse a server's plugins in Plugins ▸ Manage plugins ▸ Browse, install after a + confirmation that shows the permissions it asks for, update when a newer version appears, remove; bundles are + checked in full on the server and again in the app, downloads must match their SHA-256, installed plugins live + per user in the same sandbox; `plugin.directory.*` for scripts and MCP; the operator moderates with + `figcraft-server plugins`. Plugins & widgets 60%. +- **2026-10-12** · Plugin widgets and the panel tail (gap 6, plugin API 3; older plugins run unchanged): a widget + plugin puts a layer of its own on the canvas (Plugins ▸ Widgets, `widget.insert`), described by the plugin, + validated and built with locked layers inside; clicking a part it named calls it back with the widget's state + (kept in the layer, so it saves, copies, undoes, exports and syncs) as one undo step, and it redraws; + `widget.click` and `widget.list` for agents; a Counter example. Panels of several plugins open side by side and + plugin colour fields open the colour picker. Plugins & widgets 55%. +- **2026-10-12** · Layers and Assets panels behaviour suite (gap 4): several layers dropped together land + together, Collapse layers keeps the selection's rows open, dragging across eyes or padlocks sets several layers + at once, a Rename layers dialog for several layers (number codes as documented, regular expressions, a start + value), pages dragged to reorder, dividers, "Copy of" duplicates, Assets folders by page, frame and slash names, + and hidden-from-publishing names that decide both ways. +- **2026-10-12** · Vector multi-edit and the vector edit tail (gap 9): several vector layers in edit + mode at once (select, marquee, drag, nudge, delete, mirroring and radius across them), several + Bezier handles dragged together, rounded points beside curved segments, a stroke cap and join per + point, and the Pen connecting to its own points or continuing a selected open path. +- **2026-10-11** · Canvas navigation, guides and grids behaviour suite (gap 4): zoom, panning, the pixel + grid (now from 400%), rulers, guides (top-level frames only; select, Delete, nudge, Remove guide, + Alt-drag copies, Alt measures while dragging), layout grids (Auto counts; hidden on rotated frames), + outline mode (clicks reach layers behind) and measuring, each rule from the documentation. +- **2026-10-11** · Export and inspect behaviour suite (gap 4): exports cover what a layer paints (shadows + only where they fall, true stroke widths, no room for hidden layers, a mask group's mask), slices export what's under + them, layer names with slashes export into folders; export settings gained contents only, include bounding box and + the SVG options (outline text, ids, simplified strokes); Inspect shows colours in hex, RGB, CSS, HSL or HSB, CSS in + rem, blend modes and positions from the frame, and lists icons and images as assets. Dev Mode 70%. +- **2026-10-11** · `.fig` import's tail (gap 1): vectors open as editable vector networks with their + corner radii, handle mirroring and end caps (outlines when the network is missing or damaged); sticky notes, shapes + with text, connectors, tables and similar layers import as frames, text and vectors instead of being dropped; + set-variable and conditional prototype actions come across. File formats 58%, ready ~49%. +- **2026-10-11** · Paint and effects behaviour suite: every documented rule for fills, blend modes, + gradients, images, colour models, shadows, blurs and effect exports is a table-driven case. Fixes: Normal + groups isolate and clipping frames don't, plus darker is drawn, drop shadows hide behind translucent + layers in every export, Fill images reach the box's edges, "Normal" no longer means pass-through. + Paint 70%, effects 75%. +- **2026-10-11** · `.fig` import, best effort (gap 1): `file.open`, `file.import`, drag and drop, the CLI and + MCP open `.fig` files. A generic kiwi decoder reads each file's own schema, and the decoded nodes map by field name + onto the REST importer: layers, paints and styles, text runs, vectors as outlines, auto layout, components and + instances (expanded with their overrides and property values), variables, prototyping and images from the archive; + the rest is a warning. File formats 55%, ready ~49%. +- **2026-10-11** · Gamut warnings (gap 18): the colour picker of a Display P3 file flags colours + outside sRGB and View ▸ Soft-proof sRGB stripes them on the canvas. Showing P3 itself waits on + eframe/egui-wgpu exposing the surface colour space wgpu 30 already supports. +- **2026-10-11** · Several windows (gap 18): File ▸ New window, Open in new window on a tab or a + tab dragged off the strip; each window has its own tabs, panels and canvas, draws only when it + needs to and asks about unsaved changes when it closes (the main window too); the control channel + and MCP open, list, focus and capture windows. Hardware 60%. +- **2026-10-11** · Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls, + certificates always verified; `FIGCRAFT_CA_FILE` for a private authority), releases ship the server for Linux + x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` walks through deploying it on + a rented Linux server (systemd unit, Caddy or nginx, firewall, Docker, upgrades, backups). +- **2026-10-11** · Plugin panels and settings (gap 6, ABI v2; v1 plugins run unchanged): a plugin shows a panel of + fields and buttons drawn with FigCraft's widgets and is called back for each button or changed value (one undo step + per callback, same sandbox limits), `plugin.ui` / `plugin.event` / `plugin.close` drive panels from scripts and MCP, + and per-plugin settings are kept with the preferences; Swatch grid gained a palette panel. Plugins & widgets 45%. +- **2026-10-11** · Canvas fixes: clicks hit boolean groups where their result is (a click in a + subtracted hole reaches what's behind), a layer dragged inside a group stays in it, View ▸ Frame + outlines and View ▸ Slices draw, and a flaky idle test now waits for the render thread (gap 13). +- **2026-10-11** · Team libraries on `figcraft-server` (gap 10): Assets ▸ Libraries ▸ Publish to server keeps + the file's library on the server under the file (per-asset versions; the server's version is followed); other files + enable it by pasting a share link (a view link is enough), get "Library updates available" from a background check + and accept with overrides kept; the server counts which files use which components (Show usage, `library.usage`); + `library.swap` re-points instances, styles and variables to another library by names. Team libraries 70%. +- **2026-10-11** · Animated GIF fills (first frame on the canvas and in exports, played in the + presenter and the preview, waking the window only for the next frame) and TIFF images (gap 12). +- **2026-10-11** · The last three menu items got commands: File ▸ Open recent (kept in the + preferences, missing files greyed, Clear recent), outlines for the selection and mask outlines + (gap 21). Menu breadth 203/203. +- **2026-10-11** · Extended variable collections (a library collection's modes and variables in a + collection of the file, overridden per mode with reset, following library updates), variables + import/export (the REST local variables JSON, merged by name; CSS custom properties), gradient + stops and variant properties bound to variables (gap 14). +- **2026-10-11** · Live boolean editing: operands stay editable layers (double-click or Cmd/Ctrl-click + selects one, moving, resizing or editing its points redraws the result during the drag with every + operand outlined, per-operand operations from the boolean controls, prefix-cached recompute); + Design panel arc fields; the shape builder on strokes and open paths; one-sided variable widths + (gap 13). Feature breadth ~79%. +- **2026-10-10** · WASM plugins (#101: sandboxed wasmi runtime, manifest permissions, Plugins menu, + Run last, Manage, Rust SDK and two examples), docs refresh (#102), the multiplayer core without + networking (#103: client-scoped ids, change sets, server ordering and validation, client + reconciliation, convergence tests). Menu breadth 200/203; ready for real work ~47%. +- **2026-10-10** · Full re-measure against Figma desktop 126.10.7 under the craftrules progress-docs + standard: ready for real work ~46%, feature breadth ~76%, stage alpha; progress docs split out + (`docs/target-app-parity.md`, `gaps.md`, `roadmap.md`, `architecture.md`, the per-area parity + docs); `docs/parity.md` renamed `docs/parity-checklist.md`. +- **2026-10-10** · Day 2 (PRs #1–#99): vector edit mode depth (transform box, point snapping, arc + handles, shape builder, variable width), text depth (variable fonts, bidi, RTL names, style + browser), auto layout nesting and keyboard reordering, slots and library variables, components + at scale (2,000 instances), effects fidelity (glass, noise, texture, progressive blur), + prototyping (conditionals, expressions, device frames, responsive presentation), REST import of + prototypes and variables, SVG import fidelity, PDF print options, Dev Mode (code, annotations, + ready for dev, compare changes), comments polish, version history, keyboard shortcut parity, + accessibility, retained canvas and 50k-layer performance, no redraws while idle; v0.2.0 + released for every platform; multiplayer protocol designed. +- **2026-10-09** · Day 1: foundation (13 crates, layering, command registry, renderer, engine, + UI3 frontend, control channel), CLI and MCP, web build, PDF export, packaging, first depth + passes on text, components, prototyping and comments. + +## Revision history + +| Date | Change | Summary | +|---|---|---| +| 2026-10-15 | minor | M4: Outline stroke preserves even-odd stroke holes. Pixel comparisons cover alignment, dashes, individual side weights, vector paths and stroke-only opacity; native save/reopen, Undo/Redo and instance opacity inheritance/overrides are covered by `outline_stroke`. Existing estimates unchanged; filled-shape compositing needs separate work. | +| 2026-10-15 | minor | M7: system font discovery skips entries observed as non-regular files before opening them, avoiding the reproduced named-pipe stall. Regular-file symlinks remain supported; directory depth, font limits and priorities are unchanged. Numbers unchanged. | +| 2026-10-15 | minor | Typography spacing icons scrub with the shared number-field gesture, modifier rates and one Undo step per drag (#373). Units and selected-character targeting are preserved. Numbers unchanged. | +| 2026-10-14 | minor | System theme on first run (M7): preferences without a theme default to System; the first frame reads the OS appearance directly, including when returning from a manual theme. Saved Light/Dark choices stay explicit; unavailable OS appearance falls back to Light. Numbers unchanged. | +| 2026-10-14 | minor | Home screen recent files as cards (M7): each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged. | +| 2026-10-14 | minor | Lost images reported on loading (#207): a `.figcraft` file whose image member is damaged, too large or missing no longer loads in silence; each lost image (up to 20, then a count) is listed in `repairs` on `file.open` with its hash and the reason, and the app shows it as a toast. Saving still writes the file without the lost images. Numbers unchanged | +| 2026-10-14 | minor | SVG import budgets (#190): gzipped SVG is unpacked by FigCraft within 64 MiB before parsing (it was unpacked without a limit), so the nesting check sees the real document; SVG documents embedded in `` data URIs share one 32 MiB budget and at most 4 levels, and one past a budget is left out. Linked files stay unread. Numbers unchanged | +| 2026-10-14 | minor | Leptos code (#223): `inspect.code`, `inspect.componentCode` and `codeConnect.set` take `language: leptos`: a `#[component]` with `#[prop]` defaults, variant enums and `view!` markup, instances as calls, Code Connect `use` lines; the Dev Mode language list and rem units include it. Numbers unchanged | +| 2026-10-14 | minor | Full screen (#227): View ▸ Full screen (`view.fullscreen {on?}`, F11 on Windows and Linux, Ctrl+Cmd+F on macOS) makes the window fill the screen and leaves it again; FigCraft's own item. Numbers unchanged | +| 2026-10-14 | minor | Colour picker: wrapped rows of page and recent swatches stay inset on both sides instead of running past the picker's right edge. Numbers unchanged | +| 2026-10-14 | minor | Server snapshots (#188): figcraft-server never replaces a readable snapshot with one too large to be read back, and reads snapshots up to twice the file cap (recent batches kept for reconnecting clients may take a file over it). Numbers unchanged | +| 2026-10-14 | minor | Preferences owner-only on Unix (#200): `state.json`, which holds online owner tokens, is written with mode 0600 from the first byte, and an older world-readable file is narrowed. Numbers unchanged | +| 2026-10-14 | minor | ruzstd 0.9.1 (#182): `.fig` import requires the release that fixes RUSTSEC-2026-0344 (a zstd decompression bomb). Numbers unchanged | +| 2026-10-14 | minor | Layer cycles (#205): `validate` reports children listed twice, pages with a parent and layers not reachable from a page (cycles, detached subtrees); `repair` drops them in one linear pass. Numbers unchanged | +| 2026-10-14 | minor | AppImage CLI: `figcraft.AppImage cli ` runs the bundled `figcraft-cli` (MCP server included) on Linux without a second download. Numbers unchanged | +| 2026-10-14 | minor | Boolean outline depth (#193): nested boolean groups share one outline depth budget instead of each level getting its own. Numbers unchanged | +| 2026-10-14 | minor | Command batches roll back as one: a failed `command.batch` (MCP `batch`) restores every open document and its state, and puts the tab in front back. Numbers unchanged | +| 2026-10-14 | minor | Desktop builds no longer rebuild the text crate on every build when the craft-fonts manifest is missing. Numbers unchanged | +| 2026-10-14 | minor | Direct3D 12 on Windows: the desktop app asks wgpu for Direct3D 12 there instead of Vulkan, so it starts without Vulkan loader errors on machines with stray overlay layers. Numbers unchanged | +| 2026-10-14 | minor | Plugin metering (#192): every host call and every byte copied between a plugin and the host costs fuel, so a plugin can't stall the app through cheap-looking host calls. Numbers unchanged | +| 2026-10-14 | minor | SVG/PDF export depth (#208): explicit preflight refusal beyond 256 descendant levels, propagated through public APIs, commands and CLI, with boundary/content and destination-preservation regressions. Numbers unchanged. | +| 2026-10-13 | minor | MCP/control transport hardening (#201): frame limits during reads, bounded connections and both UI request queues, socket inactivity timeouts; percentages and estimates unchanged | +| 2026-10-13 | minor | Plugin log limits (#191): cap each UTF-8 line at 8 KiB, total log bytes at 1 MiB and lines at 1,000 per run/callback before copying guest memory or forwarding to the host. Over-budget runs fail and roll back document edits; callbacks get fresh budgets. Added sandbox boundary/forwarding tests and an engine rollback regression. Percentages and hours unchanged. | +| 2026-10-13 | minor | Live-session revert guard (M9, #198): `file.revert` is refused while its document belongs to a live session, including publishing, reconnecting and hosting before the welcome. The document, previews, branch tabs and session stay intact; leave the session before reverting a local save. Offline revert is unchanged. Scripted access and sync regressions in `live_access_tests`. Numbers unchanged. | +| 2026-10-13 | minor | Repeat last command (M1, #181): keep the underlying command and parameters across repeats, reject self-reference, and retain one undo step per repeated edit. Numbers unchanged. | +| 2026-10-13 | minor | Component playground (M3): with an instance, a component or a component set selected, the Inspect tab shows a Playground: a preview of the component and a control for each variant, boolean and text property. What is tried there changes the preview and the code in the Code section, never the file (no undo step, the canvas instance stays as it is); Reset goes back to the component's or the instance's own values. `component.playground {id?, properties?, language?, path?}` gives the same to scripts: the properties with their values, the code, and a PNG on request (`playground_tests`). Left: instance-swap and slot properties in the playground (the command takes an instance swap; the panel has no control for it), a code connect template's snippet with the tried values. Numbers unchanged. | +| 2026-10-13 | minor | Auto layout, on-canvas handles of automatic gaps (M2): the handle between spread rows or columns (and between items with an auto gap) shows the distance the layout gives the gap instead of the stored value, and dragging it makes the gap a fixed one from there, as typing a gap in the panel does; one undo step (`gap_handle_tests`). Before, the drag changed a value that had no effect. The documentation is silent on dragging an automatic gap. Numbers unchanged. | +| 2026-10-13 | minor | Variables inside styles (M3): a shared style can bind variables to fields of its value (`style.bind {style, field, variable?}`: a paint style's colours and gradient stops, an effect style's colour, radius, spread and offsets, a grid style's size, count, gutter and offset, a text style's font size, line height, letter spacing, family and style). Every layer using the style shows those fields resolved in its own modes and stays linked when a variable or a mode changes; the style keeps them resolved in the default modes. A style made from a layer keeps that layer's bindings; a new value drops bindings to what is gone. The style editor binds from its property rows (paint colour, shadow colour, blur radius, grid size) and detaches from the variable's chip; the style picker marks styles that use variables. `.fig` export and import carry the bindings on the style, REST layers hand theirs to the style they use. Left: binding text style fields and the other effect and grid fields from the editor (commands only), library variables in the picker, a style's library copy. Numbers unchanged. | +| 2026-10-13 | minor | Effects on groups, inner shadows and background blur (M3): a group is no outline of its own, so its inner shadows are cast inwards from around what its layers draw together (their pixels, over them: no shade at a seam between two layers) and its background blur and glass show the backdrop through the outlines of its layers (under them). Canvas and PNG; SVG embeds a group with an inner shadow as a render and its background blur as an image under the layers; PDF places both as images, the blur matching the PNG. Where the group holds one shape the pixels match that shape, or a frame filled like it, carrying the effect (`group_effects_tests` in render, svg and pdf). Offsets and spreads of a group's inner shadow go by whole pixels. Numbers unchanged. | +| 2026-10-13 | minor | Text made by commands on a dark page (M1): in the app, `text.create` and `node.create {type: text}` that name no fill give white text on a dark page, by the same rule as the Text tool (the page's colour decides, also inside a frame); a named `fill`, `fills` or `fillStyle` is kept. Sessions without an interface (CLI, MCP, exports) still make black text everywhere. Numbers unchanged. | +| 2026-10-13 | minor | Frames and groups, a one-step convert (M1): `object.convertToGroup` and `object.convertToFrame` (the Design panel's type menu on a frame or a group) change the layer in place, its layers staying where they are on the page. A frame turned into a group loses what only a frame has (auto layout, with the positions it gave kept and fill children fixed; fills, strokes, corner radius, clipping, layout grids, guides, scrolling) and takes its layers' box; a group turned into a frame keeps its box, with no fill and no clipping. Instances, main components, layers inside instances and empty frames are refused with the reason, all or nothing; one undo step. Numbers unchanged. | +| 2026-10-13 | minor | Prototyping, delays on mouse triggers and manual overlay offsets (M5): mouse enter, leave, down and up wait `delayMs` before their actions run (once while waiting; dropped when the screen or overlay they were on goes away; a pointer that leaves early doesn't cancel, the documentation is silent), and a manual overlay sits at `offset: [x, y]` from the top-left of the layer that opens it (without one, right below it, as before). Delay and X/Y fields in the interaction editor, the player on its own clock, REST import (`delay`, `overlayRelativePosition`), `.fig` import and export (the export's field for mouse delays is unchecked against other software). Numbers unchanged. | +| 2026-10-13 | minor | Auto layout, wrapped-line alignment (M2): a wrapping layout's lines (rows of a horizontal one, columns of a vertical one) are packed where the cross alignment says (start, centre, end; before, they always sat at the start), stretch when every child fills across the flow, or take the free space between them (`contentAlign: spaceBetween`, the Design panel's Row/Column gap mode, the alignment pad lighting the whole cross axis). Read from REST JSON and `.fig`, written to `.fig`, `align-content` in CSS, an instance override with the other alignment settings. 13 hand-computed layout cases and command tests. Numbers unchanged. | +| 2026-10-13 | minor | `.fig` export (gap 1): instance sizes (resized, filling an auto layout, scaled), variables with modes and bindings, shared styles and variable-width strokes are written and read back. Checked by opening written files in other software: resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width; number, string and boolean bindings and bindings on effects, layout guides and gradient stops come across as the values they give but did not show as bound there (their entries now also name the layer field they set, not yet checked there), and strokes wider on one side of the path are written as filled outlines, no longer editable strokes there (the stroke itself is kept as plugin data and our own import puts it back; not yet checked there). Numbers unchanged | +| 2026-10-13 | minor | Starter templates and a first run that explains itself: eight templates built in code through the command layer (mobile app, landing page, dashboard, design system starter, wireframe kit, social post sizes, presentation slides, icon set starter; original and synthetic, deterministic, about 10–190 ms each to build), `template.list` and `file.newFromTemplate` for the app, CLI and MCP, File ▸ New from template…, a Templates tab on the home screen (cards with thumbnails rendered from the documents, search, kind filter, keyboard and screen-reader names) in place of the Community placeholder, a row of templates on Recents while there are no recent files, a Getting started card that can be closed and comes back from Help ▸ Getting started (`help.gettingStarted`), and a first run that opens on the home screen. The Drafts placeholder is gone (the app has no drafts folder). Essentials ~57% → ~59% (discoverability ×0.92 → ×0.94), its hours ~60–110 h → ~55–100 h; other numbers unchanged | +| 2026-10-12 | minor | Your online files (M9): a file put on a server without its key stays its owner's after its tab closes or the app quits. The app records each one with the preferences (server, id, name, owner token, the owner's links, the codes others join with; at most 200; the token never in a command result, `ui.inspect`, a log or debug output) and Home ▸ **Online** lists them: open one as its owner (a new tab, or its tab comes to the front), Start / Stop sharing, Copy code / Copy link, Rename on the server, Remove from server (confirmed), Forget (the file stays on the server). Nothing is asked of a server until the section opens; then one request per server (`Admin owned`, a compatible addition within protocol version 2) says which files are still there, and a server that is offline or older leaves the list as recorded. Tabs of files in a live session carry a mark (shared, or open only to you), toasts about a file that isn't in front name it, and after a crash a recorded file reopens from its server as its owner, its recovery copy kept until its unsent changes are back on the server (or it is opened beside the file). `collab.files`, `collab.openFile`, `collab.forgetFile`; `collab.manageFile` takes its server and token from the record and gains `share` / `unshare`. The draft catalogs (French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese) translate the 71 strings and 5 command labels they lacked and the new ones. Numbers unchanged | +| 2026-10-12 | minor | Everyday editing partials closed (gap 4): number fields scrub a step a point (×10 with Shift, ×0.1 with Alt, on past the screen edge where the platform reports it), step with the arrow keys by the nudge preferences, take brackets, units and relative edits (`+10`, `*2`, `50%`), commit on Tab and nothing when left untouched, and on Mixed apply relative edits to each layer in one undo step; outline mode's Include hidden layers and Include object bounds (`view.outlinesHiddenLayers`, `view.outlinesObjectBounds`), drawn and clickable; Hide when publishing from the Assets panel (`component.hideWhenPublishing`); a cursor for every tool, modifier and hover target (rotate turned to its corner, pen states, eyedropper, comment, copy, not-allowed), with guides and handles keeping theirs; panel widths with limits, double-click reset and the preferences (`view.panelWidths`); a file or page made in the dark theme has a #1E1E1E page (sessions without an interface keep #F5F5F5) and text drawn on a dark page is white. Menu breadth 211/211. UI/UX fidelity 58% and the interaction pass 40–60 h unchanged at this rounding: the side-by-side comparison is what remains | +| 2026-10-12 | minor | Sharing with a join code (M9, from the owner's use: sharing wasn't intuitive): the Share popover is one flow, **Online** (the file is published to the server in Preferences, built in `wss://collab.craftapps.dev/`, and the server names its edit and view links with nine-symbol codes like `K7M-2QX-9TD`) or **Local network** (the app hosts; a 20-symbol code carries its IPv4 address, port and a 44-bit secret, with check bits), one Start sharing button, the code large with Copy code and Copy link, who is here, can edit / can view online, and Stop sharing (online: the others' links and codes are revoked and they are disconnected, while the file stays on the server with its owner in it and can be shared again with new codes; removing it is a separate, confirmed step); **Join with a code** is one field (popover, home screen, Join dialog) for a code of either kind or a link; addresses, keys and ports moved under Advanced and Preferences ▸ Server for online sharing (reset, or empty to turn online sharing off). Protocol additions within version 2 (`Resolve` / `Resolved`, `Admin code`): the server keeps a stretched hash and the wrapped token, never the code, and limits wrong codes (10 a minute per address, a 15-minute lockout after 10 wrong, 1,000 wrong an hour for everyone); codes go with their link and file. Older servers leave the links in place of a code; older apps join by link. `collab.share`, `collab.join {code}`, `code` and `share` in `collab.peers`; the browser joins by code. Numbers unchanged | +| 2026-10-12 | minor | Live sessions per document (M9, a fix from real use): the engine keeps one session per open document instead of one for the app, the link carries several under their own ids (desktop, browser, tests), the window and the `collab.*` commands follow the active document, `collab.peers` gains `session` and `sessions`, `ui.inspect`'s `live` gains `session` and `sessions`; joining opens the session's file in its own tab without touching other sessions and closing a file ends its session only. Numbers unchanged | +| 2026-10-12 | minor | `.fig` export checked by opening written files in other software (gap 1): layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft, after fixes to what the export writes (visible document and pages, the format's schema numbering, outlines, text settings, font style names, line height units); effects such as glass and noise, images and prototype interactions still unchecked there. Numbers unchanged | +| 2026-10-12 | minor | Public `figcraft-server` hardened against resource exhaustion after a security review: messages capped by what a connection is for (a file's size when open), byte-bounded send queues and a bound on bytes waiting for the hub, files checked as they grow (refused batches roll back with the reason; per-address and total caps count growth), the core's recent-batch memory bounded in bytes, no confirmation for changes that couldn't be written, team libraries only in key-published files on public servers with unpacking bounded by the file cap, usage reports kept in memory, layered IPv6 limits (/64, /56, /48), a global publish rate, connections reserved for sessions, per-address download shares, library connections cut on revoke and removal, a private data directory and key, and safer install.sh, systemd and backups. Percentages and hours unchanged | +| 2026-10-12 | minor | A public `figcraft-server` (open hosting): with `open_publish = true` anyone publishes a file without the server key and gets its edit and view links plus an owner token (128 bits, shown once, stored hashed) that lists, makes and revokes its links, renames it or removes it (`collab.manageFile`); limits per address (connections at once and a minute, messages a second, publishes an hour, files and MiB kept; IPv6 by /64; `X-Forwarded-For` believed only from `trusted_proxy`; tables capped at 10,000 addresses) and for everyone (`max_total_mb`), each refusal saying why; files published without the key expire after `idle_days` unopened (the date shown to everyone in the file); moderation (`files list --sort size|age|idle`, `block-ip`, `reports`, a Report button and `collab.report`); the operator's contact and terms shown in the Share dialog; `GET /healthz` on the same port; `packaging/server/install.sh` for a Debian/Ubuntu machine behind Caddy and a nightly backup timer. Protocol additions within version 2, gated like signals (older apps get clear refusals). Hosting infrastructure rather than a Figma feature: percentages and hours unchanged | +| 2026-10-12 | minor | Experimental `.fig` export (gap 1): `file.export {format: "fig"}`, File ▸ Export… ▸ .fig (experimental), the CLI and MCP write the whole document (layer tree, geometry, paints with image bytes, strokes, effects, text with styled runs, vector networks, auto layout, constraints, components, sets, instances with their overrides and property values, prototype interactions) with a kiwi encoder mirroring the decoder and the schema of the fields written; what's left out is a warning; verified only by round trips through our own importer. File formats 59% → 60%; import/export 84%/57% → 86%/58%; file exchange stays ×0.85; mainstream hours ~130–244 h → ~120–229 h; feature breadth (~84%), ready for real work (~53%), essentials (~57%), mainstream (~49%), beta (~155–280 h) and full parity (~265–523 h) unchanged at this rounding | +| 2026-10-12 | minor | Keyboard shortcut parity suite and bindings (gap 4): a table test with one row per shortcut in the public documentation, macOS and Windows/Linux keys each (129 rows: 126 bound, 110 before; 1 moved off a documented conflict; 2 missing; 2 kept by a platform), plus 48 pinned undocumented bindings so every default binding is in the table; newly bound: A (Frame), Shift+' (pixel grid), Alt+Cmd+\ (others' cursors, new `view.multiplayerCursors`), Ctrl+F6, Shift+Cmd+U (link), Cmd/Ctrl+] and [ (list levels while editing, new `text.listIndent` / `text.listOutdent`), Cmd/Ctrl+Backspace (ungroup), Alt+Space / Ctrl+Space (keyboard box selection, Cmd/Ctrl+arrows draw it out), suggest auto layout, go to main component, arrows that pan with nothing selected (new `view.pan`), Control suspending snapping on macOS, Space/N/C/F while presenting; symbols another layout types with Shift count without it; no default binding uses a system-reserved chord (tested). Menu breadth 207/207 (View ▸ Multiplayer cursors). UI/UX fidelity 58% and the interaction pass 40–60 h unchanged at this rounding | +| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Localization 36% → 39%, 31–54 h → 29–51 h; ready for real work unchanged (~53%); full parity ~267–527 h → ~265–524 h | +| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Localization 35% → 36%, 32–55 h → 31–54 h; ready for real work unchanged (~53%); full parity ~268–528 h → ~267–527 h | +| 2026-10-12 | minor | Prototyping tail (gap 15, the part that doesn't need video): key/gamepad triggers take controller buttons (`Gamepad A` … `Gamepad Home`, the standard layout's 17 buttons, from a Controller menu beside the key field; the REST import maps controller triggers instead of skipping them); the player reads controllers on Windows (Windows.Gaming.Input through the `windows` crate the app already used; no new dependency) and in the browser (the Gamepad API), polling only while the prototype has such a trigger and the window has the focus, and the control channel presses buttons (`ui.input {gamepad}`); macOS, Linux and FreeBSD read no controllers yet. Widgets react to clicks in a presented prototype (the canvas's callback and sandbox limits, on the prototype's copy: the file and its history don't change, restarting starts over). `proto.evaluate` computes an expression where a layer's interaction would run (MCP); the mode lookup and set-variable-mode rules are cases in a behaviour suite (they already held). Prototyping breadth 90% → 92%, ready unchanged at 74%, 3–6 h → 2–5 h; area hours 120–260 → 119–259 (core 33–75 → 32–74), Features (depth) 29–68 h → 28–67 h; percentages unchanged at this rounding; full parity ~276–538 h → ~275–537 h | +| 2026-10-12 | minor | Plugin directory on `figcraft-server` (gap 6): the server keeps plugin bundles (manifest, module, up to 32 assets; an uncompressed length-prefixed format named by its SHA-256) published with the server key (`figcraft-cli plugin publish`, `plugin.directory.publish`), checked in full before they are kept (size cap, manifest and plugin API version, a version newer than the one kept, the module compiled but never run, plain asset names) and capped (`max_plugins`, `max_plugin_mb`, `max_plugins_mb`); anyone who reaches the server lists, searches and fetches them (hash-checked again on read, a shared download budget); the operator moderates with `figcraft-server plugins [remove]`. The app's Plugins ▸ Manage plugins gains a Browse tab: a server's plugins with Install / Update / Remove, an install confirmation that shows the permissions asked for, the source and the SHA-256, the download checked against the listed hash before anything is read, plugins kept per user in hash-named folders; `plugin.directory.list` / `install` (needs `confirm: true`) / `publish` / `unpublish` for scripts and MCP. Not in the browser build yet; no public catalogue, publisher accounts, signatures, ratings or background update checks. Plugins & widgets 55%/32% → 60%/35%, 10–30 h → 8–26 h; Ecosystem dimension 34% → 38%; weighted totals unchanged at this rounding; full parity ~281–548 h → ~279–544 h | +| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Localization 30% → 35%, 40–65 h → 32–55 h; ready for real work ~52% → ~53% (52.9 → 53.1); full parity ~284–554 h → ~276–544 h; beta ~162–292 h → ~155–280 h | +| 2026-10-12 | minor | Plugin widgets and the panel tail (gap 6, plugin API 3; v1 and v2 plugins unchanged): a plugin marked as a widget puts a layer of its own on the canvas from Plugins ▸ Widgets or `widget.insert`: it describes the layer (frames with auto layout, text, rectangles, ellipses, images it carries) and FigCraft validates the description (size, layer count, depth, numbers, colours, text and image sizes) and builds it as a frame with locked layers inside; a click on a part the plugin named (on the canvas with the Move tool, a pointing hand over it, or `widget.click`) calls the plugin back in a fresh, metered sandbox with the widget's state, as one undo step, and the widget is redrawn; the state lives in the widget's own layer data, so widgets survive save and reload, copying, undo, export and live sessions; no plugin can write that data or call `widget.*`. `widget.list` lists them. A Counter example widget. Plugin panels of different plugins open side by side, and a colour field's swatch opens the colour picker (the plugin hears of the colour when a drag ends). Plugins & widgets 45%/28% → 55%/32%, 15–40 h → 10–30 h; Ecosystem dimension 28% → 34%; weighted totals unchanged at this rounding; full parity ~286–558 h → ~281–548 h | +| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): every UI string, command label and menu wording (1,899 entries) machine-drafted in each and marked as drafts awaiting native review, falling back to English per key (new strings need only English and Spanish); 0 counts as singular in French and Brazilian Portuguese; System picks the first OS language with a catalog (pt-PT and pt read pt-BR); long translations elide with a tooltip in dropdowns, two-column labels and the home cards. Localization 20% → 30%, 45–75 h → 40–65 h; ready for real work unchanged (~52%); full parity ~289–564 h → ~284–554 h; beta ~170–305 h → ~162–292 h | +| 2026-10-12 | minor | Cursor chat and spotlight in live sessions: `/` opens a chat bubble at your pointer that everyone sees as you type and that fades a few seconds after the last change; a spotlight (your avatar's menu, `collab.spotlight`) makes everyone follow your view and page until they move their own. Ephemeral presence the host validates and rate-limits, passed only to clients that read it (older clients keep working). Multiplayer & sharing breadth 15% → 17%, ready unchanged (5%), 45–90 h → 42–84 h; collaboration 13% unchanged, 49–98 h → 46–92 h; feature breadth (~84%) and ready for real work (~51%) unchanged at this rounding; full parity ~301–587 h → ~298–581 h | +| 2026-10-12 | minor | Layers and Assets panels behaviour suite written from the public documentation (6 engine tests with about 50 table rows and 5 panel tests driven through the UI; about 30 rows that the code before the fixes contradicts) and its fixes (several layers dropped together, Collapse layers, eye and padlock sweeps, Rename layers dialog with the documented number codes, page dragging, dividers, "Copy of" duplicates, Assets folders, hidden-from-publishing names both ways). Layers panel 98%/82% → 99%/85%; percentages and full parity hours unchanged at this rounding | +| 2026-10-12 | minor | Vector multi-edit and the vector edit tail, with a behaviour suite written from the public documentation (5 tests, about 30 table rows and steps, and a junk-parameter sweep of every vector command in multi-edit mode): several vector layers are edited at once (Enter, double-click or `vector.edit.begin {ids}`; a press on another layer's point, handle or segment makes it the active layer; Shift-click, marquee and lasso select across layers; dragging, the arrows, Delete, delete and heal, mirroring, smoothing, corner radius and point caps and joins reach every layer's selected points in one undo step; the pen, bend, paint bucket, cut and eraser work in the active layer); several Bezier handles are selected with Shift-click and dragged together (`vector.edit.moveHandles`); a sharp point next to a curved segment rounds (the curve is cut where the arc meets it and keeps its shape); a point can have its own stroke cap and join (`vector.edit.strokeCap`, `vector.edit.strokeJoin`, the Point section of the Design panel; canvas, PNG, SVG and PDF, outline stroke); the Pen outside edit mode connects to one of its own points and ends the path, and continues a selected open path from an end point. Left: corner smoothing beyond rectangles, the transform box around points of several layers, per-point caps and joins on dashed, variable-width and brush strokes. Shapes & vector 98%/72% → 99%/73%, 2–4 h → 1–3 h; Figma Draw extras 80%/55% → 88%/60%, 2–5 h → 1–3 h (gap 9: pen pressure on macOS and Linux tablets left). Totals recomputed by the doc's method, which rows since the last recompute had left stale: the area table's weighted breadth is 83.7% (~83%; core 94.1%, ~94%) and ready 63.1% (~63%; core 72.1%, ~72%), hours 133–284 (core 38–83); Features (depth) 61% → 63%, 46–94 h → 34–76 h; ready for real work 52.6% (~52%); mainstream weekly areas 71.0% → 72.1% (× 0.9 × 0.9 × 0.85 = 49.7%, ~49%, rounded down); essentials unchanged (~57%); full parity ~306–595 h → ~294–572 h (on top of the rows merged since: ~301–587 h → ~289–564 h; area-table totals recomputed on the merged table and rounded down: breadth ~84%, ready ~63%, core ~94%/~72%, mainstream weekly areas 72.4%, ~49%) | +| 2026-10-12 | minor | Dev resources (gap 16): links for developers (a URL and a name) on any layer: `devResource.add` / `edit` / `remove` / `list`, undoable, saved and synced; an instance also lists its main component's (and that component's set's) links read-only with where they come from, and layers inside an instance show their main layer's; URLs are checked as web links (http or https, a host, no spaces, at most 2,048 characters) and never fetched (a link added without a name is named after its URL), opened only when clicked; the Inspect tab lists, adds, edits and removes them; `get_node` and `inspect_document` return them as `devResources`. The editor extension stays out of scope (the MCP server serves agents and editors). Dev Mode breadth 93% → 96%, ready 70% → 72%, 1–3 h → 0–1 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~304–593 h → ~303–591 h | +| 2026-10-12 | minor | Extended collections' tail (gap 14): variables a library adds to a collection this file extends now count as a library update of their own (listed by name without a copy yet) and arrive inheriting when accepted, instead of waiting for the library's next other update; a library collection that is itself an extended collection can be extended again, values flowing down the chain as each file accepts updates with every level's overrides kept (checked end to end). Styles & variables breadth 94% → 95%, ready unchanged (76%), 3–5 h → 2–4 h; gap 14 1–2 h → 0–1 h (real-library checks left); feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~305–594 h → ~304–593 h | +| 2026-10-12 | minor | A branch's own version history and finer list merges (gap 17): versions saved or autosaved on a branch tab go into the branch (viewed, restored, renamed, deleted and duplicated there, saved in the file with it); merging adds a version to the main file's history and the merged or archived branch keeps its own; fills, strokes and effects merge item by item, conflicting only when the same item changed both ways, an item was removed on one side or both sides added different items. Comments, versions, find, misc breadth 85% → 87%, ready 60% → 61%, 2–5 h → 1–4 h; gap 17 1–3 h → 1–2 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~306–595 h → ~305–594 h (the Features table here still showed 80%/58%/4–8 h from before branches landed) | +| 2026-10-12 | minor | Headline percentages recomputed from the area table by its own method (weights × written scores): feature breadth ~82% → ~84% (core editor ~93% → ~94%), weighted ready ~61% → ~63%, core ready ~70% → ~72%, mainstream weekly areas 71.3% → 72.3% and mainstream ~49% → ~50%; several earlier rows had moved the area scores without carrying them into the totals. Hours are unchanged (left for the next full re-measure) | +| 2026-10-12 | minor | Text lists, hanging punctuation and decoration styles, with a behaviour suite written from the public documentation (7 layout tests and 7 command tests, about 60 table rows): lists are set per paragraph (the list buttons, shortcuts and Type settings act on the paragraphs the selection touches, the whole layer when it isn't being edited), nest up to 8 levels (Tab and Shift+Tab; numbers go 1. a. i., bullets • ◦ ▪, counted per level), Enter on an empty item and Backspace at an item's start step out a level and then out of the list, typing `- `, `* ` or `1. ` at a paragraph's start starts one; list spacing (between items, in place of paragraph spacing), hanging lists (markers outside the box) and hanging punctuation (opening punctuation outside a start-aligned edge, closing punctuation outside an end-aligned one, taking no room when lines wrap); underline and strikethrough style (solid, dotted, wavy), thickness, underline offset, skip ink and colour. Canvas, PNG, SVG and PDF; `node.set` keys (`indentation`, `listSpacing`, `hangingPunctuation`, `hangingList`, `textDecorationStyle`/`Thickness`/`Offset`/`SkipInk`/`Color`), inspect (`lineTypes`, `lineIndentations`), CSS (`text-decoration-style`/`-thickness`/`-color`, `text-underline-offset`, `text-decoration-skip-ink`) and the REST import (`lineTypes` with `lineIndentations` per paragraph, which collapsed to one list style before). Text 87%/68% → 90%/70%, 5–10 h → 3–7 h; mainstream weekly areas 71.0% → 71.2%; other percentages unchanged at this rounding; full parity ~306–595 h → ~304–592 h | +| 2026-10-12 | minor | Comments and version history behaviour suite written from the public documentation (15 tests, about 80 table rows plus stepwise checks; 4 tests failed before the fixes) and its fixes: in a live session, threads made in the session couldn't be replied to, edited, reacted to, resolved, moved, deleted or opened (their ids, from their creator's id space, were refused as out of range); restoring a version brought back that version's comments, losing newer threads and replies (comments now stay, those on layers the version lacks staying where they were); restoring now keeps the replaced design in the history as an autosave first, unless a version of it was just saved or restored; giving an autosave a title makes it a named version (it could be dropped to make room); merging a branch adds the main file as it was and then a `Merged “”` version to its history. Pinned where the documentation is silent: pins go to the innermost frame under them, copies, duplicates and duplicated pages come without comments, replying reopens a resolved thread, versions stay with whoever saved them in a live session (a shared history needs accounts). Left: a branch's own version history, comparing two versions. Comments, versions, misc 85%/60% → 85%/62%, 2–5 h → 2–4 h (this table still showed 80%/58%/4–8 h from before branches landed); percentages unchanged at this rounding; full parity ~306–595 h → ~306–594 h | +| 2026-10-11 | minor | Canvas navigation, rulers, guides, layout grids, outline mode and measuring behaviour suite written from the public documentation (18 tests, about 120 table rows and checks) and its fixes: the pixel grid shows from 400% (it waited for 800%); guides belong to the page or a top-level frame (a guide dropped over a nested frame goes to the frame holding it, and a frame moved into another keeps its guides but they stop showing, snapping and measuring); a guide can be selected, deleted with Delete, nudged with the arrows and removed from the context menu (`guide.select`, `guide.remove` without an index); Alt-dragging a guide drops a copy and holding Alt while dragging one measures to the nearest edges in the frame it crosses; columns and rows count Auto (as many fixed-size ones as fit); layout grids don't show or snap on a rotated frame; in outline mode shapes are clicked on their outlines, reaching layers behind. Left: outline mode's Include hidden layers and Include object bounds. Canvas & navigation 97%/72% → 98%/74%, 2–5 h → 1–4 h (this table still showed 95%/72%/3–6 h from before the outlines landed); percentages unchanged at this rounding; full parity ~320–615 h → ~319–614 h | +| 2026-10-11 | minor | Export and inspect behaviour suite written from the public documentation (139 cases in 19 tests; 47 failed before the fixes) and its fixes: an export covers what the layer paints, so a drop shadow adds room only where it falls, strokes on boxes and ellipses reach their real width (they added up to four times as much), hidden layers take no room and a mask group exports the mask's area; a slice exports everything under it; slashes in layer names export into folders; Copy as PNG copies the whole selection. Added: the export settings' contents only and include bounding box, and for SVG outline text (off writes ``), unique id attributes and simplified strokes (stroke attributes where exact); Inspect colour formats (hex, RGB, CSS, HSL, HSB), CSS in rem, a Blend row, positions measured from the frame around a layer (groups aren't frames), CSS for rotation, blend modes, gradient directions and kinds, text alignment, case and decoration; icons and images in the asset list (`file.exportLayers {assets}`). Import/export 80%/50% → 82%/53% (the row still showed the figures from before `.fig` import); Dev Mode 90%/64% → 93%/70%; mainstream weekly areas 68.1% → 68.4%; percentages and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | `.fig` import's tail (gap 1): vectors open as editable vector networks (points, curved segments, filled areas with their winding, per-point corner radius and handle mirroring, end caps from the end points), falling back to outlines when the network is missing or damaged; sticky notes, shapes with text, connectors, tables, code blocks, slides and similar layers import as the closest frames, text layers and vectors with a warning instead of being dropped; set-variable, set-variable-mode and conditional prototype actions come across. New parsers fuzzed on fixtures generated in code. File formats 55% → 58%, 8–15 h → 5–10 h; import/export breadth 80% → 82%, ready 50% → 54%; feature and area totals and ready for real work (~49%) unchanged; beta ~173–310 h → ~170–305 h; full parity ~321–618 h → ~318–613 h | +| 2026-10-11 | minor | Shapes and vector behaviour suite written from the public documentation and its fixes: rounded vector points are drawn and exported, a boolean group looks like its top layer (a subtraction like the bottom one), Alt draws lines from the centre, stars keep to 3–60 points, delete and heal follows the curve, Shift-click on a point at a corner of the points' box deselects it, outline stroke keeps the stroke's colour style, opacity and effects, the miter field shows degrees, individual corners only for rectangles and frames, Option/Alt+Shift+F flattens, MCP `key` reaches the edit tools. Added: corner radius and star ratio handles, Count and Ratio fields, cut drags that divide shapes into layers, paint bucket drags. Shapes & vector 97%/68% → 98%/72%, 1–3 h → 2–4 h. Totals recomputed from the area table, which earlier rows had left stale: feature breadth ~80% → ~82% (core ~91% → ~93%), Features (depth) 57% → 61% (core 68% → 69%), 60–115 h → 56–109 h, ready for real work ~49% → ~51%, mainstream ~46% → ~48%; full parity ~321–618 h → ~319–615 h (the selection suite's −3/−5 h hadn't reached the totals) | +| 2026-10-11 | minor | Paint and effects behaviour suite written from the public documentation and its fixes: Normal groups and frames isolate their children and clipping frames don't (canvas, SVG, PDF); plus darker drawn on the canvas and in PNG exports; drop shadows hidden behind what the layer covers unless shown behind transparent areas, SVG included, with filter regions that reach as far as the effects; Fill images cover the box to its edges; "Normal" names Normal in commands and the blend menu. Paint 85%/66% → 87%/70% (breadth from the GIF/TIFF row), 5–9 h → 3–6 h; effects 72% → 75%, 3–6 h → 2–5 h; percentages unchanged at this rounding; full parity ~321–618 h → ~319–616 h | +| 2026-10-11 | minor | Frames, groups, sections and masks behaviour suite written from the public documentation (15 frame, group and section tests, 6 mask tests) and its fixes: a layer a mask hides can't be clicked there (clicks reach what's behind; Select layer agrees), a mask group's box and exports are the mask's (plus any layers under it), sections stay on the page or in sections (moves, drags, pastes, grouping and new sections in frames refused or kept on the page), drawing a frame or section around layers puts those fully inside it in it, resizing a section leaves its contents where they are (and a frame resized with Cmd/Ctrl held keeps its layers' sizes too), and X/Y of layers in groups are measured in the frame (groups have no coordinate space). Frames, groups, sections, masks 95%/75% → 97%/80%, 2–4 h → 1–3 h; percentages unchanged at this rounding; full parity ~323–621 h → ~322–620 h | +| 2026-10-11 | minor | Styles and variables behaviour suite written from the public documentation (18 command-level tests) and its fixes: editing a styled fill, stroke, effect or layout grid list (directly, a paint row, an added fill or a bound variable) unlinks that style instead of being overwritten by the next style edit; applying a style replaces variables bound to what it sets; text styles go on text layers only; deleting a variable (or its collection) unbinds the layers using it, which keep their last value, and aliases to it take its values; bindings are type-checked (a colour variable can't drive a width) and number variables fill text content; aliases are checked when a variable is created. Added: duplicate, move (reorder, into a folder) and rename folder for styles, with a Local styles list grouped by type and folder (click to edit, right-click, drag to reorder); duplicate mode, set as default and move column for modes; per-platform code syntax (Web, iOS, Android) used by generated CSS and the variables JSON; font weight, paragraph spacing/indent and text content scopes for number variables. Found missing: variables inside styles. Styles & variables 92%/72% → 94%/76%, 1–2 h → 3–5 h; percentages unchanged at this rounding; full parity ~323–621 h → ~325–624 h | +| 2026-10-11 | minor | Prototyping behaviour suite written from the public documentation (7 model tests, 12 player tests with 21 gestures) and its fixes: on drag starts only for a drag its animation's way and the animation follows the pointer, key combinations (`Shift+K`) fire (and are recorded and imported), an overlay's after-delay timer starts when it opens, instances of interactive components play their variant's interactions and "Change to" changes the right instance, spring presets are physical springs that play until they settle, smart animate overshoots with spring and back easings, fixed layers draw above scrolling content; added scroll to offsets. Prototyping 88%/70% → 90%/74%, 5–9 h → 3–6 h; mainstream weekly areas 68.1% → 68.4%, mainstream ~46% → ~47%; other percentages unchanged at this rounding; full parity ~320–615 h → ~318–612 h | +| 2026-10-11 | minor | Components behaviour suite written from the public documentation (10 tests, 45 table cases) and its fixes: instances now keep overrides of auto layout padding, gap and alignment, layout grids, export settings, prototype interactions and every text property (resizing, paragraph spacing, lists…; these were lost when the main changed); moving, rotating, resizing or re-constraining layers inside an instance, and its auto layout direction, are refused (the instance itself resizes); switching variants keeps an override only where both variants had the same value (text layers keep theirs); swapping from the Assets panel keeps only text overrides; the default variant is the top-left one (instances, Assets thumbnails, code); hidden exposed instances hide their properties; pushing changes from a nested layer goes to the outer main. Added: variant property reordering and duplicate combination warnings. Components 95%/68% → 96%/72%, 6–12 h → 4–9 h; with #133's selection and transform change, core editor ready ~68% → ~69% and mainstream ~46% → ~47% (weekly areas 68.1% → 68.8%); full parity ~320–615 h → ~315–607 h (the totals now also carry #133's area 2 hours) | +| 2026-10-11 | minor | Embedded image colour profiles (ICC in PNG/JPG/WebP/TIFF, PNG sRGB/gAMA/cHRM) converted into the file's colour space when decoded. Paint 85%/66% → 90%/67% (the row had lagged the parity doc), 5–9 h → 3–6 h; totals unchanged at this rounding | +| 2026-10-11 | minor | Layers panel keyboard and stroke variables: F2 renames the selected layer (also from a layer row with the keyboard, which gets it back; Escape there cancels), Shift+F10 (`view.contextMenu`) opens the context menu at the focused row or below the canvas selection and the menu takes the keyboard, F8 (`select.area`) draws a selection box for keyboard-only users (arrows move it, Shift+arrows size it, Enter selects like a marquee; agents pass a box), long lists checked at 10k layers (revealing a layer, expanding or collapsing everything and arrow keys take at most 1.3 ms a frame in the release UI bench; the revealed row takes the keyboard); colour styles on strokes were already complete, and each side's stroke weight now binds to a number variable (`strokeTopWeight`…, bindable fields in the Design panel and REST import). Layers panel 95%/78% → 98%/82%, 2–4 h → 1–2 h; Strokes 90%/72% → 95%/75%, 1–3 h → 1–2 h; mainstream depth 67.8% → 68.1%; feature breadth (~80%), ready for real work (~49%), essentials (~57%) and mainstream (~46%) unchanged at this rounding; full parity ~321–618 h → ~320–615 h | +| 2026-10-11 | minor | Text behaviour suite written from the public documentation (13 text cases, 5 command tests) and its fixes: CJK lines break between ideographs, kana and Hangul and before and after Latin words without breaking inside them, with the line-start and line-end rules (closing punctuation, small kana and the prolonged sound mark never start a line, opening brackets never end one); a style's line height, baseline and decoration come from its own font, so lines with CJK or emoji drawn by fallback fonts keep the spacing of the others; paragraph indent applies to start-aligned text only; carets, Delete and Backspace step over whole grapheme clusters (emoji sequences, flags, combining marks), which also shape with one font; double-click and word moves take one script's run in text without spaces. Added: vertical trim (cap height to baseline; `leadingTrim`, Type settings, REST import, inspect, CSS `text-box`). Text 85%/64% → 87%/68%, 7–13 h → 5–10 h; percentages unchanged at this rounding; full parity ~323–621 h → ~321–618 h | +| 2026-10-11 | minor | Selection and transform behaviour suite written from the public documentation (5 selection tests with 38 table rows, 11 transform tests) and its fixes: Alt-resizing shrinks about the centre too (it could only grow); groups, frames with Scale or stretch constraints and several layers with different rotations scale each layer along its own axes, keeping its rotation and flip (children turned 90° grew along the wrong axis, nested turned groups too); several layers dragged past an edge mirror and flip; a flipped layer reads back the rotation typed; resizes land on whole pixels with the pixel grid on; a press inside a layer smaller than its handles moves it and the nearest handle wins (the first in reach did), with its handles hidden under 16 px; locked layers selected in the layers panel get no handles and aren't dragged with the rest; the background of a top-level frame with layers starts a marquee inside it (it moved the frame); Select inverse skips hidden layers like Select all. Added: equal spacing snapping with labelled gaps, Cmd/Ctrl while resizing a frame ignores its constraints. Selection & transform 95%/68% → 97%/72%, 6–10 h → 3–5 h; core editor ready ~67% → ~68%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~345–666 h → ~342–661 h | +| 2026-10-11 | minor | Windows QA pass (gap 8): everyday workflows on Windows 11 in the desktop app and through MCP; eight fixes: recovery copies of unsaved documents reopen after a crash or a forced quit (`file.recover`), Ctrl moves and deletes by word in text on Windows and Linux, scrubbed fields keep their value, typing on a field Tab reached edits it, Escape closes dialogs, linear gradients keep square bands, panel hints wrap, the window-close question and "Open in new window" are translated. Percentages and hours unchanged | +| 2026-10-11 | minor | Auto layout behaviour suite written from the public documentation (64 layout cases, 8 command tests) and its fixes: a frame is never smaller than its padding, only the frame's inside stroke counts when strokes are included (and new auto layout includes them), auto gaps stop at 0, grid `fr` tracks behave like the CSS `fr` unit (a track never gets less than its content), canvas stacking is drawn, exported and hit (it was stored only). Added: auto gaps evenly and around, vertical wrap, grid cell alignment, strokes and canvas stacking controls. Auto layout 95%/68% → 98%/74%, 6–12 h → 3–6 h; percentages unchanged at this rounding; full parity ~348–672 h → ~345–666 h | +| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | +| 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | +| 2026-10-11 | minor | Localization layer landed (gap 5): per-language catalogs baked in at build time with English text as keys and fallback, placeholders and plurals, command labels by id, Preferences ▸ Language (System follows the OS or the browser) applied live and `pref.language`; English catalog and a complete Spanish draft awaiting native review. Localization 10% → 20%, 60–100 h → 45–75 h; ready for real work unchanged (~47%); full parity ~363–697 h → ~348–672 h; beta ~200–365 h → ~195–355 h | +| 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | +| 2026-10-11 | minor | Plugin panels and settings landed (gap 6, ABI v2; v1 plugins unchanged): a plugin shows a panel of notes, fields (text, number, checkbox, menu, list, colour) and buttons drawn with FigCraft's widgets; a button or a changed value calls it back with every value (same limits per callback, one undo step per callback that edits, rolled back on failure), its module staying loaded while the panel is open; `plugin.ui` / `plugin.event` / `plugin.close` drive panels headless; per-plugin settings (64 KB) kept with the preferences; the Swatch grid example gained a palette panel that remembers the last palette. Plugins & widgets breadth 35% → 45%, ready 20% → 28%, 25–55 h → 15–40 h; ecosystem 20% → 28%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~373–712 h → ~363–697 h | +| 2026-10-11 | minor | Canvas fixes (gap 13 closed): boolean groups are clicked where their result is (a click in a subtracted hole reaches what's behind; with the group or an operand selected its operands are hit, so each can still be picked), a layer dragged inside a group stays in the group (frames and auto layout still take other drops), View ▸ Frame outlines and View ▸ Slices draw on the canvas, and the idle tests wait for the render thread instead of counting frames. Shapes & vector 1–3 h → 1–2 h (vector QA with real files left); percentages, feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~378–722 h → ~375–717 h | +| 2026-10-11 | minor | Stretch and scatter brushes and pen pressure landed (gap 9): eight brushes drawn in code (four stretched along the path, four scattered with spacing, size, rotation and scatter jitter from a seed) in the stroke settings, drawn everywhere strokes are and flattened by outline stroke; the Pencil records pen pressure as the stroke's width profile (Windows pens, browser styluses; macOS and Linux tablets report none) and can draw with a brush; Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–9 h; Strokes 2–4 h → 1–3 h; Hardware 50% → 55%, 15–30 h → 13–27 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~378–722 h → ~358–687 h | +| 2026-10-11 | minor | Text on a path and pattern fills landed (gap 9): text follows a path or shape (start, side, alignment; edited in place; canvas, PNG, SVG and PDF; SVG `` imports as text on a path) and pattern fills tile a layer (grid or half-shifted rows or columns, scale, spacing, alignment; live as the source changes; SVG ``, PDF tiling patterns); Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–10 h; Text 8–14 h → 7–13 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~375–715 h | +| 2026-10-11 | minor | Team libraries on `figcraft-server` landed (gap 10): a file's library is published to the server under the file and enabled in other files by a share link (per-asset versions; updates noticed in the background and accepted with overrides kept), the server counts which files use which components, and a whole library can be swapped for another by names; Team libraries breadth 40% → 70%, ready 25% → 40%, 10–20 h → 4–8 h; collaboration 12% → 13%, 55–110 h → 49–98 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~380–725 h → ~375–715 h | +| 2026-10-11 | minor | Branches landed (gap 17): File ▸ Create branch… copies the file into a branch tab that remembers its base; Review and merge shows both sides' changes (layers with Inspect-style property changes, styles, variables, comments) and the conflicts (the same property changed on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merging is one undo step and archives the branch; Update from main; branches are saved in the `.figcraft` file. Comments, versions, find, misc breadth 80% → 85%, ready 58% → 60%, 4–8 h → 2–5 h; collaboration 12% → 13%; menu breadth 204/204 (the catalog gained File ▸ Create branch…); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~378–722 h | +| 2026-10-11 | minor | Animated GIF fills and TIFF images (gap 12, without video and HEIC): GIF fills keep the original file, show the first frame on the canvas and in exports and play in the presenter and the preview (frames decoded on first play, at most 500 frames and 128 MB; always looping; the window wakes only for a GIF's next frame); TIFF images open, import, place and paste. Paint breadth 85% → 87%, 5–9 h → 4–7 h; gap 12 6–12 h → 5–10 h (video playback and HEIC left, both needing non-Rust decoders); feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | The last three catalogued menu items got commands (gap 21): File ▸ Open recent (recent files kept in the preferences, at most 20, a file that's gone greyed in the menu and dropped when picked, Clear recent), View ▸ Outlines ▸ Show outlines for selection and View ▸ Mask outlines: menu breadth 98.5% → 100% (203/203); Canvas & navigation breadth 95% → 97%, 3–6 h → 2–5 h; feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | Extended collections, variables import/export and gradient stop and variant property bindings landed (gap 14): Styles & variables breadth 85% → 92%, ready 70% → 72%, 4–8 h → 1–2 h; feature breadth unchanged (~79%); ready for real work unchanged (~47%); full parity ~385–730 h → ~380–725 h | +| 2026-10-11 | minor | Collaboration wording after the standalone server: `figcraft-server` keeps many files, persisted as a snapshot plus a log of what changed (compacted, checksummed, damaged files skipped and reported), restarts lose nothing confirmed and apps resume where they were; the app publishes its file to it; share links per file let people edit or only view (the server refuses a viewer's edits), are revocable and stored as hashes; TLS comes from a reverse proxy (the desktop app has none of its own yet); percentages and hours unchanged until the next re-measure | +| 2026-10-11 | minor | Live boolean editing, Design panel arc fields, the shape builder on strokes and open paths and one-sided variable widths landed (gap 13): Shapes & vector breadth 92% → 97%, ready 66% → 68%, 8–14 h → 1–3 h; feature breadth ~78% → ~79%; ready for real work unchanged (~47%); full parity ~390–745 h → ~385–730 h | +| 2026-10-11 | minor | Collaboration wording after the collaboration polish: styles, variables, collections, modes and comments created in a live session take ids from their creator's id space (concurrent creations no longer overwrite each other; the server checks it), others' text carets and selections show on the canvas, and typing in a layer someone else is typing in says so; percentages unchanged | +| 2026-10-11 | minor | Wording only: live sessions (#104), then the browser joining them and `figcraft://` invite links opening the desktop app; percentages and hours unchanged until the next re-measure | +| 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 | minor | Readiness by audience with hours to ~95% (full ~400–750, mainstream ~180–330, essentials ~85–160) | +| 2026-10-10 | minor | Added essentials user (~53%) and mainstream practitioner (~43%); ready for real work unchanged at ~46% | +| 2026-10-10 | minor | Alpha gate (core workflows) checked under the revised stage rules: passes, stays alpha | +| 2026-10-10 | major | Rewritten to the craftrules progress-docs shape after a full re-measure; rubric, area narrative and inventory moved to docs/target-app-parity.md, gaps to docs/gaps.md, milestones to docs/roadmap.md. Depth ~79% (self-graded, 16 areas) → ready for real work ~46% (all dimensions); full parity 115–195 h → 420–790 h | +| 2026-10-10 | minor | Day-2 refreshes after each batch of PRs (depth 58% → 79%) | +| 2026-10-09 | major | First roadmap | diff --git a/crates/engine/src/cmd/structure.rs b/crates/engine/src/cmd/structure.rs index fd766735..ca91549b 100644 --- a/crates/engine/src/cmd/structure.rs +++ b/crates/engine/src/cmd/structure.rs @@ -1,653 +1,653 @@ -//! Structure: groups, frames, masks, boolean groups, flatten, outline stroke, auto layout, pages. - -use figcraft_color::{Color, Paint}; -use figcraft_doc::*; -use figcraft_geom::{Affine, BezPath, Rect, Shape}; -use serde_json::{Value, json}; - -use super::edit::{roots, z_sorted}; -use super::{CommandSpec, ok}; -use crate::params::*; -use crate::{EngineError, Result, Session, bad, spec}; - -pub fn specs() -> Vec { - vec![ - spec!("object.group", "Group selection", "{ids?, name?}", true, |s, p| wrap(s, p, Wrap::Group)), - spec!("object.ungroup", "Ungroup selection", "{ids?}", true, ungroup), - spec!("object.frameSelection", "Frame selection", "{ids?, name?}", true, |s, p| wrap(s, p, Wrap::Frame)), - spec!("object.removeFrame", "Remove frame", "{ids?}", true, ungroup), - spec!( - "object.convertToGroup", - "Convert to group", - "{ids?} (frames become groups in place, their layers staying where they are; what only a frame has goes: auto layout, fills, strokes, corner radius, clipping, layout grids, guides and scrolling. Groups are left alone; instances, main components, layers inside instances and empty frames are refused)", - true, - |s, p| convert(s, p, "object.convertToGroup", false) - ), - spec!( - "object.convertToFrame", - "Convert to frame", - "{ids?} (groups become frames of the same box in place, their layers staying where they are: no fill, not clipping. Frames are left alone; instances, main components and layers inside instances are refused)", - true, - |s, p| convert(s, p, "object.convertToFrame", true) - ), - spec!( - "object.wrapInSection", - "Wrap in new section", - "{ids?, name?} (layers on the page or in a section; the section wraps them with some room)", - true, - |s, p| wrap(s, p, Wrap::Section) - ), - spec!( - "object.resizeToFit", - "Resize to fit", - "{ids?} (frames, sections and components shrink or grow to their contents; the contents stay put)", - true, - resize_to_fit - ), - spec!("object.mask", "Use as mask", "{ids?}", true, mask), - spec!("object.releaseMask", "Release mask", "{ids?}", true, |s, p| { - for i in req_targets("object.releaseMask", s, p)? { - s.doc_mut()?.get_mut(i)?.is_mask = false; - } - ok() - }), - spec!("bool.union", "Union selection", BOOL_PARAMS, true, |s, p| crate::booleans::apply(s, "bool.union", p, BoolOp::Union)), - spec!("bool.subtract", "Subtract selection", BOOL_PARAMS, true, |s, p| crate::booleans::apply(s, "bool.subtract", p, BoolOp::Subtract)), - spec!("bool.intersect", "Intersect selection", BOOL_PARAMS, true, |s, p| crate::booleans::apply(s, "bool.intersect", p, BoolOp::Intersect)), - spec!("bool.exclude", "Exclude selection", BOOL_PARAMS, true, |s, p| crate::booleans::apply(s, "bool.exclude", p, BoolOp::Exclude)), - spec!("bool.flatten", "Flatten boolean", "{ids?}", true, flatten), - spec!("vector.flatten", "Flatten", "{ids?}", true, flatten), - spec!("vector.outlineStroke", "Outline stroke", "{ids?}", true, outline_stroke), - spec!("layout.add", "Add auto layout", "{ids?, direction? (horizontal|vertical|grid)}", true, layout_add), - spec!("layout.remove", "Remove auto layout", "{ids?}", true, |s, p| { - for i in req_targets("layout.remove", s, p)? { - // Hugging needs auto layout and the children that filled have nothing to fill: both - // become fixed (the frame keeps filling its own auto layout parent). - let doc = s.doc_mut()?; - doc.get_mut(i)?.layout = None; - figcraft_layout::coherent_sizing(doc, i); - } - ok() - }), - spec!("layout.suggest", "Suggest auto layout", "{ids?}", true, layout_add), - spec!("object.moveToPage", "Move to page", "{ids?, page}", true, move_to_page), - spec!("object.setThumbnail", "Set as thumbnail", "{id?}", true, |s, p| { - let id = targets(s, p).first().copied().ok_or_else(|| bad("object.setThumbnail", "select a frame"))?; - let doc = s.doc_mut()?; - for n in doc.ids().collect::>() { - if let Some(node) = doc.node(n) - && node.plugin_data.contains_key("figcraft.thumbnail") - { - doc.get_mut(n)?.plugin_data.remove("figcraft.thumbnail"); - } - } - doc.get_mut(id)?.plugin_data.insert("figcraft.thumbnail".into(), "true".into()); - ok() - }), - spec!("object.rasterize", "Rasterize selection", "{ids?, scale? (default 2)}", true, rasterize), - spec!("vector.reverse", "Reverse path direction", "{ids?}", true, |s, p| { - for i in req_targets("vector.reverse", s, p)? { - if let NodeKind::Vector { network } = &mut s.doc_mut()?.get_mut(i)?.kind { - for seg in &mut network.segments { - std::mem::swap(&mut seg.a, &mut seg.b); - std::mem::swap(&mut seg.ta, &mut seg.tb); - } - } - } - ok() - }), - spec!("vector.simplify", "Simplify vector", "{ids?, tolerance?}", true, simplify), - spec!("vector.offset", "Offset path", "{ids?, offset}", true, offset_path), - spec!("vector.join", "Join selection", "{ids?} (in vector edit mode: joins the selected points)", true, |s, p| { - crate::vectoredit::join_in_edit_mode(s, false).unwrap_or_else(|| join(s, p)) - }), - spec!("vector.smoothJoin", "Smooth join selection", "{ids?} (in vector edit mode: joins the selected points smoothly)", true, |s, p| { - crate::vectoredit::join_in_edit_mode(s, true).unwrap_or_else(|| join(s, p)) - }), - ] -} - -enum Wrap { - Group, - Frame, - Section, - Bool(BoolOp), -} - -/// Room a section made around layers leaves on each side. -const SECTION_PAD: f64 = 40.0; - -/// What the boolean commands do with what they're given. -const BOOL_PARAMS: &str = "{ids?} — layers inside boolean groups: sets their own operation (how each combines with the operands below it); one boolean group: sets its operation (every operand follows it); otherwise wraps the layers in a new boolean group"; - -/// Wrap the selection in a new boolean group doing `op`. -pub(crate) fn wrap_boolean(s: &mut Session, p: &Value, op: BoolOp) -> Result { - wrap(s, p, Wrap::Bool(op)) -} - -/// Wrap the selection in a new container at the topmost selected layer's place. -fn wrap(s: &mut Session, p: &Value, w: Wrap) -> Result { - let ids = req_targets("object.group", s, p)?; - let doc = s.doc_mut()?; - let ids = z_sorted(doc, &roots(doc, &ids)); - let top = *ids.last().ok_or_else(|| bad("object.group", "nothing selected"))?; - let parent = doc.get(top)?.parent.ok_or_else(|| bad("object.group", "can't group a page"))?; - // Sections sit on the page or in sections, never in frames or groups. - let board = |doc: &Document, i: NodeId| { - doc.node(i).and_then(|n| n.parent).and_then(|p| doc.node(p)).is_some_and(|p| p.is_page() || matches!(p.kind, NodeKind::Section { .. })) - }; - if matches!(w, Wrap::Section) && !ids.iter().all(|i| board(doc, *i)) { - return Err(bad("object.wrapInSection", "only layers on the page or in a section can be wrapped in a section")); - } - if !matches!(w, Wrap::Section) && ids.iter().any(|i| doc.node(*i).is_some_and(|n| matches!(n.kind, NodeKind::Section { .. }))) { - return Err(bad("object.group", "sections can't go in groups or frames")); - } - let index = doc.index_in_parent(top).map(|i| i + 1); - let mut bounds = doc.bounds_of(&ids).unwrap_or_default(); - if matches!(w, Wrap::Section) { - bounds = bounds.inflate(SECTION_PAD, SECTION_PAD); - } - let pw = doc.world_transform(parent); - let local = figcraft_geom::transform_rect_bbox(pw.inverse(), bounds); - let (kind, base) = match &w { - Wrap::Group => (NodeKind::Group, "Group"), - Wrap::Frame => (NodeKind::Frame, "Frame"), - Wrap::Section => (NodeKind::Section { contents_hidden: false }, "Section"), - Wrap::Bool(op) => (NodeKind::BooleanOp { op: *op }, op.label()), - }; - let name = string(p, "name").unwrap_or_else(|| if matches!(w, Wrap::Bool(_)) { base.to_string() } else { doc.next_name(base) }); - let gid = doc.alloc_id(); - let mut g = match w { - Wrap::Section => Node::new_section(gid, name, local.width(), local.height()), - _ => Node::new(gid, kind, name, local.width(), local.height()), - }; - g.transform = Affine::translate((local.x0, local.y0)); - if let Wrap::Bool(op) = w { - // A boolean group looks like its top layer; a subtraction like the bottom one it cuts. - let source = if op == BoolOp::Subtract { ids.first() } else { ids.last() }; - if let Some(first) = source.and_then(|i| doc.node(*i)) { - g.fills = if first.fills.is_empty() { vec![Paint::solid(Color::DEFAULT_SHAPE)] } else { first.fills.clone() }; - g.strokes = first.strokes.clone(); - g.stroke = first.stroke.clone(); - g.effects = first.effects.clone(); - } - g.blend = figcraft_color::BlendMode::Normal; - } - if let Wrap::Frame = w { - g.clip = true; - } - // Insert the container where the topmost layer is. - doc.insert(g, parent, index)?; - for i in &ids { - doc.reparent(*i, gid, None)?; - } - if !matches!(w, Wrap::Frame | Wrap::Section) { - let _ = doc.fit_to_children(gid); - } - s.set_selection(vec![gid]); - Ok(json!({"id": gid})) -} - -/// Turn frames into groups or groups into frames in place (same id, name, layers, effects, -/// opacity, exports and interactions), the layers inside staying where they are on the page. -/// Nothing changes unless every layer can convert. -fn convert(s: &mut Session, p: &Value, cmd: &'static str, to_frame: bool) -> Result { - let ids = req_targets(cmd, s, p)?; - let doc = s.doc_mut()?; - let mut todo = Vec::new(); - for i in ids { - let n = doc.get(i)?; - let refuse = |why: &str| Err(bad(cmd, format!("\"{}\" {why}", n.name))); - match (&n.kind, to_frame) { - (NodeKind::Instance { .. }, _) => return refuse("is an instance: its layers come from its main component (detach it first)"), - (NodeKind::Component { .. }, _) => return refuse("is a main component, which stays a frame"), - (NodeKind::Frame | NodeKind::Group, _) if crate::components::owning_instance(doc, i).is_some() => { - return refuse("is inside an instance: edit its main component, or detach the instance"); - } - (NodeKind::Frame, false) if n.children.is_empty() => return refuse("is empty: a group needs layers"), - (NodeKind::Frame, false) | (NodeKind::Group, true) => todo.push(i), - (NodeKind::Frame | NodeKind::Group, _) => {} - _ => return refuse("is neither a frame nor a group"), - } - } - for i in &todo { - if to_frame { - // The group's box as it is; nothing painted or clipped, so the look stays. - let n = doc.get_mut(*i)?; - n.kind = NodeKind::Frame; - n.clip = false; - n.fills.clear(); - n.strokes.clear(); - continue; - } - let n = doc.get_mut(*i)?; - n.kind = NodeKind::Group; - (n.layout, n.clip, n.overflow) = (None, false, proto::Overflow::None); - n.fills.clear(); - n.strokes.clear(); - n.grids.clear(); - n.guides.clear(); - n.corners = Corners::default(); - (n.styles.fill, n.styles.stroke, n.styles.grid) = (None, None, None); - // Variables bound to what went would bring it back. - n.bound.retain(|k, _| matches!(k.as_str(), "opacity" | "visible")); - let kids = n.children.clone(); - for k in kids { - // Out of the flow there is nothing to fill or to be absolute against. - doc.get_mut(k)?.child_layout.absolute = false; - } - figcraft_layout::coherent_sizing(doc, *i); - doc.fit_to_children(*i)?; - } - Ok(json!({"ids": todo})) -} - -fn ungroup(s: &mut Session, p: &Value) -> Result { - let ids = req_targets("object.ungroup", s, p)?; - let doc = s.doc_mut()?; - let mut out = Vec::new(); - for g in ids { - let n = doc.get(g)?; - if !(matches!(n.kind, NodeKind::Group | NodeKind::BooleanOp { .. } | NodeKind::Frame | NodeKind::Section { .. })) { - continue; - } - let Some(parent) = n.parent else { continue }; - let idx = doc.index_in_parent(g).unwrap_or(0); - let kids = n.children.clone(); - // Ungrouping a group keeps its opacity on the children (multiplied). - let op = n.opacity; - for (k, c) in kids.iter().enumerate() { - doc.reparent(*c, parent, Some(idx + k))?; - // Released operands no longer combine with anything. - if doc.get(*c)?.bool_op.is_some() { - doc.get_mut(*c)?.bool_op = None; - } - if op < 1.0 { - let cn = doc.get_mut(*c)?; - cn.opacity *= op; - } - out.push(*c); - } - doc.remove(g)?; - } - s.set_selection(out.clone()); - Ok(json!({"ids": out})) -} - -fn mask(s: &mut Session, p: &Value) -> Result { - let ids = req_targets("object.mask", s, p)?; - if ids.len() == 1 { - let doc = s.doc_mut()?; - let id = ids.first().copied().unwrap_or(NodeId(0)); - let n = doc.get_mut(id)?; - n.is_mask = !n.is_mask; - return ok(); - } - // Group, then the bottom-most child is the mask. - let r = wrap(s, &json!({"ids": ids, "name": "Mask group"}), Wrap::Group)?; - let gid = id_of(&r["id"]).ok_or_else(|| EngineError::Other("group failed".into()))?; - let doc = s.doc_mut()?; - if let Some(first) = doc.get(gid)?.children.first().copied() { - doc.get_mut(first)?.is_mask = true; - } - Ok(r) -} - -/// The outline of a node in its parent's space (shapes, text, booleans, groups). -pub fn outline_in_parent(doc: &Document, id: NodeId) -> Option { - let n = doc.node(id)?; - let (mut p, _) = figcraft_render::subtree_outline(doc, n, 0)?; - p.apply_affine(n.transform); - Some(p) -} - -fn flatten(s: &mut Session, p: &Value) -> Result { - let ids = req_targets("vector.flatten", s, p)?; - let doc = s.doc_mut()?; - let ids = z_sorted(doc, &roots(doc, &ids)); - let top = *ids.last().ok_or_else(|| bad("vector.flatten", "nothing selected"))?; - let parent = doc.get(top)?.parent.ok_or_else(|| bad("vector.flatten", "can't flatten a page"))?; - let index = doc.index_in_parent(top).map(|i| i + 1); - let pw = doc.world_transform(parent); - let mut path = BezPath::new(); - let mut even = false; - for i in &ids { - let Some(n) = doc.node(*i) else { continue }; - // Booleans flatten to their result; strokes don't get outlined. - let Some((mut op, rule)) = (match &n.kind { - NodeKind::Group | NodeKind::Frame | NodeKind::Component { .. } | NodeKind::Instance { .. } => figcraft_render::subtree_outline(doc, n, 0), - _ => figcraft_render::node_outline(doc, n), - }) else { - continue; - }; - even |= rule == figcraft_render::FillRule::EvenOdd; - // Into the common parent's space. - let w = doc.world_transform(*i); - op.apply_affine(pw.inverse() * w); - path.extend(op.elements().iter().copied()); - } - if path.elements().is_empty() { - return Err(EngineError::Disabled("vector.flatten".into(), "nothing to flatten".into())); - } - let first = ids.first().and_then(|i| doc.node(*i)).cloned(); - let bb = path.bounding_box(); - path.apply_affine(Affine::translate((-bb.x0, -bb.y0))); - let mut net = VectorNetwork::from_path(&path); - for r in &mut net.regions { - r.winding = if even { WindingRule::EvenOdd } else { WindingRule::NonZero }; - } - let vid = doc.alloc_id(); - let name = first.as_ref().map(|f| f.name.clone()).unwrap_or_else(|| "Vector".into()); - let mut v = Node::new(vid, NodeKind::Vector { network: net }, name, bb.width(), bb.height()); - v.transform = Affine::translate((bb.x0, bb.y0)); - if let Some(f) = first { - v.fills = if matches!(f.kind, NodeKind::Group | NodeKind::Line) && f.fills.is_empty() { - vec![Paint::solid(Color::DEFAULT_SHAPE)] - } else { - f.fills.clone() - }; - v.strokes = f.strokes.clone(); - v.stroke = StrokeProps { align: StrokeAlign::Center, ..f.stroke.clone() }; - v.effects = f.effects.clone(); - v.opacity = f.opacity; - v.blend = if matches!(f.blend, figcraft_color::BlendMode::PassThrough) { figcraft_color::BlendMode::Normal } else { f.blend }; - } - doc.insert(v, parent, index)?; - for i in &ids { - doc.remove(*i)?; - } - s.set_selection(vec![vid]); - Ok(json!({"id": vid})) -} - -fn outline_stroke(s: &mut Session, p: &Value) -> Result { - let ids = req_targets("vector.outlineStroke", s, p)?; - let mut out = Vec::new(); - for i in ids { - let doc = s.doc_mut()?; - let n = doc.get(i)?.clone(); - if !n.has_visible_stroke() { - continue; - } - let closed = figcraft_doc::geometry::is_closed(&n); - let Some((area, rule)) = (match &n.kind { - NodeKind::Text(_) => figcraft_render::node_outline(doc, &n) - .map(|(p, _)| (figcraft_render::draw::stroke_area(&n, &p, closed), figcraft_render::FillRule::Winding)), - _ => figcraft_render::stroke_fill(doc, &n), - }) else { - continue; - }; - let mut path = (*area).clone(); - // Simplify non-zero strokes into even-odd contours. Inside/outside rings already use - // even-odd: interpreting their same-direction contours as non-zero would fill the hole. - if rule == figcraft_render::FillRule::Winding - && let Ok(u) = figcraft_pathops::simplify(&path) - { - path = u; - } - let bb = path.bounding_box(); - path.apply_affine(Affine::translate((-bb.x0, -bb.y0))); - let mut net = VectorNetwork::from_path(&path); - for r in &mut net.regions { - r.winding = WindingRule::EvenOdd; - } - let parent = n.parent.ok_or_else(|| bad("vector.outlineStroke", "page"))?; - let idx = doc.index_in_parent(i); - let vid = doc.alloc_id(); - let mut v = Node::new(vid, NodeKind::Vector { network: net }, n.name.clone(), bb.width(), bb.height()); - v.transform = n.transform * Affine::translate((bb.x0, bb.y0)); - // The stroke's paints (and its colour style) fill the outline. - v.fills = n.strokes.clone(); - v.strokes.clear(); - v.styles.fill = n.styles.stroke; - if n.fills.iter().any(|f| f.visible) && closed { - // Keep the fill as its own layer under the outline, grouped. - doc.insert(v, parent, idx.map(|x| x + 1))?; - let fill_id = i; - { - let f = doc.get_mut(fill_id)?; - f.strokes.clear(); - } - let r = wrap(s, &json!({"ids": [fill_id, vid], "name": n.name}), Wrap::Group)?; - if let Some(g) = id_of(&r["id"]) { - out.push(g); - } - continue; - } - // Standing in for the layer, the outline keeps how it is drawn and placed. - (v.opacity, v.blend, v.effects, v.styles.effect) = (n.opacity, n.blend, n.effects.clone(), n.styles.effect); - (v.constraints, v.child_layout, v.visible, v.locked) = (n.constraints, n.child_layout.clone(), n.visible, n.locked); - doc.insert(v, parent, idx.map(|x| x + 1))?; - doc.remove(i)?; - out.push(vid); - } - s.set_selection(out.clone()); - Ok(json!({"ids": out})) -} - -/// Infer direction, gap and padding from where children are (Figma's "Add auto layout"). -fn infer_layout(doc: &Document, frame: NodeId) -> AutoLayout { - let Some(f) = doc.node(frame) else { return AutoLayout::default() }; - let boxes: Vec = - f.children.iter().filter_map(|c| doc.node(*c)).map(|c| figcraft_geom::transform_rect_bbox(c.transform, c.local_rect())).collect(); - if boxes.is_empty() { - return AutoLayout { padding: [10.0; 4], gap: 10.0, ..Default::default() }; - } - let span_x = boxes.iter().map(|b| b.x1).fold(f64::MIN, f64::max) - boxes.iter().map(|b| b.x0).fold(f64::MAX, f64::min); - let span_y = boxes.iter().map(|b| b.y1).fold(f64::MIN, f64::max) - boxes.iter().map(|b| b.y0).fold(f64::MAX, f64::min); - let vertical = span_y > span_x; - let mut sorted = boxes.clone(); - sorted.sort_by(|a, b| if vertical { a.y0.total_cmp(&b.y0) } else { a.x0.total_cmp(&b.x0) }); - let gaps: Vec = - sorted.windows(2).filter_map(|w| Some(if vertical { w.get(1)?.y0 - w.first()?.y1 } else { w.get(1)?.x0 - w.first()?.x1 })).collect(); - let gap = if gaps.is_empty() { 10.0 } else { (gaps.iter().sum::() / gaps.len() as f64).max(0.0).round() }; - let all = boxes.iter().copied().reduce(|a, b| a.union(b)).unwrap_or_default(); - let pad = [all.y0.max(0.0), (f.width - all.x1).max(0.0), (f.height - all.y1).max(0.0), all.x0.max(0.0)].map(f64::round); - AutoLayout { mode: if vertical { LayoutMode::Vertical } else { LayoutMode::Horizontal }, gap, padding: pad, ..Default::default() } -} - -fn layout_add(s: &mut Session, p: &Value) -> Result { - let ids = req_targets("layout.add", s, p)?; - let dir = string(p, "direction"); - let doc = s.doc()?; - let all_frames = ids.iter().all(|i| doc.node(*i).is_some_and(|n| n.kind.is_frame_like() && n.layout.is_none())); - let targets = if all_frames { - ids - } else { - // Wrap non-frames in a new frame without a fill, then add auto layout to it. - let r = wrap(s, &json!({"ids": ids}), Wrap::Frame)?; - let fid = id_of(&r["id"]).ok_or_else(|| EngineError::Other("wrap failed".into()))?; - let f = s.doc_mut()?.get_mut(fid)?; - f.fills.clear(); - f.clip = false; - vec![fid] - }; - let doc = s.doc_mut()?; - for f in &targets { - let mut al = infer_layout(doc, *f); - if let Some(d) = &dir { - al.mode = match d.to_ascii_lowercase().as_str() { - "vertical" | "column" => LayoutMode::Vertical, - "grid" => LayoutMode::Grid, - _ => LayoutMode::Horizontal, - }; - } - // Children in reading order along the axis. - let mut kids = doc.get(*f)?.children.clone(); - let vertical = al.mode == LayoutMode::Vertical; - kids.sort_by(|a, b| { - let pa = doc.node(*a).map(|n| figcraft_geom::transform_rect_bbox(n.transform, n.local_rect())).unwrap_or_default(); - let pb = doc.node(*b).map(|n| figcraft_geom::transform_rect_bbox(n.transform, n.local_rect())).unwrap_or_default(); - if vertical { pa.y0.total_cmp(&pb.y0) } else { pa.x0.total_cmp(&pb.x0) } - }); - let n = doc.get_mut(*f)?; - n.children = kids; - n.layout = Some(al); - n.child_layout.sizing_h = Sizing::Hug; - n.child_layout.sizing_v = Sizing::Hug; - } - s.set_selection(targets.clone()); - Ok(json!({"ids": targets})) -} - -fn move_to_page(s: &mut Session, p: &Value) -> Result { - let ids = req_targets("object.moveToPage", s, p)?; - let page = req_id("object.moveToPage", p, "page")?; - let doc = s.doc_mut()?; - if !doc.node(page).is_some_and(Node::is_page) { - return Err(bad("object.moveToPage", format!("{page} is not a page"))); - } - if doc.is_divider(page) { - return Err(bad("object.moveToPage", "a divider holds no layers")); - } - for i in z_sorted(doc, &roots(doc, &ids)) { - doc.reparent(i, page, None)?; - } - s.set_selection(vec![]); - ok() -} - -fn rasterize(s: &mut Session, p: &Value) -> Result { - let ids = req_targets("object.rasterize", s, p)?; - let scale = num_or(p, "scale", 2.0); - let mut out = Vec::new(); - for i in ids { - let doc = s.doc()?; - let Some(pix) = figcraft_render::render_node(doc, i, scale) else { continue }; - let png = figcraft_render::encode_png(&pix).map_err(EngineError::Other)?; - let b = figcraft_render::export_bounds(doc, i).unwrap_or_default(); - let parent = doc.get(i)?.parent.ok_or_else(|| bad("object.rasterize", "page"))?; - let idx = doc.index_in_parent(i); - let pw = doc.world_transform(parent); - let name = doc.get(i)?.name.clone(); - let docm = s.doc_mut()?; - let (hash, _, _) = figcraft_format::add_image(docm, png).map_err(|e| EngineError::Other(e.to_string()))?; - let rid = docm.alloc_id(); - let mut r = Node::new(rid, NodeKind::Rectangle, name, b.width(), b.height()); - r.transform = pw.inverse() * Affine::translate((b.x0, b.y0)); - r.fills = vec![Paint::image(hash, figcraft_color::ImageScaleMode::Fill)]; - docm.insert(r, parent, idx.map(|x| x + 1))?; - docm.remove(i)?; - out.push(rid); - } - s.set_selection(out.clone()); - Ok(json!({"ids": out})) -} - -fn simplify(s: &mut Session, p: &Value) -> Result { - let tol = num_or(p, "tolerance", 0.5).max(0.01); - for i in req_targets("vector.simplify", s, p)? { - let doc = s.doc_mut()?; - let n = doc.get_mut(i)?; - if let NodeKind::Vector { network } = &mut n.kind { - let fill = network.fill_path(); - let src = if fill.elements().is_empty() { network.stroke_path() } else { fill }; - let simplified = figcraft_geom::kurbo::simplify::simplify_bezpath(src.iter(), tol, &Default::default()); - let regions = !network.regions.is_empty(); - let mut net = VectorNetwork::from_path(&simplified); - if !regions { - net.regions.clear(); - } - *network = net; - } - } - ok() -} - -fn offset_path(s: &mut Session, p: &Value) -> Result { - let d = req_num("vector.offset", p, "offset")?; - for i in req_targets("vector.offset", s, p)? { - let doc = s.doc_mut()?; - let n = doc.get(i)?.clone(); - let Some((path, _)) = figcraft_render::node_outline(doc, &n) else { continue }; - let props = StrokeProps { weight: d.abs() * 2.0, join: StrokeJoin::Round, align: StrokeAlign::Center, ..Default::default() }; - let ring = figcraft_pathops::stroke_center(&path, d.abs() * 2.0, &props); - let res = if d >= 0.0 { - figcraft_pathops::boolean(&[path, ring], BoolOp::Union) - } else { - figcraft_pathops::boolean(&[path, ring], BoolOp::Subtract) - }; - let Ok(mut res) = res else { continue }; - let bb = res.bounding_box(); - res.apply_affine(Affine::translate((-bb.x0, -bb.y0))); - let nm = doc.get_mut(i)?; - let mut net = VectorNetwork::from_path(&res); - for r in &mut net.regions { - r.winding = WindingRule::EvenOdd; - } - nm.kind = NodeKind::Vector { network: net }; - nm.transform *= Affine::translate((bb.x0, bb.y0)); - nm.width = bb.width(); - nm.height = bb.height(); - } - ok() -} - -/// Join open vectors whose end points touch into one vector. -fn join(s: &mut Session, p: &Value) -> Result { - let ids = req_targets("vector.join", s, p)?; - if ids.len() < 2 { - // Close a single open path. - if let Some(i) = ids.first() { - let n = s.doc_mut()?.get_mut(*i)?; - if let NodeKind::Vector { network } = &mut n.kind - && network.regions.is_empty() - && network.vertices.len() > 2 - { - let last = network.vertices.len() - 1; - network.segments.push(Segment::line(last, 0)); - let all: Vec = (0..network.segments.len()).collect(); - network.regions.push(Region { loops: vec![all], winding: WindingRule::NonZero }); - } - } - return ok(); - } - flatten(s, &json!({"ids": ids})) -} - -/// Fit containers to their visible children's bounds, keeping the children where they are on the -/// page (auto layout frames hug instead); rotated containers fit in their own axes. -fn resize_to_fit(s: &mut Session, p: &Value) -> Result { - let cmd = "object.resizeToFit"; - let ids = req_targets(cmd, s, p)?; - let doc = s.doc_mut()?; - let mut fitted = Vec::new(); - for id in ids { - let Some(n) = doc.node(id).cloned() else { continue }; - let container = n.kind.is_frame_like() || matches!(n.kind, NodeKind::Section { .. }); - if !container || n.children.is_empty() || matches!(n.kind, NodeKind::Instance { .. }) { - continue; - } - // Auto layout frames fit by hugging their contents. - if n.layout.is_some() { - let node = doc.get_mut(id)?; - node.child_layout.sizing_h = Sizing::Hug; - node.child_layout.sizing_v = Sizing::Hug; - fitted.push(id); - continue; - } - // Children's bounds in the container's own coordinates. - let mut b: Option = None; - for c in n.children.iter().filter_map(|c| doc.node(*c)).filter(|c| c.visible) { - let r = figcraft_geom::transform_rect_bbox(c.transform, c.local_rect()); - if r.x0.is_finite() && r.y0.is_finite() && r.x1.is_finite() && r.y1.is_finite() { - b = Some(b.map_or(r, |u| u.union(r))); - } - } - let Some(b) = b else { continue }; - let (w, h) = (b.width().max(0.01), b.height().max(0.01)); - // Move the container by the offset, and its children back by it. - let shift = Affine::translate((b.x0, b.y0)); - let node = doc.get_mut(id)?; - node.transform *= shift; - node.width = w; - node.height = h; - for c in &n.children { - if let Ok(cn) = doc.get_mut(*c) { - cn.transform = shift.inverse() * cn.transform; - } - } - fitted.push(id); - } - Ok(json!({"ids": fitted})) -} +//! Structure: groups, frames, masks, boolean groups, flatten, outline stroke, auto layout, pages. + +use figcraft_color::{Color, Paint}; +use figcraft_doc::*; +use figcraft_geom::{Affine, BezPath, Rect, Shape}; +use serde_json::{Value, json}; + +use super::edit::{roots, z_sorted}; +use super::{CommandSpec, ok}; +use crate::params::*; +use crate::{EngineError, Result, Session, bad, spec}; + +pub fn specs() -> Vec { + vec![ + spec!("object.group", "Group selection", "{ids?, name?}", true, |s, p| wrap(s, p, Wrap::Group)), + spec!("object.ungroup", "Ungroup selection", "{ids?}", true, ungroup), + spec!("object.frameSelection", "Frame selection", "{ids?, name?}", true, |s, p| wrap(s, p, Wrap::Frame)), + spec!("object.removeFrame", "Remove frame", "{ids?}", true, ungroup), + spec!( + "object.convertToGroup", + "Convert to group", + "{ids?} (frames become groups in place, their layers staying where they are; what only a frame has goes: auto layout, fills, strokes, corner radius, clipping, layout grids, guides and scrolling. Groups are left alone; instances, main components, layers inside instances and empty frames are refused)", + true, + |s, p| convert(s, p, "object.convertToGroup", false) + ), + spec!( + "object.convertToFrame", + "Convert to frame", + "{ids?} (groups become frames of the same box in place, their layers staying where they are: no fill, not clipping. Frames are left alone; instances, main components and layers inside instances are refused)", + true, + |s, p| convert(s, p, "object.convertToFrame", true) + ), + spec!( + "object.wrapInSection", + "Wrap in new section", + "{ids?, name?} (layers on the page or in a section; the section wraps them with some room)", + true, + |s, p| wrap(s, p, Wrap::Section) + ), + spec!( + "object.resizeToFit", + "Resize to fit", + "{ids?} (frames, sections and components shrink or grow to their contents; the contents stay put)", + true, + resize_to_fit + ), + spec!("object.mask", "Use as mask", "{ids?}", true, mask), + spec!("object.releaseMask", "Release mask", "{ids?}", true, |s, p| { + for i in req_targets("object.releaseMask", s, p)? { + s.doc_mut()?.get_mut(i)?.is_mask = false; + } + ok() + }), + spec!("bool.union", "Union selection", BOOL_PARAMS, true, |s, p| crate::booleans::apply(s, "bool.union", p, BoolOp::Union)), + spec!("bool.subtract", "Subtract selection", BOOL_PARAMS, true, |s, p| crate::booleans::apply(s, "bool.subtract", p, BoolOp::Subtract)), + spec!("bool.intersect", "Intersect selection", BOOL_PARAMS, true, |s, p| crate::booleans::apply(s, "bool.intersect", p, BoolOp::Intersect)), + spec!("bool.exclude", "Exclude selection", BOOL_PARAMS, true, |s, p| crate::booleans::apply(s, "bool.exclude", p, BoolOp::Exclude)), + spec!("bool.flatten", "Flatten boolean", "{ids?}", true, flatten), + spec!("vector.flatten", "Flatten", "{ids?}", true, flatten), + spec!("vector.outlineStroke", "Outline stroke", "{ids?}", true, outline_stroke), + spec!("layout.add", "Add auto layout", "{ids?, direction? (horizontal|vertical|grid)}", true, layout_add), + spec!("layout.remove", "Remove auto layout", "{ids?}", true, |s, p| { + for i in req_targets("layout.remove", s, p)? { + // Hugging needs auto layout and the children that filled have nothing to fill: both + // become fixed (the frame keeps filling its own auto layout parent). + let doc = s.doc_mut()?; + doc.get_mut(i)?.layout = None; + figcraft_layout::coherent_sizing(doc, i); + } + ok() + }), + spec!("layout.suggest", "Suggest auto layout", "{ids?}", true, layout_add), + spec!("object.moveToPage", "Move to page", "{ids?, page}", true, move_to_page), + spec!("object.setThumbnail", "Set as thumbnail", "{id?}", true, |s, p| { + let id = targets(s, p).first().copied().ok_or_else(|| bad("object.setThumbnail", "select a frame"))?; + let doc = s.doc_mut()?; + for n in doc.ids().collect::>() { + if let Some(node) = doc.node(n) + && node.plugin_data.contains_key("figcraft.thumbnail") + { + doc.get_mut(n)?.plugin_data.remove("figcraft.thumbnail"); + } + } + doc.get_mut(id)?.plugin_data.insert("figcraft.thumbnail".into(), "true".into()); + ok() + }), + spec!("object.rasterize", "Rasterize selection", "{ids?, scale? (default 2)}", true, rasterize), + spec!("vector.reverse", "Reverse path direction", "{ids?}", true, |s, p| { + for i in req_targets("vector.reverse", s, p)? { + if let NodeKind::Vector { network } = &mut s.doc_mut()?.get_mut(i)?.kind { + for seg in &mut network.segments { + std::mem::swap(&mut seg.a, &mut seg.b); + std::mem::swap(&mut seg.ta, &mut seg.tb); + } + } + } + ok() + }), + spec!("vector.simplify", "Simplify vector", "{ids?, tolerance?}", true, simplify), + spec!("vector.offset", "Offset path", "{ids?, offset}", true, offset_path), + spec!("vector.join", "Join selection", "{ids?} (in vector edit mode: joins the selected points)", true, |s, p| { + crate::vectoredit::join_in_edit_mode(s, false).unwrap_or_else(|| join(s, p)) + }), + spec!("vector.smoothJoin", "Smooth join selection", "{ids?} (in vector edit mode: joins the selected points smoothly)", true, |s, p| { + crate::vectoredit::join_in_edit_mode(s, true).unwrap_or_else(|| join(s, p)) + }), + ] +} + +enum Wrap { + Group, + Frame, + Section, + Bool(BoolOp), +} + +/// Room a section made around layers leaves on each side. +const SECTION_PAD: f64 = 40.0; + +/// What the boolean commands do with what they're given. +const BOOL_PARAMS: &str = "{ids?} — layers inside boolean groups: sets their own operation (how each combines with the operands below it); one boolean group: sets its operation (every operand follows it); otherwise wraps the layers in a new boolean group"; + +/// Wrap the selection in a new boolean group doing `op`. +pub(crate) fn wrap_boolean(s: &mut Session, p: &Value, op: BoolOp) -> Result { + wrap(s, p, Wrap::Bool(op)) +} + +/// Wrap the selection in a new container at the topmost selected layer's place. +fn wrap(s: &mut Session, p: &Value, w: Wrap) -> Result { + let ids = req_targets("object.group", s, p)?; + let doc = s.doc_mut()?; + let ids = z_sorted(doc, &roots(doc, &ids)); + let top = *ids.last().ok_or_else(|| bad("object.group", "nothing selected"))?; + let parent = doc.get(top)?.parent.ok_or_else(|| bad("object.group", "can't group a page"))?; + // Sections sit on the page or in sections, never in frames or groups. + let board = |doc: &Document, i: NodeId| { + doc.node(i).and_then(|n| n.parent).and_then(|p| doc.node(p)).is_some_and(|p| p.is_page() || matches!(p.kind, NodeKind::Section { .. })) + }; + if matches!(w, Wrap::Section) && !ids.iter().all(|i| board(doc, *i)) { + return Err(bad("object.wrapInSection", "only layers on the page or in a section can be wrapped in a section")); + } + if !matches!(w, Wrap::Section) && ids.iter().any(|i| doc.node(*i).is_some_and(|n| matches!(n.kind, NodeKind::Section { .. }))) { + return Err(bad("object.group", "sections can't go in groups or frames")); + } + let index = doc.index_in_parent(top).map(|i| i + 1); + let mut bounds = doc.bounds_of(&ids).unwrap_or_default(); + if matches!(w, Wrap::Section) { + bounds = bounds.inflate(SECTION_PAD, SECTION_PAD); + } + let pw = doc.world_transform(parent); + let local = figcraft_geom::transform_rect_bbox(pw.inverse(), bounds); + let (kind, base) = match &w { + Wrap::Group => (NodeKind::Group, "Group"), + Wrap::Frame => (NodeKind::Frame, "Frame"), + Wrap::Section => (NodeKind::Section { contents_hidden: false }, "Section"), + Wrap::Bool(op) => (NodeKind::BooleanOp { op: *op }, op.label()), + }; + let name = string(p, "name").unwrap_or_else(|| if matches!(w, Wrap::Bool(_)) { base.to_string() } else { doc.next_name(base) }); + let gid = doc.alloc_id(); + let mut g = match w { + Wrap::Section => Node::new_section(gid, name, local.width(), local.height()), + _ => Node::new(gid, kind, name, local.width(), local.height()), + }; + g.transform = Affine::translate((local.x0, local.y0)); + if let Wrap::Bool(op) = w { + // A boolean group looks like its top layer; a subtraction like the bottom one it cuts. + let source = if op == BoolOp::Subtract { ids.first() } else { ids.last() }; + if let Some(first) = source.and_then(|i| doc.node(*i)) { + g.fills = if first.fills.is_empty() { vec![Paint::solid(Color::DEFAULT_SHAPE)] } else { first.fills.clone() }; + g.strokes = first.strokes.clone(); + g.stroke = first.stroke.clone(); + g.effects = first.effects.clone(); + } + g.blend = figcraft_color::BlendMode::Normal; + } + if let Wrap::Frame = w { + g.clip = true; + } + // Insert the container where the topmost layer is. + doc.insert(g, parent, index)?; + for i in &ids { + doc.reparent(*i, gid, None)?; + } + if !matches!(w, Wrap::Frame | Wrap::Section) { + let _ = doc.fit_to_children(gid); + } + s.set_selection(vec![gid]); + Ok(json!({"id": gid})) +} + +/// Turn frames into groups or groups into frames in place (same id, name, layers, effects, +/// opacity, exports and interactions), the layers inside staying where they are on the page. +/// Nothing changes unless every layer can convert. +fn convert(s: &mut Session, p: &Value, cmd: &'static str, to_frame: bool) -> Result { + let ids = req_targets(cmd, s, p)?; + let doc = s.doc_mut()?; + let mut todo = Vec::new(); + for i in ids { + let n = doc.get(i)?; + let refuse = |why: &str| Err(bad(cmd, format!("\"{}\" {why}", n.name))); + match (&n.kind, to_frame) { + (NodeKind::Instance { .. }, _) => return refuse("is an instance: its layers come from its main component (detach it first)"), + (NodeKind::Component { .. }, _) => return refuse("is a main component, which stays a frame"), + (NodeKind::Frame | NodeKind::Group, _) if crate::components::owning_instance(doc, i).is_some() => { + return refuse("is inside an instance: edit its main component, or detach the instance"); + } + (NodeKind::Frame, false) if n.children.is_empty() => return refuse("is empty: a group needs layers"), + (NodeKind::Frame, false) | (NodeKind::Group, true) => todo.push(i), + (NodeKind::Frame | NodeKind::Group, _) => {} + _ => return refuse("is neither a frame nor a group"), + } + } + for i in &todo { + if to_frame { + // The group's box as it is; nothing painted or clipped, so the look stays. + let n = doc.get_mut(*i)?; + n.kind = NodeKind::Frame; + n.clip = false; + n.fills.clear(); + n.strokes.clear(); + continue; + } + let n = doc.get_mut(*i)?; + n.kind = NodeKind::Group; + (n.layout, n.clip, n.overflow) = (None, false, proto::Overflow::None); + n.fills.clear(); + n.strokes.clear(); + n.grids.clear(); + n.guides.clear(); + n.corners = Corners::default(); + (n.styles.fill, n.styles.stroke, n.styles.grid) = (None, None, None); + // Variables bound to what went would bring it back. + n.bound.retain(|k, _| matches!(k.as_str(), "opacity" | "visible")); + let kids = n.children.clone(); + for k in kids { + // Out of the flow there is nothing to fill or to be absolute against. + doc.get_mut(k)?.child_layout.absolute = false; + } + figcraft_layout::coherent_sizing(doc, *i); + doc.fit_to_children(*i)?; + } + Ok(json!({"ids": todo})) +} + +fn ungroup(s: &mut Session, p: &Value) -> Result { + let ids = req_targets("object.ungroup", s, p)?; + let doc = s.doc_mut()?; + let mut out = Vec::new(); + for g in ids { + let n = doc.get(g)?; + if !(matches!(n.kind, NodeKind::Group | NodeKind::BooleanOp { .. } | NodeKind::Frame | NodeKind::Section { .. })) { + continue; + } + let Some(parent) = n.parent else { continue }; + let idx = doc.index_in_parent(g).unwrap_or(0); + let kids = n.children.clone(); + // Ungrouping a group keeps its opacity on the children (multiplied). + let op = n.opacity; + for (k, c) in kids.iter().enumerate() { + doc.reparent(*c, parent, Some(idx + k))?; + // Released operands no longer combine with anything. + if doc.get(*c)?.bool_op.is_some() { + doc.get_mut(*c)?.bool_op = None; + } + if op < 1.0 { + let cn = doc.get_mut(*c)?; + cn.opacity *= op; + } + out.push(*c); + } + doc.remove(g)?; + } + s.set_selection(out.clone()); + Ok(json!({"ids": out})) +} + +fn mask(s: &mut Session, p: &Value) -> Result { + let ids = req_targets("object.mask", s, p)?; + if ids.len() == 1 { + let doc = s.doc_mut()?; + let id = ids.first().copied().unwrap_or(NodeId(0)); + let n = doc.get_mut(id)?; + n.is_mask = !n.is_mask; + return ok(); + } + // Group, then the bottom-most child is the mask. + let r = wrap(s, &json!({"ids": ids, "name": "Mask group"}), Wrap::Group)?; + let gid = id_of(&r["id"]).ok_or_else(|| EngineError::Other("group failed".into()))?; + let doc = s.doc_mut()?; + if let Some(first) = doc.get(gid)?.children.first().copied() { + doc.get_mut(first)?.is_mask = true; + } + Ok(r) +} + +/// The outline of a node in its parent's space (shapes, text, booleans, groups). +pub fn outline_in_parent(doc: &Document, id: NodeId) -> Option { + let n = doc.node(id)?; + let (mut p, _) = figcraft_render::subtree_outline(doc, n, 0)?; + p.apply_affine(n.transform); + Some(p) +} + +fn flatten(s: &mut Session, p: &Value) -> Result { + let ids = req_targets("vector.flatten", s, p)?; + let doc = s.doc_mut()?; + let ids = z_sorted(doc, &roots(doc, &ids)); + let top = *ids.last().ok_or_else(|| bad("vector.flatten", "nothing selected"))?; + let parent = doc.get(top)?.parent.ok_or_else(|| bad("vector.flatten", "can't flatten a page"))?; + let index = doc.index_in_parent(top).map(|i| i + 1); + let pw = doc.world_transform(parent); + let mut path = BezPath::new(); + let mut even = false; + for i in &ids { + let Some(n) = doc.node(*i) else { continue }; + // Booleans flatten to their result; strokes don't get outlined. + let Some((mut op, rule)) = (match &n.kind { + NodeKind::Group | NodeKind::Frame | NodeKind::Component { .. } | NodeKind::Instance { .. } => figcraft_render::subtree_outline(doc, n, 0), + _ => figcraft_render::node_outline(doc, n), + }) else { + continue; + }; + even |= rule == figcraft_render::FillRule::EvenOdd; + // Into the common parent's space. + let w = doc.world_transform(*i); + op.apply_affine(pw.inverse() * w); + path.extend(op.elements().iter().copied()); + } + if path.elements().is_empty() { + return Err(EngineError::Disabled("vector.flatten".into(), "nothing to flatten".into())); + } + let first = ids.first().and_then(|i| doc.node(*i)).cloned(); + let bb = path.bounding_box(); + path.apply_affine(Affine::translate((-bb.x0, -bb.y0))); + let mut net = VectorNetwork::from_path(&path); + for r in &mut net.regions { + r.winding = if even { WindingRule::EvenOdd } else { WindingRule::NonZero }; + } + let vid = doc.alloc_id(); + let name = first.as_ref().map(|f| f.name.clone()).unwrap_or_else(|| "Vector".into()); + let mut v = Node::new(vid, NodeKind::Vector { network: net }, name, bb.width(), bb.height()); + v.transform = Affine::translate((bb.x0, bb.y0)); + if let Some(f) = first { + v.fills = if matches!(f.kind, NodeKind::Group | NodeKind::Line) && f.fills.is_empty() { + vec![Paint::solid(Color::DEFAULT_SHAPE)] + } else { + f.fills.clone() + }; + v.strokes = f.strokes.clone(); + v.stroke = StrokeProps { align: StrokeAlign::Center, ..f.stroke.clone() }; + v.effects = f.effects.clone(); + v.opacity = f.opacity; + v.blend = if matches!(f.blend, figcraft_color::BlendMode::PassThrough) { figcraft_color::BlendMode::Normal } else { f.blend }; + } + doc.insert(v, parent, index)?; + for i in &ids { + doc.remove(*i)?; + } + s.set_selection(vec![vid]); + Ok(json!({"id": vid})) +} + +fn outline_stroke(s: &mut Session, p: &Value) -> Result { + let ids = req_targets("vector.outlineStroke", s, p)?; + let mut out = Vec::new(); + for i in ids { + let doc = s.doc_mut()?; + let n = doc.get(i)?.clone(); + if !n.has_visible_stroke() { + continue; + } + let closed = figcraft_doc::geometry::is_closed(&n); + let Some((area, rule)) = (match &n.kind { + NodeKind::Text(_) => figcraft_render::node_outline(doc, &n) + .map(|(p, _)| (figcraft_render::draw::stroke_area(&n, &p, closed), figcraft_render::FillRule::Winding)), + _ => figcraft_render::stroke_fill(doc, &n), + }) else { + continue; + }; + let mut path = (*area).clone(); + // Simplify non-zero strokes into even-odd contours. Inside/outside rings already use + // even-odd: interpreting their same-direction contours as non-zero would fill the hole. + if rule == figcraft_render::FillRule::Winding + && let Ok(u) = figcraft_pathops::simplify(&path) + { + path = u; + } + let bb = path.bounding_box(); + path.apply_affine(Affine::translate((-bb.x0, -bb.y0))); + let mut net = VectorNetwork::from_path(&path); + for r in &mut net.regions { + r.winding = WindingRule::EvenOdd; + } + let parent = n.parent.ok_or_else(|| bad("vector.outlineStroke", "page"))?; + let idx = doc.index_in_parent(i); + let vid = doc.alloc_id(); + let mut v = Node::new(vid, NodeKind::Vector { network: net }, n.name.clone(), bb.width(), bb.height()); + v.transform = n.transform * Affine::translate((bb.x0, bb.y0)); + // The stroke's paints (and its colour style) fill the outline. + v.fills = n.strokes.clone(); + v.strokes.clear(); + v.styles.fill = n.styles.stroke; + if n.fills.iter().any(|f| f.visible) && closed { + // Keep the fill as its own layer under the outline, grouped. + doc.insert(v, parent, idx.map(|x| x + 1))?; + let fill_id = i; + { + let f = doc.get_mut(fill_id)?; + f.strokes.clear(); + } + let r = wrap(s, &json!({"ids": [fill_id, vid], "name": n.name}), Wrap::Group)?; + if let Some(g) = id_of(&r["id"]) { + out.push(g); + } + continue; + } + // Standing in for the layer, the outline keeps how it is drawn and placed. + (v.opacity, v.blend, v.effects, v.styles.effect) = (n.opacity, n.blend, n.effects.clone(), n.styles.effect); + (v.constraints, v.child_layout, v.visible, v.locked) = (n.constraints, n.child_layout.clone(), n.visible, n.locked); + doc.insert(v, parent, idx.map(|x| x + 1))?; + doc.remove(i)?; + out.push(vid); + } + s.set_selection(out.clone()); + Ok(json!({"ids": out})) +} + +/// Infer direction, gap and padding from where children are (Figma's "Add auto layout"). +fn infer_layout(doc: &Document, frame: NodeId) -> AutoLayout { + let Some(f) = doc.node(frame) else { return AutoLayout::default() }; + let boxes: Vec = + f.children.iter().filter_map(|c| doc.node(*c)).map(|c| figcraft_geom::transform_rect_bbox(c.transform, c.local_rect())).collect(); + if boxes.is_empty() { + return AutoLayout { padding: [10.0; 4], gap: 10.0, ..Default::default() }; + } + let span_x = boxes.iter().map(|b| b.x1).fold(f64::MIN, f64::max) - boxes.iter().map(|b| b.x0).fold(f64::MAX, f64::min); + let span_y = boxes.iter().map(|b| b.y1).fold(f64::MIN, f64::max) - boxes.iter().map(|b| b.y0).fold(f64::MAX, f64::min); + let vertical = span_y > span_x; + let mut sorted = boxes.clone(); + sorted.sort_by(|a, b| if vertical { a.y0.total_cmp(&b.y0) } else { a.x0.total_cmp(&b.x0) }); + let gaps: Vec = + sorted.windows(2).filter_map(|w| Some(if vertical { w.get(1)?.y0 - w.first()?.y1 } else { w.get(1)?.x0 - w.first()?.x1 })).collect(); + let gap = if gaps.is_empty() { 10.0 } else { (gaps.iter().sum::() / gaps.len() as f64).max(0.0).round() }; + let all = boxes.iter().copied().reduce(|a, b| a.union(b)).unwrap_or_default(); + let pad = [all.y0.max(0.0), (f.width - all.x1).max(0.0), (f.height - all.y1).max(0.0), all.x0.max(0.0)].map(f64::round); + AutoLayout { mode: if vertical { LayoutMode::Vertical } else { LayoutMode::Horizontal }, gap, padding: pad, ..Default::default() } +} + +fn layout_add(s: &mut Session, p: &Value) -> Result { + let ids = req_targets("layout.add", s, p)?; + let dir = string(p, "direction"); + let doc = s.doc()?; + let all_frames = ids.iter().all(|i| doc.node(*i).is_some_and(|n| n.kind.is_frame_like() && n.layout.is_none())); + let targets = if all_frames { + ids + } else { + // Wrap non-frames in a new frame without a fill, then add auto layout to it. + let r = wrap(s, &json!({"ids": ids}), Wrap::Frame)?; + let fid = id_of(&r["id"]).ok_or_else(|| EngineError::Other("wrap failed".into()))?; + let f = s.doc_mut()?.get_mut(fid)?; + f.fills.clear(); + f.clip = false; + vec![fid] + }; + let doc = s.doc_mut()?; + for f in &targets { + let mut al = infer_layout(doc, *f); + if let Some(d) = &dir { + al.mode = match d.to_ascii_lowercase().as_str() { + "vertical" | "column" => LayoutMode::Vertical, + "grid" => LayoutMode::Grid, + _ => LayoutMode::Horizontal, + }; + } + // Children in reading order along the axis. + let mut kids = doc.get(*f)?.children.clone(); + let vertical = al.mode == LayoutMode::Vertical; + kids.sort_by(|a, b| { + let pa = doc.node(*a).map(|n| figcraft_geom::transform_rect_bbox(n.transform, n.local_rect())).unwrap_or_default(); + let pb = doc.node(*b).map(|n| figcraft_geom::transform_rect_bbox(n.transform, n.local_rect())).unwrap_or_default(); + if vertical { pa.y0.total_cmp(&pb.y0) } else { pa.x0.total_cmp(&pb.x0) } + }); + let n = doc.get_mut(*f)?; + n.children = kids; + n.layout = Some(al); + n.child_layout.sizing_h = Sizing::Hug; + n.child_layout.sizing_v = Sizing::Hug; + } + s.set_selection(targets.clone()); + Ok(json!({"ids": targets})) +} + +fn move_to_page(s: &mut Session, p: &Value) -> Result { + let ids = req_targets("object.moveToPage", s, p)?; + let page = req_id("object.moveToPage", p, "page")?; + let doc = s.doc_mut()?; + if !doc.node(page).is_some_and(Node::is_page) { + return Err(bad("object.moveToPage", format!("{page} is not a page"))); + } + if doc.is_divider(page) { + return Err(bad("object.moveToPage", "a divider holds no layers")); + } + for i in z_sorted(doc, &roots(doc, &ids)) { + doc.reparent(i, page, None)?; + } + s.set_selection(vec![]); + ok() +} + +fn rasterize(s: &mut Session, p: &Value) -> Result { + let ids = req_targets("object.rasterize", s, p)?; + let scale = num_or(p, "scale", 2.0); + let mut out = Vec::new(); + for i in ids { + let doc = s.doc()?; + let Some(pix) = figcraft_render::render_node(doc, i, scale) else { continue }; + let png = figcraft_render::encode_png(&pix).map_err(EngineError::Other)?; + let b = figcraft_render::export_bounds(doc, i).unwrap_or_default(); + let parent = doc.get(i)?.parent.ok_or_else(|| bad("object.rasterize", "page"))?; + let idx = doc.index_in_parent(i); + let pw = doc.world_transform(parent); + let name = doc.get(i)?.name.clone(); + let docm = s.doc_mut()?; + let (hash, _, _) = figcraft_format::add_image(docm, png).map_err(|e| EngineError::Other(e.to_string()))?; + let rid = docm.alloc_id(); + let mut r = Node::new(rid, NodeKind::Rectangle, name, b.width(), b.height()); + r.transform = pw.inverse() * Affine::translate((b.x0, b.y0)); + r.fills = vec![Paint::image(hash, figcraft_color::ImageScaleMode::Fill)]; + docm.insert(r, parent, idx.map(|x| x + 1))?; + docm.remove(i)?; + out.push(rid); + } + s.set_selection(out.clone()); + Ok(json!({"ids": out})) +} + +fn simplify(s: &mut Session, p: &Value) -> Result { + let tol = num_or(p, "tolerance", 0.5).max(0.01); + for i in req_targets("vector.simplify", s, p)? { + let doc = s.doc_mut()?; + let n = doc.get_mut(i)?; + if let NodeKind::Vector { network } = &mut n.kind { + let fill = network.fill_path(); + let src = if fill.elements().is_empty() { network.stroke_path() } else { fill }; + let simplified = figcraft_geom::kurbo::simplify::simplify_bezpath(src.iter(), tol, &Default::default()); + let regions = !network.regions.is_empty(); + let mut net = VectorNetwork::from_path(&simplified); + if !regions { + net.regions.clear(); + } + *network = net; + } + } + ok() +} + +fn offset_path(s: &mut Session, p: &Value) -> Result { + let d = req_num("vector.offset", p, "offset")?; + for i in req_targets("vector.offset", s, p)? { + let doc = s.doc_mut()?; + let n = doc.get(i)?.clone(); + let Some((path, _)) = figcraft_render::node_outline(doc, &n) else { continue }; + let props = StrokeProps { weight: d.abs() * 2.0, join: StrokeJoin::Round, align: StrokeAlign::Center, ..Default::default() }; + let ring = figcraft_pathops::stroke_center(&path, d.abs() * 2.0, &props); + let res = if d >= 0.0 { + figcraft_pathops::boolean(&[path, ring], BoolOp::Union) + } else { + figcraft_pathops::boolean(&[path, ring], BoolOp::Subtract) + }; + let Ok(mut res) = res else { continue }; + let bb = res.bounding_box(); + res.apply_affine(Affine::translate((-bb.x0, -bb.y0))); + let nm = doc.get_mut(i)?; + let mut net = VectorNetwork::from_path(&res); + for r in &mut net.regions { + r.winding = WindingRule::EvenOdd; + } + nm.kind = NodeKind::Vector { network: net }; + nm.transform *= Affine::translate((bb.x0, bb.y0)); + nm.width = bb.width(); + nm.height = bb.height(); + } + ok() +} + +/// Join open vectors whose end points touch into one vector. +fn join(s: &mut Session, p: &Value) -> Result { + let ids = req_targets("vector.join", s, p)?; + if ids.len() < 2 { + // Close a single open path. + if let Some(i) = ids.first() { + let n = s.doc_mut()?.get_mut(*i)?; + if let NodeKind::Vector { network } = &mut n.kind + && network.regions.is_empty() + && network.vertices.len() > 2 + { + let last = network.vertices.len() - 1; + network.segments.push(Segment::line(last, 0)); + let all: Vec = (0..network.segments.len()).collect(); + network.regions.push(Region { loops: vec![all], winding: WindingRule::NonZero }); + } + } + return ok(); + } + flatten(s, &json!({"ids": ids})) +} + +/// Fit containers to their visible children's bounds, keeping the children where they are on the +/// page (auto layout frames hug instead); rotated containers fit in their own axes. +fn resize_to_fit(s: &mut Session, p: &Value) -> Result { + let cmd = "object.resizeToFit"; + let ids = req_targets(cmd, s, p)?; + let doc = s.doc_mut()?; + let mut fitted = Vec::new(); + for id in ids { + let Some(n) = doc.node(id).cloned() else { continue }; + let container = n.kind.is_frame_like() || matches!(n.kind, NodeKind::Section { .. }); + if !container || n.children.is_empty() || matches!(n.kind, NodeKind::Instance { .. }) { + continue; + } + // Auto layout frames fit by hugging their contents. + if n.layout.is_some() { + let node = doc.get_mut(id)?; + node.child_layout.sizing_h = Sizing::Hug; + node.child_layout.sizing_v = Sizing::Hug; + fitted.push(id); + continue; + } + // Children's bounds in the container's own coordinates. + let mut b: Option = None; + for c in n.children.iter().filter_map(|c| doc.node(*c)).filter(|c| c.visible) { + let r = figcraft_geom::transform_rect_bbox(c.transform, c.local_rect()); + if r.x0.is_finite() && r.y0.is_finite() && r.x1.is_finite() && r.y1.is_finite() { + b = Some(b.map_or(r, |u| u.union(r))); + } + } + let Some(b) = b else { continue }; + let (w, h) = (b.width().max(0.01), b.height().max(0.01)); + // Move the container by the offset, and its children back by it. + let shift = Affine::translate((b.x0, b.y0)); + let node = doc.get_mut(id)?; + node.transform *= shift; + node.width = w; + node.height = h; + for c in &n.children { + if let Ok(cn) = doc.get_mut(*c) { + cn.transform = shift.inverse() * cn.transform; + } + } + fitted.push(id); + } + Ok(json!({"ids": fitted})) +} diff --git a/crates/engine/tests/outline_stroke.rs b/crates/engine/tests/outline_stroke.rs index 49e8a8b2..477f77fe 100644 --- a/crates/engine/tests/outline_stroke.rs +++ b/crates/engine/tests/outline_stroke.rs @@ -1,174 +1,174 @@ -//! Outline stroke keeps the renderer's holes and existing property ownership. -#[cfg(test)] -mod tests { - use figcraft_doc::{Document, NodeId, NodeKind}; - use figcraft_engine::Session; - use figcraft_render::{Options, View, render_view}; - use serde_json::{Value, json}; - - fn run(s: &mut Session, command: &str, params: Value) -> Value { - s.execute(command, ¶ms).unwrap() - } - - fn rectangle(s: &mut Session, opacity: f64) -> NodeId { - NodeId( - run( - s, - "shape.rectangle", - json!({ - "x": 24, "y": 24, "width": 64, "height": 64, - "props": {"fill": "#FF0000", "stroke": "#0000FF", "strokeWeight": 8, "opacity": opacity} - }), - )["id"] - .as_u64() - .unwrap(), - ) - } - - fn pixels(doc: &Document) -> Vec { - render_view(doc, doc.first_page().unwrap(), &View { origin: (0.0, 0.0), scale: 1.0, width: 112, height: 112 }, &Options::default()) - .unwrap() - .data() - .to_vec() - } - - fn outlined(s: &mut Session, id: NodeId) -> NodeId { - NodeId(run(s, "vector.outlineStroke", json!({"ids": [id]}))["ids"][0].as_u64().unwrap()) - } - - fn same_pixels(actual: &[u8], expected: &[u8]) { - assert_eq!(actual.len(), expected.len()); - let differences: Vec<_> = actual.iter().zip(expected).enumerate().filter(|(_, (a, b))| a != b).take(8).collect(); - assert!(differences.is_empty(), "rendered channels changed (byte, actual/expected): {differences:?}"); - } - - #[test] - fn outline_keeps_rings_through_undo_and_native_reopen() { - for vector in [false, true] { - let mut s = Session::new(); - let mut id = rectangle(&mut s, 1.0); - if vector { - id = NodeId(run(&mut s, "vector.flatten", json!({"ids": [id]}))["id"].as_u64().unwrap()); - } - let before = s.doc().unwrap().clone(); - let expected = pixels(&before); - let depth = s.undo_depth(); - let group = outlined(&mut s, id); - let after = s.doc().unwrap().clone(); - same_pixels(&pixels(&after), &expected); - assert!(after.node(id).unwrap().strokes.is_empty()); - assert_eq!(s.selection(), vec![group]); - assert_eq!(s.undo_depth(), (depth.0 + 1, 0)); - let (loaded, notes) = figcraft_format::load_bytes(&figcraft_format::save_bytes(&after, None).unwrap()).unwrap(); - assert!(notes.is_empty(), "{notes:?}"); - same_pixels(&pixels(&loaded), &expected); - assert!(s.undo().unwrap()); - assert_eq!(serde_json::to_value(s.doc().unwrap()).unwrap(), serde_json::to_value(&before).unwrap()); - assert!(s.redo().unwrap()); - assert_eq!(serde_json::to_value(s.doc().unwrap()).unwrap(), serde_json::to_value(&after).unwrap()); - } - } - - #[test] - fn stroke_only_keeps_holes_at_each_opacity() { - for opacity in [0.0, 0.25, 0.5, 1.0] { - let mut s = Session::new(); - let id = rectangle(&mut s, opacity); - run(&mut s, "object.removeFill", json!({"ids": [id]})); - let before = pixels(s.doc().unwrap()); - let vector = outlined(&mut s, id); - let doc = s.doc().unwrap(); - assert!(matches!(doc.node(vector).unwrap().kind, NodeKind::Vector { .. })); - assert_eq!(doc.node(vector).unwrap().opacity, opacity as f32); - same_pixels(&pixels(doc), &before); - } - } - - #[test] - fn outline_keeps_stroke_holes_and_alignment() { - for props in [ - json!({"strokeAlign": "inside"}), - json!({"strokeAlign": "outside"}), - json!({"strokeAlign": "center"}), - json!({"strokeSides": [2, 4, 8, 16]}), - json!({"strokeAlign": "center", "dashes": [8, 4]}), - ] { - for filled in [false, true] { - let mut s = Session::new(); - let id = rectangle(&mut s, 1.0); - run(&mut s, "node.set", json!({"ids": [id], "props": props})); - if !filled { - run(&mut s, "object.removeFill", json!({"ids": [id]})); - } - let before = pixels(s.doc().unwrap()); - outlined(&mut s, id); - same_pixels(&pixels(s.doc().unwrap()), &before); - } - } - } - - #[test] - fn outlining_an_instance_keeps_its_inherited_opacity_link() { - let mut s = Session::new(); - let component = NodeId( - run( - &mut s, - "frame.create", - json!({ - "width": 64, "height": 64, - "props": {"fill": "#FF0000", "stroke": "#0000FF", "strokeWeight": 8, "opacity": 0.5} - }), - )["id"] - .as_u64() - .unwrap(), - ); - run(&mut s, "component.create", json!({"ids": [component]})); - let instance = NodeId(run(&mut s, "instance.create", json!({"component": component, "x": 200}))["id"].as_u64().unwrap()); - outlined(&mut s, instance); - assert_eq!(s.doc().unwrap().node(instance).unwrap().opacity, 0.5); - assert!(!s.doc().unwrap().node(instance).unwrap().overrides.contains("opacity")); - run(&mut s, "node.set", json!({"id": component, "props": {"opacity": 0.25}})); - let n = s.doc().unwrap().node(instance).unwrap(); - assert_eq!(n.opacity, 0.25, "the instance still follows its main"); - assert!(!n.overrides.contains("opacity")); - } - - #[test] - fn outlining_a_main_layer_keeps_its_instance_override() { - let mut s = Session::new(); - let frame = NodeId(run(&mut s, "frame.create", json!({"width": 100, "height": 100}))["id"].as_u64().unwrap()); - let layer = NodeId( - run( - &mut s, - "shape.rectangle", - json!({ - "parent": frame, "x": 24, "y": 24, "width": 64, "height": 64, - "props": {"fill": "#FF0000", "stroke": "#0000FF", "strokeWeight": 8} - }), - )["id"] - .as_u64() - .unwrap(), - ); - let component = run(&mut s, "component.create", json!({"ids": [frame]}))["id"].clone(); - run(&mut s, "instance.create", json!({"component": component, "x": 200})); - let copy = s.doc().unwrap().nodes().find(|n| n.source == Some(layer)).unwrap().id; - run(&mut s, "node.set", json!({"id": copy, "props": {"opacity": 0.25}})); - let before = s.doc().unwrap().clone(); - let view = View { origin: (0.0, 0.0), scale: 1.0, width: 312, height: 112 }; - let expected = render_view(&before, s.page().unwrap(), &view, &Options::default()).unwrap(); - let group = outlined(&mut s, layer); - let doc = s.doc().unwrap(); - let actual = render_view(doc, s.page().unwrap(), &view, &Options::default()).unwrap(); - same_pixels(actual.data(), expected.data()); - let instance_group = doc.node(copy).unwrap(); - assert!(matches!(instance_group.kind, NodeKind::Group)); - assert_eq!(instance_group.source, Some(group)); - assert_eq!(instance_group.opacity, 0.25); - assert!(instance_group.overrides.contains("opacity")); - assert!(s.undo().unwrap()); - assert_eq!(serde_json::to_value(s.doc().unwrap()).unwrap(), serde_json::to_value(&before).unwrap()); - assert!(s.redo().unwrap()); - let actual = render_view(s.doc().unwrap(), s.page().unwrap(), &view, &Options::default()).unwrap(); - same_pixels(actual.data(), expected.data()); - } -} +//! Outline stroke keeps the renderer's holes and existing property ownership. +#[cfg(test)] +mod tests { + use figcraft_doc::{Document, NodeId, NodeKind}; + use figcraft_engine::Session; + use figcraft_render::{Options, View, render_view}; + use serde_json::{Value, json}; + + fn run(s: &mut Session, command: &str, params: Value) -> Value { + s.execute(command, ¶ms).unwrap() + } + + fn rectangle(s: &mut Session, opacity: f64) -> NodeId { + NodeId( + run( + s, + "shape.rectangle", + json!({ + "x": 24, "y": 24, "width": 64, "height": 64, + "props": {"fill": "#FF0000", "stroke": "#0000FF", "strokeWeight": 8, "opacity": opacity} + }), + )["id"] + .as_u64() + .unwrap(), + ) + } + + fn pixels(doc: &Document) -> Vec { + render_view(doc, doc.first_page().unwrap(), &View { origin: (0.0, 0.0), scale: 1.0, width: 112, height: 112 }, &Options::default()) + .unwrap() + .data() + .to_vec() + } + + fn outlined(s: &mut Session, id: NodeId) -> NodeId { + NodeId(run(s, "vector.outlineStroke", json!({"ids": [id]}))["ids"][0].as_u64().unwrap()) + } + + fn same_pixels(actual: &[u8], expected: &[u8]) { + assert_eq!(actual.len(), expected.len()); + let differences: Vec<_> = actual.iter().zip(expected).enumerate().filter(|(_, (a, b))| a != b).take(8).collect(); + assert!(differences.is_empty(), "rendered channels changed (byte, actual/expected): {differences:?}"); + } + + #[test] + fn outline_keeps_rings_through_undo_and_native_reopen() { + for vector in [false, true] { + let mut s = Session::new(); + let mut id = rectangle(&mut s, 1.0); + if vector { + id = NodeId(run(&mut s, "vector.flatten", json!({"ids": [id]}))["id"].as_u64().unwrap()); + } + let before = s.doc().unwrap().clone(); + let expected = pixels(&before); + let depth = s.undo_depth(); + let group = outlined(&mut s, id); + let after = s.doc().unwrap().clone(); + same_pixels(&pixels(&after), &expected); + assert!(after.node(id).unwrap().strokes.is_empty()); + assert_eq!(s.selection(), vec![group]); + assert_eq!(s.undo_depth(), (depth.0 + 1, 0)); + let (loaded, notes) = figcraft_format::load_bytes(&figcraft_format::save_bytes(&after, None).unwrap()).unwrap(); + assert!(notes.is_empty(), "{notes:?}"); + same_pixels(&pixels(&loaded), &expected); + assert!(s.undo().unwrap()); + assert_eq!(serde_json::to_value(s.doc().unwrap()).unwrap(), serde_json::to_value(&before).unwrap()); + assert!(s.redo().unwrap()); + assert_eq!(serde_json::to_value(s.doc().unwrap()).unwrap(), serde_json::to_value(&after).unwrap()); + } + } + + #[test] + fn stroke_only_keeps_holes_at_each_opacity() { + for opacity in [0.0, 0.25, 0.5, 1.0] { + let mut s = Session::new(); + let id = rectangle(&mut s, opacity); + run(&mut s, "object.removeFill", json!({"ids": [id]})); + let before = pixels(s.doc().unwrap()); + let vector = outlined(&mut s, id); + let doc = s.doc().unwrap(); + assert!(matches!(doc.node(vector).unwrap().kind, NodeKind::Vector { .. })); + assert_eq!(doc.node(vector).unwrap().opacity, opacity as f32); + same_pixels(&pixels(doc), &before); + } + } + + #[test] + fn outline_keeps_stroke_holes_and_alignment() { + for props in [ + json!({"strokeAlign": "inside"}), + json!({"strokeAlign": "outside"}), + json!({"strokeAlign": "center"}), + json!({"strokeSides": [2, 4, 8, 16]}), + json!({"strokeAlign": "center", "dashes": [8, 4]}), + ] { + for filled in [false, true] { + let mut s = Session::new(); + let id = rectangle(&mut s, 1.0); + run(&mut s, "node.set", json!({"ids": [id], "props": props})); + if !filled { + run(&mut s, "object.removeFill", json!({"ids": [id]})); + } + let before = pixels(s.doc().unwrap()); + outlined(&mut s, id); + same_pixels(&pixels(s.doc().unwrap()), &before); + } + } + } + + #[test] + fn outlining_an_instance_keeps_its_inherited_opacity_link() { + let mut s = Session::new(); + let component = NodeId( + run( + &mut s, + "frame.create", + json!({ + "width": 64, "height": 64, + "props": {"fill": "#FF0000", "stroke": "#0000FF", "strokeWeight": 8, "opacity": 0.5} + }), + )["id"] + .as_u64() + .unwrap(), + ); + run(&mut s, "component.create", json!({"ids": [component]})); + let instance = NodeId(run(&mut s, "instance.create", json!({"component": component, "x": 200}))["id"].as_u64().unwrap()); + outlined(&mut s, instance); + assert_eq!(s.doc().unwrap().node(instance).unwrap().opacity, 0.5); + assert!(!s.doc().unwrap().node(instance).unwrap().overrides.contains("opacity")); + run(&mut s, "node.set", json!({"id": component, "props": {"opacity": 0.25}})); + let n = s.doc().unwrap().node(instance).unwrap(); + assert_eq!(n.opacity, 0.25, "the instance still follows its main"); + assert!(!n.overrides.contains("opacity")); + } + + #[test] + fn outlining_a_main_layer_keeps_its_instance_override() { + let mut s = Session::new(); + let frame = NodeId(run(&mut s, "frame.create", json!({"width": 100, "height": 100}))["id"].as_u64().unwrap()); + let layer = NodeId( + run( + &mut s, + "shape.rectangle", + json!({ + "parent": frame, "x": 24, "y": 24, "width": 64, "height": 64, + "props": {"fill": "#FF0000", "stroke": "#0000FF", "strokeWeight": 8} + }), + )["id"] + .as_u64() + .unwrap(), + ); + let component = run(&mut s, "component.create", json!({"ids": [frame]}))["id"].clone(); + run(&mut s, "instance.create", json!({"component": component, "x": 200})); + let copy = s.doc().unwrap().nodes().find(|n| n.source == Some(layer)).unwrap().id; + run(&mut s, "node.set", json!({"id": copy, "props": {"opacity": 0.25}})); + let before = s.doc().unwrap().clone(); + let view = View { origin: (0.0, 0.0), scale: 1.0, width: 312, height: 112 }; + let expected = render_view(&before, s.page().unwrap(), &view, &Options::default()).unwrap(); + let group = outlined(&mut s, layer); + let doc = s.doc().unwrap(); + let actual = render_view(doc, s.page().unwrap(), &view, &Options::default()).unwrap(); + same_pixels(actual.data(), expected.data()); + let instance_group = doc.node(copy).unwrap(); + assert!(matches!(instance_group.kind, NodeKind::Group)); + assert_eq!(instance_group.source, Some(group)); + assert_eq!(instance_group.opacity, 0.25); + assert!(instance_group.overrides.contains("opacity")); + assert!(s.undo().unwrap()); + assert_eq!(serde_json::to_value(s.doc().unwrap()).unwrap(), serde_json::to_value(&before).unwrap()); + assert!(s.redo().unwrap()); + let actual = render_view(s.doc().unwrap(), s.page().unwrap(), &view, &Options::default()).unwrap(); + same_pixels(actual.data(), expected.data()); + } +} diff --git a/docs/gaps.md b/docs/gaps.md index 96620b05..cdd882a2 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,448 +1,448 @@ -# Where FigCraft falls short of Figma - -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (Outline stroke preserves stroke holes; reviewed Russian catalog and integer plurals; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail) · **Target:** Figma Design (desktop app 126.10.7) - -Every known shortfall against Figma, one entry each, ranked by how much it stops a Figma user from -doing real work in FigCraft. Agents: pick work from the top unless the owner says otherwise. The -numbers behind the ranking are in [`target-app-parity.md`](target-app-parity.md). Hours are one -Opus 5.5 agent working sequentially, tests included, calibrated as described there. "Estimated" -everywhere unless marked measured. - -## Ranked - -No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passes. Beta blockers are marked. - -| # | Gap | Kind | Hours | Doc | -|---|---|---|---|---| -| 1 | `.fig` files: best-effort import with editable vector networks and stand-ins for non-design layers landed, and an experimental export (round trips through our own importer; written files opened in other software, where layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft, instances keep their size and variables with modes and colour styles come across); real-file checks and the small tail left (**beta blocker** until checked on real files, gap 3) | File format | 5–10 | [file-format-parity](file-format-parity.md) | -| 2 | Multiplayer and sharing are a preview, without accounts (**beta blocker**) | Feature | 42–84 | [collaboration-parity](collaboration-parity.md) | -| 3 | Nothing checked against real Figma files | Fidelity | 15–30 | [target-app-parity](target-app-parity.md) | -| 4 | Interaction fidelity not compared tool by tool | UI/UX | 40–60 | [ui-parity](ui-parity.md) | -| 5 | Localization: English, reviewed Russian, plus unreviewed Spanish (and Latin American Spanish), French, German, Brazilian Portuguese, Japanese, Korean and Simplified Chinese drafts | Localization | 29–51 | [localization-parity](localization-parity.md) | -| 6 | Plugins: widgets take clicks only, a self-hosted directory but no public catalogue; Figma's plugins don't run | Ecosystem | 8–26 | [target-app-parity](target-app-parity.md) | -| 7 | CPU canvas where Figma uses the GPU | Hardware/performance | 30–60 | [hardware-parity](hardware-parity.md) | -| 8 | Platform QA: Linux and the browser left (Windows pass done 2026-10-11) | Stability/platforms | 10–20 | [hardware-parity](hardware-parity.md) | -| 9 | Figma Draw: pen pressure on macOS/Linux tablets (multi-edit, brushes, pressure, text on a path and pattern fills landed) | Feature | 1–3 | [target-app-parity](target-app-parity.md) | -| 10 | Team libraries: no team-wide lists or accounts; analytics are counts | Feature | 4–8 | [components-parity](components-parity.md) | -| 11 | Spell check | Feature | 4–8 | [target-app-parity](target-app-parity.md) | -| 12 | Video fills; HEIC images | File format | 5–10 | [file-format-parity](file-format-parity.md) | -| 13 | Vector tail: outline-stroke compositing (stroke rings preserved) | Feature | — | [target-app-parity](target-app-parity.md) | -| 14 | Variables: extended collections checked on real libraries (the tail landed 2026-10-12) | Feature | 0–1 | [components-parity](components-parity.md) | -| 15 | Prototyping: media triggers and actions; controllers on macOS, Linux and FreeBSD (controller buttons on key/gamepad triggers, widgets in prototypes landed) | Feature | 2–5 | [prototyping-parity](prototyping-parity.md) | -| 16 | Dev Mode: import lines inside generated layer trees; the editor extension is left to the MCP server (code connect, Dev Mode plugins and dev resources landed) | Feature | 0–1 | [dev-mode-parity](dev-mode-parity.md) | -| 17 | Branching: branches in live sessions, lists other than fills, strokes and effects merged whole (a branch's own version history and per-item list merges landed 2026-10-12) | Feature | 1–2 | [collaboration-parity](collaboration-parity.md) | -| 18 | Display P3 on screen: blocked upstream (egui-wgpu picks an 8-bit sRGB surface); gamut warning and soft-proof sRGB landed, as did several windows | Hardware | 4–8 | [hardware-parity](hardware-parity.md) | -| 19 | AI features | AI | 30–60 | [target-app-parity](target-app-parity.md) | -| 20 | Mobile apps (view, mirror, comment) | Platforms | 20–40 | [hardware-parity](hardware-parity.md) | -| 21 | Menu items without a command: none left (landed 2026-10-11) | Feature | — | [parity-checklist](parity-checklist.md) | -| 22 | Screen readers never run; browser has no accessibility tree | UI/UX | 4–8 | [ui-parity](ui-parity.md) | -| 23 | CMYK output intent for print PDFs | File format | 2–4 | [file-format-parity](file-format-parity.md) | - -## Entries - -### 1. `.fig` files: the import's tail -- **Landed (2026-10-11):** `.fig` files open (best effort): the zip, its deflate/zstd chunks and the kiwi schema are decoded generically and mapped by field name onto the REST importer, so pages, the layer tree, geometry, paints (styles resolved), strokes, effects, text with styled runs, vectors, auto layout (grid included), constraints, components, sets, instances (expanded from their main with overrides, derived layout and property values), styles, variables with modes and bindings, prototyping and image fills from the archive come across; anything else is a warning (`crates/format/src/fig`). Then (2026-10-11): vectors open as editable vector networks (per-point corner radius, handle mirroring, end caps; outlines when the network blob is missing or damaged), sticky notes, shapes with text, connectors, tables, code blocks, slides and similar layers import as the closest frames, text layers and vectors with a warning, and set-variable, set-variable-mode and conditional prototype actions come across. `file.open`, - `file.import`, drag and drop, the CLI and MCP's `open_file` read `.fig`; the file is never saved - over. -- **Landed (2026-10-12):** an experimental export: `file.export {format: "fig"}`, File ▸ Export… ▸ - .fig (experimental), `figcraft-cli export --format fig` and MCP's `export` write the whole document - with a kiwi encoder mirroring the decoder and a schema of the fields written: the layer tree, - geometry, paints with image bytes, strokes, effects, text with styled runs, vector networks, auto - layout, constraints, components, sets, instances with their overrides and property values, and - prototype interactions; shared style links, variables, comments, Dev Mode data and the rest are - left out with a warning ([file-format-parity](file-format-parity.md#export-to-fig-experimental)). - The import gained ellipse arcs, boolean operation types and component set descriptions. Then - (2026-10-12): written files were checked by opening them in other software: layers, fills, - gradients, strokes, shadows, vectors, components and text draw as in FigCraft, after the export - learned to say that the document and its pages are visible and opaque, to number its schema as - the format does, to write outlines, text settings, standard font style names and line height - units. Then (2026-10-13): instances keep their own size (an override addressed by the main's id, - derived data for their layers, a scale factor), variables with modes and bindings, shared styles - and variable-width strokes are written and read back; checked there: resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width; number, string and boolean bindings and bindings on effects, layout guides and gradient stops come across as the values they give but did not show as bound there (their entries now also name the layer field they set, not yet checked there), and strokes wider on one side of the path are written as filled outlines, no longer editable strokes there (the stroke itself is kept as plugin data and our own import puts it back; not yet checked there), - like effects such as glass and noise, images and prototype interactions. -- **Missing:** connector labels, elbowed or curved connectors without a saved path (drawn straight), - table cells beyond their own layers, filled areas of a vector with their own fills, per-point stroke - joins, variables bound to fields the editor can't bind, fixed or content-sized grid tracks and video - fills aren't read; text lays out again with - local fonts. Nothing has been checked against real-world files yet (gap 3). The export is checked - only against our own importer: whether other apps open its files is unknown. -- **Evidence:** fixtures generated in code (`crates/format/src/fig/tests.rs` and `tail_tests.rs`: kiwi - round trips, every mapped feature, vector network blobs, truncation, bit flips and junk never panic); `crates/engine/src/fig_import_tests.rs`. - Export: 60 generated documents round trip through the importer layer by layer, written files - decode with the generic decoder and list parents first, the encoder agrees with the decoder, - junk documents never panic (`crates/format/src/fig/export_tests.rs`, - `crates/engine/src/fig_export_tests.rs`). -- **Impact:** existing work can be brought across, but only real files will show how much of it. -- **Estimate:** 5–10 h for the read tail (real-file fixes, connector labels, table cells); checking the export against real files a further 5–15 h (with gap 3). - -### 2. Multiplayer and sharing are a preview, without accounts -- **Missing:** accounts, teams and projects, per-person permissions, audio, - shared comments and version history. Comments and versions are local to the file. -- **Evidence:** the core landed in #103 (`crates/collab`: client-scoped ids, change sets, server - ordering and validation, client reconciliation, binary messages, randomized convergence tests over - real commands), and live sessions run over the network (`crates/collab-net`: hosting from the - app, invite links that open the app, document sync between apps, live cursors, selections, - text carets, avatars and follow, cursor chat and spotlight, and the browser build joins) with per-person undo, refused - edits rolled back with a reason, reconnects that resume and send what was made offline, and - styles, variables and comments that several people create at once all kept; sessions are per - document (each open file has its own or none, several at once, closing a file ends only its - own); files are shared with a short join code (online through the server in Preferences, or on - the local network with a code that carries the host's address) and joined by typing it; the app - remembers the files it put online and lists them on the home screen (open again as owner, stop - and start sharing, rename, remove, forget), per computer; a standalone - `figcraft-server` keeps many files (snapshot plus log, compacted; restarts lose nothing - confirmed), takes files published from the app and hands out share links that let people edit - or only view; the desktop app and the browser reach it over `wss://` through a TLS reverse - proxy, and releases ship it with a deployment guide (`docs/server.md`). But there are no - accounts (a link is the access) (`docs/collab-protocol.md`). -- **Impact:** Figma's defining feature; most teams use it every day. Solo designers can work - without it. -- **Estimate:** 42–84 h: accounts, teams and per-person permissions, shared comments and versions - (numbers from before the server landed; re-measured next). - -### 3. Nothing checked against real Figma files -- **Missing:** a corpus of real documents (design systems, app screens, prototypes) imported and - compared with Figma's own PNG/SVG/PDF exports. -- **Evidence:** every corpus in the repo is synthetic (`crates/svg/tests/corpus`, the synthetic - design system in `crates/engine/src/synthetic_ds.rs`). Scores were self-graded by the agents that - built the features. -- **Impact:** unknown fidelity in auto layout sizing, text metrics, effects and component - resolution on real files; the numbers in this repo can't be trusted beyond "estimated" until it - exists. -- **Measured so far:** text box sizes and auto layout results of our own example documents, - against the same documents re-saved by other software (which lays text out again). Three text - rules explained every difference and are applied (whole-pixel line heights, advances on a - 1024-per-em grid, letter spacing between glyphs only): all 230 self-sizing text boxes in 7 - documents now come out to the pixel (24 of 131 before, in the design-system example), and 208 of - its 231 other layers match in size and position (115 before). The other 23 hang on one grid - auto layout frame whose row positions came back as written while its cells got shorter. One font - family (Inter), left-aligned text; real files, other fonts, effects and component resolution are - still unmeasured (`crates/text/src/metrics_tests.rs`, and a local check over such documents in - `crates/engine/src/text_metrics_local_tests.rs`). -- **Estimate:** 15–30 h once `.fig` or REST import of real files is available; needs files whose - licence allows use (owner to supply or approve; never committed if not redistributable, see - craftrules `standards/test-corpora.md`). - -### 4. Interaction fidelity not compared tool by tool -- **Missing:** a side-by-side pass of every tool and panel: cursors, modifier keys while dragging, - hover states, popovers, menus, snapping thresholds. -- **Landed (2026-10-11):** behaviour suites written from the public documentation for auto layout, - selection and transform, text, and canvas navigation with rulers, guides, layout grids, outline - mode and measuring, then (2026-10-12) comments and version history, and the layers and Assets - panels (`crates/engine/src/*_behaviour_tests.rs`, `crates/ui-egui/src/layers_behaviour_tests.rs`), - each with its fixes; then keyboard shortcuts per platform (`shortcut_parity_tests.rs`: 129 - documented rows, 126 bound, 1 moved off a documented conflict, 2 missing, 48 undocumented - bindings pinned), with 16 rows newly bound, Control suspending snapping on macOS and shifted - symbols on other keyboard layouts; then the everyday partials of - [`ui-parity.md`](ui-parity.md): number fields, outline mode's options, Hide when publishing, a - cursor for every tool and modifier, panel widths, dark pages (`field_tests`, `cursor_tests`, - `cursors_tests`, `outline_options_tests`, `hide_publishing_tests`, `panel_width_tests`, - `theme_pages_tests`). -- **Evidence:** ROADMAP item 7 ("Left: tool by tool, panel by panel against the incumbent"). -- **Impact:** Figma users notice the feel within minutes; it decides whether they stay. -- **Estimate:** 40–60 h. Detail in [`ui-parity.md`](ui-parity.md). - -### 5. Localization: English, reviewed Russian, plus unreviewed drafts -- **Missing:** native reviews of the Spanish, French, German, Brazilian Portuguese, Japanese, - Korean and Simplified Chinese drafts and the Latin American Spanish overlay, the 25 newest strings - in five drafts, Hindi, Arabic, Indonesian and Vietnamese, localized engine messages and number - formatting; CJK UI text on the web without the craft-fonts CJK build. -- **Evidence (measured from source):** the layer landed 2026-10-11: catalogs in - `crates/ui-egui/locales/` (1,371 UI keys plus 524 command labels and 29 menu wordings), - Preferences ▸ Language, `pref.language`; `en.toml` is complete, and `es.toml`, `fr.toml`, - `de.toml`, `pt-BR.toml`, `ja.toml`, `ko.toml` and `zh-Hans.toml` (2026-10-12) are machine drafts - (all but `zh-Hans.toml` lack the 25 newest strings), with `es-419.toml` a Latin American overlay - listing only what differs from Spanish (Spanish and Russian are held to completeness by the check; the - others show English for strings added later). Russian adds all 2,165 current entries, - reviewed and approved by VanTan, with one/few/many integer plurals. - CJK UI text uses an installed CJK face the desktop app loads when something on screen needs it - (`crates/ui-egui/src/cjk.rs`). Engine error messages and Dev Mode values stay English. -- **Impact:** non-English designers get machine drafts nobody has reviewed yet, and English - errors. -- **Estimate:** 3–7 h per language plus native review, 4–6 h for engine messages and numbers; - 29–51 h for the key languages left and the drafts' follow-ups. Detail in - [`localization-parity.md`](localization-parity.md); how to add one in - [`translating.md`](translating.md). - -### 6. Plugins: widgets take clicks only, a self-hosted directory but no public catalogue; Figma's plugins don't run -- **Landed (#101):** sandboxed WebAssembly plugins (wasmi, metered, memory cap, no WASI, also in - the browser) acting through the command registry, one undo step per run, manifest permissions, - Plugins menu, Run last plugin, Manage plugins, `plugin.*` commands, a Rust SDK and two examples - ([`plugins.md`](plugins.md)). Plugin panels and settings (ABI v2, 2026-10-11): a plugin shows a - panel of fields and buttons drawn with FigCraft's widgets and is called back for each button or - changed value (one undo step per callback, same sandbox limits), driven headless with - `plugin.ui` / `plugin.event` / `plugin.close`; per-plugin settings kept with the preferences. - Widgets (plugin API 3, 2026-10-12): a plugin draws a layer of its own on the canvas (Plugins ▸ - Widgets, `widget.insert`), validated and built with locked layers inside, called back for clicks - on the parts it names (one undo step each, a fresh metered sandbox, the state kept in the layer - so it saves, copies, undoes, exports and syncs), `widget.click` / `widget.list` for agents, a - Counter example; panels of several plugins side by side; the colour picker on plugin colour - fields. A plugin directory on `figcraft-server` (2026-10-12): bundles published with the server - key and checked in full (`figcraft-cli plugin publish`), browsed, installed after a permissions - confirmation and updated from Plugins ▸ Manage plugins ▸ Browse, downloads checked against their - SHA-256, `plugin.directory.*` for scripts and MCP, moderation with `figcraft-server plugins`. -- **Missing:** a public catalogue and Community files, publisher accounts and signatures, ratings - and install counts, background update checks, the directory in the browser build; widgets take only clicks (no - text input, property menus, hover or timers) and draw only frames, text, shapes and images; - no images or sliders in panels. Figma's own plugins are JavaScript against Figma's plugin API and - don't run. -- **Impact:** teams that depend on specific Figma plugins (icons, content, accessibility checks) - can't bring them. FigCraft's CLI, control channel and MCP server cover agent automation better - than Figma does. -- **Estimate:** 8–26 h for the missing pieces of FigCraft's own API. Running Figma's plugins - needs a JS engine and an API shim: **owner decision needed**. - -### 7. CPU canvas where Figma uses the GPU -- **Missing:** GPU rendering of the canvas. FigCraft renders with tiny-skia on the CPU (banded, - parallel, retained) and shows the image through wgpu. A new zoom can't reuse the retained - image; when renders from scratch take a frame or more, the whole view is previewed with a - sixteenth of the pixels first, then refined. -- **Evidence (measured, `cargo xtask bench`):** a 50k-layer page ~8 ms zoomed out with levels of - detail, single-layer edits at 10k layers ~0.2 ms; full 1440×900 renders 7–15 ms at 10k layers. - Heavy effects (glass, background blur, noise) and 4K/5K canvases at 2x are where CPU time grows. -- **Impact:** large files and effect-heavy designs; battery on laptops. -- **Estimate:** 30–60 h for a GPU path (vello or wgpu shaders) held to the CPU renderer as the - oracle. - -### 8. Platform QA: Linux and the browser left -- **Missing:** regular runs on Linux and the browser; real screen readers; a repeat of the Windows - pass on other machines (scale factors above 100%, pen tablets, file drops from Explorer). -- **Evidence:** CI tests on Linux only (`.github/workflows/ci.yml`); release builds exist for every - platform (v0.2.0) but no issue has been filed yet. Windows: QA pass done 2026-10-11, findings in - PR (Windows 11, the desktop app driven over the control channel, the same workflow through MCP): - eight bugs fixed, among them unsaved changes lost when the app was killed or crashed, - Ctrl+arrows and Ctrl+Backspace acting on the whole line in text, scrubbed fields snapping back - and skewed linear gradients. -- **Impact:** unknown bugs on the platforms where FigCraft is most attractive (Linux has no Figma - desktop app). -- **Estimate:** 10–20 h, partly human (hardware). - -### 9. Figma Draw: tablet pressure on macOS and Linux -- **Missing:** pen pressure on macOS and Linux tablets (winit reports none there). -- **Landed (2026-10-12):** multi-edit: several vector layers in edit mode at once (Enter or - double-click with them selected, `vector.edit.begin {ids}`); points selected, marqueed, dragged, - nudged, deleted, mirrored and rounded across layers in one undo step; the pen, bend, paint bucket, - cut and eraser work in the layer last pressed (`vector_multi_behaviour_tests`). -- **Landed:** text on a path (`text.onPath`, `text.pathOptions`, `text.removeFromPath`; the - Design panel's Typography section; canvas, PNG, SVG and PDF as outlines; SVG `` imports - as text on a path), pattern fills (`PaintKind::Pattern`, `fill.pattern`, the colour picker's - Pattern tab; SVG ``, PDF tiling patterns), stretch and scatter brushes (`stroke.brush`, - eight presets drawn in code; canvas, PNG, SVG, PDF and outline stroke) and pen pressure for the - Pencil (Windows pens and browser styluses; recorded as the stroke's width profile, which brushes - follow). -- **Evidence:** winit reports no pressure on macOS and Linux tablets. -- **Impact:** illustration-flavoured work moved into Figma Draw in 2025. -- **Estimate:** 1–3 h once winit (or a platform tablet API behind it) reports pressure there. - -### 10. Team libraries: no team-wide lists or accounts; analytics are counts -- **Missing:** libraries listed for a whole team or organization (each file enables a library by - its link), accounts and per-person library permissions, analytics beyond instance counts per - component and file (insertions and detaches over time), update notices pushed by the server - (apps ask every 30 seconds while the Assets panel shows). -- **Evidence:** a file's library is published to `figcraft-server` under the file and enabled in - other files by a share link, with per-asset versions, updates with overrides kept, usage counts - and swapping a whole library by names ([`server.md`](server.md#team-libraries), - [`components-parity.md`](components-parity.md)). -- **Impact:** design-system teams. -- **Estimate:** 4–8 h, the accounts part after gap 2. - -### 11. Spell check -- **Missing:** spelling underline and suggestions while editing text. -- **Evidence:** no spell-check code in the repo. -- **Estimate:** 4–8 h with a permissively licensed dictionary and checker (provenance per - `ATTRIBUTION.md`). - -### 12. Video fills; HEIC images -- **Landed (2026-10-11):** animated GIF fills (the file kept as is, the first frame on the canvas - and in exports, played in the presenter and the preview) and TIFF images. -- **Missing:** Figma takes JPG, PNG, HEIC, WebP, GIF and TIFF images and MP4, MOV and WebM video - (help center, checked 2026-10-10). FigCraft decodes PNG, JPEG, WebP, GIF, TIFF and BMP; HEIC and - video need decoders that are C libraries (libheif, FFmpeg) or each platform's media framework. -- **Estimate:** 5–10 h (video playback in the presenter is most of it; it can reuse the GIF clock). - -### 13. Vector tail: outline-stroke compositing -- **Landed (2026-10-15):** Outline stroke keeps the renderer's even-odd stroke rings; regression pixels, Undo/Redo, native save/reopen and instance opacity inheritance/overrides in `outline_stroke`. -- **Missing:** preserving filled-shape opacity, blend and effects on the composite. A follow-up must preserve component inheritance and references when deciding which node owns these properties. -- **Landed (2026-10-12):** several Bezier handles selected with Shift-click and dragged together, - sharp points beside curved segments rounded, a stroke cap and join per point (canvas, PNG, SVG, - PDF, outline stroke), the Pen outside edit mode connecting to its own points and continuing a - selected open path. Left: corner smoothing beyond rectangles and frames, the transform box around - points of several layers, per-point caps and joins on dashed, variable-width and brush strokes. -- **Landed (2026-10-11):** boolean groups are clicked where their result is (a click in a - subtracted hole reaches what's behind; with the group or an operand selected its operands are - hit, so each can still be picked; "Select layer" lists the group only where its result is), live - boolean editing (operands selected by double-click or Cmd/Ctrl-click; - moving, resizing or editing an operand's points redraws the result during the drag, with every - operand outlined; per-operand operations from the boolean controls and `node.set`; a drag frame - recombines only the operands from the edited one up), Design panel Arc fields (Start, Sweep, - Ratio; Mixed across ellipses; bindable), the shape builder on strokes and open paths (strokes take - part as their outlined areas), one-sided variable widths (Alt-drag a width handle; canvas, PNG, - SVG, PDF and outline stroke). -- **Estimate:** compositing not yet re-estimated; vector QA with real files remains part of gap 3. - -### 14. Variables: extended collections on real libraries -- **Landed:** extended collections (`library.extendCollection`: a library collection's modes and - variables in a collection of this file, values overridden per mode with reset, following library - updates, variables of its own), variables import/export (`variable.export` / `variable.import`: - the REST local variables JSON, merged by name; CSS custom properties), gradient stops and variant - properties bound to variables. -- **Landed (2026-10-12):** variables a library adds to a collection this file extends are a library - update of their own (`library.updates` lists them by name, without an `id` until they have a - copy) and arrive inheriting when accepted; a library collection that is itself an extended - collection (a brand theme extending a core one) can be extended again, its values flowing down - the chain as each file accepts updates, every level's overrides kept. -- **Missing:** checked with synthetic files only; extending a collection of the same file isn't - offered (extending takes library collections). -- **Estimate:** 0–1 h. - -### 15. Prototyping: media triggers and actions, controllers off Windows -- **Landed (2026-10-12):** key/gamepad triggers take controller buttons (`Gamepad A` … `Gamepad - Home`, the standard layout's 17 buttons; a Controller menu beside the key field; REST files' - controller triggers import); the player reads controllers on Windows and in the browser (Gamepad - API) and takes `ui.input {gamepad}` on the control channel; widgets react to clicks in a - presented prototype, on the prototype's own copy; `proto.evaluate` computes expressions (mode - lookups included) where a layer's interaction would run (`crates/engine/src/proto_tail_tests.rs`, - `crates/ui-egui/src/present_tail_tests.rs`, `crates/format/src/rest/gamepad_tests.rs`). -- **Missing:** video triggers (on media hit/end) and play/pause/mute/seek actions (the REST - importer warns and skips them, `crates/format/src/rest/proto.rs`); controllers on macOS, Linux - and FreeBSD (the usual Rust controller library links libudev on Linux; a pure-Rust reader per - platform, or an owner decision on that dependency); PlayStation and Switch button labels. -- **Estimate:** 2–5 h, the media part after video fills (gap 12). - -### 16. Dev Mode: the tail after dev resources -- **Landed (2026-10-11):** code connect: a component or set links to its code component per - language (React, SwiftUI, Compose): an import path, the name and a template whose `{property}` - and `{property ? "a" : "b"}` placeholders take the instance's values, escaped per language and - never executed. `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet - instead of generated code (inside generated layer trees too); the panel's Connect to code edits - it with a live preview. Links are stored on the component (saved, synced, undoable) and travel - as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`, validated and capped) - so code owners can version them. Plugins whose manifest asks only to read run from the Inspect - tab. -- **Landed (2026-10-12):** dev resources: links for developers (a URL and a name) on any layer - (`devResource.add` / `edit` / `remove` / `list`, undoable, saved and synced). An instance also - lists its main component's links (and its set's) read-only with where they come from; layers - inside an instance show their main layer's. URLs are checked as web links (http or https, a - host, no spaces, capped) and never fetched: a link added without a name is named after its URL, - and a link opens only when clicked. The Inspect tab lists, adds, edits and removes them; - `get_node` / `inspect_document` return them as `devResources`. -- **Missing:** import lines for connected instances inside a - generated layer tree (only the selected layer's own import is shown); publishing mappings from - the code repository with a CLI. An editor extension is out of scope: the MCP server already - serves design data, code and the connected snippets to agents and editors. -- **Estimate:** 0–1 h. - -### 17. Branching and merging -- **Landed (2026-10-11):** branches of a file (`branch.*`): File ▸ Create branch… (or the Branches - list in version history) copies the file into a branch tab that remembers the file as it was (its - base) and makes its ids in its own id space; edits on the branch are saved with the main file. - Review and merge (the banner on a branch tab) shows the main file and the branch side by side or - as an overlay, what changed on each side since the base (layers with their property changes as - Inspect shows them; styles, variables, comments) and the conflicts: the same property changed on - both sides, or a layer deleted on one side while the other edited it or what's inside it. Each - conflict is resolved for the branch or main; nothing conflicting merges until it is. Merging is - one undo step on the main file and archives the branch; Update from main brings main's changes - into the branch the same way; branches can be archived without merging. Three-way merge over the - collaboration change sets (`figcraft_collab::merge`), fuzz-tested with random real commands. -- **Landed (2026-10-12):** a branch's own version history: versions saved on a branch tab (by hand - or autosaved) go into the branch, not the main file, and are viewed, restored (one undo step on - the branch), renamed, deleted and duplicated there; they are saved in the file with the branch. - Merging adds a version to the main file's history (Merged "name"); the merged or archived - branch keeps its own history and opens with it. Fills, strokes and effects merge item by item: - different items changed on each side, or items added at the end on one side, merge without a - conflict; only the same item changed both ways, an item removed on one side, or different items - added on both sides conflict (as the whole list). -- **Missing:** branches of a file in a live session are refused; other lists (text runs, export - settings, guides…) still conflict as a whole; the branch history has no entries of its own for - its creation or updates from main (only saved versions, autosaves and the merge on main). -- **Estimate:** 1–2 h. - -### 18. Display P3 on screen (several windows landed) -- **Landed (2026-10-11):** several windows: File ▸ New window, a tab's Open in new window or a tab - dragged off the strip; each window has its own tabs, panels and canvas and draws only when it - needs to; closing one asks about unsaved changes; `window.new`, `window.list`, `window.focus` and - `window` on any control request. -- **Missing:** wide-gamut colour on the canvas (egui draws sRGB, so P3 colours outside sRGB clip on - screen; exports keep them); a screen-reader tree in windows other than the main one (eframe 0.36 - gives only the main window one); window positions restored at the next start. -- **Estimate:** 4–8 h. - -### 19. AI features -- **Missing:** Figma AI (first draft, rename layers, remove background, image generation and - editing, visual search, Make). -- **Estimate:** 30–60 h with openly licensed local models. **Owner decision needed.** - -### 20. Mobile apps -- **Missing:** Figma's iOS and Android apps view and comment on files and mirror prototypes. - FigCraft's web build runs in mobile browsers but is not designed for touch. -- **Estimate:** 20–40 h for a touch-first viewer and mirror. - -### 21. Menu items without a command (measured): none left -- **Landed (2026-10-11):** `file.openRecent` (File ▸ Open recent: recent files kept in the - preferences, at most 20, greyed when the file is gone and dropped when picked, Clear recent), - `view.outlinesSelection` (outline mode for the selected layers only) and `view.maskOutlines`; - the plugin pair landed in #101. Every item in the menu catalog has a command - ([`parity-checklist.md`](parity-checklist.md)). -- **Estimate:** none left. - -### 22. Screen readers never run; browser has no accessibility tree -- **Evidence:** AccessKit names and keyboard navigation landed (#96), checked by tests only; - eframe's web backend doesn't send the tree yet. -- **Estimate:** 4–8 h plus human QA with VoiceOver, NVDA, Narrator and Orca. - -### 23. CMYK output intent for print PDFs -- **Evidence:** CMYK conversion is a documented naive formula; no CMYK ICC profile can ship in the - repository (licensing). -- **Estimate:** 2–4 h once an openly licensed profile is found. - -## Revision history - -| Date | Change | Summary | -|---|---|---| -| 2026-10-15 | minor | M4: Outline stroke preserves even-odd stroke holes. Pixel comparisons cover alignment, dashes, individual side weights, vector paths and stroke-only opacity; native save/reopen, Undo/Redo and instance opacity inheritance/overrides are covered by `outline_stroke`. Existing estimates unchanged; filled-shape compositing needs separate work. | -| 2026-10-14 | minor | Russian catalog (M9), reviewed and approved by VanTan: all 2,165 current entries, Русский in Preferences and regional ru tags. Russian integer plurals use one/few/many; contributor display name VanTan added. Existing estimates unchanged. | -| 2026-10-13 | minor | Gap 1: the experimental `.fig` export writes instance sizes (resized, filling an auto layout, scaled), variables with modes and bindings, shared styles and variable-width strokes, and the import reads them back. Checked by opening written files in other software: resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width; number, string and boolean bindings and bindings on effects, layout guides and gradient stops come across as the values they give but did not show as bound there (their entries now also name the layer field they set, not yet checked there), and strokes wider on one side of the path are written as filled outlines, no longer editable strokes there (the stroke itself is kept as plugin data and our own import puts it back; not yet checked there). Hours and the ranking unchanged | -| 2026-10-12 | minor | Gap 2: your online files landed (the app records the files it put on a server with their owner tokens, Home ▸ Online lists them and opens them again as their owner, with sharing, rename, removal and forget from the list; one request per server tells which are still there; tabs mark files in a live session). What is left is unchanged: accounts, teams and per-person permissions, shared comments and versions. Hours unchanged | -| 2026-10-12 | minor | Gap 2: sharing with a join code (Share ▸ Online or Local network, one Start sharing button, a short code to give others, one join field for a code or a link; the server names links with codes and limits wrong ones) added to the landed list; usability, so the estimate and ranking are unchanged | -| 2026-10-12 | minor | Gap 3: first measured fidelity data. Text box sizes and auto layout results of our example documents were compared with the same documents re-saved by other software; three text rules (whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only) were found and applied, and all 230 self-sizing text boxes in 7 documents now match to the pixel (one font family). Real files are still unchecked: hours and the ranking unchanged | -| 2026-10-12 | minor | Gap 2: live sessions are per document (each open file has its own session or none, several run at once, the window and the `collab.*` commands follow the file in front, closing a file ends its session only) added to the landed list; a fix, so the estimate and ranking are unchanged | -| 2026-10-12 | minor | Gap 1: the experimental `.fig` export now opens elsewhere: written files were checked by opening them in other software: layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft; effects such as glass and noise, images and prototype interactions are still unchecked there, and variables are not written. Hours and the ranking unchanged | -| 2026-10-12 | minor | Gap 1: an experimental `.fig` export landed (the whole document, from the same public format notes as the import; checked only by round trips through our own importer); export hours 15–30 h → 5–15 h (checks against real files); gap 1's read tail (5–10 h) and the ranking unchanged | -| 2026-10-12 | minor | Gap 4: keyboard shortcut parity suite per platform (129 documented rows: 126 bound, 1 moved, 2 missing; 48 undocumented bindings pinned) added to the landed list; estimate and ranking unchanged | -| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Gap 5 31–54 h → 29–51 h, rank unchanged | -| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Gap 5 32–55 h → 31–54 h, rank unchanged | -| 2026-10-12 | minor | Gap 15: controller buttons on key/gamepad triggers (Windows and the browser), widgets reacting in prototypes and `proto.evaluate` landed; gap 15 now media plus controllers off Windows, 3–6 h → 2–5 h; ranking unchanged | -| 2026-10-12 | minor | Plugin directory on `figcraft-server` (publish with the server key, browse, install after a permissions confirmation, hash-checked downloads, updates by version, operator moderation) added to gap 6's landed list; gap 6 10–30 h → 8–26 h; ranking unchanged | -| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Gap 5 40–65 h → 32–55 h, rank unchanged | -| 2026-10-12 | minor | Plugin widgets (API 3) and the panel tail (panels side by side, colour picker on plugin fields) added to gap 6's landed list; gap 6 now 10–30 h; ranking unchanged | -| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): every UI string, command label and menu wording (1,899 entries) machine-drafted in each and marked as drafts awaiting native review, falling back to English per key (new strings need only English and Spanish); 0 counts as singular in French and Brazilian Portuguese; System picks the first OS language with a catalog (pt-PT and pt read pt-BR); long translations elide with a tooltip in dropdowns, two-column labels and the home cards. Gap 5 45–75 h → 40–65 h, rank unchanged | -| 2026-10-12 | minor | Cursor chat and spotlight in live sessions (gap 2): `/` opens a chat bubble at your pointer, a spotlight makes everyone follow your view; ephemeral presence the host validates and rate-limits. Gap 2 45–90 h → 42–84 h | -| 2026-10-12 | minor | Layers and Assets panels behaviour suite and fixes added to gap 4's landed list; ranking and hours unchanged | -| 2026-10-12 | minor | Vector multi-edit and the vector edit tail, with a behaviour suite written from the public documentation (5 tests, about 30 table rows and steps, and a junk-parameter sweep of every vector command in multi-edit mode): several vector layers are edited at once (Enter, double-click or `vector.edit.begin {ids}`; a press on another layer's point, handle or segment makes it the active layer; Shift-click, marquee and lasso select across layers; dragging, the arrows, Delete, delete and heal, mirroring, smoothing, corner radius and point caps and joins reach every layer's selected points in one undo step; the pen, bend, paint bucket, cut and eraser work in the active layer); several Bezier handles are selected with Shift-click and dragged together (`vector.edit.moveHandles`); a sharp point next to a curved segment rounds (the curve is cut where the arc meets it and keeps its shape); a point can have its own stroke cap and join (`vector.edit.strokeCap`, `vector.edit.strokeJoin`, the Point section of the Design panel; canvas, PNG, SVG and PDF, outline stroke); the Pen outside edit mode connects to one of its own points and ends the path, and continues a selected open path from an end point. Left: corner smoothing beyond rectangles, the transform box around points of several layers, per-point caps and joins on dashed, variable-width and brush strokes. Gap 9 2–5 h → 1–3 h (pen pressure on macOS and Linux tablets left, blocked on winit). Shapes & vector 98%/72% → 99%/73%, 2–4 h → 1–3 h; Figma Draw extras 80%/55% → 88%/60%, 2–5 h → 1–3 h (gap 9: pen pressure on macOS and Linux tablets left). Totals recomputed by the doc's method, which rows since the last recompute had left stale: the area table's weighted breadth is 83.7% (~83%; core 94.1%, ~94%) and ready 63.1% (~63%; core 72.1%, ~72%), hours 133–284 (core 38–83); Features (depth) 61% → 63%, 46–94 h → 34–76 h; ready for real work 52.6% (~52%); mainstream weekly areas 71.0% → 72.1% (× 0.9 × 0.9 × 0.85 = 49.7%, ~49%, rounded down); essentials unchanged (~57%); full parity ~306–595 h → ~294–572 h (on top of the rows merged since: ~301–587 h → ~289–564 h; area-table totals recomputed on the merged table and rounded down: breadth ~84%, ready ~63%, core ~94%/~72%, mainstream weekly areas 72.4%, ~49%) | -| 2026-10-12 | minor | Dev resources (gap 16): links for developers (a URL and a name) on any layer: `devResource.add` / `edit` / `remove` / `list`, undoable, saved and synced; an instance also lists its main component's (and that component's set's) links read-only with where they come from, and layers inside an instance show their main layer's; URLs are checked as web links (http or https, a host, no spaces, at most 2,048 characters) and never fetched (a link added without a name is named after its URL), opened only when clicked; the Inspect tab lists, adds, edits and removes them; `get_node` and `inspect_document` return them as `devResources`. The editor extension stays out of scope (the MCP server serves agents and editors). Dev Mode breadth 93% → 96%, ready 70% → 72%, 1–3 h → 0–1 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~304–593 h → ~303–591 h | -| 2026-10-12 | minor | Extended collections' tail (gap 14): variables a library adds to a collection this file extends now count as a library update of their own (listed by name without a copy yet) and arrive inheriting when accepted, instead of waiting for the library's next other update; a library collection that is itself an extended collection can be extended again, values flowing down the chain as each file accepts updates with every level's overrides kept (checked end to end). Styles & variables breadth 94% → 95%, ready unchanged (76%), 3–5 h → 2–4 h; gap 14 1–2 h → 0–1 h (real-library checks left); feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~305–594 h → ~304–593 h | -| 2026-10-12 | minor | A branch's own version history and finer list merges (gap 17): versions saved or autosaved on a branch tab go into the branch (viewed, restored, renamed, deleted and duplicated there, saved in the file with it); merging adds a version to the main file's history and the merged or archived branch keeps its own; fills, strokes and effects merge item by item, conflicting only when the same item changed both ways, an item was removed on one side or both sides added different items. Comments, versions, find, misc breadth 85% → 87%, ready 60% → 61%, 2–5 h → 1–4 h; gap 17 1–3 h → 1–2 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~306–595 h → ~305–594 h | -| 2026-10-12 | minor | Comments and version history behaviour suite and fixes added to gap 4's landed list (gap 17 unchanged: a branch's own version history is still left); ranking and hours unchanged | -| 2026-10-11 | minor | `.fig` import's tail (gap 1): vectors open as editable vector networks (points, curved segments, filled areas with their winding, per-point corner radius and handle mirroring, end caps from the end points), falling back to outlines when the network is missing or damaged; sticky notes, shapes with text, connectors, tables, code blocks, slides and similar layers import as the closest frames, text layers and vectors with a warning instead of being dropped; set-variable, set-variable-mode and conditional prototype actions come across. New parsers fuzzed on fixtures generated in code. File formats 55% → 58%, 8–15 h → 5–10 h; import/export breadth 80% → 82%, ready 50% → 54%; feature and area totals and ready for real work (~49%) unchanged; beta ~173–310 h → ~170–305 h; full parity ~321–618 h → ~318–613 h | -| 2026-10-11 | minor | `.fig` import, best effort (gap 1): `.fig` files open (best effort): the zip, its deflate/zstd chunks and the kiwi schema are decoded generically and mapped by field name onto the REST importer, so pages, the layer tree, geometry, paints (styles resolved), strokes, effects, text with styled runs, vectors (as outlines), auto layout (grid included), constraints, components, sets, instances (expanded from their main with overrides, derived layout and property values), styles, variables with modes and bindings, prototyping and image fills from the archive come across; anything else is a warning. Gap 1 30–60 h → 8–15 h (vector networks, other layer kinds, real-file checks; export 15–30 h on top, as before) | -| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | -| 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | -| 2026-10-11 | minor | Localization layer landed (gap 5): per-language catalogs baked in at build time with English text as keys and fallback, placeholders and plurals, command labels by id, Preferences ▸ Language (System follows the OS or the browser) applied live and `pref.language`; English catalog and a complete Spanish draft awaiting native review. Localization 10% → 20%, 60–100 h → 45–75 h; ready for real work unchanged (~47%); full parity ~363–697 h → ~348–672 h | -| 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | -| 2026-10-11 | minor | Plugin panels and settings landed (gap 6, ABI v2; v1 plugins unchanged): a plugin shows a panel of notes, fields (text, number, checkbox, menu, list, colour) and buttons drawn with FigCraft's widgets; a button or a changed value calls it back with every value (same limits per callback, one undo step per callback that edits, rolled back on failure), its module staying loaded while the panel is open; `plugin.ui` / `plugin.event` / `plugin.close` drive panels headless; per-plugin settings (64 KB) kept with the preferences; the Swatch grid example gained a palette panel that remembers the last palette. Plugins & widgets breadth 35% → 45%, ready 20% → 28%, 25–55 h → 15–40 h; ecosystem 20% → 28%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~373–712 h → ~363–697 h | -| 2026-10-11 | minor | Canvas fixes (gap 13 closed): boolean groups are clicked where their result is (a click in a subtracted hole reaches what's behind; with the group or an operand selected its operands are hit, so each can still be picked), a layer dragged inside a group stays in the group (frames and auto layout still take other drops), View ▸ Frame outlines and View ▸ Slices draw on the canvas, and the idle tests wait for the render thread instead of counting frames. Shapes & vector 1–3 h → 1–2 h (vector QA with real files left); percentages, feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~378–722 h → ~375–717 h | -| 2026-10-11 | minor | Stretch and scatter brushes and pen pressure landed (gap 9): eight brushes drawn in code (four stretched along the path, four scattered with spacing, size, rotation and scatter jitter from a seed) in the stroke settings, drawn everywhere strokes are and flattened by outline stroke; the Pencil records pen pressure as the stroke's width profile (Windows pens, browser styluses; macOS and Linux tablets report none) and can draw with a brush; Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–9 h; Strokes 2–4 h → 1–3 h; Hardware 50% → 55%, 15–30 h → 13–27 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~378–722 h → ~373–712 h | -| 2026-10-11 | minor | Text on a path and pattern fills landed (gap 9): text follows a path or shape (start, side, alignment; edited in place; canvas, PNG, SVG and PDF; SVG `` imports as text on a path) and pattern fills tile a layer (grid or half-shifted rows or columns, scale, spacing, alignment; live as the source changes; SVG ``, PDF tiling patterns); Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–10 h; Text 8–14 h → 7–13 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~375–715 h | -| 2026-10-11 | minor | Team libraries on `figcraft-server` landed (gap 10): a file's library is published to the server under the file and enabled in other files by a share link (per-asset versions; updates noticed in the background and accepted with overrides kept), the server counts which files use which components, and a whole library can be swapped for another by names; Team libraries breadth 40% → 70%, ready 25% → 40%, 10–20 h → 4–8 h; collaboration 12% → 13%, 55–110 h → 49–98 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~380–725 h → ~375–715 h | -| 2026-10-11 | minor | Branches landed (gap 17): File ▸ Create branch… copies the file into a branch tab that remembers its base; Review and merge shows both sides' changes (layers with Inspect-style property changes, styles, variables, comments) and the conflicts (the same property changed on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merging is one undo step and archives the branch; Update from main; branches are saved in the `.figcraft` file. Comments, versions, find, misc breadth 80% → 85%, ready 58% → 60%, 4–8 h → 2–5 h; collaboration 12% → 13%; menu breadth 204/204 (the catalog gained File ▸ Create branch…); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~378–722 h | -| 2026-10-11 | minor | Animated GIF fills and TIFF images (gap 12, without video and HEIC): GIF fills keep the original file, show the first frame on the canvas and in exports and play in the presenter and the preview (frames decoded on first play, at most 500 frames and 128 MB; always looping; the window wakes only for a GIF's next frame); TIFF images open, import, place and paste. Paint breadth 85% → 87%, 5–9 h → 4–7 h; gap 12 6–12 h → 5–10 h (video playback and HEIC left, both needing non-Rust decoders); feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | The last three catalogued menu items got commands (gap 21): File ▸ Open recent (recent files kept in the preferences, at most 20, a file that's gone greyed in the menu and dropped when picked, Clear recent), View ▸ Outlines ▸ Show outlines for selection and View ▸ Mask outlines: menu breadth 98.5% → 100% (203/203); Canvas & navigation breadth 95% → 97%, 3–6 h → 2–5 h; feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | Extended collections, variables import/export and gradient stop and variant property bindings landed (gap 14): Styles & variables breadth 85% → 92%, ready 70% → 72%, 4–8 h → 1–2 h; feature breadth unchanged (~79%); ready for real work unchanged (~47%); full parity ~385–730 h → ~380–725 h | -| 2026-10-11 | minor | Collaboration wording after the standalone server: `figcraft-server` keeps many files, persisted as a snapshot plus a log of what changed (compacted, checksummed, damaged files skipped and reported), restarts lose nothing confirmed and apps resume where they were; the app publishes its file to it; share links per file let people edit or only view (the server refuses a viewer's edits), are revocable and stored as hashes; TLS comes from a reverse proxy (the desktop app has none of its own yet); percentages and hours unchanged until the next re-measure | -| 2026-10-11 | minor | Live boolean editing, Design panel arc fields, the shape builder on strokes and open paths and one-sided variable widths landed (gap 13): Shapes & vector breadth 92% → 97%, ready 66% → 68%, 8–14 h → 1–3 h; feature breadth ~78% → ~79%; ready for real work unchanged (~47%); full parity ~390–745 h → ~385–730 h | -| 2026-10-11 | minor | Collaboration wording after the collaboration polish: styles, variables, collections, modes and comments created in a live session take ids from their creator's id space (concurrent creations no longer overwrite each other; the server checks it), others' text carets and selections show on the canvas, and typing in a layer someone else is typing in says so; percentages unchanged | -| 2026-10-11 | minor | Wording only: live sessions (#104), then the browser joining them and `figcraft://` invite links opening the desktop app; percentages and hours unchanged until the next re-measure | -| 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 | minor | Marked beta blockers; no alpha blockers (the alpha gate passes) | -| 2026-10-10 | major | Created from the 2026-10-10 re-measure against Figma desktop 126.10.7 (replaces the gap list that lived in ROADMAP.md) | +# Where FigCraft falls short of Figma + +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (Outline stroke preserves stroke holes; reviewed Russian catalog and integer plurals; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail) · **Target:** Figma Design (desktop app 126.10.7) + +Every known shortfall against Figma, one entry each, ranked by how much it stops a Figma user from +doing real work in FigCraft. Agents: pick work from the top unless the owner says otherwise. The +numbers behind the ranking are in [`target-app-parity.md`](target-app-parity.md). Hours are one +Opus 5.5 agent working sequentially, tests included, calibrated as described there. "Estimated" +everywhere unless marked measured. + +## Ranked + +No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passes. Beta blockers are marked. + +| # | Gap | Kind | Hours | Doc | +|---|---|---|---|---| +| 1 | `.fig` files: best-effort import with editable vector networks and stand-ins for non-design layers landed, and an experimental export (round trips through our own importer; written files opened in other software, where layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft, instances keep their size and variables with modes and colour styles come across); real-file checks and the small tail left (**beta blocker** until checked on real files, gap 3) | File format | 5–10 | [file-format-parity](file-format-parity.md) | +| 2 | Multiplayer and sharing are a preview, without accounts (**beta blocker**) | Feature | 42–84 | [collaboration-parity](collaboration-parity.md) | +| 3 | Nothing checked against real Figma files | Fidelity | 15–30 | [target-app-parity](target-app-parity.md) | +| 4 | Interaction fidelity not compared tool by tool | UI/UX | 40–60 | [ui-parity](ui-parity.md) | +| 5 | Localization: English, reviewed Russian, plus unreviewed Spanish (and Latin American Spanish), French, German, Brazilian Portuguese, Japanese, Korean and Simplified Chinese drafts | Localization | 29–51 | [localization-parity](localization-parity.md) | +| 6 | Plugins: widgets take clicks only, a self-hosted directory but no public catalogue; Figma's plugins don't run | Ecosystem | 8–26 | [target-app-parity](target-app-parity.md) | +| 7 | CPU canvas where Figma uses the GPU | Hardware/performance | 30–60 | [hardware-parity](hardware-parity.md) | +| 8 | Platform QA: Linux and the browser left (Windows pass done 2026-10-11) | Stability/platforms | 10–20 | [hardware-parity](hardware-parity.md) | +| 9 | Figma Draw: pen pressure on macOS/Linux tablets (multi-edit, brushes, pressure, text on a path and pattern fills landed) | Feature | 1–3 | [target-app-parity](target-app-parity.md) | +| 10 | Team libraries: no team-wide lists or accounts; analytics are counts | Feature | 4–8 | [components-parity](components-parity.md) | +| 11 | Spell check | Feature | 4–8 | [target-app-parity](target-app-parity.md) | +| 12 | Video fills; HEIC images | File format | 5–10 | [file-format-parity](file-format-parity.md) | +| 13 | Vector tail: outline-stroke compositing (stroke rings preserved) | Feature | — | [target-app-parity](target-app-parity.md) | +| 14 | Variables: extended collections checked on real libraries (the tail landed 2026-10-12) | Feature | 0–1 | [components-parity](components-parity.md) | +| 15 | Prototyping: media triggers and actions; controllers on macOS, Linux and FreeBSD (controller buttons on key/gamepad triggers, widgets in prototypes landed) | Feature | 2–5 | [prototyping-parity](prototyping-parity.md) | +| 16 | Dev Mode: import lines inside generated layer trees; the editor extension is left to the MCP server (code connect, Dev Mode plugins and dev resources landed) | Feature | 0–1 | [dev-mode-parity](dev-mode-parity.md) | +| 17 | Branching: branches in live sessions, lists other than fills, strokes and effects merged whole (a branch's own version history and per-item list merges landed 2026-10-12) | Feature | 1–2 | [collaboration-parity](collaboration-parity.md) | +| 18 | Display P3 on screen: blocked upstream (egui-wgpu picks an 8-bit sRGB surface); gamut warning and soft-proof sRGB landed, as did several windows | Hardware | 4–8 | [hardware-parity](hardware-parity.md) | +| 19 | AI features | AI | 30–60 | [target-app-parity](target-app-parity.md) | +| 20 | Mobile apps (view, mirror, comment) | Platforms | 20–40 | [hardware-parity](hardware-parity.md) | +| 21 | Menu items without a command: none left (landed 2026-10-11) | Feature | — | [parity-checklist](parity-checklist.md) | +| 22 | Screen readers never run; browser has no accessibility tree | UI/UX | 4–8 | [ui-parity](ui-parity.md) | +| 23 | CMYK output intent for print PDFs | File format | 2–4 | [file-format-parity](file-format-parity.md) | + +## Entries + +### 1. `.fig` files: the import's tail +- **Landed (2026-10-11):** `.fig` files open (best effort): the zip, its deflate/zstd chunks and the kiwi schema are decoded generically and mapped by field name onto the REST importer, so pages, the layer tree, geometry, paints (styles resolved), strokes, effects, text with styled runs, vectors, auto layout (grid included), constraints, components, sets, instances (expanded from their main with overrides, derived layout and property values), styles, variables with modes and bindings, prototyping and image fills from the archive come across; anything else is a warning (`crates/format/src/fig`). Then (2026-10-11): vectors open as editable vector networks (per-point corner radius, handle mirroring, end caps; outlines when the network blob is missing or damaged), sticky notes, shapes with text, connectors, tables, code blocks, slides and similar layers import as the closest frames, text layers and vectors with a warning, and set-variable, set-variable-mode and conditional prototype actions come across. `file.open`, + `file.import`, drag and drop, the CLI and MCP's `open_file` read `.fig`; the file is never saved + over. +- **Landed (2026-10-12):** an experimental export: `file.export {format: "fig"}`, File ▸ Export… ▸ + .fig (experimental), `figcraft-cli export --format fig` and MCP's `export` write the whole document + with a kiwi encoder mirroring the decoder and a schema of the fields written: the layer tree, + geometry, paints with image bytes, strokes, effects, text with styled runs, vector networks, auto + layout, constraints, components, sets, instances with their overrides and property values, and + prototype interactions; shared style links, variables, comments, Dev Mode data and the rest are + left out with a warning ([file-format-parity](file-format-parity.md#export-to-fig-experimental)). + The import gained ellipse arcs, boolean operation types and component set descriptions. Then + (2026-10-12): written files were checked by opening them in other software: layers, fills, + gradients, strokes, shadows, vectors, components and text draw as in FigCraft, after the export + learned to say that the document and its pages are visible and opaque, to number its schema as + the format does, to write outlines, text settings, standard font style names and line height + units. Then (2026-10-13): instances keep their own size (an override addressed by the main's id, + derived data for their layers, a scale factor), variables with modes and bindings, shared styles + and variable-width strokes are written and read back; checked there: resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width; number, string and boolean bindings and bindings on effects, layout guides and gradient stops come across as the values they give but did not show as bound there (their entries now also name the layer field they set, not yet checked there), and strokes wider on one side of the path are written as filled outlines, no longer editable strokes there (the stroke itself is kept as plugin data and our own import puts it back; not yet checked there), + like effects such as glass and noise, images and prototype interactions. +- **Missing:** connector labels, elbowed or curved connectors without a saved path (drawn straight), + table cells beyond their own layers, filled areas of a vector with their own fills, per-point stroke + joins, variables bound to fields the editor can't bind, fixed or content-sized grid tracks and video + fills aren't read; text lays out again with + local fonts. Nothing has been checked against real-world files yet (gap 3). The export is checked + only against our own importer: whether other apps open its files is unknown. +- **Evidence:** fixtures generated in code (`crates/format/src/fig/tests.rs` and `tail_tests.rs`: kiwi + round trips, every mapped feature, vector network blobs, truncation, bit flips and junk never panic); `crates/engine/src/fig_import_tests.rs`. + Export: 60 generated documents round trip through the importer layer by layer, written files + decode with the generic decoder and list parents first, the encoder agrees with the decoder, + junk documents never panic (`crates/format/src/fig/export_tests.rs`, + `crates/engine/src/fig_export_tests.rs`). +- **Impact:** existing work can be brought across, but only real files will show how much of it. +- **Estimate:** 5–10 h for the read tail (real-file fixes, connector labels, table cells); checking the export against real files a further 5–15 h (with gap 3). + +### 2. Multiplayer and sharing are a preview, without accounts +- **Missing:** accounts, teams and projects, per-person permissions, audio, + shared comments and version history. Comments and versions are local to the file. +- **Evidence:** the core landed in #103 (`crates/collab`: client-scoped ids, change sets, server + ordering and validation, client reconciliation, binary messages, randomized convergence tests over + real commands), and live sessions run over the network (`crates/collab-net`: hosting from the + app, invite links that open the app, document sync between apps, live cursors, selections, + text carets, avatars and follow, cursor chat and spotlight, and the browser build joins) with per-person undo, refused + edits rolled back with a reason, reconnects that resume and send what was made offline, and + styles, variables and comments that several people create at once all kept; sessions are per + document (each open file has its own or none, several at once, closing a file ends only its + own); files are shared with a short join code (online through the server in Preferences, or on + the local network with a code that carries the host's address) and joined by typing it; the app + remembers the files it put online and lists them on the home screen (open again as owner, stop + and start sharing, rename, remove, forget), per computer; a standalone + `figcraft-server` keeps many files (snapshot plus log, compacted; restarts lose nothing + confirmed), takes files published from the app and hands out share links that let people edit + or only view; the desktop app and the browser reach it over `wss://` through a TLS reverse + proxy, and releases ship it with a deployment guide (`docs/server.md`). But there are no + accounts (a link is the access) (`docs/collab-protocol.md`). +- **Impact:** Figma's defining feature; most teams use it every day. Solo designers can work + without it. +- **Estimate:** 42–84 h: accounts, teams and per-person permissions, shared comments and versions + (numbers from before the server landed; re-measured next). + +### 3. Nothing checked against real Figma files +- **Missing:** a corpus of real documents (design systems, app screens, prototypes) imported and + compared with Figma's own PNG/SVG/PDF exports. +- **Evidence:** every corpus in the repo is synthetic (`crates/svg/tests/corpus`, the synthetic + design system in `crates/engine/src/synthetic_ds.rs`). Scores were self-graded by the agents that + built the features. +- **Impact:** unknown fidelity in auto layout sizing, text metrics, effects and component + resolution on real files; the numbers in this repo can't be trusted beyond "estimated" until it + exists. +- **Measured so far:** text box sizes and auto layout results of our own example documents, + against the same documents re-saved by other software (which lays text out again). Three text + rules explained every difference and are applied (whole-pixel line heights, advances on a + 1024-per-em grid, letter spacing between glyphs only): all 230 self-sizing text boxes in 7 + documents now come out to the pixel (24 of 131 before, in the design-system example), and 208 of + its 231 other layers match in size and position (115 before). The other 23 hang on one grid + auto layout frame whose row positions came back as written while its cells got shorter. One font + family (Inter), left-aligned text; real files, other fonts, effects and component resolution are + still unmeasured (`crates/text/src/metrics_tests.rs`, and a local check over such documents in + `crates/engine/src/text_metrics_local_tests.rs`). +- **Estimate:** 15–30 h once `.fig` or REST import of real files is available; needs files whose + licence allows use (owner to supply or approve; never committed if not redistributable, see + craftrules `standards/test-corpora.md`). + +### 4. Interaction fidelity not compared tool by tool +- **Missing:** a side-by-side pass of every tool and panel: cursors, modifier keys while dragging, + hover states, popovers, menus, snapping thresholds. +- **Landed (2026-10-11):** behaviour suites written from the public documentation for auto layout, + selection and transform, text, and canvas navigation with rulers, guides, layout grids, outline + mode and measuring, then (2026-10-12) comments and version history, and the layers and Assets + panels (`crates/engine/src/*_behaviour_tests.rs`, `crates/ui-egui/src/layers_behaviour_tests.rs`), + each with its fixes; then keyboard shortcuts per platform (`shortcut_parity_tests.rs`: 129 + documented rows, 126 bound, 1 moved off a documented conflict, 2 missing, 48 undocumented + bindings pinned), with 16 rows newly bound, Control suspending snapping on macOS and shifted + symbols on other keyboard layouts; then the everyday partials of + [`ui-parity.md`](ui-parity.md): number fields, outline mode's options, Hide when publishing, a + cursor for every tool and modifier, panel widths, dark pages (`field_tests`, `cursor_tests`, + `cursors_tests`, `outline_options_tests`, `hide_publishing_tests`, `panel_width_tests`, + `theme_pages_tests`). +- **Evidence:** ROADMAP item 7 ("Left: tool by tool, panel by panel against the incumbent"). +- **Impact:** Figma users notice the feel within minutes; it decides whether they stay. +- **Estimate:** 40–60 h. Detail in [`ui-parity.md`](ui-parity.md). + +### 5. Localization: English, reviewed Russian, plus unreviewed drafts +- **Missing:** native reviews of the Spanish, French, German, Brazilian Portuguese, Japanese, + Korean and Simplified Chinese drafts and the Latin American Spanish overlay, the 25 newest strings + in five drafts, Hindi, Arabic, Indonesian and Vietnamese, localized engine messages and number + formatting; CJK UI text on the web without the craft-fonts CJK build. +- **Evidence (measured from source):** the layer landed 2026-10-11: catalogs in + `crates/ui-egui/locales/` (1,371 UI keys plus 524 command labels and 29 menu wordings), + Preferences ▸ Language, `pref.language`; `en.toml` is complete, and `es.toml`, `fr.toml`, + `de.toml`, `pt-BR.toml`, `ja.toml`, `ko.toml` and `zh-Hans.toml` (2026-10-12) are machine drafts + (all but `zh-Hans.toml` lack the 25 newest strings), with `es-419.toml` a Latin American overlay + listing only what differs from Spanish (Spanish and Russian are held to completeness by the check; the + others show English for strings added later). Russian adds all 2,165 current entries, + reviewed and approved by VanTan, with one/few/many integer plurals. + CJK UI text uses an installed CJK face the desktop app loads when something on screen needs it + (`crates/ui-egui/src/cjk.rs`). Engine error messages and Dev Mode values stay English. +- **Impact:** non-English designers get machine drafts nobody has reviewed yet, and English + errors. +- **Estimate:** 3–7 h per language plus native review, 4–6 h for engine messages and numbers; + 29–51 h for the key languages left and the drafts' follow-ups. Detail in + [`localization-parity.md`](localization-parity.md); how to add one in + [`translating.md`](translating.md). + +### 6. Plugins: widgets take clicks only, a self-hosted directory but no public catalogue; Figma's plugins don't run +- **Landed (#101):** sandboxed WebAssembly plugins (wasmi, metered, memory cap, no WASI, also in + the browser) acting through the command registry, one undo step per run, manifest permissions, + Plugins menu, Run last plugin, Manage plugins, `plugin.*` commands, a Rust SDK and two examples + ([`plugins.md`](plugins.md)). Plugin panels and settings (ABI v2, 2026-10-11): a plugin shows a + panel of fields and buttons drawn with FigCraft's widgets and is called back for each button or + changed value (one undo step per callback, same sandbox limits), driven headless with + `plugin.ui` / `plugin.event` / `plugin.close`; per-plugin settings kept with the preferences. + Widgets (plugin API 3, 2026-10-12): a plugin draws a layer of its own on the canvas (Plugins ▸ + Widgets, `widget.insert`), validated and built with locked layers inside, called back for clicks + on the parts it names (one undo step each, a fresh metered sandbox, the state kept in the layer + so it saves, copies, undoes, exports and syncs), `widget.click` / `widget.list` for agents, a + Counter example; panels of several plugins side by side; the colour picker on plugin colour + fields. A plugin directory on `figcraft-server` (2026-10-12): bundles published with the server + key and checked in full (`figcraft-cli plugin publish`), browsed, installed after a permissions + confirmation and updated from Plugins ▸ Manage plugins ▸ Browse, downloads checked against their + SHA-256, `plugin.directory.*` for scripts and MCP, moderation with `figcraft-server plugins`. +- **Missing:** a public catalogue and Community files, publisher accounts and signatures, ratings + and install counts, background update checks, the directory in the browser build; widgets take only clicks (no + text input, property menus, hover or timers) and draw only frames, text, shapes and images; + no images or sliders in panels. Figma's own plugins are JavaScript against Figma's plugin API and + don't run. +- **Impact:** teams that depend on specific Figma plugins (icons, content, accessibility checks) + can't bring them. FigCraft's CLI, control channel and MCP server cover agent automation better + than Figma does. +- **Estimate:** 8–26 h for the missing pieces of FigCraft's own API. Running Figma's plugins + needs a JS engine and an API shim: **owner decision needed**. + +### 7. CPU canvas where Figma uses the GPU +- **Missing:** GPU rendering of the canvas. FigCraft renders with tiny-skia on the CPU (banded, + parallel, retained) and shows the image through wgpu. A new zoom can't reuse the retained + image; when renders from scratch take a frame or more, the whole view is previewed with a + sixteenth of the pixels first, then refined. +- **Evidence (measured, `cargo xtask bench`):** a 50k-layer page ~8 ms zoomed out with levels of + detail, single-layer edits at 10k layers ~0.2 ms; full 1440×900 renders 7–15 ms at 10k layers. + Heavy effects (glass, background blur, noise) and 4K/5K canvases at 2x are where CPU time grows. +- **Impact:** large files and effect-heavy designs; battery on laptops. +- **Estimate:** 30–60 h for a GPU path (vello or wgpu shaders) held to the CPU renderer as the + oracle. + +### 8. Platform QA: Linux and the browser left +- **Missing:** regular runs on Linux and the browser; real screen readers; a repeat of the Windows + pass on other machines (scale factors above 100%, pen tablets, file drops from Explorer). +- **Evidence:** CI tests on Linux only (`.github/workflows/ci.yml`); release builds exist for every + platform (v0.2.0) but no issue has been filed yet. Windows: QA pass done 2026-10-11, findings in + PR (Windows 11, the desktop app driven over the control channel, the same workflow through MCP): + eight bugs fixed, among them unsaved changes lost when the app was killed or crashed, + Ctrl+arrows and Ctrl+Backspace acting on the whole line in text, scrubbed fields snapping back + and skewed linear gradients. +- **Impact:** unknown bugs on the platforms where FigCraft is most attractive (Linux has no Figma + desktop app). +- **Estimate:** 10–20 h, partly human (hardware). + +### 9. Figma Draw: tablet pressure on macOS and Linux +- **Missing:** pen pressure on macOS and Linux tablets (winit reports none there). +- **Landed (2026-10-12):** multi-edit: several vector layers in edit mode at once (Enter or + double-click with them selected, `vector.edit.begin {ids}`); points selected, marqueed, dragged, + nudged, deleted, mirrored and rounded across layers in one undo step; the pen, bend, paint bucket, + cut and eraser work in the layer last pressed (`vector_multi_behaviour_tests`). +- **Landed:** text on a path (`text.onPath`, `text.pathOptions`, `text.removeFromPath`; the + Design panel's Typography section; canvas, PNG, SVG and PDF as outlines; SVG `` imports + as text on a path), pattern fills (`PaintKind::Pattern`, `fill.pattern`, the colour picker's + Pattern tab; SVG ``, PDF tiling patterns), stretch and scatter brushes (`stroke.brush`, + eight presets drawn in code; canvas, PNG, SVG, PDF and outline stroke) and pen pressure for the + Pencil (Windows pens and browser styluses; recorded as the stroke's width profile, which brushes + follow). +- **Evidence:** winit reports no pressure on macOS and Linux tablets. +- **Impact:** illustration-flavoured work moved into Figma Draw in 2025. +- **Estimate:** 1–3 h once winit (or a platform tablet API behind it) reports pressure there. + +### 10. Team libraries: no team-wide lists or accounts; analytics are counts +- **Missing:** libraries listed for a whole team or organization (each file enables a library by + its link), accounts and per-person library permissions, analytics beyond instance counts per + component and file (insertions and detaches over time), update notices pushed by the server + (apps ask every 30 seconds while the Assets panel shows). +- **Evidence:** a file's library is published to `figcraft-server` under the file and enabled in + other files by a share link, with per-asset versions, updates with overrides kept, usage counts + and swapping a whole library by names ([`server.md`](server.md#team-libraries), + [`components-parity.md`](components-parity.md)). +- **Impact:** design-system teams. +- **Estimate:** 4–8 h, the accounts part after gap 2. + +### 11. Spell check +- **Missing:** spelling underline and suggestions while editing text. +- **Evidence:** no spell-check code in the repo. +- **Estimate:** 4–8 h with a permissively licensed dictionary and checker (provenance per + `ATTRIBUTION.md`). + +### 12. Video fills; HEIC images +- **Landed (2026-10-11):** animated GIF fills (the file kept as is, the first frame on the canvas + and in exports, played in the presenter and the preview) and TIFF images. +- **Missing:** Figma takes JPG, PNG, HEIC, WebP, GIF and TIFF images and MP4, MOV and WebM video + (help center, checked 2026-10-10). FigCraft decodes PNG, JPEG, WebP, GIF, TIFF and BMP; HEIC and + video need decoders that are C libraries (libheif, FFmpeg) or each platform's media framework. +- **Estimate:** 5–10 h (video playback in the presenter is most of it; it can reuse the GIF clock). + +### 13. Vector tail: outline-stroke compositing +- **Landed (2026-10-15):** Outline stroke keeps the renderer's even-odd stroke rings; regression pixels, Undo/Redo, native save/reopen and instance opacity inheritance/overrides in `outline_stroke`. +- **Missing:** preserving filled-shape opacity, blend and effects on the composite. A follow-up must preserve component inheritance and references when deciding which node owns these properties. +- **Landed (2026-10-12):** several Bezier handles selected with Shift-click and dragged together, + sharp points beside curved segments rounded, a stroke cap and join per point (canvas, PNG, SVG, + PDF, outline stroke), the Pen outside edit mode connecting to its own points and continuing a + selected open path. Left: corner smoothing beyond rectangles and frames, the transform box around + points of several layers, per-point caps and joins on dashed, variable-width and brush strokes. +- **Landed (2026-10-11):** boolean groups are clicked where their result is (a click in a + subtracted hole reaches what's behind; with the group or an operand selected its operands are + hit, so each can still be picked; "Select layer" lists the group only where its result is), live + boolean editing (operands selected by double-click or Cmd/Ctrl-click; + moving, resizing or editing an operand's points redraws the result during the drag, with every + operand outlined; per-operand operations from the boolean controls and `node.set`; a drag frame + recombines only the operands from the edited one up), Design panel Arc fields (Start, Sweep, + Ratio; Mixed across ellipses; bindable), the shape builder on strokes and open paths (strokes take + part as their outlined areas), one-sided variable widths (Alt-drag a width handle; canvas, PNG, + SVG, PDF and outline stroke). +- **Estimate:** compositing not yet re-estimated; vector QA with real files remains part of gap 3. + +### 14. Variables: extended collections on real libraries +- **Landed:** extended collections (`library.extendCollection`: a library collection's modes and + variables in a collection of this file, values overridden per mode with reset, following library + updates, variables of its own), variables import/export (`variable.export` / `variable.import`: + the REST local variables JSON, merged by name; CSS custom properties), gradient stops and variant + properties bound to variables. +- **Landed (2026-10-12):** variables a library adds to a collection this file extends are a library + update of their own (`library.updates` lists them by name, without an `id` until they have a + copy) and arrive inheriting when accepted; a library collection that is itself an extended + collection (a brand theme extending a core one) can be extended again, its values flowing down + the chain as each file accepts updates, every level's overrides kept. +- **Missing:** checked with synthetic files only; extending a collection of the same file isn't + offered (extending takes library collections). +- **Estimate:** 0–1 h. + +### 15. Prototyping: media triggers and actions, controllers off Windows +- **Landed (2026-10-12):** key/gamepad triggers take controller buttons (`Gamepad A` … `Gamepad + Home`, the standard layout's 17 buttons; a Controller menu beside the key field; REST files' + controller triggers import); the player reads controllers on Windows and in the browser (Gamepad + API) and takes `ui.input {gamepad}` on the control channel; widgets react to clicks in a + presented prototype, on the prototype's own copy; `proto.evaluate` computes expressions (mode + lookups included) where a layer's interaction would run (`crates/engine/src/proto_tail_tests.rs`, + `crates/ui-egui/src/present_tail_tests.rs`, `crates/format/src/rest/gamepad_tests.rs`). +- **Missing:** video triggers (on media hit/end) and play/pause/mute/seek actions (the REST + importer warns and skips them, `crates/format/src/rest/proto.rs`); controllers on macOS, Linux + and FreeBSD (the usual Rust controller library links libudev on Linux; a pure-Rust reader per + platform, or an owner decision on that dependency); PlayStation and Switch button labels. +- **Estimate:** 2–5 h, the media part after video fills (gap 12). + +### 16. Dev Mode: the tail after dev resources +- **Landed (2026-10-11):** code connect: a component or set links to its code component per + language (React, SwiftUI, Compose): an import path, the name and a template whose `{property}` + and `{property ? "a" : "b"}` placeholders take the instance's values, escaped per language and + never executed. `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet + instead of generated code (inside generated layer trees too); the panel's Connect to code edits + it with a live preview. Links are stored on the component (saved, synced, undoable) and travel + as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`, validated and capped) + so code owners can version them. Plugins whose manifest asks only to read run from the Inspect + tab. +- **Landed (2026-10-12):** dev resources: links for developers (a URL and a name) on any layer + (`devResource.add` / `edit` / `remove` / `list`, undoable, saved and synced). An instance also + lists its main component's links (and its set's) read-only with where they come from; layers + inside an instance show their main layer's. URLs are checked as web links (http or https, a + host, no spaces, capped) and never fetched: a link added without a name is named after its URL, + and a link opens only when clicked. The Inspect tab lists, adds, edits and removes them; + `get_node` / `inspect_document` return them as `devResources`. +- **Missing:** import lines for connected instances inside a + generated layer tree (only the selected layer's own import is shown); publishing mappings from + the code repository with a CLI. An editor extension is out of scope: the MCP server already + serves design data, code and the connected snippets to agents and editors. +- **Estimate:** 0–1 h. + +### 17. Branching and merging +- **Landed (2026-10-11):** branches of a file (`branch.*`): File ▸ Create branch… (or the Branches + list in version history) copies the file into a branch tab that remembers the file as it was (its + base) and makes its ids in its own id space; edits on the branch are saved with the main file. + Review and merge (the banner on a branch tab) shows the main file and the branch side by side or + as an overlay, what changed on each side since the base (layers with their property changes as + Inspect shows them; styles, variables, comments) and the conflicts: the same property changed on + both sides, or a layer deleted on one side while the other edited it or what's inside it. Each + conflict is resolved for the branch or main; nothing conflicting merges until it is. Merging is + one undo step on the main file and archives the branch; Update from main brings main's changes + into the branch the same way; branches can be archived without merging. Three-way merge over the + collaboration change sets (`figcraft_collab::merge`), fuzz-tested with random real commands. +- **Landed (2026-10-12):** a branch's own version history: versions saved on a branch tab (by hand + or autosaved) go into the branch, not the main file, and are viewed, restored (one undo step on + the branch), renamed, deleted and duplicated there; they are saved in the file with the branch. + Merging adds a version to the main file's history (Merged "name"); the merged or archived + branch keeps its own history and opens with it. Fills, strokes and effects merge item by item: + different items changed on each side, or items added at the end on one side, merge without a + conflict; only the same item changed both ways, an item removed on one side, or different items + added on both sides conflict (as the whole list). +- **Missing:** branches of a file in a live session are refused; other lists (text runs, export + settings, guides…) still conflict as a whole; the branch history has no entries of its own for + its creation or updates from main (only saved versions, autosaves and the merge on main). +- **Estimate:** 1–2 h. + +### 18. Display P3 on screen (several windows landed) +- **Landed (2026-10-11):** several windows: File ▸ New window, a tab's Open in new window or a tab + dragged off the strip; each window has its own tabs, panels and canvas and draws only when it + needs to; closing one asks about unsaved changes; `window.new`, `window.list`, `window.focus` and + `window` on any control request. +- **Missing:** wide-gamut colour on the canvas (egui draws sRGB, so P3 colours outside sRGB clip on + screen; exports keep them); a screen-reader tree in windows other than the main one (eframe 0.36 + gives only the main window one); window positions restored at the next start. +- **Estimate:** 4–8 h. + +### 19. AI features +- **Missing:** Figma AI (first draft, rename layers, remove background, image generation and + editing, visual search, Make). +- **Estimate:** 30–60 h with openly licensed local models. **Owner decision needed.** + +### 20. Mobile apps +- **Missing:** Figma's iOS and Android apps view and comment on files and mirror prototypes. + FigCraft's web build runs in mobile browsers but is not designed for touch. +- **Estimate:** 20–40 h for a touch-first viewer and mirror. + +### 21. Menu items without a command (measured): none left +- **Landed (2026-10-11):** `file.openRecent` (File ▸ Open recent: recent files kept in the + preferences, at most 20, greyed when the file is gone and dropped when picked, Clear recent), + `view.outlinesSelection` (outline mode for the selected layers only) and `view.maskOutlines`; + the plugin pair landed in #101. Every item in the menu catalog has a command + ([`parity-checklist.md`](parity-checklist.md)). +- **Estimate:** none left. + +### 22. Screen readers never run; browser has no accessibility tree +- **Evidence:** AccessKit names and keyboard navigation landed (#96), checked by tests only; + eframe's web backend doesn't send the tree yet. +- **Estimate:** 4–8 h plus human QA with VoiceOver, NVDA, Narrator and Orca. + +### 23. CMYK output intent for print PDFs +- **Evidence:** CMYK conversion is a documented naive formula; no CMYK ICC profile can ship in the + repository (licensing). +- **Estimate:** 2–4 h once an openly licensed profile is found. + +## Revision history + +| Date | Change | Summary | +|---|---|---| +| 2026-10-15 | minor | M4: Outline stroke preserves even-odd stroke holes. Pixel comparisons cover alignment, dashes, individual side weights, vector paths and stroke-only opacity; native save/reopen, Undo/Redo and instance opacity inheritance/overrides are covered by `outline_stroke`. Existing estimates unchanged; filled-shape compositing needs separate work. | +| 2026-10-14 | minor | Russian catalog (M9), reviewed and approved by VanTan: all 2,165 current entries, Русский in Preferences and regional ru tags. Russian integer plurals use one/few/many; contributor display name VanTan added. Existing estimates unchanged. | +| 2026-10-13 | minor | Gap 1: the experimental `.fig` export writes instance sizes (resized, filling an auto layout, scaled), variables with modes and bindings, shared styles and variable-width strokes, and the import reads them back. Checked by opening written files in other software: resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width; number, string and boolean bindings and bindings on effects, layout guides and gradient stops come across as the values they give but did not show as bound there (their entries now also name the layer field they set, not yet checked there), and strokes wider on one side of the path are written as filled outlines, no longer editable strokes there (the stroke itself is kept as plugin data and our own import puts it back; not yet checked there). Hours and the ranking unchanged | +| 2026-10-12 | minor | Gap 2: your online files landed (the app records the files it put on a server with their owner tokens, Home ▸ Online lists them and opens them again as their owner, with sharing, rename, removal and forget from the list; one request per server tells which are still there; tabs mark files in a live session). What is left is unchanged: accounts, teams and per-person permissions, shared comments and versions. Hours unchanged | +| 2026-10-12 | minor | Gap 2: sharing with a join code (Share ▸ Online or Local network, one Start sharing button, a short code to give others, one join field for a code or a link; the server names links with codes and limits wrong ones) added to the landed list; usability, so the estimate and ranking are unchanged | +| 2026-10-12 | minor | Gap 3: first measured fidelity data. Text box sizes and auto layout results of our example documents were compared with the same documents re-saved by other software; three text rules (whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only) were found and applied, and all 230 self-sizing text boxes in 7 documents now match to the pixel (one font family). Real files are still unchecked: hours and the ranking unchanged | +| 2026-10-12 | minor | Gap 2: live sessions are per document (each open file has its own session or none, several run at once, the window and the `collab.*` commands follow the file in front, closing a file ends its session only) added to the landed list; a fix, so the estimate and ranking are unchanged | +| 2026-10-12 | minor | Gap 1: the experimental `.fig` export now opens elsewhere: written files were checked by opening them in other software: layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft; effects such as glass and noise, images and prototype interactions are still unchecked there, and variables are not written. Hours and the ranking unchanged | +| 2026-10-12 | minor | Gap 1: an experimental `.fig` export landed (the whole document, from the same public format notes as the import; checked only by round trips through our own importer); export hours 15–30 h → 5–15 h (checks against real files); gap 1's read tail (5–10 h) and the ranking unchanged | +| 2026-10-12 | minor | Gap 4: keyboard shortcut parity suite per platform (129 documented rows: 126 bound, 1 moved, 2 missing; 48 undocumented bindings pinned) added to the landed list; estimate and ranking unchanged | +| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Gap 5 31–54 h → 29–51 h, rank unchanged | +| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Gap 5 32–55 h → 31–54 h, rank unchanged | +| 2026-10-12 | minor | Gap 15: controller buttons on key/gamepad triggers (Windows and the browser), widgets reacting in prototypes and `proto.evaluate` landed; gap 15 now media plus controllers off Windows, 3–6 h → 2–5 h; ranking unchanged | +| 2026-10-12 | minor | Plugin directory on `figcraft-server` (publish with the server key, browse, install after a permissions confirmation, hash-checked downloads, updates by version, operator moderation) added to gap 6's landed list; gap 6 10–30 h → 8–26 h; ranking unchanged | +| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Gap 5 40–65 h → 32–55 h, rank unchanged | +| 2026-10-12 | minor | Plugin widgets (API 3) and the panel tail (panels side by side, colour picker on plugin fields) added to gap 6's landed list; gap 6 now 10–30 h; ranking unchanged | +| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): every UI string, command label and menu wording (1,899 entries) machine-drafted in each and marked as drafts awaiting native review, falling back to English per key (new strings need only English and Spanish); 0 counts as singular in French and Brazilian Portuguese; System picks the first OS language with a catalog (pt-PT and pt read pt-BR); long translations elide with a tooltip in dropdowns, two-column labels and the home cards. Gap 5 45–75 h → 40–65 h, rank unchanged | +| 2026-10-12 | minor | Cursor chat and spotlight in live sessions (gap 2): `/` opens a chat bubble at your pointer, a spotlight makes everyone follow your view; ephemeral presence the host validates and rate-limits. Gap 2 45–90 h → 42–84 h | +| 2026-10-12 | minor | Layers and Assets panels behaviour suite and fixes added to gap 4's landed list; ranking and hours unchanged | +| 2026-10-12 | minor | Vector multi-edit and the vector edit tail, with a behaviour suite written from the public documentation (5 tests, about 30 table rows and steps, and a junk-parameter sweep of every vector command in multi-edit mode): several vector layers are edited at once (Enter, double-click or `vector.edit.begin {ids}`; a press on another layer's point, handle or segment makes it the active layer; Shift-click, marquee and lasso select across layers; dragging, the arrows, Delete, delete and heal, mirroring, smoothing, corner radius and point caps and joins reach every layer's selected points in one undo step; the pen, bend, paint bucket, cut and eraser work in the active layer); several Bezier handles are selected with Shift-click and dragged together (`vector.edit.moveHandles`); a sharp point next to a curved segment rounds (the curve is cut where the arc meets it and keeps its shape); a point can have its own stroke cap and join (`vector.edit.strokeCap`, `vector.edit.strokeJoin`, the Point section of the Design panel; canvas, PNG, SVG and PDF, outline stroke); the Pen outside edit mode connects to one of its own points and ends the path, and continues a selected open path from an end point. Left: corner smoothing beyond rectangles, the transform box around points of several layers, per-point caps and joins on dashed, variable-width and brush strokes. Gap 9 2–5 h → 1–3 h (pen pressure on macOS and Linux tablets left, blocked on winit). Shapes & vector 98%/72% → 99%/73%, 2–4 h → 1–3 h; Figma Draw extras 80%/55% → 88%/60%, 2–5 h → 1–3 h (gap 9: pen pressure on macOS and Linux tablets left). Totals recomputed by the doc's method, which rows since the last recompute had left stale: the area table's weighted breadth is 83.7% (~83%; core 94.1%, ~94%) and ready 63.1% (~63%; core 72.1%, ~72%), hours 133–284 (core 38–83); Features (depth) 61% → 63%, 46–94 h → 34–76 h; ready for real work 52.6% (~52%); mainstream weekly areas 71.0% → 72.1% (× 0.9 × 0.9 × 0.85 = 49.7%, ~49%, rounded down); essentials unchanged (~57%); full parity ~306–595 h → ~294–572 h (on top of the rows merged since: ~301–587 h → ~289–564 h; area-table totals recomputed on the merged table and rounded down: breadth ~84%, ready ~63%, core ~94%/~72%, mainstream weekly areas 72.4%, ~49%) | +| 2026-10-12 | minor | Dev resources (gap 16): links for developers (a URL and a name) on any layer: `devResource.add` / `edit` / `remove` / `list`, undoable, saved and synced; an instance also lists its main component's (and that component's set's) links read-only with where they come from, and layers inside an instance show their main layer's; URLs are checked as web links (http or https, a host, no spaces, at most 2,048 characters) and never fetched (a link added without a name is named after its URL), opened only when clicked; the Inspect tab lists, adds, edits and removes them; `get_node` and `inspect_document` return them as `devResources`. The editor extension stays out of scope (the MCP server serves agents and editors). Dev Mode breadth 93% → 96%, ready 70% → 72%, 1–3 h → 0–1 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~304–593 h → ~303–591 h | +| 2026-10-12 | minor | Extended collections' tail (gap 14): variables a library adds to a collection this file extends now count as a library update of their own (listed by name without a copy yet) and arrive inheriting when accepted, instead of waiting for the library's next other update; a library collection that is itself an extended collection can be extended again, values flowing down the chain as each file accepts updates with every level's overrides kept (checked end to end). Styles & variables breadth 94% → 95%, ready unchanged (76%), 3–5 h → 2–4 h; gap 14 1–2 h → 0–1 h (real-library checks left); feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~305–594 h → ~304–593 h | +| 2026-10-12 | minor | A branch's own version history and finer list merges (gap 17): versions saved or autosaved on a branch tab go into the branch (viewed, restored, renamed, deleted and duplicated there, saved in the file with it); merging adds a version to the main file's history and the merged or archived branch keeps its own; fills, strokes and effects merge item by item, conflicting only when the same item changed both ways, an item was removed on one side or both sides added different items. Comments, versions, find, misc breadth 85% → 87%, ready 60% → 61%, 2–5 h → 1–4 h; gap 17 1–3 h → 1–2 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~306–595 h → ~305–594 h | +| 2026-10-12 | minor | Comments and version history behaviour suite and fixes added to gap 4's landed list (gap 17 unchanged: a branch's own version history is still left); ranking and hours unchanged | +| 2026-10-11 | minor | `.fig` import's tail (gap 1): vectors open as editable vector networks (points, curved segments, filled areas with their winding, per-point corner radius and handle mirroring, end caps from the end points), falling back to outlines when the network is missing or damaged; sticky notes, shapes with text, connectors, tables, code blocks, slides and similar layers import as the closest frames, text layers and vectors with a warning instead of being dropped; set-variable, set-variable-mode and conditional prototype actions come across. New parsers fuzzed on fixtures generated in code. File formats 55% → 58%, 8–15 h → 5–10 h; import/export breadth 80% → 82%, ready 50% → 54%; feature and area totals and ready for real work (~49%) unchanged; beta ~173–310 h → ~170–305 h; full parity ~321–618 h → ~318–613 h | +| 2026-10-11 | minor | `.fig` import, best effort (gap 1): `.fig` files open (best effort): the zip, its deflate/zstd chunks and the kiwi schema are decoded generically and mapped by field name onto the REST importer, so pages, the layer tree, geometry, paints (styles resolved), strokes, effects, text with styled runs, vectors (as outlines), auto layout (grid included), constraints, components, sets, instances (expanded from their main with overrides, derived layout and property values), styles, variables with modes and bindings, prototyping and image fills from the archive come across; anything else is a warning. Gap 1 30–60 h → 8–15 h (vector networks, other layer kinds, real-file checks; export 15–30 h on top, as before) | +| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | +| 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | +| 2026-10-11 | minor | Localization layer landed (gap 5): per-language catalogs baked in at build time with English text as keys and fallback, placeholders and plurals, command labels by id, Preferences ▸ Language (System follows the OS or the browser) applied live and `pref.language`; English catalog and a complete Spanish draft awaiting native review. Localization 10% → 20%, 60–100 h → 45–75 h; ready for real work unchanged (~47%); full parity ~363–697 h → ~348–672 h | +| 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | +| 2026-10-11 | minor | Plugin panels and settings landed (gap 6, ABI v2; v1 plugins unchanged): a plugin shows a panel of notes, fields (text, number, checkbox, menu, list, colour) and buttons drawn with FigCraft's widgets; a button or a changed value calls it back with every value (same limits per callback, one undo step per callback that edits, rolled back on failure), its module staying loaded while the panel is open; `plugin.ui` / `plugin.event` / `plugin.close` drive panels headless; per-plugin settings (64 KB) kept with the preferences; the Swatch grid example gained a palette panel that remembers the last palette. Plugins & widgets breadth 35% → 45%, ready 20% → 28%, 25–55 h → 15–40 h; ecosystem 20% → 28%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~373–712 h → ~363–697 h | +| 2026-10-11 | minor | Canvas fixes (gap 13 closed): boolean groups are clicked where their result is (a click in a subtracted hole reaches what's behind; with the group or an operand selected its operands are hit, so each can still be picked), a layer dragged inside a group stays in the group (frames and auto layout still take other drops), View ▸ Frame outlines and View ▸ Slices draw on the canvas, and the idle tests wait for the render thread instead of counting frames. Shapes & vector 1–3 h → 1–2 h (vector QA with real files left); percentages, feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~378–722 h → ~375–717 h | +| 2026-10-11 | minor | Stretch and scatter brushes and pen pressure landed (gap 9): eight brushes drawn in code (four stretched along the path, four scattered with spacing, size, rotation and scatter jitter from a seed) in the stroke settings, drawn everywhere strokes are and flattened by outline stroke; the Pencil records pen pressure as the stroke's width profile (Windows pens, browser styluses; macOS and Linux tablets report none) and can draw with a brush; Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–9 h; Strokes 2–4 h → 1–3 h; Hardware 50% → 55%, 15–30 h → 13–27 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~378–722 h → ~373–712 h | +| 2026-10-11 | minor | Text on a path and pattern fills landed (gap 9): text follows a path or shape (start, side, alignment; edited in place; canvas, PNG, SVG and PDF; SVG `` imports as text on a path) and pattern fills tile a layer (grid or half-shifted rows or columns, scale, spacing, alignment; live as the source changes; SVG ``, PDF tiling patterns); Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–10 h; Text 8–14 h → 7–13 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~375–715 h | +| 2026-10-11 | minor | Team libraries on `figcraft-server` landed (gap 10): a file's library is published to the server under the file and enabled in other files by a share link (per-asset versions; updates noticed in the background and accepted with overrides kept), the server counts which files use which components, and a whole library can be swapped for another by names; Team libraries breadth 40% → 70%, ready 25% → 40%, 10–20 h → 4–8 h; collaboration 12% → 13%, 55–110 h → 49–98 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~380–725 h → ~375–715 h | +| 2026-10-11 | minor | Branches landed (gap 17): File ▸ Create branch… copies the file into a branch tab that remembers its base; Review and merge shows both sides' changes (layers with Inspect-style property changes, styles, variables, comments) and the conflicts (the same property changed on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merging is one undo step and archives the branch; Update from main; branches are saved in the `.figcraft` file. Comments, versions, find, misc breadth 80% → 85%, ready 58% → 60%, 4–8 h → 2–5 h; collaboration 12% → 13%; menu breadth 204/204 (the catalog gained File ▸ Create branch…); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~378–722 h | +| 2026-10-11 | minor | Animated GIF fills and TIFF images (gap 12, without video and HEIC): GIF fills keep the original file, show the first frame on the canvas and in exports and play in the presenter and the preview (frames decoded on first play, at most 500 frames and 128 MB; always looping; the window wakes only for a GIF's next frame); TIFF images open, import, place and paste. Paint breadth 85% → 87%, 5–9 h → 4–7 h; gap 12 6–12 h → 5–10 h (video playback and HEIC left, both needing non-Rust decoders); feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | The last three catalogued menu items got commands (gap 21): File ▸ Open recent (recent files kept in the preferences, at most 20, a file that's gone greyed in the menu and dropped when picked, Clear recent), View ▸ Outlines ▸ Show outlines for selection and View ▸ Mask outlines: menu breadth 98.5% → 100% (203/203); Canvas & navigation breadth 95% → 97%, 3–6 h → 2–5 h; feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | Extended collections, variables import/export and gradient stop and variant property bindings landed (gap 14): Styles & variables breadth 85% → 92%, ready 70% → 72%, 4–8 h → 1–2 h; feature breadth unchanged (~79%); ready for real work unchanged (~47%); full parity ~385–730 h → ~380–725 h | +| 2026-10-11 | minor | Collaboration wording after the standalone server: `figcraft-server` keeps many files, persisted as a snapshot plus a log of what changed (compacted, checksummed, damaged files skipped and reported), restarts lose nothing confirmed and apps resume where they were; the app publishes its file to it; share links per file let people edit or only view (the server refuses a viewer's edits), are revocable and stored as hashes; TLS comes from a reverse proxy (the desktop app has none of its own yet); percentages and hours unchanged until the next re-measure | +| 2026-10-11 | minor | Live boolean editing, Design panel arc fields, the shape builder on strokes and open paths and one-sided variable widths landed (gap 13): Shapes & vector breadth 92% → 97%, ready 66% → 68%, 8–14 h → 1–3 h; feature breadth ~78% → ~79%; ready for real work unchanged (~47%); full parity ~390–745 h → ~385–730 h | +| 2026-10-11 | minor | Collaboration wording after the collaboration polish: styles, variables, collections, modes and comments created in a live session take ids from their creator's id space (concurrent creations no longer overwrite each other; the server checks it), others' text carets and selections show on the canvas, and typing in a layer someone else is typing in says so; percentages unchanged | +| 2026-10-11 | minor | Wording only: live sessions (#104), then the browser joining them and `figcraft://` invite links opening the desktop app; percentages and hours unchanged until the next re-measure | +| 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 | minor | Marked beta blockers; no alpha blockers (the alpha gate passes) | +| 2026-10-10 | major | Created from the 2026-10-10 re-measure against Figma desktop 126.10.7 (replaces the gap list that lived in ROADMAP.md) | diff --git a/docs/roadmap.md b/docs/roadmap.md index 336eb1ad..2ac93002 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,125 +1,125 @@ -# FigCraft roadmap: milestones and what's next - -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (Outline stroke preserves stroke holes; recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading, SVG import budgets, Leptos code, full screen, server snapshots, owner-only preferences, ruzstd 0.9.1, layer cycle repair, batch rollback, Direct3D 12 on Windows, plugin metering; vector edit mode regions: hover hatch, selection and drag; repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; `.fig` import, best effort; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%) · **Target:** Figma Design (desktop app 126.10.7) - -Forward-looking plan. Where FigCraft stands today: [`ROADMAP.md`](../ROADMAP.md) and -[`target-app-parity.md`](target-app-parity.md); the full ranked list: [`gaps.md`](gaps.md). Hours -are one Opus 5.5 agent; most milestones split across 4–6 agents on disjoint crates. There is no -`cargo xtask scorecard` in this repo; the measured number is `cargo xtask parity`. - -## Current focus - -1. **`.fig` import's tail**: editable vector networks, the skipped layer kinds that matter and - fixes from real files (the best-effort import landed 2026-10-11): 8–15 h. -2. **Real-file corpus and fidelity measurement**: import real files, compare against Figma's own - exports, turn estimated percentages into measured ones: 15–30 h. -3. **Tool-by-tool interaction pass** against the running Figma ([`ui-parity.md`](ui-parity.md)): - 40–60 h, parallel by tool. -4. **Languages** on the landed localization layer: native reviews of the es, fr, de, pt-BR, ja, - ko and zh-Hans drafts and the es-419 overlay (people), then the other key languages: 3–7 h per language - plus native review. -5. **Multiplayer M9**: accounts and teams on top of the live sessions and the standalone server (the WebSocket transport, hosting from the app, invite links that open the app, document sync, live cursors, selections, text carets, avatars and follow, per-user undo, reconnect with offline edits, collision-free ids for styles, variables, collections and comments, joining from the browser, and `figcraft-server` with persistence, publishing from the app and share links to edit or view landed), per [`collab-protocol.md`](collab-protocol.md) and [`server.md`](server.md): 45–90 h. - -## Alpha gate - -The core workflows a Figma Design user runs every day, checked end to end on macOS (the main -platform) with work saved to `.figcraft` and reopened. Evidence is the repo's tests and the -builders' side-by-side checks with synthetic files; nothing has been run on real Figma files yet. - -| Workflow | Works end to end? | Evidence | Hours to pass | -|---|---|---|---| -| Design screens: frames, shapes, vectors, text, auto layout, styles; save, reopen, export assets | yes | tools and panels in `crates/engine` and `crates/ui-egui`; `.figcraft` round trips (`crates/format/src/large_tests.rs`); PNG/JPG/SVG/PDF export tests | — | -| Build and use a design system: components, variants, properties, variables and modes, libraries | yes | `components_scale_tests.rs` (300 components, 2,000 instances), `library_tests.rs`, `library_variables_tests.rs`; libraries are local files, not shared | — | -| Prototype and present: interactions, flows, overlays, smart animate, variables | yes | `proto_*_tests.rs`, `present_*_tests.rs` | — | -| Hand off to developers: Dev Mode inspect, code, measurements, asset export, ready for dev | yes | `devinspect_tests.rs`, `component_codegen_tests.rs`, `devcompare_tests.rs`, `annotations_tests.rs` | — | -| Review with others: comments, versions, sharing the work | partial (not blocking) | comments with threads, mentions and resolve plus version history work inside a file (`comments_tests.rs`, `version_tests.rs`); the file is shared by passing it along; live sessions (preview: hosted from the desktop app or a self-hosted `figcraft-server` with edit and view links, joined from the app or the browser, undo per person); no accounts or teams ([collaboration-parity](collaboration-parity.md)) | beta gap: 45–90 | -| Continue existing Figma work (open `.fig`) | partial (beta gap) | best-effort `.fig` import (`crates/format/src/fig`, tested on generated fixtures, not yet on real files); REST JSON import works for files fetched through Figma's API ([file-format-parity](file-format-parity.md)) | beta gap: 8–15 | - -**Passes, so FigCraft stays alpha:** every daily design workflow works end to end on FigCraft's -own files, which save and reopen, and ready for real work (~47%) is above the ~40% bar. The two -gaps are real but don't break a workflow done in FigCraft: review works asynchronously through -comments in a shared file, and opening `.fig` is the step that moves work across from Figma, which -the standard requires for beta, not alpha. Both stay at the top of [`gaps.md`](gaps.md) as beta -blockers. - -## Milestones - -| Milestone | Status | Left | Hours | -|---|---|---|---| -| M0 Foundation: workspace, model, renderer, engine, UI shell, app, control channel | done (2026-10-09) | — | — | -| M0.9 CLI + MCP, web build, release packaging, PDF export | done (v0.2.0 released 2026-10-10 for every platform) | — | — | -| M1 Core editing completeness (tools, panels, menus, clipboard, files) | ~77% | interaction pass (Open recent, the outline mode variants and keyboard box selection landed 2026-10-11; repeat commands preserve the underlying edit and undo steps, #181; the Layout panel's constrain-proportions link scales the other side, #220) | 6–12 + pass | -| M2 Layout depth | ~80% | grid `fr` rows, real-file QA | 6–12 | -| M3 Components & styles depth | ~78% (extended collections, variables import/export, gradient stop and variant bindings landed 2026-10-11) | real libraries, component playground | 7–14 | -| M4 Vector & text depth | ~80% (live booleans, arc fields, shape builder on strokes, boolean hit testing on the result, one-sided widths, text on a path, pattern fills, brushes and pen pressure landed 2026-10-11; vector edit mode regions 2026-10-13; stroke holes preserved 2026-10-15) | outline-stroke compositing, spell check, Figma Draw multi-edit | 7–18 | -| M5 Prototype & present | ~85% | media, controllers on macOS/Linux/FreeBSD, separate window | 2–5 | -| M6 Interop & Dev Mode | ~65% (code connect, Dev Mode plugins and best-effort `.fig` import landed 2026-10-11) | `.fig` import's tail, video/HEIC (animated GIFs and TIFF landed 2026-10-11), CMYK intent | 15–30 | -| M7 Release & fidelity (signed builds, look & feel, shortcuts) | builds done; fidelity pass started | tool-by-tool pass | 40–60 | -| M8 Hardening & performance (bench, retained canvas, accessibility, idle) | ~60% | real-file corpus, platform QA, GPU canvas, screen readers | 60–110 | -| M9 Real-time collaboration | core done (#103: ids, change sets, server ordering, convergence); live sessions (WebSocket transport with limits and reconnect, hosting from the app, invite links, document sync between apps, cursors, selections, avatars, follow, `collab.*` commands); per-user undo, rejection handling, offline queue and resuming after a reconnect; styles, variables, collections, modes and comments created in a session get ids from their creator's space; others' text carets and selections on the canvas; the browser client (joins desktop-hosted sessions); `figcraft://` links open the app and join (Windows registers per user; macOS and Linux packages declare the scheme); the standalone `figcraft-server` (persistence with compaction, restarts that lose nothing confirmed, publishing from the app, share links to edit or view, revocable); team libraries kept on the server and enabled by a share link, with usage counts, and swapping a library; `wss://` from the desktop app, server releases and a deployment guide | accounts, teams | 45–90 | -| M10 Localization (new) | not started | layer + 11 languages | 60–100 | -| M11 Plugins | runtime, API, menu, manager and SDK done (#101); plugin panels and settings (ABI v2); widgets (API 3), panels side by side, colour picker on plugin fields; a plugin directory on `figcraft-server` (publish, browse, install, update) | a public catalogue, publisher accounts and signatures, the directory in the browser, richer widget events; Figma plugin compatibility is an owner decision | 8–26 | -| M12 AI features (new, owner decision) | not started | local models | 30–60 | - -## Next, after the current focus - -- Team libraries for a whole team (lists, accounts and permissions) and deeper library analytics (4–8 h, after - accounts), branching (8–15 h). -- GPU canvas held to the CPU renderer (30–60 h). -- Figma Draw multi-edit and pen pressure on macOS/Linux tablets (2–5 h). -- Mobile viewer and prototype mirror (20–40 h). - -## Revision history - -| Date | Change | Summary | -|---|---|---| -| 2026-10-15 | minor | M4: Outline stroke preserves even-odd stroke holes. Pixel comparisons cover alignment, dashes, individual side weights, vector paths and stroke-only opacity; native save/reopen, Undo/Redo and instance opacity inheritance/overrides are covered by `outline_stroke`. Existing estimates unchanged; filled-shape compositing needs separate work. | -| 2026-10-14 | minor | M7: the Small caps button in Type settings showed a box where the UI font has no small-capital `ɢ` (builds without Inter use egui's fonts); its sample is now a capital and a smaller capital, drawn from ordinary letters every UI font has. Numbers unchanged | -| 2026-10-14 | minor | M7: Home screen recent files as cards: each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged. | -| 2026-10-14 | minor | Lost images reported on loading (#207): a `.figcraft` file whose image member is damaged, too large or missing no longer loads in silence; each lost image (up to 20, then a count) is listed in `repairs` on `file.open` with its hash and the reason, and the app shows it as a toast. Saving still writes the file without the lost images. Numbers unchanged | -| 2026-10-14 | minor | SVG import budgets (#190): gzipped SVG is unpacked by FigCraft within 64 MiB before parsing (it was unpacked without a limit), so the nesting check sees the real document; SVG documents embedded in `` data URIs share one 32 MiB budget and at most 4 levels, and one past a budget is left out. Linked files stay unread. Numbers unchanged | -| 2026-10-14 | minor | Leptos code (#223): `inspect.code`, `inspect.componentCode` and `codeConnect.set` take `language: leptos`: a `#[component]` with `#[prop]` defaults, variant enums and `view!` markup, instances as calls, Code Connect `use` lines; the Dev Mode language list and rem units include it. Numbers unchanged | -| 2026-10-14 | minor | Full screen (#227): View ▸ Full screen (`view.fullscreen {on?}`, F11 on Windows and Linux, Ctrl+Cmd+F on macOS) makes the window fill the screen and leaves it again; FigCraft's own item. Numbers unchanged | -| 2026-10-14 | minor | Colour picker: wrapped rows of page and recent swatches stay inset on both sides instead of running past the picker's right edge. Numbers unchanged | -| 2026-10-14 | minor | Server snapshots (#188): figcraft-server never replaces a readable snapshot with one too large to be read back, and reads snapshots up to twice the file cap (recent batches kept for reconnecting clients may take a file over it). Numbers unchanged | -| 2026-10-14 | minor | Preferences owner-only on Unix (#200): `state.json`, which holds online owner tokens, is written with mode 0600 from the first byte, and an older world-readable file is narrowed. Numbers unchanged | -| 2026-10-14 | minor | ruzstd 0.9.1 (#182): `.fig` import requires the release that fixes RUSTSEC-2026-0344 (a zstd decompression bomb). Numbers unchanged | -| 2026-10-14 | minor | Layer cycles (#205): `validate` reports children listed twice, pages with a parent and layers not reachable from a page (cycles, detached subtrees); `repair` drops them in one linear pass. Numbers unchanged | -| 2026-10-14 | minor | AppImage CLI: `figcraft.AppImage cli ` runs the bundled `figcraft-cli` (MCP server included) on Linux without a second download. Numbers unchanged | -| 2026-10-14 | minor | Boolean outline depth (#193): nested boolean groups share one outline depth budget instead of each level getting its own. Numbers unchanged | -| 2026-10-14 | minor | Command batches roll back as one: a failed `command.batch` (MCP `batch`) restores every open document and its state, and puts the tab in front back. Numbers unchanged | -| 2026-10-14 | minor | Desktop builds no longer rebuild the text crate on every build when the craft-fonts manifest is missing. Numbers unchanged | -| 2026-10-14 | minor | Direct3D 12 on Windows: the desktop app asks wgpu for Direct3D 12 there instead of Vulkan, so it starts without Vulkan loader errors on machines with stray overlay layers. Numbers unchanged | -| 2026-10-14 | minor | Plugin metering (#192): every host call and every byte copied between a plugin and the host costs fuel, so a plugin can't stall the app through cheap-looking host calls. Numbers unchanged | -| 2026-10-13 | minor | M4: Vector edit mode regions: the filled area under the pointer is hatched (clear of points and segments, holes left out, octave-folded spacing fixed to the shape), and a click, Shift-click or drag selects and moves areas with their points (`vector_regions_tests`, `regions_tests`). Estimates unchanged. | -| 2026-10-13 | minor | M1, #220: The Layout panel's constrain-proportions link keeps the ratio on a W or H edit; the property edit dropped it, so the link only worked while dragging resize handles. Both sides in one edit stay as typed, the `w`/`h` aliases work the same, each layer of a multi-selection keeps its own ratio, and a zero side has no ratio to keep (transform behaviour suite). Estimates unchanged. | -| 2026-10-13 | minor | M1, #181: Repeat last command keeps the underlying command and parameters instead of recording itself; a stored self-reference is refused. Three cumulative repeats and their individual undo steps are covered by regression tests. Estimates unchanged. | -| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Focus item 4 adds the zh-Hans draft's review | -| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Focus item 4 no longer lists Latin American Spanish | -| 2026-10-12 | minor | M5: prototyping tail: controller buttons on key/gamepad triggers (Windows and the browser), widgets reacting in prototypes, `proto.evaluate`; M5's tail 3–6 h → 2–5 h | -| 2026-10-12 | minor | M11: a plugin directory on `figcraft-server` (bundles published with the server key and checked in full, browsed and installed from Plugins ▸ Manage plugins after a permissions confirmation, hash-checked downloads, updates by version, `plugin.directory.*`, `figcraft-cli plugin publish`); 10–30 h → 8–26 h | -| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Current focus 4 now lists the drafts' native reviews | -| 2026-10-12 | minor | M11: plugin widgets (API 3: plugin-drawn layers on the canvas, redrawn on clicks from state kept in the document), panels of several plugins side by side and the colour picker on plugin colour fields; 15–40 h → 10–30 h | -| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): every UI string, command label and menu wording (1,899 entries) machine-drafted in each and marked as drafts awaiting native review, falling back to English per key (new strings need only English and Spanish); 0 counts as singular in French and Brazilian Portuguese; System picks the first OS language with a catalog (pt-PT and pt read pt-BR); long translations elide with a tooltip in dropdowns, two-column labels and the home cards. Current focus 4 now lists Japanese and Korean | -| 2026-10-11 | minor | `.fig` import, best effort: current focus 1 and the alpha gate's `.fig` row move to the import's tail (30–60 h → 8–15 h); M6 ~55% → ~65%, 37–75 h → 15–30 h | -| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | -| 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | -| 2026-10-11 | minor | Localization layer landed (gap 5): per-language catalogs baked in at build time with English text as keys and fallback, placeholders and plurals, command labels by id, Preferences ▸ Language (System follows the OS or the browser) applied live and `pref.language`; English catalog and a complete Spanish draft awaiting native review. Localization 10% → 20%, 60–100 h → 45–75 h; ready for real work unchanged (~47%); full parity ~363–697 h → ~348–672 h; beta ~200–365 h → ~195–355 h | -| 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | -| 2026-10-11 | minor | Plugin panels and settings landed (gap 6, ABI v2; v1 plugins unchanged): a plugin shows a panel of notes, fields (text, number, checkbox, menu, list, colour) and buttons drawn with FigCraft's widgets; a button or a changed value calls it back with every value (same limits per callback, one undo step per callback that edits, rolled back on failure), its module staying loaded while the panel is open; `plugin.ui` / `plugin.event` / `plugin.close` drive panels headless; per-plugin settings (64 KB) kept with the preferences; the Swatch grid example gained a palette panel that remembers the last palette. Plugins & widgets breadth 35% → 45%, ready 20% → 28%, 25–55 h → 15–40 h; ecosystem 20% → 28%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~373–712 h → ~363–697 h | -| 2026-10-11 | minor | Canvas fixes (gap 13 closed): boolean groups are clicked where their result is (a click in a subtracted hole reaches what's behind; with the group or an operand selected its operands are hit, so each can still be picked), a layer dragged inside a group stays in the group (frames and auto layout still take other drops), View ▸ Frame outlines and View ▸ Slices draw on the canvas, and the idle tests wait for the render thread instead of counting frames. Shapes & vector 1–3 h → 1–2 h (vector QA with real files left); percentages, feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~378–722 h → ~375–717 h | -| 2026-10-11 | minor | Stretch and scatter brushes and pen pressure landed (gap 9): eight brushes drawn in code (four stretched along the path, four scattered with spacing, size, rotation and scatter jitter from a seed) in the stroke settings, drawn everywhere strokes are and flattened by outline stroke; the Pencil records pen pressure as the stroke's width profile (Windows pens, browser styluses; macOS and Linux tablets report none) and can draw with a brush; Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–9 h; Strokes 2–4 h → 1–3 h; Hardware 50% → 55%, 15–30 h → 13–27 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~378–722 h → ~373–712 h | -| 2026-10-11 | minor | Text on a path and pattern fills landed (gap 9): text follows a path or shape (start, side, alignment; edited in place; canvas, PNG, SVG and PDF; SVG `` imports as text on a path) and pattern fills tile a layer (grid or half-shifted rows or columns, scale, spacing, alignment; live as the source changes; SVG ``, PDF tiling patterns); Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–10 h; Text 8–14 h → 7–13 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~375–715 h | -| 2026-10-11 | minor | Team libraries on `figcraft-server` landed (gap 10): a file's library is published to the server under the file and enabled in other files by a share link (per-asset versions; updates noticed in the background and accepted with overrides kept), the server counts which files use which components, and a whole library can be swapped for another by names; Team libraries breadth 40% → 70%, ready 25% → 40%, 10–20 h → 4–8 h; collaboration 12% → 13%, 55–110 h → 49–98 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~380–725 h → ~375–715 h | -| 2026-10-11 | minor | Branches landed (gap 17): File ▸ Create branch… copies the file into a branch tab that remembers its base; Review and merge shows both sides' changes (layers with Inspect-style property changes, styles, variables, comments) and the conflicts (the same property changed on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merging is one undo step and archives the branch; Update from main; branches are saved in the `.figcraft` file. Comments, versions, find, misc breadth 80% → 85%, ready 58% → 60%, 4–8 h → 2–5 h; collaboration 12% → 13%; menu breadth 204/204 (the catalog gained File ▸ Create branch…); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~378–722 h | -| 2026-10-11 | minor | Animated GIF fills and TIFF images (gap 12, without video and HEIC): GIF fills keep the original file, show the first frame on the canvas and in exports and play in the presenter and the preview (frames decoded on first play, at most 500 frames and 128 MB; always looping; the window wakes only for a GIF's next frame); TIFF images open, import, place and paste. Paint breadth 85% → 87%, 5–9 h → 4–7 h; gap 12 6–12 h → 5–10 h (video playback and HEIC left, both needing non-Rust decoders); feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | The last three catalogued menu items got commands (gap 21): File ▸ Open recent (recent files kept in the preferences, at most 20, a file that's gone greyed in the menu and dropped when picked, Clear recent), View ▸ Outlines ▸ Show outlines for selection and View ▸ Mask outlines: menu breadth 98.5% → 100% (203/203); Canvas & navigation breadth 95% → 97%, 3–6 h → 2–5 h; feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | Extended collections, variables import/export and gradient stop and variant property bindings landed (gap 14): Styles & variables breadth 85% → 92%, ready 70% → 72%, 4–8 h → 1–2 h; feature breadth unchanged (~79%); ready for real work unchanged (~47%); full parity ~385–730 h → ~380–725 h | -| 2026-10-11 | minor | Collaboration wording after the standalone server: `figcraft-server` keeps many files, persisted as a snapshot plus a log of what changed (compacted, checksummed, damaged files skipped and reported), restarts lose nothing confirmed and apps resume where they were; the app publishes its file to it; share links per file let people edit or only view (the server refuses a viewer's edits), are revocable and stored as hashes; TLS comes from a reverse proxy (the desktop app has none of its own yet); percentages and hours unchanged until the next re-measure | -| 2026-10-11 | minor | Live boolean editing, Design panel arc fields, the shape builder on strokes and open paths and one-sided variable widths landed (gap 13): Shapes & vector breadth 92% → 97%, ready 66% → 68%, 8–14 h → 1–3 h; feature breadth ~78% → ~79%; ready for real work unchanged (~47%); full parity ~390–745 h → ~385–730 h | -| 2026-10-11 | minor | Collaboration wording after the collaboration polish: styles, variables, collections, modes and comments created in a live session take ids from their creator's id space (concurrent creations no longer overwrite each other; the server checks it), others' text carets and selections show on the canvas, and typing in a layer someone else is typing in says so; percentages unchanged | -| 2026-10-11 | minor | Wording only: live sessions (#104), then the browser joining them and `figcraft://` invite links opening the desktop app; percentages and hours unchanged until the next re-measure | -| 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 | minor | Added the alpha gate (core-workflow table): passes, stage stays alpha | -| 2026-10-10 | major | Created: milestone table moved from ROADMAP.md with statuses re-checked; added M10–M12; current focus re-planned | +# FigCraft roadmap: milestones and what's next + +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (Outline stroke preserves stroke holes; recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading, SVG import budgets, Leptos code, full screen, server snapshots, owner-only preferences, ruzstd 0.9.1, layer cycle repair, batch rollback, Direct3D 12 on Windows, plugin metering; vector edit mode regions: hover hatch, selection and drag; repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; `.fig` import, best effort; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%) · **Target:** Figma Design (desktop app 126.10.7) + +Forward-looking plan. Where FigCraft stands today: [`ROADMAP.md`](../ROADMAP.md) and +[`target-app-parity.md`](target-app-parity.md); the full ranked list: [`gaps.md`](gaps.md). Hours +are one Opus 5.5 agent; most milestones split across 4–6 agents on disjoint crates. There is no +`cargo xtask scorecard` in this repo; the measured number is `cargo xtask parity`. + +## Current focus + +1. **`.fig` import's tail**: editable vector networks, the skipped layer kinds that matter and + fixes from real files (the best-effort import landed 2026-10-11): 8–15 h. +2. **Real-file corpus and fidelity measurement**: import real files, compare against Figma's own + exports, turn estimated percentages into measured ones: 15–30 h. +3. **Tool-by-tool interaction pass** against the running Figma ([`ui-parity.md`](ui-parity.md)): + 40–60 h, parallel by tool. +4. **Languages** on the landed localization layer: native reviews of the es, fr, de, pt-BR, ja, + ko and zh-Hans drafts and the es-419 overlay (people), then the other key languages: 3–7 h per language + plus native review. +5. **Multiplayer M9**: accounts and teams on top of the live sessions and the standalone server (the WebSocket transport, hosting from the app, invite links that open the app, document sync, live cursors, selections, text carets, avatars and follow, per-user undo, reconnect with offline edits, collision-free ids for styles, variables, collections and comments, joining from the browser, and `figcraft-server` with persistence, publishing from the app and share links to edit or view landed), per [`collab-protocol.md`](collab-protocol.md) and [`server.md`](server.md): 45–90 h. + +## Alpha gate + +The core workflows a Figma Design user runs every day, checked end to end on macOS (the main +platform) with work saved to `.figcraft` and reopened. Evidence is the repo's tests and the +builders' side-by-side checks with synthetic files; nothing has been run on real Figma files yet. + +| Workflow | Works end to end? | Evidence | Hours to pass | +|---|---|---|---| +| Design screens: frames, shapes, vectors, text, auto layout, styles; save, reopen, export assets | yes | tools and panels in `crates/engine` and `crates/ui-egui`; `.figcraft` round trips (`crates/format/src/large_tests.rs`); PNG/JPG/SVG/PDF export tests | — | +| Build and use a design system: components, variants, properties, variables and modes, libraries | yes | `components_scale_tests.rs` (300 components, 2,000 instances), `library_tests.rs`, `library_variables_tests.rs`; libraries are local files, not shared | — | +| Prototype and present: interactions, flows, overlays, smart animate, variables | yes | `proto_*_tests.rs`, `present_*_tests.rs` | — | +| Hand off to developers: Dev Mode inspect, code, measurements, asset export, ready for dev | yes | `devinspect_tests.rs`, `component_codegen_tests.rs`, `devcompare_tests.rs`, `annotations_tests.rs` | — | +| Review with others: comments, versions, sharing the work | partial (not blocking) | comments with threads, mentions and resolve plus version history work inside a file (`comments_tests.rs`, `version_tests.rs`); the file is shared by passing it along; live sessions (preview: hosted from the desktop app or a self-hosted `figcraft-server` with edit and view links, joined from the app or the browser, undo per person); no accounts or teams ([collaboration-parity](collaboration-parity.md)) | beta gap: 45–90 | +| Continue existing Figma work (open `.fig`) | partial (beta gap) | best-effort `.fig` import (`crates/format/src/fig`, tested on generated fixtures, not yet on real files); REST JSON import works for files fetched through Figma's API ([file-format-parity](file-format-parity.md)) | beta gap: 8–15 | + +**Passes, so FigCraft stays alpha:** every daily design workflow works end to end on FigCraft's +own files, which save and reopen, and ready for real work (~47%) is above the ~40% bar. The two +gaps are real but don't break a workflow done in FigCraft: review works asynchronously through +comments in a shared file, and opening `.fig` is the step that moves work across from Figma, which +the standard requires for beta, not alpha. Both stay at the top of [`gaps.md`](gaps.md) as beta +blockers. + +## Milestones + +| Milestone | Status | Left | Hours | +|---|---|---|---| +| M0 Foundation: workspace, model, renderer, engine, UI shell, app, control channel | done (2026-10-09) | — | — | +| M0.9 CLI + MCP, web build, release packaging, PDF export | done (v0.2.0 released 2026-10-10 for every platform) | — | — | +| M1 Core editing completeness (tools, panels, menus, clipboard, files) | ~77% | interaction pass (Open recent, the outline mode variants and keyboard box selection landed 2026-10-11; repeat commands preserve the underlying edit and undo steps, #181; the Layout panel's constrain-proportions link scales the other side, #220) | 6–12 + pass | +| M2 Layout depth | ~80% | grid `fr` rows, real-file QA | 6–12 | +| M3 Components & styles depth | ~78% (extended collections, variables import/export, gradient stop and variant bindings landed 2026-10-11) | real libraries, component playground | 7–14 | +| M4 Vector & text depth | ~80% (live booleans, arc fields, shape builder on strokes, boolean hit testing on the result, one-sided widths, text on a path, pattern fills, brushes and pen pressure landed 2026-10-11; vector edit mode regions 2026-10-13; stroke holes preserved 2026-10-15) | outline-stroke compositing, spell check, Figma Draw multi-edit | 7–18 | +| M5 Prototype & present | ~85% | media, controllers on macOS/Linux/FreeBSD, separate window | 2–5 | +| M6 Interop & Dev Mode | ~65% (code connect, Dev Mode plugins and best-effort `.fig` import landed 2026-10-11) | `.fig` import's tail, video/HEIC (animated GIFs and TIFF landed 2026-10-11), CMYK intent | 15–30 | +| M7 Release & fidelity (signed builds, look & feel, shortcuts) | builds done; fidelity pass started | tool-by-tool pass | 40–60 | +| M8 Hardening & performance (bench, retained canvas, accessibility, idle) | ~60% | real-file corpus, platform QA, GPU canvas, screen readers | 60–110 | +| M9 Real-time collaboration | core done (#103: ids, change sets, server ordering, convergence); live sessions (WebSocket transport with limits and reconnect, hosting from the app, invite links, document sync between apps, cursors, selections, avatars, follow, `collab.*` commands); per-user undo, rejection handling, offline queue and resuming after a reconnect; styles, variables, collections, modes and comments created in a session get ids from their creator's space; others' text carets and selections on the canvas; the browser client (joins desktop-hosted sessions); `figcraft://` links open the app and join (Windows registers per user; macOS and Linux packages declare the scheme); the standalone `figcraft-server` (persistence with compaction, restarts that lose nothing confirmed, publishing from the app, share links to edit or view, revocable); team libraries kept on the server and enabled by a share link, with usage counts, and swapping a library; `wss://` from the desktop app, server releases and a deployment guide | accounts, teams | 45–90 | +| M10 Localization (new) | not started | layer + 11 languages | 60–100 | +| M11 Plugins | runtime, API, menu, manager and SDK done (#101); plugin panels and settings (ABI v2); widgets (API 3), panels side by side, colour picker on plugin fields; a plugin directory on `figcraft-server` (publish, browse, install, update) | a public catalogue, publisher accounts and signatures, the directory in the browser, richer widget events; Figma plugin compatibility is an owner decision | 8–26 | +| M12 AI features (new, owner decision) | not started | local models | 30–60 | + +## Next, after the current focus + +- Team libraries for a whole team (lists, accounts and permissions) and deeper library analytics (4–8 h, after + accounts), branching (8–15 h). +- GPU canvas held to the CPU renderer (30–60 h). +- Figma Draw multi-edit and pen pressure on macOS/Linux tablets (2–5 h). +- Mobile viewer and prototype mirror (20–40 h). + +## Revision history + +| Date | Change | Summary | +|---|---|---| +| 2026-10-15 | minor | M4: Outline stroke preserves even-odd stroke holes. Pixel comparisons cover alignment, dashes, individual side weights, vector paths and stroke-only opacity; native save/reopen, Undo/Redo and instance opacity inheritance/overrides are covered by `outline_stroke`. Existing estimates unchanged; filled-shape compositing needs separate work. | +| 2026-10-14 | minor | M7: the Small caps button in Type settings showed a box where the UI font has no small-capital `ɢ` (builds without Inter use egui's fonts); its sample is now a capital and a smaller capital, drawn from ordinary letters every UI font has. Numbers unchanged | +| 2026-10-14 | minor | M7: Home screen recent files as cards: each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged. | +| 2026-10-14 | minor | Lost images reported on loading (#207): a `.figcraft` file whose image member is damaged, too large or missing no longer loads in silence; each lost image (up to 20, then a count) is listed in `repairs` on `file.open` with its hash and the reason, and the app shows it as a toast. Saving still writes the file without the lost images. Numbers unchanged | +| 2026-10-14 | minor | SVG import budgets (#190): gzipped SVG is unpacked by FigCraft within 64 MiB before parsing (it was unpacked without a limit), so the nesting check sees the real document; SVG documents embedded in `` data URIs share one 32 MiB budget and at most 4 levels, and one past a budget is left out. Linked files stay unread. Numbers unchanged | +| 2026-10-14 | minor | Leptos code (#223): `inspect.code`, `inspect.componentCode` and `codeConnect.set` take `language: leptos`: a `#[component]` with `#[prop]` defaults, variant enums and `view!` markup, instances as calls, Code Connect `use` lines; the Dev Mode language list and rem units include it. Numbers unchanged | +| 2026-10-14 | minor | Full screen (#227): View ▸ Full screen (`view.fullscreen {on?}`, F11 on Windows and Linux, Ctrl+Cmd+F on macOS) makes the window fill the screen and leaves it again; FigCraft's own item. Numbers unchanged | +| 2026-10-14 | minor | Colour picker: wrapped rows of page and recent swatches stay inset on both sides instead of running past the picker's right edge. Numbers unchanged | +| 2026-10-14 | minor | Server snapshots (#188): figcraft-server never replaces a readable snapshot with one too large to be read back, and reads snapshots up to twice the file cap (recent batches kept for reconnecting clients may take a file over it). Numbers unchanged | +| 2026-10-14 | minor | Preferences owner-only on Unix (#200): `state.json`, which holds online owner tokens, is written with mode 0600 from the first byte, and an older world-readable file is narrowed. Numbers unchanged | +| 2026-10-14 | minor | ruzstd 0.9.1 (#182): `.fig` import requires the release that fixes RUSTSEC-2026-0344 (a zstd decompression bomb). Numbers unchanged | +| 2026-10-14 | minor | Layer cycles (#205): `validate` reports children listed twice, pages with a parent and layers not reachable from a page (cycles, detached subtrees); `repair` drops them in one linear pass. Numbers unchanged | +| 2026-10-14 | minor | AppImage CLI: `figcraft.AppImage cli ` runs the bundled `figcraft-cli` (MCP server included) on Linux without a second download. Numbers unchanged | +| 2026-10-14 | minor | Boolean outline depth (#193): nested boolean groups share one outline depth budget instead of each level getting its own. Numbers unchanged | +| 2026-10-14 | minor | Command batches roll back as one: a failed `command.batch` (MCP `batch`) restores every open document and its state, and puts the tab in front back. Numbers unchanged | +| 2026-10-14 | minor | Desktop builds no longer rebuild the text crate on every build when the craft-fonts manifest is missing. Numbers unchanged | +| 2026-10-14 | minor | Direct3D 12 on Windows: the desktop app asks wgpu for Direct3D 12 there instead of Vulkan, so it starts without Vulkan loader errors on machines with stray overlay layers. Numbers unchanged | +| 2026-10-14 | minor | Plugin metering (#192): every host call and every byte copied between a plugin and the host costs fuel, so a plugin can't stall the app through cheap-looking host calls. Numbers unchanged | +| 2026-10-13 | minor | M4: Vector edit mode regions: the filled area under the pointer is hatched (clear of points and segments, holes left out, octave-folded spacing fixed to the shape), and a click, Shift-click or drag selects and moves areas with their points (`vector_regions_tests`, `regions_tests`). Estimates unchanged. | +| 2026-10-13 | minor | M1, #220: The Layout panel's constrain-proportions link keeps the ratio on a W or H edit; the property edit dropped it, so the link only worked while dragging resize handles. Both sides in one edit stay as typed, the `w`/`h` aliases work the same, each layer of a multi-selection keeps its own ratio, and a zero side has no ratio to keep (transform behaviour suite). Estimates unchanged. | +| 2026-10-13 | minor | M1, #181: Repeat last command keeps the underlying command and parameters instead of recording itself; a stored self-reference is refused. Three cumulative repeats and their individual undo steps are covered by regression tests. Estimates unchanged. | +| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Focus item 4 adds the zh-Hans draft's review | +| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Focus item 4 no longer lists Latin American Spanish | +| 2026-10-12 | minor | M5: prototyping tail: controller buttons on key/gamepad triggers (Windows and the browser), widgets reacting in prototypes, `proto.evaluate`; M5's tail 3–6 h → 2–5 h | +| 2026-10-12 | minor | M11: a plugin directory on `figcraft-server` (bundles published with the server key and checked in full, browsed and installed from Plugins ▸ Manage plugins after a permissions confirmation, hash-checked downloads, updates by version, `plugin.directory.*`, `figcraft-cli plugin publish`); 10–30 h → 8–26 h | +| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Current focus 4 now lists the drafts' native reviews | +| 2026-10-12 | minor | M11: plugin widgets (API 3: plugin-drawn layers on the canvas, redrawn on clicks from state kept in the document), panels of several plugins side by side and the colour picker on plugin colour fields; 15–40 h → 10–30 h | +| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): every UI string, command label and menu wording (1,899 entries) machine-drafted in each and marked as drafts awaiting native review, falling back to English per key (new strings need only English and Spanish); 0 counts as singular in French and Brazilian Portuguese; System picks the first OS language with a catalog (pt-PT and pt read pt-BR); long translations elide with a tooltip in dropdowns, two-column labels and the home cards. Current focus 4 now lists Japanese and Korean | +| 2026-10-11 | minor | `.fig` import, best effort: current focus 1 and the alpha gate's `.fig` row move to the import's tail (30–60 h → 8–15 h); M6 ~55% → ~65%, 37–75 h → 15–30 h | +| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | +| 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | +| 2026-10-11 | minor | Localization layer landed (gap 5): per-language catalogs baked in at build time with English text as keys and fallback, placeholders and plurals, command labels by id, Preferences ▸ Language (System follows the OS or the browser) applied live and `pref.language`; English catalog and a complete Spanish draft awaiting native review. Localization 10% → 20%, 60–100 h → 45–75 h; ready for real work unchanged (~47%); full parity ~363–697 h → ~348–672 h; beta ~200–365 h → ~195–355 h | +| 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | +| 2026-10-11 | minor | Plugin panels and settings landed (gap 6, ABI v2; v1 plugins unchanged): a plugin shows a panel of notes, fields (text, number, checkbox, menu, list, colour) and buttons drawn with FigCraft's widgets; a button or a changed value calls it back with every value (same limits per callback, one undo step per callback that edits, rolled back on failure), its module staying loaded while the panel is open; `plugin.ui` / `plugin.event` / `plugin.close` drive panels headless; per-plugin settings (64 KB) kept with the preferences; the Swatch grid example gained a palette panel that remembers the last palette. Plugins & widgets breadth 35% → 45%, ready 20% → 28%, 25–55 h → 15–40 h; ecosystem 20% → 28%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~373–712 h → ~363–697 h | +| 2026-10-11 | minor | Canvas fixes (gap 13 closed): boolean groups are clicked where their result is (a click in a subtracted hole reaches what's behind; with the group or an operand selected its operands are hit, so each can still be picked), a layer dragged inside a group stays in the group (frames and auto layout still take other drops), View ▸ Frame outlines and View ▸ Slices draw on the canvas, and the idle tests wait for the render thread instead of counting frames. Shapes & vector 1–3 h → 1–2 h (vector QA with real files left); percentages, feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~378–722 h → ~375–717 h | +| 2026-10-11 | minor | Stretch and scatter brushes and pen pressure landed (gap 9): eight brushes drawn in code (four stretched along the path, four scattered with spacing, size, rotation and scatter jitter from a seed) in the stroke settings, drawn everywhere strokes are and flattened by outline stroke; the Pencil records pen pressure as the stroke's width profile (Windows pens, browser styluses; macOS and Linux tablets report none) and can draw with a brush; Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–9 h; Strokes 2–4 h → 1–3 h; Hardware 50% → 55%, 15–30 h → 13–27 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~378–722 h → ~373–712 h | +| 2026-10-11 | minor | Text on a path and pattern fills landed (gap 9): text follows a path or shape (start, side, alignment; edited in place; canvas, PNG, SVG and PDF; SVG `` imports as text on a path) and pattern fills tile a layer (grid or half-shifted rows or columns, scale, spacing, alignment; live as the source changes; SVG ``, PDF tiling patterns); Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–10 h; Text 8–14 h → 7–13 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~375–715 h | +| 2026-10-11 | minor | Team libraries on `figcraft-server` landed (gap 10): a file's library is published to the server under the file and enabled in other files by a share link (per-asset versions; updates noticed in the background and accepted with overrides kept), the server counts which files use which components, and a whole library can be swapped for another by names; Team libraries breadth 40% → 70%, ready 25% → 40%, 10–20 h → 4–8 h; collaboration 12% → 13%, 55–110 h → 49–98 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~380–725 h → ~375–715 h | +| 2026-10-11 | minor | Branches landed (gap 17): File ▸ Create branch… copies the file into a branch tab that remembers its base; Review and merge shows both sides' changes (layers with Inspect-style property changes, styles, variables, comments) and the conflicts (the same property changed on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merging is one undo step and archives the branch; Update from main; branches are saved in the `.figcraft` file. Comments, versions, find, misc breadth 80% → 85%, ready 58% → 60%, 4–8 h → 2–5 h; collaboration 12% → 13%; menu breadth 204/204 (the catalog gained File ▸ Create branch…); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~378–722 h | +| 2026-10-11 | minor | Animated GIF fills and TIFF images (gap 12, without video and HEIC): GIF fills keep the original file, show the first frame on the canvas and in exports and play in the presenter and the preview (frames decoded on first play, at most 500 frames and 128 MB; always looping; the window wakes only for a GIF's next frame); TIFF images open, import, place and paste. Paint breadth 85% → 87%, 5–9 h → 4–7 h; gap 12 6–12 h → 5–10 h (video playback and HEIC left, both needing non-Rust decoders); feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | The last three catalogued menu items got commands (gap 21): File ▸ Open recent (recent files kept in the preferences, at most 20, a file that's gone greyed in the menu and dropped when picked, Clear recent), View ▸ Outlines ▸ Show outlines for selection and View ▸ Mask outlines: menu breadth 98.5% → 100% (203/203); Canvas & navigation breadth 95% → 97%, 3–6 h → 2–5 h; feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | Extended collections, variables import/export and gradient stop and variant property bindings landed (gap 14): Styles & variables breadth 85% → 92%, ready 70% → 72%, 4–8 h → 1–2 h; feature breadth unchanged (~79%); ready for real work unchanged (~47%); full parity ~385–730 h → ~380–725 h | +| 2026-10-11 | minor | Collaboration wording after the standalone server: `figcraft-server` keeps many files, persisted as a snapshot plus a log of what changed (compacted, checksummed, damaged files skipped and reported), restarts lose nothing confirmed and apps resume where they were; the app publishes its file to it; share links per file let people edit or only view (the server refuses a viewer's edits), are revocable and stored as hashes; TLS comes from a reverse proxy (the desktop app has none of its own yet); percentages and hours unchanged until the next re-measure | +| 2026-10-11 | minor | Live boolean editing, Design panel arc fields, the shape builder on strokes and open paths and one-sided variable widths landed (gap 13): Shapes & vector breadth 92% → 97%, ready 66% → 68%, 8–14 h → 1–3 h; feature breadth ~78% → ~79%; ready for real work unchanged (~47%); full parity ~390–745 h → ~385–730 h | +| 2026-10-11 | minor | Collaboration wording after the collaboration polish: styles, variables, collections, modes and comments created in a live session take ids from their creator's id space (concurrent creations no longer overwrite each other; the server checks it), others' text carets and selections show on the canvas, and typing in a layer someone else is typing in says so; percentages unchanged | +| 2026-10-11 | minor | Wording only: live sessions (#104), then the browser joining them and `figcraft://` invite links opening the desktop app; percentages and hours unchanged until the next re-measure | +| 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 | minor | Added the alpha gate (core-workflow table): passes, stage stays alpha | +| 2026-10-10 | major | Created: milestone table moved from ROADMAP.md with statuses re-checked; added M10–M12; current focus re-planned | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 1f17a8c0..3289a570 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,703 +1,703 @@ -# FigCraft parity with Figma - -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (Outline stroke preserves stroke holes; reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS) - -The authoritative assessment of how close FigCraft is to Figma Design. [`ROADMAP.md`](../ROADMAP.md) -summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu checklist is -[`parity-checklist.md`](parity-checklist.md). Deeper checklists: -[`ui-parity.md`](ui-parity.md), [`file-format-parity.md`](file-format-parity.md), -[`hardware-parity.md`](hardware-parity.md), [`localization-parity.md`](localization-parity.md), -[`auto-layout-parity.md`](auto-layout-parity.md), [`components-parity.md`](components-parity.md), -[`prototyping-parity.md`](prototyping-parity.md), [`dev-mode-parity.md`](dev-mode-parity.md), -[`collaboration-parity.md`](collaboration-parity.md). - -## Headline - -| Number | Value | Kind | -|---|---|---| -| Menu/command breadth | **100%** (211 / 211 catalogued menu items have a live command) | **measured** (`cargo xtask parity`, 2026-10-12) | -| Feature breadth (does each Figma Design feature exist, weighted) | **~85%** (core editor areas 1–16: ~94%) | estimated from the area table below | -| Ready for real work | **~53%** | estimated (dimension table below: written weights × written per-dimension scores) | -| Essentials user | **~59%** | estimated ([below](#essentials-user): core features 77.8% × discounts 0.92 × 0.94 × 0.88) | -| Mainstream practitioner | **~50%** | estimated ([below](#mainstream-practitioner): weekly areas 72.7% × discounts 0.9 × 0.9 × 0.85) | -| Remaining to beta | **~155–280 h** one Opus 5.5 agent (~32–64 h wall-clock with 5 parallel agents) | estimated | -| Remaining to full parity | **~262–519 h** one agent (~83–145 h wall-clock with 5 parallel agents) | estimated | -| Stage | **alpha**: the alpha gate passes ([roadmap](roadmap.md#alpha-gate)); multiplayer and checking the best-effort `.fig` import on real files are beta gaps (see [ROADMAP](../ROADMAP.md#stage)) | | - -The previous figures (ROADMAP of 2026-10-10, "feature depth ~79%", "full parity 115–195 h") were -self-graded against a 16-area rubric that left out, by design, multiplayer, plugins, cloud -libraries and AI features, and that counted no file-format blocker. Figma users rely on all of -those every day, so this re-measure weights them in. The core-editor depth figure drops from ~79% -to ~67% because nothing has yet been checked against real Figma files or side by side tool by -tool; presence was being graded as behaviour in places. - -## Readiness by audience - -| Audience | Ready % | Opus 5.5 agent wall-clock hours to ~95% | Work that dominates | -|---|---|---|---| -| Full target (ready for real work) | ~53% | ~268–522 h | multiplayer (42–84), localization (29–51), plugins (8–26), the interaction pass (40–60), GPU canvas (30–60), AI (30–60), the `.fig` import's tail (5–10) | -| Mainstream practitioner | ~50% | ~120–229 h | core editor depth incl. the `.fig` import's tail (49–98), the interaction pass (40–60), real-file corpus and platform QA (25–50), checking the experimental `.fig` export opens elsewhere (5–15) | -| Essentials user | ~59% | ~55–100 h | the `.fig` import's tail so the files people send open well (5–10), core tool polish (25–45), interaction pass on the core tools (15–25), stability QA (10–20) | - -Hours are one agent working sequentially, calibrated from this repo's history (about 1 agent-hour -per 1,000 added lines, 1–2.5 h per feature PR, ~7 h per honest depth point in the polish tail; see -[how the hours are calibrated](#how-the-hours-are-calibrated)). Each audience's work is a subset of -the next. About 70–80% parallelizes across 4–6 agents on disjoint crates (roughly ÷4 in -wall-clock); the `.fig` reader, the multiplayer core and a plugin runtime each need one owner, and -native-speaker review, real-file licensing and screen-reader QA need people. - -The full number is an additive weighted sum over the dimension table; mainstream and essentials use -multiplicative discounts, as the standard says. - -## What was measured, and how - -- **Target and version.** Figma desktop for macOS, `CFBundleShortVersionString` 126.10.7, - installed at `/Applications/Figma.app`. FigCraft is measured as of `origin/main` d452da3 - (2026-10-10, PR #99). -- **The installed app, names and public listings only.** FigCraft is clean-room - ([`AGENTS.md`](../AGENTS.md)): nothing inside the bundle's code or web assets was read. From - `Info.plist`: one document type, `com.figma.document`, extensions `fig`, `jam`, `deck` - (MIME `application/x-figma`), and the `figma://` URL scheme. The `.lproj` folders are - Electron/Chromium locale packs (60+), not Figma's UI languages; the UI languages come from - Figma's help center (see [`localization-parity.md`](localization-parity.md)). The bundle ships - GPU, Plugin and Renderer helper processes (GPU rendering; a sandboxed plugin runtime). -- **The menu catalog.** `crates/engine/src/catalog.rs` holds Figma's main menu tree (202 items in - File, Edit, View, Object, Text, Arrange, Vector, Plugins, Preferences, Libraries, Help), written - from public documentation and black-box observation (local notes in the gitignored - `plan/figma/03-menu-tree.md`). It leaves out the Widgets menu and dynamic plugin lists, so the - 100% overstates breadth against the full menu a little. -- **From source** (no build; 2026-10-10): 449 command ids registered with `spec!`, 963 `#[test]` - functions in 177 files, ~108,800 lines of Rust in 13 crates + 3 apps + xtask (before #101–#103, which added the `plugin` and `collab` crates and ~7k lines), 140 key bindings - and 13 gesture rows in `figcraft_engine::keymap`, 21 files in the SVG import corpus, 0 - translated UI strings (no localization layer exists). -- **Figma's feature set**: help center, release notes and Config 2025 announcements (Figma Draw, - grid auto layout, slots, glass/noise/texture effects, progressive blur), known to the assessor up - to mid-2026, plus web checks on 2026-10-10 (supported image/video types, UI languages, pen - pressure in Figma Draw). -- **Side by side.** Look & feel was compared with Figma UI3 on the owner's Mac (screenshots kept - local, never committed). No real Figma file has been opened in FigCraft (no `.fig` reader), and - no rendered frame has been compared pixel for pixel against Figma's own export. - -## Weights - -Weights are the share of a typical product designer's working time in Figma Design, written down -so they can be argued with. Areas 1–16 keep the original rubric's weights (they sum to 94, not the 100 the rubric claimed); -areas 17–21 are added (sum 19) because Figma users depend on them. Percentages below divide by the -total, 113. -Out of scope as separate products: FigJam, Figma Slides, Sites, Make, Buzz, Weave (the `.jam` and -`.deck` document kinds belong to them). - -## By feature area - -Breadth: does it exist. Ready: depth, correctness and fidelity for real projects. Hours: one Opus -5.5 agent, sequential, tests included. - -| # | Area | Weight | Breadth | Ready | Remaining | Hours | -|---|---|---|---|---|---|---| -| 1 | Canvas & navigation | 6 | 98% | 74% | Outline mode's Include hidden layers and Include object bounds, Display P3 on screen, QA at extreme zooms (behaviour suite for navigation, rulers, guides, layout grids and outline mode landed: guide selection, Auto grid counts, outline-mode clicks) | 1–4 | -| 2 | Selection & transform | 8 | 97% | 72% | Handles, measuring and snapping checked tool by tool against Figma and real files (snap distance, handle reach, cursors); children turned off a right angle scale without skew, approximately (behaviour suite from the documentation, rotated groups and mixed selections, equal spacing snapping, Cmd/Ctrl to ignore constraints and marquees inside frames landed) | 3–5 | -| 3 | Layers panel | 5 | 99% | 85% | QA with long real files (behaviour suite from the documentation, Rename layers dialog, sweeping eyes and padlocks, page dragging and dividers, several layers dropped together, keyboard box selection, F2 rename, Shift+F10 context menu and 10k-layer timings landed) | 1–2 | -| 4 | Shapes & vector | 8 | 99% | 73% | Outline-stroke compositing; corner smoothing on polygons, stars and vectors; the transform box around points of several layers; per-point caps and joins on dashed, variable-width and brush strokes; vector QA with real files (outline stroke rings covered by `outline_stroke`; behaviour suites from the documentation, corner radius and star ratio handles, rounded vector points drawn, also beside curved segments, a cap and join per point, several handles dragged together, cut and paint bucket drags, delete and heal along the curve, a boolean group's look by operation and the miter angle landed) | 1–3 | -| 5 | Paint | 7 | 90% | 71% | Video fills, wide-gamut canvas, HEIC images; plus darker in PDF (behaviour suite from the documentation, pass-through and isolating groups, clipping frames that don't isolate, plus darker, animated GIF fills, TIFF images and embedded image colour profiles landed) | 2–5 | -| 6 | Strokes | 3 | 95% | 75% | Stroke QA with real files (colour styles on strokes, stroke weight and per-side weight variables, brush strokes landed, see 20) | 1–2 | -| 7 | Effects | 4 | 100% | 75% | Glass and progressive blur tuned side by side (behaviour suite from the documentation, inner shadows and background blur on groups, shadows hidden behind translucent layers on the canvas and in every export landed) | 2–5 | -| 8 | Frames, groups, sections, masks | 5 | 97% | 80% | Visual QA with real files (a one-step frame/group convert, and a behaviour suite from the documentation landed: masked-out areas don't take clicks, mask group bounds and exports follow the mask, sections kept out of frames and groups, frames and sections drawn around layers take them, section resizing leaves contents in place, X/Y measured in the frame) | 1–3 | -| 9 | Auto layout & constraints | 9 | 98% | 74% | Behaviour against real Figma files (behaviour suite from the documentation, `fr` tracks like CSS, vertical wrap, auto gaps evenly/around, cell alignment and wrapped-line alignment landed) ([auto-layout-parity](auto-layout-parity.md)) | 3–6 | -| 10 | Text | 8 | 90% | 70% | Spell check, text metrics on real files and in other fonts (text box sizes measured against documents re-saved by other software: all 230 self-sizing boxes in 7 documents come out to the pixel, one font family; behaviour suite with CJK line breaking, vertical trim and carets by grapheme cluster landed; lists per paragraph with nesting and list spacing, hanging punctuation and lists, decoration style, thickness, offset, skip ink and colour landed) | 3–7 | -| 11 | Components & variants | 9 | 96% | 72% | Real (not synthetic) libraries, `.fig` libraries (a component playground for variant, boolean and text properties, a behaviour suite from the documentation, overrides of padding, gap, grids, exports and interactions, refused geometry overrides, variant-switch and Assets-swap override rules, top-left default variant landed) ([components-parity](components-parity.md)) | 4–9 | -| 12 | Styles & variables | 5 | 95% | 76% | Real-library QA (variables inside styles, behaviour suite from the documentation, style folders and order, mode order, code syntax, extended collections with their tail (new library variables as updates, chains through libraries) and variables import/export landed) ([components-parity](components-parity.md)) | 2–4 | -| 13 | Prototyping | 6 | 92% | 74% | Video triggers and media actions, controllers on macOS, Linux and FreeBSD, real prototype QA (behaviour suite from the documentation, delays on mouse triggers, manual overlay offsets, key combinations, controller buttons on key/gamepad triggers (Windows and the browser), widgets reacting in prototypes, interactive components playing their variants, drag-driven animations, physical springs landed) ([prototyping-parity](prototyping-parity.md)) | 2–5 | -| 14 | Import/export | 6 | 86% | 58% | `.fig` import's tail (best-effort import, editable vector networks, stand-ins for non-design layers and variable prototype actions landed; no real-file checks yet), checking the rest of the experimental `.fig` export (landed; written files open and draw in other software) on effects, images, prototypes and real files, HEIC/video import, CMYK output intent ([file-format-parity](file-format-parity.md)) | 5–10 | -| 15 | Dev Mode / inspect | 2 | 96% | 72% | Import lines for connected instances inside generated layer trees; an editor extension is left to the MCP server (inspect behaviour suite with colour formats, rem and icon and image assets, code connect, plugins in Dev Mode and dev resources links landed) ([dev-mode-parity](dev-mode-parity.md)) | 0–1 | -| 16 | Comments, versions, find, misc | 3 | 87% | 63% | Comments are local only; comparing two versions; branches in live sessions; text runs and other lists besides fills, strokes and effects merge whole (branches with review and merge, their own version history and per-item list merges, and a comments and version history behaviour suite from the documentation landed) | 1–3 | -| 17 | Multiplayer & sharing | 8 | 17% | 5% | Core landed (`crates/collab`: ids, change sets, server ordering, convergence tested); live sessions (preview): WebSocket transport, hosting from the desktop app, joining from the app or the browser, invite links that open the app, cursors, selections and follow, cursor chat and spotlight, per-person undo, offline resume; a self-hosted `figcraft-server` with persistence and share links to edit or view, which can be open to the public (owner tokens, limits per address, expiry, moderation). Left: accounts and teams, audio ([collaboration-parity](collaboration-parity.md)) | 42–84 | -| 18 | Plugins & widgets | 4 | 60% | 35% | Own sandboxed WASM plugins landed (#101: manifest, permissions, menu, Run last, Manage, SDK), with log byte/count limits checked before allocation and host forwarding ([logging limits](plugins.md#logging-limits)), plugin panels called back per button or changed value and per-plugin settings (ABI v2), then widgets (API 3: plugin-drawn layers on the canvas with state kept in the document, redrawn on clicks; a Counter example), panels side by side and a colour picker for plugin colour fields, then a plugin directory on `figcraft-server` (publish with the server key, browse, install after a permissions confirmation, hash-checked downloads, updates by version). Left: a public catalogue, publisher accounts and signatures, ratings, background update checks, the directory in the browser; widgets take clicks only (no text input, property menus, hover or timers) and draw frames, text, shapes and images only; Figma's plugins are JavaScript and don't run (a JS engine would be an owner decision) ([plugins.md](plugins.md)) | 8–26 | -| 19 | Team libraries & organization | 2 | 70% | 40% | Libraries kept on `figcraft-server` under their file and enabled by a share link (per-asset versions, background update checks, overrides kept), usage counts per component and file, swapping a library by names. Left: team-wide library lists, accounts and permissions, analytics over time, pushed update notices | 4–8 | -| 20 | Figma Draw extras | 2 | 88% | 60% | Have: variable width, lasso, shape builder, noise/texture, text on a path, pattern fills, stretch and scatter brushes, pen pressure (Windows pens, browser styluses), multi-edit (several vector layers in edit mode at once). Missing: pressure from macOS/Linux tablets | 1–3 | -| 21 | AI features | 3 | 0% | 0% | First draft, rename layers, image edit/remove background, visual search, Make: needs local models (owner decision) | 30–60 | -| | **Weighted (÷113)** | 113 | **~85%** | **~63%** | | **117–255** | -| | Core editor only (1–16, ÷94) | 94 | ~94% | ~72% | | 32–74 | - -## By dimension (ready for real work) - -| Dimension | Weight | Ready | Hours | Evidence | Doc | -|---|---|---|---|---|---| -| Features (depth) | 35 | 63% | 28–67 (areas 1–13, 15, 16, 20) | The table above; core editor ~72% | this file | -| UI/UX fidelity | 15 | 58% | 40–60 | Look & feel compared side by side with UI3 (~65% by the builders); tool-by-tool interaction (modifiers while dragging, hover states, popovers) not yet compared (a cursor for every tool and modifier, number fields and panel widths built from the documentation, not compared); accessibility names and keyboard navigation landed | [ui-parity](ui-parity.md) | -| File formats | 12 | 60% | 5–10 | `.fig` read, best effort, with editable vector networks (tested on generated fixtures, not real files), `.fig` write experimental (round trips through our own importer; written files open and draw in other software); REST JSON import, SVG in/out, PNG/JPG/WebP/PDF export solid, with a behaviour suite from the documentation | [file-format-parity](file-format-parity.md) | -| Collaboration | 10 | 13% | 52–104 | Local comments, version history and branches with review and merge; the multiplayer core (sync model, server ordering, client reconciliation, convergence tests) landed in #103, and live sessions (preview) co-edit a file hosted by a desktop app or kept on a self-hosted `figcraft-server` with edit and view links, joined from the app or the browser, with cursor chat and spotlight (`docs/collab-protocol.md`) | [collaboration-parity](collaboration-parity.md) | -| Stability | 8 | 55% | 20–40 | Never-crash lints, command guard with rollback, recovery copies, 963 tests incl. junk-parameter sweeps and a seeded retained-render walk; but two days old, one release (v0.2.0), no field reports, QA mostly on macOS | [gaps](gaps.md) | -| Performance | 6 | 60% | 30–60 | `cargo xtask bench`: 50k-layer page ~8 ms zoomed out, 10k-layer edits ~0.2 ms, 2,000-instance design system edits 4–64 ms; CPU (tiny-skia) canvas where Figma renders on the GPU; nothing timed against Figma on the same file | [hardware-parity](hardware-parity.md) | -| Hardware | 3 | 60% | 10–21 | Trackpad pinch and HiDPI yes; pen pressure on Windows and from browser styluses (none from macOS/Linux tablets); several windows (screen-reader tree in the main one only); no GPU canvas, no P3 on screen | [hardware-parity](hardware-parity.md) | -| Localization | 3 | 39% | 29–51 | The layer, English, Russian reviewed by VanTan, and machine drafts of Spanish (with a Latin American overlay), French, German, Brazilian Portuguese, Japanese, Korean and Simplified Chinese (none reviewed by a native speaker; every draft covers all current strings), CJK UI text in installed faces loaded on demand (web: only the craft-fonts CJK build); engine messages stay English | [localization-parity](localization-parity.md) | -| Platforms | 3 | 75% | 10–20 | macOS, Windows (x64/x86/arm64), Linux, FreeBSD, web all packaged (v0.2.0); Figma also has iOS/Android apps; non-macOS QA thin | [hardware-parity](hardware-parity.md) | -| Ecosystem/plugins | 3 | 38% | 8–26 | Own WASM plugins with a Rust SDK and three examples (#101), plugin panels and settings (ABI v2), widgets that take clicks (API 3), a self-hosted plugin directory on `figcraft-server`; no Figma plugin compatibility, no public catalogue or community files; the CLI, control channel and MCP server are ahead of Figma for agents | [gaps](gaps.md) | -| AI features | 2 | 0% | 30–60 | None | [gaps](gaps.md) | -| **Ready for real work** | 100 | **~53%** | **~262–519** | | | - -Dimension hours add up to the full-parity total without double counting: the feature-area rows -for import/export (14), multiplayer and team libraries (17, 19), plugins (18) and AI (21) are -counted under their dimensions. - -## Essentials user - -Someone who uses only the core of Figma: a casual or beginner user making a few screens, a social -graphic or a simple mockup. Same method, with the core features a casual user touches. - -**1. Core features, weights and depth** (depth from the area table and checklists above): - -| Feature | Weight | Depth | Notes | -|---|---|---|---| -| Create, open, save and reopen a file; pages | 10 | 85% | home screen, `.figcraft` round trips, Open recent | -| Pan and zoom | 10 | 80% | wheel, pinch, Space-drag, zoom shortcuts | -| Select, move, resize, rotate, align, group, duplicate | 15 | 75% | not yet compared tool by tool | -| Frames with device presets | 8 | 80% | | -| Rectangle, ellipse, line, arrow, corner radius | 8 | 80% | | -| Text: type, font, size, weight, colour, alignment | 12 | 70% | no spell check | -| Colour picker, fills and strokes; place and crop images | 10 | 72% | no HEIC images | -| Layers panel: rename, hide, lock, reorder | 7 | 80% | | -| Basic auto layout (add, gap, padding) | 6 | 70% | | -| Undo / redo | 5 | 90% | unlimited, one step per gesture | -| Export PNG, JPG, SVG, PDF | 6 | 85% | | -| Copy and paste, incl. images from other apps | 3 | 75% | | -| **Weighted** | 100 | **77.8%** | | - -Left out: components and variables beyond using one, prototyping, Dev Mode, advanced vector and -type, and everything the mainstream number leaves out. - -**2. Discounts a casual user actually hits:** - -| Discount | Factor | Evidence | -|---|---|---| -| Launch and stability | ×0.92 | signed and notarized macOS build, installers for every platform (v0.2.0), never-crash rules and recovery copies; no field record yet | -| Discoverability and UI clarity | ×0.94 | the UI3 layout Figma users know, Actions palette, keyboard shortcuts panel; a first run opens on the home screen with a Getting started card (six tips with their shortcuts, closable, back from the Help menu) and eight starter templates to open (also File ▸ New from template…); no Community files, no guided tour, and nobody new to the app has been watched using it | -| Opening files people send them | ×0.88 | what people send a Figma user is a Figma link or a `.fig`; a `.fig` opens (best effort, not yet checked on real files), a link doesn't. Images and SVG do | - -77.8% × 0.92 × 0.94 × 0.88 = **~59%** (estimated). User evidence: none yet (see above). - -## Mainstream practitioner - -The same question for the typical professional Figma Design user: a product designer who designs -screens, maintains or uses a design system, prototypes and hands off to developers every week. -Computed per craftrules `standards/progress-docs.md`. - -**1. Areas used weekly, with weights.** Feature areas 1–16 of the table above, with their weights -(sum 94). Left out as the standard says: plugins and widgets (18), AI (21), team and organization -libraries (19, cloud/team admin), Figma Draw extras (20, not weekly for a product designer), -localization beyond English, specialist hardware. Real-time multiplayer (17) is cloud collaboration -and is left out as an area, but its effect on working with Figma users is counted in discount 3. - -**2. Average depth** ("Ready" column, weighted): **72.7%**. - -**3. Discounts for what still stops real work:** - -| Discount | Factor | Evidence | -|---|---|---| -| Interaction fidelity | ×0.90 | UI3 look compared side by side (~65% by the builders); no tool-by-tool comparison of cursors, modifier keys while dragging, snapping thresholds or popovers yet; Ctrl to suspend snapping on macOS missing ([ui-parity](ui-parity.md)) | -| Stability on real machines | ×0.90 | Never-crash lints, command guard with rollback, recovery copies and 963 tests, but no field record at all: the repository is private, v0.2.0 has had no outside users or issue reports, CI tests run on Linux only and hands-on QA is on macOS. Harsher than VectorCraft's ×0.92 because there is no outside evidence either way | -| File exchange with Figma users | ×0.85 | A Figma designer's work lives in shared Figma files. FigCraft opens a `.fig` (best effort, not yet checked on real files) and writes an experimental one (written files open and draw in other software; effects, images and prototypes unchecked there); incoming work can come through the REST API JSON (one way, needs an access token), outgoing only as SVG, PDF, PNG or code. Teammates, reviewers and developers would all need FigCraft ([file-format-parity](file-format-parity.md), [collaboration-parity](collaboration-parity.md)) | - -72.7% × 0.90 × 0.90 × 0.85 = **~50%** (estimated; 50.03%, rounded down). - -**4. User evidence (2026-10-10):** 0 issues, 0 praise or "switched from Figma" reports, 0 comments -from anyone but the maintainers (`gh issue list`, `gh api .../issues/comments`; the repository is -private, 1 star, PRs only from maintainers). No outside evidence either way; nothing raises the -number. - -**Why it is lower than ready for real work (~53%), unusually.** The full number averages -dimensions additively, so the missing `.fig` exchange costs only the file-format dimension's 12% -weight, and FigCraft earns points the mainstream number doesn't count (platforms 75%: Linux, -FreeBSD and the web, where Figma has no desktop app). The mainstream number applies file exchange -as a multiplier on everything, because a Figma practitioner's whole working week is shared through -Figma files. Both are computed from written weights; neither was moved to match the other. The -mainstream number rises fastest with `.fig` import and the experimental `.fig` export checked on real files (the ×0.85 toward ×0.95: ~53%) and the -interaction pass. - -## How the hours are calibrated - -From this repository's own history (`git log`, `gh pr list`): - -- The whole app was built in ~25 wall-clock hours: first commit 2026-10-09 15:38 UTC, PR #99 - merged 2026-10-10 16:53 UTC; ~49k lines on day 1 before PRs, then 99 PRs adding ~81k lines. The - ROADMAP of that period records 4–6 agents in parallel, so ~110–150 agent-hours in all: about - **1 agent-hour per ~1,000 added lines**, and a typical feature PR (1,000–2,500 lines, e.g. - #84 arc handles, #92 PDF print options, #96 accessibility) took **1–2.5 agent-hours**. -- Between the ROADMAP revisions of 2026-10-09 22:19 UTC and 2026-10-10 16:53 UTC (~18.5 h wall, - 78 PRs, ~75–110 agent-hours) the self-graded depth went 58% → 79%: **~4–5 agent-hours per - self-graded point**. This re-measure grades about 0.6 points for each of those, which puts an - honest point nearer 7 agent-hours in the polish tail; new subsystems (a `.fig` reader, - multiplayer, a plugin runtime) are estimated bottom-up instead, since nothing comparable has been - built here yet. -- Parallelism: most of the work splits across 4–6 agents on disjoint crates (the history shows it - working); multiplayer, `.fig` import and the plugin runtime each need one owner for their core. - Needs a human: native-speaker review for every language, the plugin-runtime and AI-model - decisions, real-world Figma files to test with (licensing), Windows/Linux/screen-reader QA. - -## Area detail: what has landed and what is left - -Carried over from the ROADMAP's "Where we're lacking" list (2026-10-10, before this re-measure). -Hours in this narrative are the builders' estimates; the tables above supersede them. - -1. **Vector edit mode**: *landed:* edit-mode toolbar (move, pen, bend, paint bucket, lasso, cut), - pen that extends, branches and closes paths (vector networks), paint bucket fill/unfill of any - closed area (faces with holes), point/segment selection, handle mirroring (none/angle/angle and - length) with a Point panel (position, corner radius, mirroring), smooth/sharp points, join and - merge, delete vs delete-and-heal, nudging, one undo step per gesture, eraser (⇧E), shape builder - (⇧M: the regions of overlapping shapes, click or drag to merge, Alt to remove, curves kept), - variable width strokes (width profile presets with previews in the stroke settings, the Variable - width tool ⇧W: drag the path to add a width point, slide points, drag their handles, Delete; - smooth interpolation per subpath, dashes, caps and joins, canvas/PNG/SVG/PDF and outline stroke), - a transform box around several selected points (corners and edges scale them, Shift keeps - proportions, Alt from the centre; just outside a corner rotates, Shift in 15° steps; drag inside - to move; the Design panel's X/Y/W/H and flip/rotate act on the points), point snapping while - dragging points or handles and with both pens (to the points and segment midpoints of this and - other vectors, the usual layer targets and the pixel grid, with guides and a marker on the point - landed on; Ctrl/Cmd turns it off), arc handles on the canvas (sweep, start and ratio with a - live readout, Shift snapping, closing at the start, the other part from the gap; rotated and - flipped ellipses, any zoom), Design panel Arc fields (Start, Sweep, Ratio; bindable), live - boolean editing (operands selected by double-click or Cmd/Ctrl-click; moving, resizing or - editing an operand's points redraws the result during the drag with every operand outlined; - per-operand operations from the boolean controls; only the operands from the edited one up are - recombined; clicks hit a boolean group where its result is), the shape builder on strokes and open paths (strokes as their outlined areas), and - one-sided variable widths (Alt-drag a width handle; canvas, exports and outline stroke), and - filled regions with the Move tool (the area under the pointer hatched, clear of points and - segments, its spacing by octave folding fitted to measurements at several zoom levels and fixed - to the shape; click, Shift-click and drag to select and move areas with their points). - *Left:* vector QA with real files. 1–2 h. -2. **Text editing depth**: *landed:* OpenType features panel, small caps, lists, title case, - truncation/max lines, font picker with previews and recents, word/line selection, IME commit, - inline IME composition (underlined at the caret, reflowing, candidate window at the caret; the - document only gets the committed text), variable fonts (axes and named instances read from the - font, named instances as styles, Type settings ▸ Variable sliders, per-range axis values applied - to shaping, outlines and PDF), range-aware Typography panel with "Mixed". Range fills in the Fill - section. Right-to-left and mixed-direction text (Unicode bidi: Arabic shaping, mirrored brackets, - caret, clicks and selection follow the runs; arrow keys move visually). Right-to-left and - mixed-direction names in the panels (layers, pages, Assets, tabs, the file header, canvas frame - titles: visual order, joined Arabic, the ellipsis at a right-to-left name's left end; rename fields - whose caret, clicks, drags and arrow keys follow the shown text; system fonts for scripts the UI - fonts lack). The text style browser in the Typography section (list and grid views in each - style's 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, "Mixed" when the - chars use several styles); text styles link chars, not just layers (editing what a style sets - unlinks the chars edited). Live previews of a hovered named instance or font style and of a - dragged axis slider (one undo step on release, Escape cancels). Visual QA: list markers keep the - paragraph's font, renames start with the name selected and centred, Max lines shows while - truncating. Right-to-left paragraphs mirror lists and indents: bullets and numbers (".1") on - the right, wrapped lines hanging from the right, the paragraph indent on the right, trailing - spaces hanging outside the line (canvas, PNG, PDF and SVG). A behaviour suite from the public - documentation (`crates/text/src/behaviour_tests.rs`, `crates/engine/src/text_behaviour_tests.rs`): - Auto / % / px line heights with the leading split above and below every line, letter spacing in - % and px, paragraph spacing and indent (start-aligned text only), resizing and truncation, - decorations from the font's metrics; CJK line breaking (between ideographs, kana and Hangul, no - break inside Latin words, kinsoku for closing and opening punctuation, small kana and the - prolonged sound mark); a style's metrics from its own font, not a fallback; carets, Delete and - Backspace by grapheme cluster; double-click and word moves by script run; IME composition with - CJK; vertical trim (cap height to baseline). The tail suite (`crates/text/src/tail_tests.rs`, - `crates/engine/src/text_tail_tests.rs`): lists per paragraph, nested up to 8 levels with Tab and - Shift+Tab (1. a. i. and • ◦ ▪ per level, counts restarting after plain paragraphs), Enter on an - empty item and Backspace at an item's start stepping out, `- `/`* `/`1. ` starting a list; list - spacing between items; hanging lists and hanging punctuation; decoration style (solid, dotted, - wavy), thickness, underline offset, skip ink and colour, with inspect, CSS and the REST import. - *Left:* visual QA; hanging punctuation in right-to-left paragraphs and centred text, list - numbering that continues across a plain paragraph and a list's own start number (documentation - silent); decoration colour in SVG exports (drawn with the text's fill, like run fills); Korean - keeps breaking by syllable and Han runs aren't split by a dictionary (documentation silent). 0.5 h. -3. **Auto layout and sections depth**: *landed:* on-canvas padding/gap handles (also wrapped-row - and grid column/row gaps, grid tracks outlined), min/max width/height (UI, Hug/Fill/Fixed, - hugging text wraps at its max), ignore auto layout (absolute position) with constraints, text - baseline alignment, grid tracks (fixed/hug/`fr`), grid cells and spans, wrap row gap UI, - wrap-aware drag-reorder (by row) and dragging into a grid cell, nested hug/fill settling in one - pass (wrapping fill text, three levels deep, grid rows), min/max redistribution, coherent - Fill/Hug rules, drag across nested auto layout frames (deepest one wins, insertion line, out to - the canvas or a plain frame). Arrow keys move auto layout children one place (along the flow - past hidden and absolute siblings, Up/Down between wrapped rows, to the neighbouring grid cell - (automatically placed children swap, anchored ones take or swap cells), and at the end of a - flow or across a row's axis into the neighbouring nested auto layout frame when the frame - around flows that way; one undo step, none when nothing can move). Visual QA of nested - hug/fill/fixed/min/max/wrap/grid samples against the documented rules (Fill turns a hugging - parent Fixed, min/max redistribution, wrap inside fill, absolute children following - constraints as hug frames grow). Sections: hide contents (name, outline and "Contents hidden" - on the canvas; contents skipped by rendering, hit testing and labels; dimmed in the layers - panel; Design panel checkbox and context menu), Wrap in new section, frames in sections get - canvas labels and are top-level frames (flows start there, navigation, Export frames to PDF), - "Navigate to" a section shows its frame shown last, else its first; ready for dev turns - "Changed" when the contents are edited, resolved with a reason; Compare changes shows what a - changed frame or section looked like when it was marked next to how it is now, with the - changed layers and their property changes. *Left:* whether `fr` rows of a fixed-height grid - should make room for fixed-size children (now `minmax(0, 1fr)`-like). 1–2 h. -4. **Components depth**: *landed:* component properties (edit/delete/bind) with the properties - panel, add variant, instance swap UI, per-layer overrides with reset, restore main, instance - structure protection; Assets panel with live thumbnails, search and drag-to-canvas (drops into - the frame or auto layout slot under the pointer); exposed nested instances (their properties - show on the main's instances); component descriptions (edited on the main, shown on - instances); local libraries (publish a file's components, sets and styles with per-asset - versions, enable library files, place library components and apply library styles as - read-only linked copies, "Library updates available" with overrides kept on update); slots - (a frame of a main made a slot property with preferred components; instances fill it with any - content of their own, kept across main updates, variant switches, copy/paste and detach, with - the main's default content until edited; reset slot; slot badges in the layers panel and on the - canvas; fill menu in the properties panel; drops from the Assets panel and the canvas land in - slots); nested overrides kept when the main moves or swaps a nested instance, and on nested - instances copied out of an instance (re-linked to their own main); canvas drags can no longer - pull layers out of an instance; library variables (publishing includes the file's variable - collections with their modes, aliases and scopes, versioned per variable and collection, without - variables hidden from publishing; files using the library see its collections read-only under - the library's name in the Variables editor and the variable pickers; binding, aliasing or setting - a library collection's mode on a frame copies the variables in as read-only linked copies, so the - file renders without the library; library components keep their variable bindings and modes; - "Library updates available" covers changed variables and bound layers follow). Explicit variable - modes in the Design panel: a frame's mode per collection, Auto (inherited) or its own, with - enabled libraries' collections listed under the library's name (picking a mode copies the - collection in). Instance boolean and text properties bound to boolean and string variables - (following modes, renames, reset and detach); layout grid settings in the Design panel whose - count, size, margin/offset and gutter bind to number variables; fills, strokes, effects and - layout grids reordered by dragging their rows, their variable bindings following. Components at - scale, QA'd with a synthetic design system (about 300 components: buttons, inputs, cards, navs - and badges as sets of size × state × type × theme variants, boolean/text/instance swap/slot - properties, instances nested four deep, text and paint styles, light/dark variables bound across - it) used 2,000 times with overrides at every level: main edits keep every override and match a - full rebuild exactly; values from variables (a theme switch, a bound corner radius) are no longer - recorded as overrides; detaching a variable on an instance layer is an override that survives main - edits; exposed nested instances stay exposed and swapped nested instances keep their layer names - through main edits; layers a swapped nested instance's main adds keep their ids and overrides. - Instance swap properties have preferred values (offered first). Alt-drop from the Assets panel - onto an instance swaps it (same-set instance preferred, highlighted while dragging); instances - pasted into other files follow their mains (library copies shared, published components used - from their library, other mains left behind as a restorable missing main). The Assets panel lays - out only the tiles in view and renders thumbnails as they show (cached, re-rendered only for - changed components). A behaviour suite from the public documentation checks overrides (what an - instance takes, what it refuses: geometry, constraints and resizing of its layers, its auto - layout direction), variant switches (overrides stay where both variants agreed; text layers keep - theirs), swaps (by layer name; from the Assets panel only text overrides), the top-left default - variant, detach, reset and push. *Left:* QA with real-world (not synthetic) libraries. 1 h. -5. **Prototyping depth**: *landed:* on-canvas connections with drag-to-connect, interaction editor, - flows, smart animate, overlays, scroll/fixed/sticky, change-to and set-variable in the presenter. - Custom bezier and custom spring easings with editor fields; device frames (phone, tablet, - laptop, desktop bodies drawn in code) around the presented frame; overlays animate out with - their opening transition reversed; several selected layers edit their interactions together - (or replace mixed ones); scrolling inside device frames (the frame sized to the device's screen, - clipped or scrolling by wheel and drag, fixed layers pinned to the screen by their constraints, - nested "Scroll to", preserve scroll position); a resizable preview pane over the canvas that - plays the selected frame and follows edits live; device rotation (phones and tablets turn to - landscape: saved in the file, imported from REST files, drawn turned in the presenter and the - pane); presentation options (fit, fit width, fill, 100%, with Z to cycle them and R to - restart, the device frame shown or hidden, kept between presentations; what overflows the - window pans); scroll momentum (drags fling and slow down the same at any frame rate, the wheel - coasts, a drag past the ends stretches and springs back); responsive presentation (the presented - frame resized to the device's screen, or the window without a device, its constraints and auto - layout responding live; in the Options menu and the Prototype panel, saved in the file); device - colours (each model's colour variants, bodies drawn in code) and custom device sizes (a bare - screen with width and height fields, imported from REST files); flow descriptions in the - presenter's flow picker and under each flow; conditional actions (if / else if / else over - variable expressions) and set-variable-mode actions run in the presenter (from REST files or - commands). Conditionals are built in the interaction editor: an interaction's actions are a list - (add, reorder by dragging a row's grip, also inside conditional blocks, or from its menu, remove, - open one to edit it; one undo step per move), "Add action ▸ Conditional" adds an if whose - condition comes from a builder (a variable from the variable picker, an operator, a value or - another variable) or is typed as an expression (`count >= 3 and not done`, checked when entered, - what's wrong shown under the field), each block with its own actions (the same editors, - nested), plus "Else if" and "Else"; set variable values can be expressions (`count + 1`, - `"Hi " + name`); expressions read a variable's value in a given mode of its collection - (`accent[Dark]`, also from REST files' variable mode lookups) and the mode a collection is in - where the interaction runs (`mode(Theme) == "Dark"`, following set variable mode actions); every - edit is an undo step (`proto.action.*`, `proto.conditional.*`). Hotspot hints: a click where - nothing reacts flashes the screen's hotspots (an Options menu toggle, on by default); the - presenter's Inspect panel shows the hovered layer's properties. Overlays opened through commands - close on a click outside unless told not to, as in the editor. A behaviour suite written from - the public documentation covers triggers, actions, overlays, animations, easing, scrolling and - interactive components ([prototyping-parity](prototyping-parity.md)); its fixes: drag-driven - animations go their way and follow the pointer, key combinations, overlay timers from when they - open, instances play their variant's interactions, physical springs, smart animate overshoot, - fixed layers on top; scroll to takes an offset. *Left:* QA against larger real-world - prototypes. 1–2 h. -6. **Interop**: *landed:* image crop handles; REST API file JSON import (`file.open` / `file.import` - / CLI: pages, layer tree, geometry, paints, strokes, effects, text runs, vectors from geometry - paths, auto layout, constraints, grids, components, sets, instances with overrides, component - properties, styles; image fills from an `images` folder or placeholders; warnings for the rest), - now with prototyping (interactions with their triggers, actions, transitions and easing, flows, - scrolling, the prototype device; conditional actions (if / else if / else over expressions), - set variable with variable references and expressions, set variable mode, all run by the - presenter) and variables (the local variables JSON passed along, next to the file or embedded: - collections, modes, aliases, scopes, layer bindings and explicit modes). - Export panel: a row per export setting (typed or preset sizes like `2x` / `512w` / `512h`, PNG, - JPG, WebP, SVG and PDF, suffix, advanced options: contents only, include bounding box, and for - SVG outline text, id attributes and simplified strokes), a preview, and "Export N layers" (`file.exportLayers`) in the - panel and the Export dialog. Paste from other apps: images, SVG and text (the newest clipboard - content wins over our own copy; pasted SVG lands centred in the view). SVG import is editable and - checked against a corpus of synthetic files (expected pixels worked out by hand, layer structure, - export and re-import): groups named from their ids with transforms baked into the layers; - rectangles (corner radius), ellipses and lines kept as those shapes; gradients with exact - handles, `reflect`/`repeat` and focal points; patterns as image fills; clip paths and masks as - vector, luminance and alpha masks; shadow and blur filter chains as drop shadows, inner shadows - and layer blurs; blend modes; text as text layers with runs, anchors and stacked lines; images - and nested SVG images; import warnings for what's left out; linked files never read; hostile - input fuzzed. SVG export keeps alpha and luminance masks. PDF export: one page per frame in points (Export frames to - PDF, the Export panel, `figcraft-cli export -o x.pdf`), vectors, gradients and strokes as - vectors, real selectable text in subset fonts with clickable hyperlinks, images embedded once, - opacity and blend modes, clip content, alpha/luminance/vector masks, shadows and layer and - background blurs as images; checked by rendering the PDFs back against PNG exports. PDF print - options in the Export panel (per PDF export setting), the Export dialog, `file.export`, - `file.exportLayers`, `file.exportFramesPdf` and `figcraft-cli export`: a bleed showing what lies - past the frame (its fills reach out, its children are clipped at the bleed), crop and bleed - marks outside it, MediaBox / BleedBox / TrimBox, and CMYK colour (a documented, deterministic - naive conversion; images and rasterised effects too). - *Left:* .fig import (best effort), a CMYK output intent (no CMYK ICC profile can ship in the - repository), SVG filters other than shadows and blurs (left out with a warning). 8–16 h. -7. **Look & feel and interaction-fidelity pass**: *landed:* keyboard shortcuts audited against the - public shortcut reference: one keymap (`figcraft_engine::keymap`) drives the menus, the canvas, - the control channel and MCP, with macOS and Windows/Linux keys where they differ (mask, bring to - front/back, distribute, tidy up, layout grids, pixel preview), Text-only and canvas-only bindings - (Cmd/Ctrl+K: link while typing, Actions on the canvas), Cmd/Ctrl+/ for Actions, `+`/`-` zoom, - Shift+0/1/2 from any layout, Cmd/Ctrl+0, hold Z for the zoom tool (Alt zooms out, drag an area), - two quick digits for an exact opacity, boolean keys (Alt+Shift+U/S/I/E), remove fill/stroke - (Alt+/, Shift+/), outlines on Shift+Cmd+O, pixel grid on Shift+' (and Cmd+'), Shift+E / Shift+D and the other panel keys, Ctrl+Tab between files, Shift+B (and B) for the paint bucket in vector edit mode; the Keyboard - shortcuts panel docks at the bottom with a tab per group and search, keycaps per platform. - A shortcut parity suite (`shortcut_parity_tests.rs`) checks every documented shortcut on both - platforms (129 rows: 128 bound, including moving the rotation origin with Alt+R and placing an - object with Enter from the toolbar; 1 moved off a documented conflict) and pins the 48 undocumented - bindings; others' cursors (`Alt+Cmd+\`), list levels while editing (Cmd/Ctrl+] and [), the - keyboard box selection on Alt+Space / Ctrl+Space, arrows that pan with nothing selected, - Control to suspend snapping on macOS and Space/N/C/F while presenting landed with it - ([ui-parity](ui-parity.md#keyboard-shortcuts)). - *Left:* tool by tool, panel by panel against the incumbent (cursors, modifier keys while - dragging, hover states, menus, popovers); per-keyboard-layout shortcut tables. 40–60 h. -8. **Collaboration (local):** comments landed: pins pinned to frames and layers that move with them - (to other pages too) and stay where they were when the layer is deleted, your pins dragged onto - another frame, crowded pins clustered when zoomed out (a click zooms them apart), Shift+C to show - or hide them; threads where you edit and delete your own messages, emoji reactions, @mentions - with a picker of the names used in the file (your name from Preferences), read and unread state, - resolve, and "Copy link" (`figcraft://comment/`: pasting or `comment.open` goes to the page, - zooms to the frame and opens the thread); the Comments panel with search (text, names, frame), - filters (open, resolved, yours, unread), sorting by date or page, each thread's frame and page, - and Up/Down to step through threads; comments on the presented frame (Comments button or - Shift+C: read and reply). All of it through `comment.*` commands. Version history - landed (named versions saved in the `.figcraft` file; restore, rename, duplicate, delete; panel; - view a version on the canvas read-only before restoring it; autosave checkpoints every 30 - minutes, folded in the panel). A behaviour suite written from the public documentation covers - both: restoring keeps the comments and first keeps the replaced design as an autosave, a titled - autosave becomes a named version, merging a branch records the main file before and after, and - threads made in a live session can be replied to and resolved. Real-time collaboration is designed (`docs/collab-protocol.md`: - WebSocket sessions the app can host itself, server-ordered per-property changes, live cursors) - and its core landed (`crates/collab`, no networking yet): client-scoped node ids, every saved - layer field and the document-level data (styles, variables, comments, images…) as synced - properties, diffs after a command that cost the size of the edit, fractional sibling positions, - a session server that orders and validates batches (cycles, deleted layers, id spaces, sizes) - and a client that shows its own edits at once and replays them over others'; derived data - (auto layout, instances, bindings) is repaired by the author after concurrent edits; binary - messages (a moved layer is 43 bytes, a cursor 23) with a JSON form; a randomized multi-client - test over real commands converges. Live sessions run over the network (hosting from the app, - invite links, document sync, live cursors, selections, avatars, follow). Undo is per person in - a session (`edit.undo` takes back only your own steps, leaving what others changed since; - creations and deletions invert; a gesture or command group stays one step); refused edits roll - back with a toast; a dropped connection keeps editing and resumes as the same client with what - it missed (or a snapshot), then sends what was made offline. Randomized sessions with undo, - redo and dropped connections converge, and a checker confirms no undo reverts someone else's - change. - Plugins landed: sandboxed WebAssembly plugins (a metered wasmi - interpreter with a memory cap and no WASI, also in the browser) whose API is the command - registry; each run is one undo step and is rolled back when it fails (error, trap, endless loop, - memory); a `figcraft-plugin.json` manifest declares menu commands with parameters and read or - write access with an optional command allowlist, and files, other documents, the undo history, - dialogs and other plugins are always off limits. Plugins ▸ Development ▸ Import plugin from - manifest… (kept in the preferences, the module re-read on every run), the installed plugins in - the Plugins menu with a parameters form drawn from the manifest, Run last plugin (⌥⌘P), Manage - plugins, and `plugin.*` commands for scripts, the CLI and MCP; a Rust SDK and two example - plugins (Tidy names, Swatch grid) in `plugins/`, built by `cargo xtask plugins`. Then plugin - panels (ABI v2: fields and buttons drawn with FigCraft's widgets, the plugin called back for - each button or changed value as one undo step, `plugin.ui` / `plugin.event` / `plugin.close` - for scripts and MCP) and per-plugin settings kept with the preferences. Then widgets (API 3: - a plugin describes a layer of its own, validated and built as a frame with locked layers; clicks - on its named parts call the plugin back in a fresh sandbox with the state kept in the layer, one - undo step each; `widget.insert` / `widget.click` / `widget.list`; a Counter example), panels of - several plugins side by side and the colour picker on plugin colour fields. Then a plugin - directory on `figcraft-server`: bundles published with the server key and checked in full, - browsed and installed from Plugins ▸ Manage plugins after a permissions confirmation, downloads - checked against their SHA-256, updates by version. *Left:* a public catalogue, publisher - accounts and signatures; richer widget events; the directory in the browser. 8–26 h. -9. **Hardening and performance**: *landed:* `cargo xtask bench` (synthetic 1k/10k/50k-layer - pages: engine, render and headless UI-frame timings). The canvas keeps its last image and - re-renders only what a pan uncovers or an edit touches: moving one layer at 10k layers re-renders - in ~0.2 ms instead of ~8 ms (0.8 vs 24 ms at 50k zoomed out), a 40 px pan in ~0.7 ms instead of - ~7.5 ms. The layers panel lays out only the rows in view (every screen expanded at 50k: 39 → - 0.6 ms a frame); text layouts are shared instead of re-formatted per lookup; overflowing text is - culled by its glyphs. At 10k: full 1440×900 renders 7–15 ms, single-layer edits ~10 µs, hit - tests ~26 µs. Zoomed out (50% and below) the canvas draws levels of detail: layers under 2 px - land as their averaged colour, text under 4 px is greeked into bars, small rectangles and clip - layers skip the path pipeline (exports and closer zooms stay exact): a whole 50k-layer page - renders in ~8 ms instead of ~50 ms. `.figcraft` at 50k layers saves in ~0.09 s instead of ~1 s - and loads in ~0.16 s instead of ~0.86 s (JSON streamed to and from the document without a - value tree, layer fields at their defaults left out: 63 → 22 MB of JSON, old files still - open). The retained canvas is held to a full render: a seeded random walk (edits, undo/redo, - pans and zooms over nested, rotated and clipping frames, groups, masks, booleans, auto layout, - effects, text, instances, variables and styles) checks every step, and found clip edges on a - pixel boundary letting a column through in some renders. Drags show live on the canvas (every - finished render is shown, not only the newest one asked for), and a button held still no - longer rebuilds the preview and re-renders every frame. - Components at scale (`cargo xtask bench`, synthetic design system of about 300 - components used by 2,000 instances): edits of mains reach their copies directly instead of - rebuilding every instance (value, size and layer added/removed edits; other edits rebuild each - affected instance once, inner mains first), bound variables no longer touch unchanged layers on - every edit, auto layout re-flows only where sizes or places changed: recolouring or resizing a - main's layer used ~900 times 188/226 → 4/18 ms, adding a layer to it 430 → 21 ms, a label style - in all 72 button variants (1,800 copies) 450 → 57 ms, switching a variant, reset, detach, push - and paste ~450 → under 3 ms, accepting a library update 460 → 64 ms; the Assets panel with 2,000 - assets 3.4 → 1.3 ms a frame. Accessibility: the custom-painted - controls name themselves for screen readers (AccessKit on desktop: role, name, value and state - for tools, layer rows with their type, level, selection and hidden/locked state, fields, - dropdowns, tabs, menu items, and the canvas, which describes the selection); keyboard navigation: - F6 / Shift+F6 move between the toolbar, layers, canvas and properties, Tab / Shift+Tab move - within a region with a focus ring, Enter or Space activate, the arrows go along the toolbar and - through the layer rows, menus and tool flyouts opened from the keyboard take it and give it back, - Escape returns to the canvas (where Tab selects the next layer again instead of wandering into - the tab strip); reduced motion follows the system's setting or a preference; secondary and - accent text meet WCAG AA in both themes. - An idle window draws no frames: it redrew ~240 times a second - (the window title was re-sent every frame, and each viewport command asks for another), ~33% of - a core in a debug build, and now sits at 0 frames and ~0% CPU; the toast, the autosave - checkpoint and the presenter's "after delay" timers wake it at their deadline, the interaction - editor's curve preview plays when it shows, changes or is hovered, and a screenshot keeps frames - coming only until it arrives (`ui.inspect` `framesDrawn` and `repaintCauses` show what asked for - frames; headless tests hold it). *Left:* a real-world file corpus, Windows/Linux/browser - QA, runs with real screen readers (NVDA, Narrator, VoiceOver, Orca), the AccessKit tree in the - browser (eframe's web backend doesn't send it yet). Landed since: F2 rename from the keyboard, - Shift+F10 context menus at the focused layer row or the canvas selection, keyboard box selection - (F8). 27–44 h. - -## Inventory: what exists today - -Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count there was stale: 963 `#[test]` functions as of this re-measure. - -- **Architecture:** 15 crates with enforced layering (`cargo xtask layers`): geom, color, doc, - layout, text, pathops, plugin, collab, render, format, svg, engine, ui-egui (+ mcp, pdf). Every action - is a command (449 command ids); undo is unlimited via structural sharing (`Arc` node snapshots); gestures are - one undo step; `command.batch` runs several commands as one step. -- **Document model:** pages, frames, groups, sections, rectangles, ellipses (arcs/rings), polygons, - stars, lines/arrows, vector networks, text with styled runs, boolean groups, components, component - sets (variants), instances with per-layer overrides, slices; fills (solid, linear/radial/angular/ - diamond gradients, images with fill/fit/crop/tile + adjustments), multiple strokes with - inside/center/outside alignment, per-side weights, dashes, caps/arrowheads, joins; effects (drop - and inner shadow with spread that offsets the outline exactly: ellipses, stars, vectors, text; - inner shadows over the fills and under the children and strokes; drop shadows hidden behind what - the layer covers, translucent fills included, unless shown behind transparent areas; layer blur; background blur - that sees the whole backdrop, inside clipping frames, faded groups and masks too; both blurs - uniform or progressive (the radius ramps between two points, dragged on the canvas); noise in - mono, duo and multi colour with grain size, density and blend mode; texture with clip to shape; - glass with light angle and intensity, refraction, depth, dispersion, frost and splay; the same in - PNG, PDF and SVG exports, SVG embedding renders of noise grains, texture, progressive and - background blur and glass); corner radii with - smoothing; constraints; auto layout (horizontal, vertical, wrap, grid; hug/fill/fixed; min/max; - absolute children); layout grids; export settings; prototype interactions, flows and transitions; - paint/text/effect/grid styles; variables with collections, modes and aliases. -- **Renderer:** CPU (tiny-skia) scene renderer used for canvas, export and the web: isolated layers - for opacity/blend/effects, clip content, masks, booleans (linesweeper), text (harfrust shaping, - skrifa outlines, font fallback), aligned strokes with caching. -- **Engine:** pointer tools (move/marquee/deep select/resize/rotate/Alt-duplicate with smart-guide - snapping, frame/section/slice/shapes/line/arrow with Shift/Alt, text, pen, pencil, hand, - eyedropper), Cmd/Ctrl-click and Cmd/Ctrl-marquee into nested layers, Shift-marquee toggling, - Select layer (every layer under the click), measuring (Alt-hover distances from the selection to - layers, parents and ruler guides, Cmd+Alt for nested layers, from a guide being dragged to the layers of its frame, extension lines, one box inside the - other, overlaps, live distances to the neighbours while Alt-dragging, Shift+Alt for auto layout - padding and gaps; labels that never overlap at any zoom), text editing (caret, selection, words, - lines), auto layout reorder by dragging, drop into frames, align/distribute/tidy, z-order, - flip/rotate, the scale tool (panel, and canvas handles that scale everything about its anchor - point or from the centre with Alt, with a live factor, one undo step per drag), booleans, flatten, - outline stroke, components (create, instances, sync with overrides, reset, detach, push changes, - variants, properties, slots), styles and variables, prototype commands, find/replace, batch rename, - copy/paste (internal + SVG/text), file new/open/save/import/export (native `.figcraft`, SVG - in/out, PNG/JPG/WebP), version snapshots. -- **UI (egui, swappable):** the incumbent's 2026 layout: tab strip that is also the window's title - bar (frameless on Windows/Linux with drawn window buttons, drag, double-click maximise and edge - resizing; macOS window buttons inset into it), icon rail, file header, pages and - layers tree (rename, visibility/lock, drag to reorder/reparent), Assets panel, floating bottom - toolbar with flyouts and mode switcher, Design / Prototype / Inspect panels (Dev Mode: properties with their styles and variables, annotations - with pinned properties and categories drawn as coloured callouts on the canvas (filtered by category), - a ready-for-dev list, a focus view on one frame with previous / next, Compare changes since a frame - was marked ready (before / after side by side or overlaid, property-level diffs), CSS/HTML/React/SwiftUI/Compose - code where components take their properties as parameters and instances are calls, components - connected to their code components showing the real snippet, read-only plugins), colour picker - (HSB square, hue/alpha, hex, gradient stops, image fills), main menu generated from the catalog, - context menu, Actions palette (⌘K), keyboard shortcuts from one keymap per platform (a tabbed, - searchable Keyboard shortcuts panel), keyboard navigation (F6 between regions, Tab within them, a - focus ring) and screen-reader names for the custom controls (AccessKit), presentation player with - transitions, light and dark themes, home screen, About/Preferences/shortcuts/variables dialogs. -- **Automation:** desktop control channel (`--control `: execute commands, inspect document/UI, - synthetic pointer/keys/typing, menu invocation, screenshots, renders). -- **Tests:** 963 `#[test]` functions (2026-10-10) including junk-parameter sweeps over every command (and every vector-edit command while editing), importer fuzzing and render regressions. - -## Revision history - -| Date | Change | Summary | -|---|---|---| -| 2026-10-15 | minor | M4: Outline stroke preserves even-odd stroke holes. Pixel comparisons cover alignment, dashes, individual side weights, vector paths and stroke-only opacity; native save/reopen, Undo/Redo and instance opacity inheritance/overrides are covered by `outline_stroke`. Existing estimates unchanged; filled-shape compositing needs separate work. | -| 2026-10-14 | minor | Russian catalog (M9), reviewed and approved by VanTan: all 2,165 current entries, Русский in Preferences and regional ru tags. Russian integer plurals use one/few/many; contributor display name VanTan added. Existing estimates unchanged. | -| 2026-10-13 | minor | Vector edit mode regions: the filled area under the pointer is hatched (clear of points and segments, holes left out, spacing by octave folding fitted to measurements at several zoom levels, fixed to the shape); click, Shift-click and drag select and move areas with their points (drawn unselected); a click on an empty spot clears them and stays in edit mode (see [ui-parity](ui-parity.md)) | -| 2026-10-13 | minor | Plugin log limits (#191): cap each UTF-8 line at 8 KiB, total log bytes at 1 MiB and lines at 1,000 per run/callback before copying guest memory or forwarding to the host. Over-budget runs fail and roll back document edits; callbacks get fresh budgets. Added sandbox boundary/forwarding tests and an engine rollback regression. Percentages and hours unchanged. | -| 2026-10-13 | minor | Component playground (M3): with an instance, a component or a component set selected, the Inspect tab shows a Playground: a preview of the component and a control for each variant, boolean and text property. What is tried there changes the preview and the code in the Code section, never the file (no undo step, the canvas instance stays as it is); Reset goes back to the component's or the instance's own values. `component.playground {id?, properties?, language?, path?}` gives the same to scripts: the properties with their values, the code, and a PNG on request (`playground_tests`). Left: instance-swap and slot properties in the playground (the command takes an instance swap; the panel has no control for it), a code connect template's snippet with the tried values. Numbers unchanged. | -| 2026-10-13 | minor | Auto layout, on-canvas handles of automatic gaps (M2): the handle between spread rows or columns (and between items with an auto gap) shows the distance the layout gives the gap instead of the stored value, and dragging it makes the gap a fixed one from there, as typing a gap in the panel does; one undo step (`gap_handle_tests`). Before, the drag changed a value that had no effect. The documentation is silent on dragging an automatic gap. Numbers unchanged. | -| 2026-10-13 | minor | Variables inside styles (M3): a shared style can bind variables to fields of its value (`style.bind {style, field, variable?}`: a paint style's colours and gradient stops, an effect style's colour, radius, spread and offsets, a grid style's size, count, gutter and offset, a text style's font size, line height, letter spacing, family and style). Every layer using the style shows those fields resolved in its own modes and stays linked when a variable or a mode changes; the style keeps them resolved in the default modes. A style made from a layer keeps that layer's bindings; a new value drops bindings to what is gone. The style editor binds from its property rows (paint colour, shadow colour, blur radius, grid size) and detaches from the variable's chip; the style picker marks styles that use variables. `.fig` export and import carry the bindings on the style, REST layers hand theirs to the style they use. Left: binding text style fields and the other effect and grid fields from the editor (commands only), library variables in the picker, a style's library copy. Numbers unchanged. | -| 2026-10-13 | minor | Effects on groups, inner shadows and background blur (M3): a group is no outline of its own, so its inner shadows are cast inwards from around what its layers draw together (their pixels, over them: no shade at a seam between two layers) and its background blur and glass show the backdrop through the outlines of its layers (under them). Canvas and PNG; SVG embeds a group with an inner shadow as a render and its background blur as an image under the layers; PDF places both as images, the blur matching the PNG. Where the group holds one shape the pixels match that shape, or a frame filled like it, carrying the effect (`group_effects_tests` in render, svg and pdf). Offsets and spreads of a group's inner shadow go by whole pixels. Numbers unchanged. | -| 2026-10-13 | minor | Text made by commands on a dark page (M1): in the app, `text.create` and `node.create {type: text}` that name no fill give white text on a dark page, by the same rule as the Text tool (the page's colour decides, also inside a frame); a named `fill`, `fills` or `fillStyle` is kept. Sessions without an interface (CLI, MCP, exports) still make black text everywhere. Numbers unchanged. | -| 2026-10-13 | minor | Frames and groups, a one-step convert (M1): `object.convertToGroup` and `object.convertToFrame` (the Design panel's type menu on a frame or a group) change the layer in place, its layers staying where they are on the page. A frame turned into a group loses what only a frame has (auto layout, with the positions it gave kept and fill children fixed; fills, strokes, corner radius, clipping, layout grids, guides, scrolling) and takes its layers' box; a group turned into a frame keeps its box, with no fill and no clipping. Instances, main components, layers inside instances and empty frames are refused with the reason, all or nothing; one undo step. Numbers unchanged. | -| 2026-10-13 | minor | Prototyping, delays on mouse triggers and manual overlay offsets (M5): mouse enter, leave, down and up wait `delayMs` before their actions run (once while waiting; dropped when the screen or overlay they were on goes away; a pointer that leaves early doesn't cancel, the documentation is silent), and a manual overlay sits at `offset: [x, y]` from the top-left of the layer that opens it (without one, right below it, as before). Delay and X/Y fields in the interaction editor, the player on its own clock, REST import (`delay`, `overlayRelativePosition`), `.fig` import and export (the export's field for mouse delays is unchecked against other software). Numbers unchanged. | -| 2026-10-13 | minor | Auto layout, wrapped-line alignment (M2): a wrapping layout's lines (rows of a horizontal one, columns of a vertical one) are packed where the cross alignment says (start, centre, end; before, they always sat at the start), stretch when every child fills across the flow, or take the free space between them (`contentAlign: spaceBetween`, the Design panel's Row/Column gap mode, the alignment pad lighting the whole cross axis). Read from REST JSON and `.fig`, written to `.fig`, `align-content` in CSS, an instance override with the other alignment settings. 13 hand-computed layout cases and command tests. Numbers unchanged. | -| 2026-10-13 | minor | Starter templates and a first run that explains itself: eight templates built in code through the command layer (mobile app, landing page, dashboard, design system starter, wireframe kit, social post sizes, presentation slides, icon set starter; original and synthetic, deterministic, about 10–190 ms each to build), `template.list` and `file.newFromTemplate` for the app, CLI and MCP, File ▸ New from template…, a Templates tab on the home screen (cards with thumbnails rendered from the documents, search, kind filter, keyboard and screen-reader names) in place of the Community placeholder, a row of templates on Recents while there are no recent files, a Getting started card that can be closed and comes back from Help ▸ Getting started (`help.gettingStarted`), and a first run that opens on the home screen. The Drafts placeholder is gone (the app has no drafts folder). Essentials: discoverability ×0.92 → ×0.94 (templates and first-run tips exist; no Community files, no tour, no user evidence), 77.8% × 0.92 × 0.94 × 0.88 = 59.2%, ~57% → ~59%; essentials hours ~60–110 h → ~55–100 h (the onboarding and templates item is done); menu breadth stays 211/211 (the two new menu items are FigCraft's own and not counted); every other number unchanged | -| 2026-10-12 | minor | Everyday editing partials closed (gap 4): number fields scrub a step a point (×10 with Shift, ×0.1 with Alt, on past the screen edge where the platform reports it), step with the arrow keys by the nudge preferences, take brackets, units and relative edits (`+10`, `*2`, `50%`), commit on Tab and nothing when left untouched, and on Mixed apply relative edits to each layer in one undo step; outline mode's Include hidden layers and Include object bounds (`view.outlinesHiddenLayers`, `view.outlinesObjectBounds`), drawn and clickable; Hide when publishing from the Assets panel (`component.hideWhenPublishing`); a cursor for every tool, modifier and hover target (rotate turned to its corner, pen states, eyedropper, comment, copy, not-allowed), with guides and handles keeping theirs; panel widths with limits, double-click reset and the preferences (`view.panelWidths`); a file or page made in the dark theme has a #1E1E1E page (sessions without an interface keep #F5F5F5) and text drawn on a dark page is white. Menu breadth 211/211. UI/UX fidelity 58% and the interaction pass 40–60 h unchanged at this rounding: the side-by-side comparison is what remains | -| 2026-10-12 | minor | Text metrics, first measured fidelity data (gap 3): our example documents, exported and re-saved by other software, were read back and every text layer's stored box, line heights, baselines and glyph positions compared with ours. Three rules explained every row and were applied: a line's height is rounded to whole pixels (halves up; Auto, % and px alike; the baseline keeps its fraction inside the rounded line); glyph advances and kerning are each rounded to 1/1024 em before scaling by the font size; letter spacing is not added after a line's last visible glyph. Before: 24 of 131 text boxes of the design-system example matched (118 in width, 24 in height); after: 131 of 131, and all 230 self-sizing text boxes across the 7 documents (Inter only, 11–120 px, letter spacing −6% to 8%, one wrapped paragraph). Glyph positions within 0.001 px: 223 of 1,832 → 1,781; the rest are kerning pairs at weights where the installed font files differ slightly, at most 0.05 px. Frames and other layers matching in size and position: 115 of 231 → 208; the other 23 hang on one grid frame whose row positions came back as written. Not measured: other font families, fractional px line heights, centred or right-aligned text with letter spacing, lists, mixed sizes on a line. Wording only; no number moves | -| 2026-10-12 | minor | `.fig` export (gap 1): written files were checked by opening them in other software: layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft; effects such as glass and noise, images and prototype interactions are still unchecked there, and variables are not written. Wording only; no number moves | -| 2026-10-12 | minor | A public `figcraft-server` (open hosting): with `open_publish = true` anyone publishes a file without the server key and gets its edit and view links plus an owner token (128 bits, shown once, stored hashed) that lists, makes and revokes its links, renames it or removes it (`collab.manageFile`); limits per address (connections at once and a minute, messages a second, publishes an hour, files and MiB kept; IPv6 by /64; `X-Forwarded-For` believed only from `trusted_proxy`; tables capped at 10,000 addresses) and for everyone (`max_total_mb`), each refusal saying why; files published without the key expire after `idle_days` unopened (the date shown to everyone in the file); moderation (`files list --sort size|age|idle`, `block-ip`, `reports`, a Report button and `collab.report`); the operator's contact and terms shown in the Share dialog; `GET /healthz` on the same port; `packaging/server/install.sh` for a Debian/Ubuntu machine behind Caddy and a nightly backup timer. Protocol additions within version 2, gated like signals (older apps get clear refusals). Hosting infrastructure rather than a Figma feature: percentages and hours unchanged | -| 2026-10-12 | minor | Experimental `.fig` export (gap 1): the whole document written from the same public format notes as the import, verified only by round trips through our own importer. Import/export breadth 84% → 86%, ready 57% → 58%; file formats 59% → 60%; file exchange with Figma users stays ×0.85 (nothing shows another app opens the files); mainstream hours ~130–244 h → ~120–229 h (`.fig` export 15–30 h → checking it elsewhere 5–15 h); feature breadth (~84%), ready for real work (~53%), essentials (~57%), mainstream (~49%), beta and full parity hours unchanged at this rounding | -| 2026-10-12 | minor | Keyboard shortcut parity suite and bindings (gap 4): a table test with one row per shortcut in the public documentation, macOS and Windows/Linux keys each (129 rows: 126 bound, 110 before; 1 moved off a documented conflict; 2 missing; 2 kept by a platform), plus 48 pinned undocumented bindings so every default binding is in the table; newly bound: A (Frame), Shift+' (pixel grid), Alt+Cmd+\ (others' cursors, new `view.multiplayerCursors`), Ctrl+F6, Shift+Cmd+U (link), Cmd/Ctrl+] and [ (list levels while editing, new `text.listIndent` / `text.listOutdent`), Cmd/Ctrl+Backspace (ungroup), Alt+Space / Ctrl+Space (keyboard box selection, Cmd/Ctrl+arrows draw it out), suggest auto layout, go to main component, arrows that pan with nothing selected (new `view.pan`), Control suspending snapping on macOS, Space/N/C/F while presenting; symbols another layout types with Shift count without it; no default binding uses a system-reserved chord (tested). Menu breadth 207/207 (View ▸ Multiplayer cursors). UI/UX fidelity 58% and the interaction pass 40–60 h unchanged at this rounding | -| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Localization 36% → 39%, 31–54 h → 29–51 h; ready for real work unchanged (~53%); full parity ~267–527 h → ~265–524 h | -| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Localization 35% → 36%, 32–55 h → 31–54 h; ready for real work unchanged (~53%); full parity ~268–528 h → ~267–527 h | -| 2026-10-12 | minor | Prototyping tail (gap 15, the part that doesn't need video): key/gamepad triggers take controller buttons (`Gamepad A` … `Gamepad Home`, the standard layout's 17 buttons, from a Controller menu beside the key field; the REST import maps controller triggers instead of skipping them); the player reads controllers on Windows (Windows.Gaming.Input through the `windows` crate the app already used; no new dependency) and in the browser (the Gamepad API), polling only while the prototype has such a trigger and the window has the focus, and the control channel presses buttons (`ui.input {gamepad}`); macOS, Linux and FreeBSD read no controllers yet. Widgets react to clicks in a presented prototype (the canvas's callback and sandbox limits, on the prototype's copy: the file and its history don't change, restarting starts over). `proto.evaluate` computes an expression where a layer's interaction would run (MCP); the mode lookup and set-variable-mode rules are cases in a behaviour suite (they already held). Prototyping breadth 90% → 92%, ready unchanged at 74%, 3–6 h → 2–5 h; area hours 120–260 → 119–259 (core 33–75 → 32–74), Features (depth) 29–68 h → 28–67 h; percentages unchanged at this rounding; full parity ~276–538 h → ~275–537 h | -| 2026-10-12 | minor | Plugin directory on `figcraft-server` (gap 6): the server keeps plugin bundles (manifest, module, up to 32 assets; an uncompressed length-prefixed format named by its SHA-256) published with the server key (`figcraft-cli plugin publish`, `plugin.directory.publish`), checked in full before they are kept (size cap, manifest and plugin API version, a version newer than the one kept, the module compiled but never run, plain asset names) and capped (`max_plugins`, `max_plugin_mb`, `max_plugins_mb`); anyone who reaches the server lists, searches and fetches them (hash-checked again on read, a shared download budget); the operator moderates with `figcraft-server plugins [remove]`. The app's Plugins ▸ Manage plugins gains a Browse tab: a server's plugins with Install / Update / Remove, an install confirmation that shows the permissions asked for, the source and the SHA-256, the download checked against the listed hash before anything is read, plugins kept per user in hash-named folders; `plugin.directory.list` / `install` (needs `confirm: true`) / `publish` / `unpublish` for scripts and MCP. Not in the browser build yet; no public catalogue, publisher accounts, signatures, ratings or background update checks. Plugins & widgets 55%/32% → 60%/35%, 10–30 h → 8–26 h; Ecosystem dimension 34% → 38%; weighted totals unchanged at this rounding; full parity ~281–548 h → ~279–544 h | -| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Localization 30% → 35% (drafts, not reviewed: graded low), 40–65 h → 32–55 h; ready for real work ~52% → ~53% (52.9 → 53.1 by the dimension table); full parity ~284–554 h → ~276–544 h; beta ~162–292 h → ~155–280 h (Japanese and Korean leave the beta list; their native reviews need people) | -| 2026-10-12 | minor | Plugin widgets and the panel tail (gap 6, plugin API 3; v1 and v2 plugins unchanged): a plugin marked as a widget puts a layer of its own on the canvas from Plugins ▸ Widgets or `widget.insert`: it describes the layer (frames with auto layout, text, rectangles, ellipses, images it carries) and FigCraft validates the description (size, layer count, depth, numbers, colours, text and image sizes) and builds it as a frame with locked layers inside; a click on a part the plugin named (on the canvas with the Move tool, a pointing hand over it, or `widget.click`) calls the plugin back in a fresh, metered sandbox with the widget's state, as one undo step, and the widget is redrawn; the state lives in the widget's own layer data, so widgets survive save and reload, copying, undo, export and live sessions; no plugin can write that data or call `widget.*`. `widget.list` lists them. A Counter example widget. Plugin panels of different plugins open side by side, and a colour field's swatch opens the colour picker (the plugin hears of the colour when a drag ends). Plugins & widgets 45%/28% → 55%/32%, 15–40 h → 10–30 h; Ecosystem dimension 28% → 34%; weighted totals unchanged at this rounding; full parity ~286–558 h → ~281–548 h | -| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): every UI string, command label and menu wording (1,899 entries) machine-drafted in each and marked as drafts awaiting native review, falling back to English per key (new strings need only English and Spanish); 0 counts as singular in French and Brazilian Portuguese; System picks the first OS language with a catalog (pt-PT and pt read pt-BR); long translations elide with a tooltip in dropdowns, two-column labels and the home cards. Localization 20% → 30%, 45–75 h → 40–65 h; ready for real work unchanged (~52%; 52.6 → 52.9); full parity ~289–564 h → ~284–554 h; beta ~170–305 h → ~162–292 h (Figma's languages left on the way to beta: Japanese and Korean, 7–12 h) | -| 2026-10-12 | minor | Cursor chat and spotlight in live sessions: `/` opens a chat bubble at your pointer that everyone sees as you type and that fades a few seconds after the last change; a spotlight (your avatar's menu, `collab.spotlight`) makes everyone follow your view and page, latecomers included, until they move their own. Both are ephemeral presence the host validates and rate-limits, passed only to clients that read them. Multiplayer & sharing breadth 15% → 17%, ready unchanged (5%), 45–90 h → 42–84 h; the Collaboration dimension's hours 55–110 → 52–104 h (the dimension hours still add up to the total); feature breadth (~84%) and ready for real work (~51%) unchanged at this rounding; full parity ~301–587 h → ~298–581 h | -| 2026-10-12 | minor | Layers and Assets panels behaviour suite written from the public documentation (6 engine tests with about 50 table rows and 5 panel tests driven through the UI; about 30 rows that the code before the fixes contradicts) and its fixes: several layers dropped together in the layers panel land together (layers from apart split around the drop point); Collapse layers keeps the selection's rows open (it closed everything); the eye and padlock stay under the pointer (they vanished as it reached them, so clicks could miss) and dragging across them sets several layers in one undo step; several layers selected open a Rename layers dialog (match as text or a regular expression, Rename to, current name and number buttons, a start value, preview) instead of renaming the top one; `object.batchRename` counts `$n` up and `$N` down (they were swapped), pads with `$nnn`, takes `start`, regular expressions with `$1`…`$9`, `` $` `` / `$'` and codes in the replacement; pages are dragged to reorder, a blank page named with a hyphen is a divider (never opened, holds no layers), duplicates are named "Copy of …", the last page that isn't a divider can't be deleted; the Assets panel files components by page, frame and slash names in folders that fold away (`component.list` gives each `folder`); a leading `.` or `_` now keeps a component out of the library even after it was published, and removing it brings the component back. Pinned where the documentation is silent: Alt-click on an arrow, numbering in stacking order, hidden layers and locked frames taking drops, locking deselects. Layers panel 98%/82% → 99%/85%; percentages and full parity hours unchanged at this rounding | -| 2026-10-12 | minor | Vector multi-edit and the vector edit tail, with a behaviour suite written from the public documentation (5 tests, about 30 table rows and steps, and a junk-parameter sweep of every vector command in multi-edit mode): several vector layers are edited at once (Enter, double-click or `vector.edit.begin {ids}`; a press on another layer's point, handle or segment makes it the active layer; Shift-click, marquee and lasso select across layers; dragging, the arrows, Delete, delete and heal, mirroring, smoothing, corner radius and point caps and joins reach every layer's selected points in one undo step; the pen, bend, paint bucket, cut and eraser work in the active layer); several Bezier handles are selected with Shift-click and dragged together (`vector.edit.moveHandles`); a sharp point next to a curved segment rounds (the curve is cut where the arc meets it and keeps its shape); a point can have its own stroke cap and join (`vector.edit.strokeCap`, `vector.edit.strokeJoin`, the Point section of the Design panel; canvas, PNG, SVG and PDF, outline stroke); the Pen outside edit mode connects to one of its own points and ends the path, and continues a selected open path from an end point. Left: corner smoothing beyond rectangles, the transform box around points of several layers, per-point caps and joins on dashed, variable-width and brush strokes. Shapes & vector 98%/72% → 99%/73%, 2–4 h → 1–3 h; Figma Draw extras 80%/55% → 88%/60%, 2–5 h → 1–3 h (gap 9: pen pressure on macOS and Linux tablets left). Totals recomputed by the doc's method, which rows since the last recompute had left stale: the area table's weighted breadth is 83.7% (~83%; core 94.1%, ~94%) and ready 63.1% (~63%; core 72.1%, ~72%), hours 133–284 (core 38–83); Features (depth) 61% → 63%, 46–94 h → 34–76 h; ready for real work 52.6% (~52%); mainstream weekly areas 71.0% → 72.1% (× 0.9 × 0.9 × 0.85 = 49.7%, ~49%, rounded down); essentials unchanged (~57%); full parity ~306–595 h → ~294–572 h (on top of the rows merged since: ~301–587 h → ~289–564 h; area-table totals recomputed on the merged table and rounded down: breadth ~84%, ready ~63%, core ~94%/~72%, mainstream weekly areas 72.4%, ~49%) | -| 2026-10-12 | minor | Dev resources (gap 16): links for developers (a URL and a name) on any layer: `devResource.add` / `edit` / `remove` / `list`, undoable, saved and synced; an instance also lists its main component's (and that component's set's) links read-only with where they come from, and layers inside an instance show their main layer's; URLs are checked as web links (http or https, a host, no spaces, at most 2,048 characters) and never fetched (a link added without a name is named after its URL), opened only when clicked; the Inspect tab lists, adds, edits and removes them; `get_node` and `inspect_document` return them as `devResources`. The editor extension stays out of scope (the MCP server serves agents and editors). Dev Mode breadth 93% → 96%, ready 70% → 72%, 1–3 h → 0–1 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~304–593 h → ~303–591 h | -| 2026-10-12 | minor | Extended collections' tail (gap 14): variables a library adds to a collection this file extends now count as a library update of their own (listed by name without a copy yet) and arrive inheriting when accepted, instead of waiting for the library's next other update; a library collection that is itself an extended collection can be extended again, values flowing down the chain as each file accepts updates with every level's overrides kept (checked end to end). Styles & variables breadth 94% → 95%, ready unchanged (76%), 3–5 h → 2–4 h; gap 14 1–2 h → 0–1 h (real-library checks left); feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~305–594 h → ~304–593 h | -| 2026-10-12 | minor | A branch's own version history and finer list merges (gap 17): versions saved or autosaved on a branch tab go into the branch (viewed, restored, renamed, deleted and duplicated there, saved in the file with it); merging adds a version to the main file's history and the merged or archived branch keeps its own; fills, strokes and effects merge item by item, conflicting only when the same item changed both ways, an item was removed on one side or both sides added different items. Comments, versions, find, misc breadth 85% → 87%, ready 60% → 61%, 2–5 h → 1–4 h; gap 17 1–3 h → 1–2 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~306–595 h → ~305–594 h | -| 2026-10-12 | minor | Headline percentages recomputed from the area table by its own method (weights × written scores): feature breadth ~82% → ~84% (core editor ~93% → ~94%), weighted ready ~61% → ~63%, core ready ~70% → ~72%, mainstream weekly areas 71.3% → 72.3% and mainstream ~49% → ~50%; several earlier rows had moved the area scores without carrying them into the totals. Hours are unchanged (left for the next full re-measure) | -| 2026-10-12 | minor | Text lists, hanging punctuation and decoration styles, with a behaviour suite written from the public documentation (7 layout tests and 7 command tests, about 60 table rows): lists are set per paragraph (the list buttons, shortcuts and Type settings act on the paragraphs the selection touches, the whole layer when it isn't being edited), nest up to 8 levels (Tab and Shift+Tab; numbers go 1. a. i., bullets • ◦ ▪, counted per level), Enter on an empty item and Backspace at an item's start step out a level and then out of the list, typing `- `, `* ` or `1. ` at a paragraph's start starts one; list spacing (between items, in place of paragraph spacing), hanging lists (markers outside the box) and hanging punctuation (opening punctuation outside a start-aligned edge, closing punctuation outside an end-aligned one, taking no room when lines wrap); underline and strikethrough style (solid, dotted, wavy), thickness, underline offset, skip ink and colour. Canvas, PNG, SVG and PDF; `node.set` keys (`indentation`, `listSpacing`, `hangingPunctuation`, `hangingList`, `textDecorationStyle`/`Thickness`/`Offset`/`SkipInk`/`Color`), inspect (`lineTypes`, `lineIndentations`), CSS (`text-decoration-style`/`-thickness`/`-color`, `text-underline-offset`, `text-decoration-skip-ink`) and the REST import (`lineTypes` with `lineIndentations` per paragraph, which collapsed to one list style before). Text 87%/68% → 90%/70%, 5–10 h → 3–7 h; mainstream weekly areas 71.0% → 71.2%; other percentages unchanged at this rounding; full parity ~306–595 h → ~304–592 h | -| 2026-10-12 | minor | Comments and version history behaviour suite written from the public documentation (15 tests, about 80 table rows plus stepwise checks; 4 tests failed before the fixes) and its fixes: in a live session, threads made in the session couldn't be replied to, edited, reacted to, resolved, moved, deleted or opened (their ids, from their creator's id space, were refused as out of range); restoring a version brought back that version's comments, losing newer threads and replies (comments now stay, those on layers the version lacks staying where they were); restoring now keeps the replaced design in the history as an autosave first, unless a version of it was just saved or restored; giving an autosave a title makes it a named version (it could be dropped to make room); merging a branch adds the main file as it was and then a `Merged “”` version to its history. Pinned where the documentation is silent: pins go to the innermost frame under them, copies, duplicates and duplicated pages come without comments, replying reopens a resolved thread, versions stay with whoever saved them in a live session (a shared history needs accounts). Left: a branch's own version history, comparing two versions. Comments, versions, find, misc 85%/60% → 85%/62%, 2–5 h → 2–4 h; mainstream weekly areas 71.0% → 71.1%; percentages unchanged at this rounding; full parity ~306–595 h → ~306–594 h | -| 2026-10-11 | minor | Canvas navigation, rulers, guides, layout grids, outline mode and measuring behaviour suite written from the public documentation (18 tests, about 120 table rows and checks) and its fixes: the pixel grid shows from 400% (it waited for 800%); guides belong to the page or a top-level frame (a guide dropped over a nested frame goes to the frame holding it, and a frame moved into another keeps its guides but they stop showing, snapping and measuring); a guide can be selected, deleted with Delete, nudged with the arrows and removed from the context menu (`guide.select`, `guide.remove` without an index); Alt-dragging a guide drops a copy and holding Alt while dragging one measures to the nearest edges in the frame it crosses; columns and rows count Auto (as many fixed-size ones as fit); layout grids don't show or snap on a rotated frame; in outline mode shapes are clicked on their outlines, reaching layers behind. Left: outline mode's Include hidden layers and Include object bounds. Canvas & navigation 97%/72% → 98%/74%, 2–5 h → 1–4 h; feature breadth (~80%), ready for real work (~49%), core editor (~68%), essentials (~57%) and mainstream (~46%) unchanged at this rounding; full parity ~320–615 h → ~319–614 h | -| 2026-10-11 | minor | Export and inspect behaviour suite written from the public documentation (139 cases in 19 tests; 47 failed before the fixes) and its fixes: an export covers what the layer paints, so a drop shadow adds room only where it falls, strokes on boxes and ellipses reach their real width (they added up to four times as much), hidden layers take no room and a mask group exports the mask's area; a slice exports everything under it; slashes in layer names export into folders; Copy as PNG copies the whole selection. Added: the export settings' contents only and include bounding box, and for SVG outline text (off writes ``), unique id attributes and simplified strokes (stroke attributes where exact); Inspect colour formats (hex, RGB, CSS, HSL, HSB), CSS in rem, a Blend row, positions measured from the frame around a layer (groups aren't frames), CSS for rotation, blend modes, gradient directions and kinds, text alignment, case and decoration; icons and images in the asset list (`file.exportLayers {assets}`). Import/export 80%/50% → 82%/53%; Dev Mode 90%/64% → 93%/70%; file formats 55% → 56%; mainstream weekly areas 68.1% → 68.4%; feature breadth (~80%), ready for real work (~49%), essentials (~57%), mainstream (~46%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | `.fig` import's tail (gap 1): vectors open as editable vector networks (points, curved segments, filled areas with their winding, per-point corner radius and handle mirroring, end caps from the end points), falling back to outlines when the network is missing or damaged; sticky notes, shapes with text, connectors, tables, code blocks, slides and similar layers import as the closest frames, text layers and vectors with a warning instead of being dropped; set-variable, set-variable-mode and conditional prototype actions come across. New parsers fuzzed on fixtures generated in code. File formats 55% → 58%, 8–15 h → 5–10 h; import/export breadth 80% → 82%, ready 50% → 54%; feature and area totals and ready for real work (~49%) unchanged; beta ~173–310 h → ~170–305 h; full parity ~321–618 h → ~318–613 h | -| 2026-10-11 | minor | Shapes and vector behaviour suite written from the public documentation (10 shape tests, 6 vector tests and 3 handle tests: about 70 table rows plus stepwise checks) and its fixes: rounded vector points are drawn and exported (the radius was stored only; a vector's corner radius now rounds every point); a boolean group looks like its top layer, a subtraction like the bottom one (always the bottom); Alt draws a line from its centre; stars keep to 3–60 points; delete and heal fits the curve through the deleted point; Shift-clicking a selected point at a corner of the points' box takes it out (it scaled); outline stroke turns the stroke's colour style into the fill's and keeps opacity and effects; the miter field shows the miter angle (it showed the ratio under that name); individual corners only for rectangles and frames; Option/Alt+Shift+F flattens; edit-mode keys reach the edit tools through MCP `key` (Delete removed the layer). Added: corner radius and star ratio handles on the canvas, Count and Ratio fields for polygons and stars, cut tool drags that divide a shape into layers, paint bucket drags across areas. Shapes & vector 97%/68% → 98%/72%, 1–2 h → 2–4 h (the suite found gaps the estimate didn't count: corner smoothing beyond rectangles, a join or cap per point). Totals recomputed by the doc's method, which earlier rows had left stale: the area table's weighted figures are breadth ~82% (core ~93%) and ready ~61% (core ~69%), not the ~80%/~58% (~91%/~68%) carried since #130; Features (depth) follows the weighted ready, 57% → 61%; ready for real work ~49% → ~51%; mainstream ~46% → ~48% (weekly areas 67.8% → 69.4%); the selection suite's area 2 hours (−3/−5) hadn't reached the totals, and neither it nor the text suite had reached the Features (depth) row: Features 60–115 h → 56–109 h, full parity ~321–618 h → ~319–615 h (−3/−5 for area 2, +1/+2 for area 4) | -| 2026-10-11 | minor | Paint and effects behaviour suite written from the public documentation (14 paint and 9 effects tests with table rows for fills, opacity, every blend mode by its formula, pass-through and Normal containers, gradient handles and stops, image scale modes and adjustments, colour models, the colour profile, shadows, blurs, effect order and exports; 3 command tests) and its fixes: groups and frames set to Normal blend their children among themselves first, and clipping frames no longer isolate their children (blend modes inside a clipping frame met only the frame's children, not its fill or what is behind it) — canvas, SVG (`isolation`; clips put on what blends, since a clipped group isolates) and PDF (isolated groups); plus darker is drawn (it was multiply) on the canvas and in PNG exports; drop shadows hide behind what the layer covers, translucent fills included, unless shown behind transparent areas, in SVG too (it never cut them out) and the SVG filter region reaches as far as the effects; Fill images cover the box to its edges (upscaled ones faded out at the edge); "Normal" names Normal, not pass-through, in commands and the blend menu (the menu's Normal set pass-through). Added: a blend menu for each paint in the colour picker. Paint 66% → 70%, 4–7 h → 3–6 h; effects 72% → 75%, 3–6 h → 2–5 h; percentages unchanged at this rounding; full parity ~321–618 h → ~319–616 h | -| 2026-10-11 | minor | Frames, groups, sections and masks behaviour suite written from the public documentation (15 frame, group and section tests, 6 mask tests) and its fixes: a layer a mask hides can't be clicked there (clicks reach what's behind; Select layer agrees), a mask group's box and exports are the mask's (plus any layers under it), sections stay on the page or in sections (moves, drags, pastes, grouping and new sections in frames refused or kept on the page), drawing a frame or section around layers puts those fully inside it in it, resizing a section leaves its contents where they are (and a frame resized with Cmd/Ctrl held keeps its layers' sizes too), and X/Y of layers in groups are measured in the frame (groups have no coordinate space). Frames, groups, sections, masks 95%/75% → 97%/80%, 2–4 h → 1–3 h; percentages unchanged at this rounding; full parity ~323–621 h → ~322–620 h | -| 2026-10-11 | minor | Styles and variables behaviour suite written from the public documentation (18 command-level tests) and its fixes: editing a styled fill, stroke, effect or layout grid list (directly, a paint row, an added fill or a bound variable) unlinks that style instead of being overwritten by the next style edit; applying a style replaces variables bound to what it sets; text styles go on text layers only; deleting a variable (or its collection) unbinds the layers using it, which keep their last value, and aliases to it take its values; bindings are type-checked (a colour variable can't drive a width) and number variables fill text content; aliases are checked when a variable is created. Added: duplicate, move (reorder, into a folder) and rename folder for styles, with a Local styles list grouped by type and folder (click to edit, right-click, drag to reorder); duplicate mode, set as default and move column for modes; per-platform code syntax (Web, iOS, Android) used by generated CSS and the variables JSON; font weight, paragraph spacing/indent and text content scopes for number variables. Found missing: variables inside styles. Styles & variables 92%/72% → 94%/76%, 1–2 h → 3–5 h; percentages unchanged at this rounding; full parity ~323–621 h → ~325–624 h | -| 2026-10-11 | minor | Prototyping behaviour suite written from the public documentation (7 model tests, 12 player tests with 21 gestures) and its fixes: on drag starts only for a drag its animation's way and the animation follows the pointer, key combinations (`Shift+K`) fire (and are recorded and imported), an overlay's after-delay timer starts when it opens, instances of interactive components play their variant's interactions and "Change to" changes the right instance, spring presets are physical springs that play until they settle, smart animate overshoots with spring and back easings, fixed layers draw above scrolling content; added scroll to offsets. Prototyping 88%/70% → 90%/74%, 5–9 h → 3–6 h; mainstream weekly areas 68.1% → 68.4%, mainstream ~46% → ~47%; other percentages unchanged at this rounding; full parity ~320–615 h → ~318–612 h | -| 2026-10-11 | minor | Components behaviour suite written from the public documentation (10 tests, 45 table cases) and its fixes: instances now keep overrides of auto layout padding, gap and alignment, layout grids, export settings, prototype interactions and every text property (resizing, paragraph spacing, lists…; these were lost when the main changed); moving, rotating, resizing or re-constraining layers inside an instance, and its auto layout direction, are refused (the instance itself resizes); switching variants keeps an override only where both variants had the same value (text layers keep theirs); swapping from the Assets panel keeps only text overrides; the default variant is the top-left one (instances, Assets thumbnails, code); hidden exposed instances hide their properties; pushing changes from a nested layer goes to the outer main. Added: variant property reordering and duplicate combination warnings. Components 95%/68% → 96%/72%, 6–12 h → 4–9 h; with #133's selection and transform change, core editor ready ~68% → ~69% and mainstream ~46% → ~47% (weekly areas 68.1% → 68.8%); full parity ~320–615 h → ~315–607 h (the totals now also carry #133's area 2 hours) | -| 2026-10-11 | minor | Embedded image colour profiles: placed and opened images are converted once, when decoded, from their own colour space into the file's (sRGB or Display P3): ICC profiles in PNG (`iCCP`), JPG (APP2), WebP and TIFF, matrix/curve and table profiles in RGB or grey (moxcms, already built for the `image` crate), and a PNG's `sRGB`, `gAMA` and `cHRM` chunks; images without a profile, or with one that can't be used (CMYK, damaged, over 4 MB), are taken as sRGB as before. The original file is kept (PDF exports still embed it); pixel exports carry the converted pixels with the file's profile. The image fill's details show the source profile (Unknown — treated as sRGB when there's none) and `image.place` reports it as `imageProfile`. Paint breadth 87% → 90%, ready 66% → 67%, 4–7 h → 3–6 h; feature breadth (~80%), ready for real work (~49%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | Layers panel keyboard and stroke variables: F2 renames the selected layer (also from a layer row with the keyboard, which gets it back; Escape there cancels), Shift+F10 (`view.contextMenu`) opens the context menu at the focused row or below the canvas selection and the menu takes the keyboard, F8 (`select.area`) draws a selection box for keyboard-only users (arrows move it, Shift+arrows size it, Enter selects like a marquee; agents pass a box), long lists checked at 10k layers (revealing a layer, expanding or collapsing everything and arrow keys take at most 1.3 ms a frame in the release UI bench; the revealed row takes the keyboard); colour styles on strokes were already complete, and each side's stroke weight now binds to a number variable (`strokeTopWeight`…, bindable fields in the Design panel and REST import). Layers panel 95%/78% → 98%/82%, 2–4 h → 1–2 h; Strokes 90%/72% → 95%/75%, 1–3 h → 1–2 h; mainstream depth 67.8% → 68.1%; feature breadth (~80%), ready for real work (~49%), essentials (~57%) and mainstream (~46%) unchanged at this rounding; full parity ~321–618 h → ~320–615 h | -| 2026-10-11 | minor | Text behaviour suite written from the public documentation (13 text cases, 5 command tests) and its fixes: CJK lines break between ideographs, kana and Hangul and before and after Latin words without breaking inside them, with the line-start and line-end rules (closing punctuation, small kana and the prolonged sound mark never start a line, opening brackets never end one); a style's line height, baseline and decoration come from its own font, so lines with CJK or emoji drawn by fallback fonts keep the spacing of the others; paragraph indent applies to start-aligned text only; carets, Delete and Backspace step over whole grapheme clusters (emoji sequences, flags, combining marks), which also shape with one font; double-click and word moves take one script's run in text without spaces. Added: vertical trim (cap height to baseline; `leadingTrim`, Type settings, REST import, inspect, CSS `text-box`). Text 85%/64% → 87%/68%, 7–13 h → 5–10 h; percentages unchanged at this rounding; full parity ~323–621 h → ~321–618 h | -| 2026-10-11 | minor | Selection and transform behaviour suite written from the public documentation (5 selection tests with 38 table rows, 11 transform tests) and its fixes: Alt-resizing shrinks about the centre too (it could only grow); groups, frames with Scale or stretch constraints and several layers with different rotations scale each layer along its own axes, keeping its rotation and flip (children turned 90° grew along the wrong axis, nested turned groups too); several layers dragged past an edge mirror and flip; a flipped layer reads back the rotation typed; resizes land on whole pixels with the pixel grid on; a press inside a layer smaller than its handles moves it and the nearest handle wins (the first in reach did), with its handles hidden under 16 px; locked layers selected in the layers panel get no handles and aren't dragged with the rest; the background of a top-level frame with layers starts a marquee inside it (it moved the frame); Select inverse skips hidden layers like Select all. Added: equal spacing snapping with labelled gaps, Cmd/Ctrl while resizing a frame ignores its constraints. Selection & transform 95%/68% → 97%/72%, 6–10 h → 3–5 h; core editor ready ~67% → ~68%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~345–666 h → ~342–661 h | -| 2026-10-11 | minor | `.fig` import, best effort (gap 1): `.fig` files open (best effort): the zip, its deflate/zstd chunks and the kiwi schema are decoded generically and mapped by field name onto the REST importer, so pages, the layer tree, geometry, paints (styles resolved), strokes, effects, text with styled runs, vectors (as outlines), auto layout (grid included), constraints, components, sets, instances (expanded from their main with overrides, derived layout and property values), styles, variables with modes and bindings, prototyping and image fills from the archive come across; anything else is a warning. Import/export breadth 65% → 80%, ready 35% → 50%, 30–60 h → 8–15 h; file formats 40% → 55%, 30–60 h → 8–15 h; feature breadth ~79% → ~80% (core ~90% → ~91%), areas ready ~57% → ~58% (core ~67% → ~68%); ready for real work ~47% → ~49%; essentials ~53% → ~57% (opening files people send ×0.80 → ×0.88); mainstream ~43% → ~46% (weekly areas 66.8% → 67.8%, file exchange ×0.80 → ×0.85); beta ~195–355 h → ~173–310 h; full parity ~345–666 h → ~322–620 h | -| 2026-10-11 | minor | Auto layout behaviour suite written from the public documentation (64 layout cases, 8 command tests) and its fixes: a frame is never smaller than its padding, only the frame's inside stroke counts when strokes are included (and new auto layout includes them), auto gaps stop at 0, grid `fr` tracks behave like the CSS `fr` unit (a track never gets less than its content), canvas stacking is drawn, exported and hit (it was stored only). Added: auto gaps evenly and around, vertical wrap, grid cell alignment, strokes and canvas stacking controls. Auto layout 95%/68% → 98%/74%, 6–12 h → 3–6 h; percentages unchanged at this rounding; full parity ~348–672 h → ~345–666 h | -| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | -| 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | -| 2026-10-11 | minor | Localization layer landed (gap 5): per-language catalogs baked in at build time with English text as keys and fallback, placeholders and plurals, command labels by id, Preferences ▸ Language (System follows the OS or the browser) applied live and `pref.language`; English catalog and a complete Spanish draft awaiting native review. Localization 10% → 20%, 60–100 h → 45–75 h; ready for real work unchanged (~47%); full parity ~363–697 h → ~348–672 h; beta ~200–365 h → ~195–355 h | -| 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | -| 2026-10-11 | minor | Plugin panels and settings landed (gap 6, ABI v2; v1 plugins unchanged): a plugin shows a panel of notes, fields (text, number, checkbox, menu, list, colour) and buttons drawn with FigCraft's widgets; a button or a changed value calls it back with every value (same limits per callback, one undo step per callback that edits, rolled back on failure), its module staying loaded while the panel is open; `plugin.ui` / `plugin.event` / `plugin.close` drive panels headless; per-plugin settings (64 KB) kept with the preferences; the Swatch grid example gained a palette panel that remembers the last palette. Plugins & widgets breadth 35% → 45%, ready 20% → 28%, 25–55 h → 15–40 h; ecosystem 20% → 28%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~373–712 h → ~363–697 h | -| 2026-10-11 | minor | Canvas fixes (gap 13 closed): boolean groups are clicked where their result is (a click in a subtracted hole reaches what's behind; with the group or an operand selected its operands are hit, so each can still be picked), a layer dragged inside a group stays in the group (frames and auto layout still take other drops), View ▸ Frame outlines and View ▸ Slices draw on the canvas, and the idle tests wait for the render thread instead of counting frames. Shapes & vector 1–3 h → 1–2 h (vector QA with real files left); percentages, feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~378–722 h → ~375–717 h | -| 2026-10-11 | minor | Stretch and scatter brushes and pen pressure landed (gap 9): eight brushes drawn in code (four stretched along the path, four scattered with spacing, size, rotation and scatter jitter from a seed) in the stroke settings, drawn everywhere strokes are and flattened by outline stroke; the Pencil records pen pressure as the stroke's width profile (Windows pens, browser styluses; macOS and Linux tablets report none) and can draw with a brush; Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–9 h; Strokes 2–4 h → 1–3 h; Hardware 50% → 55%, 15–30 h → 13–27 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~378–722 h → ~358–687 h | -| 2026-10-11 | minor | Text on a path and pattern fills landed (gap 9): text follows a path or shape (start, side, alignment; edited in place; canvas, PNG, SVG and PDF; SVG `` imports as text on a path) and pattern fills tile a layer (grid or half-shifted rows or columns, scale, spacing, alignment; live as the source changes; SVG ``, PDF tiling patterns); Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–10 h; Text 8–14 h → 7–13 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~375–715 h | -| 2026-10-11 | minor | Team libraries on `figcraft-server` landed (gap 10): a file's library is published to the server under the file and enabled in other files by a share link (per-asset versions; updates noticed in the background and accepted with overrides kept), the server counts which files use which components, and a whole library can be swapped for another by names; Team libraries breadth 40% → 70%, ready 25% → 40%, 10–20 h → 4–8 h; collaboration 12% → 13%, 55–110 h → 49–98 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~380–725 h → ~375–715 h | -| 2026-10-11 | minor | Branches landed (gap 17): File ▸ Create branch… copies the file into a branch tab that remembers its base; Review and merge shows both sides' changes (layers with Inspect-style property changes, styles, variables, comments) and the conflicts (the same property changed on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merging is one undo step and archives the branch; Update from main; branches are saved in the `.figcraft` file. Comments, versions, find, misc breadth 80% → 85%, ready 58% → 60%, 4–8 h → 2–5 h; collaboration 12% → 13%; menu breadth 204/204 (the catalog gained File ▸ Create branch…); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~378–722 h | -| 2026-10-11 | minor | Animated GIF fills and TIFF images (gap 12, without video and HEIC): GIF fills keep the original file, show the first frame on the canvas and in exports and play in the presenter and the preview (frames decoded on first play, at most 500 frames and 128 MB; always looping; the window wakes only for a GIF's next frame); TIFF images open, import, place and paste. Paint breadth 85% → 87%, 5–9 h → 4–7 h; gap 12 6–12 h → 5–10 h (video playback and HEIC left, both needing non-Rust decoders); feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | The last three catalogued menu items got commands (gap 21): File ▸ Open recent (recent files kept in the preferences, at most 20, a file that's gone greyed in the menu and dropped when picked, Clear recent), View ▸ Outlines ▸ Show outlines for selection and View ▸ Mask outlines: menu breadth 98.5% → 100% (203/203); Canvas & navigation breadth 95% → 97%, 3–6 h → 2–5 h; feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | -| 2026-10-11 | minor | Extended collections, variables import/export and gradient stop and variant property bindings landed (gap 14): Styles & variables breadth 85% → 92%, ready 70% → 72%, 4–8 h → 1–2 h; feature breadth unchanged (~79%); ready for real work unchanged (~47%); full parity ~385–730 h → ~380–725 h | -| 2026-10-11 | minor | Collaboration wording after the standalone server: `figcraft-server` keeps many files, persisted as a snapshot plus a log of what changed (compacted, checksummed, damaged files skipped and reported), restarts lose nothing confirmed and apps resume where they were; the app publishes its file to it; share links per file let people edit or only view (the server refuses a viewer's edits), are revocable and stored as hashes; TLS comes from a reverse proxy (the desktop app has none of its own yet); percentages and hours unchanged until the next re-measure | -| 2026-10-11 | minor | Live boolean editing, Design panel arc fields, the shape builder on strokes and open paths and one-sided variable widths landed (gap 13): Shapes & vector breadth 92% → 97%, ready 66% → 68%, 8–14 h → 1–3 h; feature breadth ~78% → ~79%; ready for real work unchanged (~47%); full parity ~390–745 h → ~385–730 h | -| 2026-10-11 | minor | Wording only: live sessions (#104), then the browser joining them and `figcraft://` invite links opening the desktop app; percentages and hours unchanged until the next re-measure | -| 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 | minor | Readiness-by-audience table: hours to ~95% for full (~400–750 h), mainstream (~180–330 h) and essentials (~85–160 h). Full number confirmed as an additive weighted sum; unchanged | -| 2026-10-10 | minor | Added the essentials user number (~53%: core features 77.8% × 0.92 launch/stability × 0.92 discoverability × 0.8 opening files people send) | -| 2026-10-10 | minor | Added the mainstream practitioner number (~43%: weekly areas 66.5% × 0.9 interaction × 0.9 stability × 0.8 file exchange) and user-evidence count (none). Checked that ready for real work (~46%) is built from the written dimension weights and scores; unchanged | -| 2026-10-10 | minor | Stage checked against the alpha gate: stays alpha | -| 2026-10-10 | major | Created from the ROADMAP's parity rubric, honest assessment and area narrative. Full re-measure against Figma desktop 126.10.7: added multiplayer, plugins, team libraries, Figma Draw extras and AI as weighted areas; split breadth from ready-for-real-work; core depth re-graded ~79% → ~67%; hours calibrated from git history; full parity 115–195 h → 420–790 h | +# FigCraft parity with Figma + +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (Outline stroke preserves stroke holes; reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS) + +The authoritative assessment of how close FigCraft is to Figma Design. [`ROADMAP.md`](../ROADMAP.md) +summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu checklist is +[`parity-checklist.md`](parity-checklist.md). Deeper checklists: +[`ui-parity.md`](ui-parity.md), [`file-format-parity.md`](file-format-parity.md), +[`hardware-parity.md`](hardware-parity.md), [`localization-parity.md`](localization-parity.md), +[`auto-layout-parity.md`](auto-layout-parity.md), [`components-parity.md`](components-parity.md), +[`prototyping-parity.md`](prototyping-parity.md), [`dev-mode-parity.md`](dev-mode-parity.md), +[`collaboration-parity.md`](collaboration-parity.md). + +## Headline + +| Number | Value | Kind | +|---|---|---| +| Menu/command breadth | **100%** (211 / 211 catalogued menu items have a live command) | **measured** (`cargo xtask parity`, 2026-10-12) | +| Feature breadth (does each Figma Design feature exist, weighted) | **~85%** (core editor areas 1–16: ~94%) | estimated from the area table below | +| Ready for real work | **~53%** | estimated (dimension table below: written weights × written per-dimension scores) | +| Essentials user | **~59%** | estimated ([below](#essentials-user): core features 77.8% × discounts 0.92 × 0.94 × 0.88) | +| Mainstream practitioner | **~50%** | estimated ([below](#mainstream-practitioner): weekly areas 72.7% × discounts 0.9 × 0.9 × 0.85) | +| Remaining to beta | **~155–280 h** one Opus 5.5 agent (~32–64 h wall-clock with 5 parallel agents) | estimated | +| Remaining to full parity | **~262–519 h** one agent (~83–145 h wall-clock with 5 parallel agents) | estimated | +| Stage | **alpha**: the alpha gate passes ([roadmap](roadmap.md#alpha-gate)); multiplayer and checking the best-effort `.fig` import on real files are beta gaps (see [ROADMAP](../ROADMAP.md#stage)) | | + +The previous figures (ROADMAP of 2026-10-10, "feature depth ~79%", "full parity 115–195 h") were +self-graded against a 16-area rubric that left out, by design, multiplayer, plugins, cloud +libraries and AI features, and that counted no file-format blocker. Figma users rely on all of +those every day, so this re-measure weights them in. The core-editor depth figure drops from ~79% +to ~67% because nothing has yet been checked against real Figma files or side by side tool by +tool; presence was being graded as behaviour in places. + +## Readiness by audience + +| Audience | Ready % | Opus 5.5 agent wall-clock hours to ~95% | Work that dominates | +|---|---|---|---| +| Full target (ready for real work) | ~53% | ~268–522 h | multiplayer (42–84), localization (29–51), plugins (8–26), the interaction pass (40–60), GPU canvas (30–60), AI (30–60), the `.fig` import's tail (5–10) | +| Mainstream practitioner | ~50% | ~120–229 h | core editor depth incl. the `.fig` import's tail (49–98), the interaction pass (40–60), real-file corpus and platform QA (25–50), checking the experimental `.fig` export opens elsewhere (5–15) | +| Essentials user | ~59% | ~55–100 h | the `.fig` import's tail so the files people send open well (5–10), core tool polish (25–45), interaction pass on the core tools (15–25), stability QA (10–20) | + +Hours are one agent working sequentially, calibrated from this repo's history (about 1 agent-hour +per 1,000 added lines, 1–2.5 h per feature PR, ~7 h per honest depth point in the polish tail; see +[how the hours are calibrated](#how-the-hours-are-calibrated)). Each audience's work is a subset of +the next. About 70–80% parallelizes across 4–6 agents on disjoint crates (roughly ÷4 in +wall-clock); the `.fig` reader, the multiplayer core and a plugin runtime each need one owner, and +native-speaker review, real-file licensing and screen-reader QA need people. + +The full number is an additive weighted sum over the dimension table; mainstream and essentials use +multiplicative discounts, as the standard says. + +## What was measured, and how + +- **Target and version.** Figma desktop for macOS, `CFBundleShortVersionString` 126.10.7, + installed at `/Applications/Figma.app`. FigCraft is measured as of `origin/main` d452da3 + (2026-10-10, PR #99). +- **The installed app, names and public listings only.** FigCraft is clean-room + ([`AGENTS.md`](../AGENTS.md)): nothing inside the bundle's code or web assets was read. From + `Info.plist`: one document type, `com.figma.document`, extensions `fig`, `jam`, `deck` + (MIME `application/x-figma`), and the `figma://` URL scheme. The `.lproj` folders are + Electron/Chromium locale packs (60+), not Figma's UI languages; the UI languages come from + Figma's help center (see [`localization-parity.md`](localization-parity.md)). The bundle ships + GPU, Plugin and Renderer helper processes (GPU rendering; a sandboxed plugin runtime). +- **The menu catalog.** `crates/engine/src/catalog.rs` holds Figma's main menu tree (202 items in + File, Edit, View, Object, Text, Arrange, Vector, Plugins, Preferences, Libraries, Help), written + from public documentation and black-box observation (local notes in the gitignored + `plan/figma/03-menu-tree.md`). It leaves out the Widgets menu and dynamic plugin lists, so the + 100% overstates breadth against the full menu a little. +- **From source** (no build; 2026-10-10): 449 command ids registered with `spec!`, 963 `#[test]` + functions in 177 files, ~108,800 lines of Rust in 13 crates + 3 apps + xtask (before #101–#103, which added the `plugin` and `collab` crates and ~7k lines), 140 key bindings + and 13 gesture rows in `figcraft_engine::keymap`, 21 files in the SVG import corpus, 0 + translated UI strings (no localization layer exists). +- **Figma's feature set**: help center, release notes and Config 2025 announcements (Figma Draw, + grid auto layout, slots, glass/noise/texture effects, progressive blur), known to the assessor up + to mid-2026, plus web checks on 2026-10-10 (supported image/video types, UI languages, pen + pressure in Figma Draw). +- **Side by side.** Look & feel was compared with Figma UI3 on the owner's Mac (screenshots kept + local, never committed). No real Figma file has been opened in FigCraft (no `.fig` reader), and + no rendered frame has been compared pixel for pixel against Figma's own export. + +## Weights + +Weights are the share of a typical product designer's working time in Figma Design, written down +so they can be argued with. Areas 1–16 keep the original rubric's weights (they sum to 94, not the 100 the rubric claimed); +areas 17–21 are added (sum 19) because Figma users depend on them. Percentages below divide by the +total, 113. +Out of scope as separate products: FigJam, Figma Slides, Sites, Make, Buzz, Weave (the `.jam` and +`.deck` document kinds belong to them). + +## By feature area + +Breadth: does it exist. Ready: depth, correctness and fidelity for real projects. Hours: one Opus +5.5 agent, sequential, tests included. + +| # | Area | Weight | Breadth | Ready | Remaining | Hours | +|---|---|---|---|---|---|---| +| 1 | Canvas & navigation | 6 | 98% | 74% | Outline mode's Include hidden layers and Include object bounds, Display P3 on screen, QA at extreme zooms (behaviour suite for navigation, rulers, guides, layout grids and outline mode landed: guide selection, Auto grid counts, outline-mode clicks) | 1–4 | +| 2 | Selection & transform | 8 | 97% | 72% | Handles, measuring and snapping checked tool by tool against Figma and real files (snap distance, handle reach, cursors); children turned off a right angle scale without skew, approximately (behaviour suite from the documentation, rotated groups and mixed selections, equal spacing snapping, Cmd/Ctrl to ignore constraints and marquees inside frames landed) | 3–5 | +| 3 | Layers panel | 5 | 99% | 85% | QA with long real files (behaviour suite from the documentation, Rename layers dialog, sweeping eyes and padlocks, page dragging and dividers, several layers dropped together, keyboard box selection, F2 rename, Shift+F10 context menu and 10k-layer timings landed) | 1–2 | +| 4 | Shapes & vector | 8 | 99% | 73% | Outline-stroke compositing; corner smoothing on polygons, stars and vectors; the transform box around points of several layers; per-point caps and joins on dashed, variable-width and brush strokes; vector QA with real files (outline stroke rings covered by `outline_stroke`; behaviour suites from the documentation, corner radius and star ratio handles, rounded vector points drawn, also beside curved segments, a cap and join per point, several handles dragged together, cut and paint bucket drags, delete and heal along the curve, a boolean group's look by operation and the miter angle landed) | 1–3 | +| 5 | Paint | 7 | 90% | 71% | Video fills, wide-gamut canvas, HEIC images; plus darker in PDF (behaviour suite from the documentation, pass-through and isolating groups, clipping frames that don't isolate, plus darker, animated GIF fills, TIFF images and embedded image colour profiles landed) | 2–5 | +| 6 | Strokes | 3 | 95% | 75% | Stroke QA with real files (colour styles on strokes, stroke weight and per-side weight variables, brush strokes landed, see 20) | 1–2 | +| 7 | Effects | 4 | 100% | 75% | Glass and progressive blur tuned side by side (behaviour suite from the documentation, inner shadows and background blur on groups, shadows hidden behind translucent layers on the canvas and in every export landed) | 2–5 | +| 8 | Frames, groups, sections, masks | 5 | 97% | 80% | Visual QA with real files (a one-step frame/group convert, and a behaviour suite from the documentation landed: masked-out areas don't take clicks, mask group bounds and exports follow the mask, sections kept out of frames and groups, frames and sections drawn around layers take them, section resizing leaves contents in place, X/Y measured in the frame) | 1–3 | +| 9 | Auto layout & constraints | 9 | 98% | 74% | Behaviour against real Figma files (behaviour suite from the documentation, `fr` tracks like CSS, vertical wrap, auto gaps evenly/around, cell alignment and wrapped-line alignment landed) ([auto-layout-parity](auto-layout-parity.md)) | 3–6 | +| 10 | Text | 8 | 90% | 70% | Spell check, text metrics on real files and in other fonts (text box sizes measured against documents re-saved by other software: all 230 self-sizing boxes in 7 documents come out to the pixel, one font family; behaviour suite with CJK line breaking, vertical trim and carets by grapheme cluster landed; lists per paragraph with nesting and list spacing, hanging punctuation and lists, decoration style, thickness, offset, skip ink and colour landed) | 3–7 | +| 11 | Components & variants | 9 | 96% | 72% | Real (not synthetic) libraries, `.fig` libraries (a component playground for variant, boolean and text properties, a behaviour suite from the documentation, overrides of padding, gap, grids, exports and interactions, refused geometry overrides, variant-switch and Assets-swap override rules, top-left default variant landed) ([components-parity](components-parity.md)) | 4–9 | +| 12 | Styles & variables | 5 | 95% | 76% | Real-library QA (variables inside styles, behaviour suite from the documentation, style folders and order, mode order, code syntax, extended collections with their tail (new library variables as updates, chains through libraries) and variables import/export landed) ([components-parity](components-parity.md)) | 2–4 | +| 13 | Prototyping | 6 | 92% | 74% | Video triggers and media actions, controllers on macOS, Linux and FreeBSD, real prototype QA (behaviour suite from the documentation, delays on mouse triggers, manual overlay offsets, key combinations, controller buttons on key/gamepad triggers (Windows and the browser), widgets reacting in prototypes, interactive components playing their variants, drag-driven animations, physical springs landed) ([prototyping-parity](prototyping-parity.md)) | 2–5 | +| 14 | Import/export | 6 | 86% | 58% | `.fig` import's tail (best-effort import, editable vector networks, stand-ins for non-design layers and variable prototype actions landed; no real-file checks yet), checking the rest of the experimental `.fig` export (landed; written files open and draw in other software) on effects, images, prototypes and real files, HEIC/video import, CMYK output intent ([file-format-parity](file-format-parity.md)) | 5–10 | +| 15 | Dev Mode / inspect | 2 | 96% | 72% | Import lines for connected instances inside generated layer trees; an editor extension is left to the MCP server (inspect behaviour suite with colour formats, rem and icon and image assets, code connect, plugins in Dev Mode and dev resources links landed) ([dev-mode-parity](dev-mode-parity.md)) | 0–1 | +| 16 | Comments, versions, find, misc | 3 | 87% | 63% | Comments are local only; comparing two versions; branches in live sessions; text runs and other lists besides fills, strokes and effects merge whole (branches with review and merge, their own version history and per-item list merges, and a comments and version history behaviour suite from the documentation landed) | 1–3 | +| 17 | Multiplayer & sharing | 8 | 17% | 5% | Core landed (`crates/collab`: ids, change sets, server ordering, convergence tested); live sessions (preview): WebSocket transport, hosting from the desktop app, joining from the app or the browser, invite links that open the app, cursors, selections and follow, cursor chat and spotlight, per-person undo, offline resume; a self-hosted `figcraft-server` with persistence and share links to edit or view, which can be open to the public (owner tokens, limits per address, expiry, moderation). Left: accounts and teams, audio ([collaboration-parity](collaboration-parity.md)) | 42–84 | +| 18 | Plugins & widgets | 4 | 60% | 35% | Own sandboxed WASM plugins landed (#101: manifest, permissions, menu, Run last, Manage, SDK), with log byte/count limits checked before allocation and host forwarding ([logging limits](plugins.md#logging-limits)), plugin panels called back per button or changed value and per-plugin settings (ABI v2), then widgets (API 3: plugin-drawn layers on the canvas with state kept in the document, redrawn on clicks; a Counter example), panels side by side and a colour picker for plugin colour fields, then a plugin directory on `figcraft-server` (publish with the server key, browse, install after a permissions confirmation, hash-checked downloads, updates by version). Left: a public catalogue, publisher accounts and signatures, ratings, background update checks, the directory in the browser; widgets take clicks only (no text input, property menus, hover or timers) and draw frames, text, shapes and images only; Figma's plugins are JavaScript and don't run (a JS engine would be an owner decision) ([plugins.md](plugins.md)) | 8–26 | +| 19 | Team libraries & organization | 2 | 70% | 40% | Libraries kept on `figcraft-server` under their file and enabled by a share link (per-asset versions, background update checks, overrides kept), usage counts per component and file, swapping a library by names. Left: team-wide library lists, accounts and permissions, analytics over time, pushed update notices | 4–8 | +| 20 | Figma Draw extras | 2 | 88% | 60% | Have: variable width, lasso, shape builder, noise/texture, text on a path, pattern fills, stretch and scatter brushes, pen pressure (Windows pens, browser styluses), multi-edit (several vector layers in edit mode at once). Missing: pressure from macOS/Linux tablets | 1–3 | +| 21 | AI features | 3 | 0% | 0% | First draft, rename layers, image edit/remove background, visual search, Make: needs local models (owner decision) | 30–60 | +| | **Weighted (÷113)** | 113 | **~85%** | **~63%** | | **117–255** | +| | Core editor only (1–16, ÷94) | 94 | ~94% | ~72% | | 32–74 | + +## By dimension (ready for real work) + +| Dimension | Weight | Ready | Hours | Evidence | Doc | +|---|---|---|---|---|---| +| Features (depth) | 35 | 63% | 28–67 (areas 1–13, 15, 16, 20) | The table above; core editor ~72% | this file | +| UI/UX fidelity | 15 | 58% | 40–60 | Look & feel compared side by side with UI3 (~65% by the builders); tool-by-tool interaction (modifiers while dragging, hover states, popovers) not yet compared (a cursor for every tool and modifier, number fields and panel widths built from the documentation, not compared); accessibility names and keyboard navigation landed | [ui-parity](ui-parity.md) | +| File formats | 12 | 60% | 5–10 | `.fig` read, best effort, with editable vector networks (tested on generated fixtures, not real files), `.fig` write experimental (round trips through our own importer; written files open and draw in other software); REST JSON import, SVG in/out, PNG/JPG/WebP/PDF export solid, with a behaviour suite from the documentation | [file-format-parity](file-format-parity.md) | +| Collaboration | 10 | 13% | 52–104 | Local comments, version history and branches with review and merge; the multiplayer core (sync model, server ordering, client reconciliation, convergence tests) landed in #103, and live sessions (preview) co-edit a file hosted by a desktop app or kept on a self-hosted `figcraft-server` with edit and view links, joined from the app or the browser, with cursor chat and spotlight (`docs/collab-protocol.md`) | [collaboration-parity](collaboration-parity.md) | +| Stability | 8 | 55% | 20–40 | Never-crash lints, command guard with rollback, recovery copies, 963 tests incl. junk-parameter sweeps and a seeded retained-render walk; but two days old, one release (v0.2.0), no field reports, QA mostly on macOS | [gaps](gaps.md) | +| Performance | 6 | 60% | 30–60 | `cargo xtask bench`: 50k-layer page ~8 ms zoomed out, 10k-layer edits ~0.2 ms, 2,000-instance design system edits 4–64 ms; CPU (tiny-skia) canvas where Figma renders on the GPU; nothing timed against Figma on the same file | [hardware-parity](hardware-parity.md) | +| Hardware | 3 | 60% | 10–21 | Trackpad pinch and HiDPI yes; pen pressure on Windows and from browser styluses (none from macOS/Linux tablets); several windows (screen-reader tree in the main one only); no GPU canvas, no P3 on screen | [hardware-parity](hardware-parity.md) | +| Localization | 3 | 39% | 29–51 | The layer, English, Russian reviewed by VanTan, and machine drafts of Spanish (with a Latin American overlay), French, German, Brazilian Portuguese, Japanese, Korean and Simplified Chinese (none reviewed by a native speaker; every draft covers all current strings), CJK UI text in installed faces loaded on demand (web: only the craft-fonts CJK build); engine messages stay English | [localization-parity](localization-parity.md) | +| Platforms | 3 | 75% | 10–20 | macOS, Windows (x64/x86/arm64), Linux, FreeBSD, web all packaged (v0.2.0); Figma also has iOS/Android apps; non-macOS QA thin | [hardware-parity](hardware-parity.md) | +| Ecosystem/plugins | 3 | 38% | 8–26 | Own WASM plugins with a Rust SDK and three examples (#101), plugin panels and settings (ABI v2), widgets that take clicks (API 3), a self-hosted plugin directory on `figcraft-server`; no Figma plugin compatibility, no public catalogue or community files; the CLI, control channel and MCP server are ahead of Figma for agents | [gaps](gaps.md) | +| AI features | 2 | 0% | 30–60 | None | [gaps](gaps.md) | +| **Ready for real work** | 100 | **~53%** | **~262–519** | | | + +Dimension hours add up to the full-parity total without double counting: the feature-area rows +for import/export (14), multiplayer and team libraries (17, 19), plugins (18) and AI (21) are +counted under their dimensions. + +## Essentials user + +Someone who uses only the core of Figma: a casual or beginner user making a few screens, a social +graphic or a simple mockup. Same method, with the core features a casual user touches. + +**1. Core features, weights and depth** (depth from the area table and checklists above): + +| Feature | Weight | Depth | Notes | +|---|---|---|---| +| Create, open, save and reopen a file; pages | 10 | 85% | home screen, `.figcraft` round trips, Open recent | +| Pan and zoom | 10 | 80% | wheel, pinch, Space-drag, zoom shortcuts | +| Select, move, resize, rotate, align, group, duplicate | 15 | 75% | not yet compared tool by tool | +| Frames with device presets | 8 | 80% | | +| Rectangle, ellipse, line, arrow, corner radius | 8 | 80% | | +| Text: type, font, size, weight, colour, alignment | 12 | 70% | no spell check | +| Colour picker, fills and strokes; place and crop images | 10 | 72% | no HEIC images | +| Layers panel: rename, hide, lock, reorder | 7 | 80% | | +| Basic auto layout (add, gap, padding) | 6 | 70% | | +| Undo / redo | 5 | 90% | unlimited, one step per gesture | +| Export PNG, JPG, SVG, PDF | 6 | 85% | | +| Copy and paste, incl. images from other apps | 3 | 75% | | +| **Weighted** | 100 | **77.8%** | | + +Left out: components and variables beyond using one, prototyping, Dev Mode, advanced vector and +type, and everything the mainstream number leaves out. + +**2. Discounts a casual user actually hits:** + +| Discount | Factor | Evidence | +|---|---|---| +| Launch and stability | ×0.92 | signed and notarized macOS build, installers for every platform (v0.2.0), never-crash rules and recovery copies; no field record yet | +| Discoverability and UI clarity | ×0.94 | the UI3 layout Figma users know, Actions palette, keyboard shortcuts panel; a first run opens on the home screen with a Getting started card (six tips with their shortcuts, closable, back from the Help menu) and eight starter templates to open (also File ▸ New from template…); no Community files, no guided tour, and nobody new to the app has been watched using it | +| Opening files people send them | ×0.88 | what people send a Figma user is a Figma link or a `.fig`; a `.fig` opens (best effort, not yet checked on real files), a link doesn't. Images and SVG do | + +77.8% × 0.92 × 0.94 × 0.88 = **~59%** (estimated). User evidence: none yet (see above). + +## Mainstream practitioner + +The same question for the typical professional Figma Design user: a product designer who designs +screens, maintains or uses a design system, prototypes and hands off to developers every week. +Computed per craftrules `standards/progress-docs.md`. + +**1. Areas used weekly, with weights.** Feature areas 1–16 of the table above, with their weights +(sum 94). Left out as the standard says: plugins and widgets (18), AI (21), team and organization +libraries (19, cloud/team admin), Figma Draw extras (20, not weekly for a product designer), +localization beyond English, specialist hardware. Real-time multiplayer (17) is cloud collaboration +and is left out as an area, but its effect on working with Figma users is counted in discount 3. + +**2. Average depth** ("Ready" column, weighted): **72.7%**. + +**3. Discounts for what still stops real work:** + +| Discount | Factor | Evidence | +|---|---|---| +| Interaction fidelity | ×0.90 | UI3 look compared side by side (~65% by the builders); no tool-by-tool comparison of cursors, modifier keys while dragging, snapping thresholds or popovers yet; Ctrl to suspend snapping on macOS missing ([ui-parity](ui-parity.md)) | +| Stability on real machines | ×0.90 | Never-crash lints, command guard with rollback, recovery copies and 963 tests, but no field record at all: the repository is private, v0.2.0 has had no outside users or issue reports, CI tests run on Linux only and hands-on QA is on macOS. Harsher than VectorCraft's ×0.92 because there is no outside evidence either way | +| File exchange with Figma users | ×0.85 | A Figma designer's work lives in shared Figma files. FigCraft opens a `.fig` (best effort, not yet checked on real files) and writes an experimental one (written files open and draw in other software; effects, images and prototypes unchecked there); incoming work can come through the REST API JSON (one way, needs an access token), outgoing only as SVG, PDF, PNG or code. Teammates, reviewers and developers would all need FigCraft ([file-format-parity](file-format-parity.md), [collaboration-parity](collaboration-parity.md)) | + +72.7% × 0.90 × 0.90 × 0.85 = **~50%** (estimated; 50.03%, rounded down). + +**4. User evidence (2026-10-10):** 0 issues, 0 praise or "switched from Figma" reports, 0 comments +from anyone but the maintainers (`gh issue list`, `gh api .../issues/comments`; the repository is +private, 1 star, PRs only from maintainers). No outside evidence either way; nothing raises the +number. + +**Why it is lower than ready for real work (~53%), unusually.** The full number averages +dimensions additively, so the missing `.fig` exchange costs only the file-format dimension's 12% +weight, and FigCraft earns points the mainstream number doesn't count (platforms 75%: Linux, +FreeBSD and the web, where Figma has no desktop app). The mainstream number applies file exchange +as a multiplier on everything, because a Figma practitioner's whole working week is shared through +Figma files. Both are computed from written weights; neither was moved to match the other. The +mainstream number rises fastest with `.fig` import and the experimental `.fig` export checked on real files (the ×0.85 toward ×0.95: ~53%) and the +interaction pass. + +## How the hours are calibrated + +From this repository's own history (`git log`, `gh pr list`): + +- The whole app was built in ~25 wall-clock hours: first commit 2026-10-09 15:38 UTC, PR #99 + merged 2026-10-10 16:53 UTC; ~49k lines on day 1 before PRs, then 99 PRs adding ~81k lines. The + ROADMAP of that period records 4–6 agents in parallel, so ~110–150 agent-hours in all: about + **1 agent-hour per ~1,000 added lines**, and a typical feature PR (1,000–2,500 lines, e.g. + #84 arc handles, #92 PDF print options, #96 accessibility) took **1–2.5 agent-hours**. +- Between the ROADMAP revisions of 2026-10-09 22:19 UTC and 2026-10-10 16:53 UTC (~18.5 h wall, + 78 PRs, ~75–110 agent-hours) the self-graded depth went 58% → 79%: **~4–5 agent-hours per + self-graded point**. This re-measure grades about 0.6 points for each of those, which puts an + honest point nearer 7 agent-hours in the polish tail; new subsystems (a `.fig` reader, + multiplayer, a plugin runtime) are estimated bottom-up instead, since nothing comparable has been + built here yet. +- Parallelism: most of the work splits across 4–6 agents on disjoint crates (the history shows it + working); multiplayer, `.fig` import and the plugin runtime each need one owner for their core. + Needs a human: native-speaker review for every language, the plugin-runtime and AI-model + decisions, real-world Figma files to test with (licensing), Windows/Linux/screen-reader QA. + +## Area detail: what has landed and what is left + +Carried over from the ROADMAP's "Where we're lacking" list (2026-10-10, before this re-measure). +Hours in this narrative are the builders' estimates; the tables above supersede them. + +1. **Vector edit mode**: *landed:* edit-mode toolbar (move, pen, bend, paint bucket, lasso, cut), + pen that extends, branches and closes paths (vector networks), paint bucket fill/unfill of any + closed area (faces with holes), point/segment selection, handle mirroring (none/angle/angle and + length) with a Point panel (position, corner radius, mirroring), smooth/sharp points, join and + merge, delete vs delete-and-heal, nudging, one undo step per gesture, eraser (⇧E), shape builder + (⇧M: the regions of overlapping shapes, click or drag to merge, Alt to remove, curves kept), + variable width strokes (width profile presets with previews in the stroke settings, the Variable + width tool ⇧W: drag the path to add a width point, slide points, drag their handles, Delete; + smooth interpolation per subpath, dashes, caps and joins, canvas/PNG/SVG/PDF and outline stroke), + a transform box around several selected points (corners and edges scale them, Shift keeps + proportions, Alt from the centre; just outside a corner rotates, Shift in 15° steps; drag inside + to move; the Design panel's X/Y/W/H and flip/rotate act on the points), point snapping while + dragging points or handles and with both pens (to the points and segment midpoints of this and + other vectors, the usual layer targets and the pixel grid, with guides and a marker on the point + landed on; Ctrl/Cmd turns it off), arc handles on the canvas (sweep, start and ratio with a + live readout, Shift snapping, closing at the start, the other part from the gap; rotated and + flipped ellipses, any zoom), Design panel Arc fields (Start, Sweep, Ratio; bindable), live + boolean editing (operands selected by double-click or Cmd/Ctrl-click; moving, resizing or + editing an operand's points redraws the result during the drag with every operand outlined; + per-operand operations from the boolean controls; only the operands from the edited one up are + recombined; clicks hit a boolean group where its result is), the shape builder on strokes and open paths (strokes as their outlined areas), and + one-sided variable widths (Alt-drag a width handle; canvas, exports and outline stroke), and + filled regions with the Move tool (the area under the pointer hatched, clear of points and + segments, its spacing by octave folding fitted to measurements at several zoom levels and fixed + to the shape; click, Shift-click and drag to select and move areas with their points). + *Left:* vector QA with real files. 1–2 h. +2. **Text editing depth**: *landed:* OpenType features panel, small caps, lists, title case, + truncation/max lines, font picker with previews and recents, word/line selection, IME commit, + inline IME composition (underlined at the caret, reflowing, candidate window at the caret; the + document only gets the committed text), variable fonts (axes and named instances read from the + font, named instances as styles, Type settings ▸ Variable sliders, per-range axis values applied + to shaping, outlines and PDF), range-aware Typography panel with "Mixed". Range fills in the Fill + section. Right-to-left and mixed-direction text (Unicode bidi: Arabic shaping, mirrored brackets, + caret, clicks and selection follow the runs; arrow keys move visually). Right-to-left and + mixed-direction names in the panels (layers, pages, Assets, tabs, the file header, canvas frame + titles: visual order, joined Arabic, the ellipsis at a right-to-left name's left end; rename fields + whose caret, clicks, drags and arrow keys follow the shown text; system fonts for scripts the UI + fonts lack). The text style browser in the Typography section (list and grid views in each + style's 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, "Mixed" when the + chars use several styles); text styles link chars, not just layers (editing what a style sets + unlinks the chars edited). Live previews of a hovered named instance or font style and of a + dragged axis slider (one undo step on release, Escape cancels). Visual QA: list markers keep the + paragraph's font, renames start with the name selected and centred, Max lines shows while + truncating. Right-to-left paragraphs mirror lists and indents: bullets and numbers (".1") on + the right, wrapped lines hanging from the right, the paragraph indent on the right, trailing + spaces hanging outside the line (canvas, PNG, PDF and SVG). A behaviour suite from the public + documentation (`crates/text/src/behaviour_tests.rs`, `crates/engine/src/text_behaviour_tests.rs`): + Auto / % / px line heights with the leading split above and below every line, letter spacing in + % and px, paragraph spacing and indent (start-aligned text only), resizing and truncation, + decorations from the font's metrics; CJK line breaking (between ideographs, kana and Hangul, no + break inside Latin words, kinsoku for closing and opening punctuation, small kana and the + prolonged sound mark); a style's metrics from its own font, not a fallback; carets, Delete and + Backspace by grapheme cluster; double-click and word moves by script run; IME composition with + CJK; vertical trim (cap height to baseline). The tail suite (`crates/text/src/tail_tests.rs`, + `crates/engine/src/text_tail_tests.rs`): lists per paragraph, nested up to 8 levels with Tab and + Shift+Tab (1. a. i. and • ◦ ▪ per level, counts restarting after plain paragraphs), Enter on an + empty item and Backspace at an item's start stepping out, `- `/`* `/`1. ` starting a list; list + spacing between items; hanging lists and hanging punctuation; decoration style (solid, dotted, + wavy), thickness, underline offset, skip ink and colour, with inspect, CSS and the REST import. + *Left:* visual QA; hanging punctuation in right-to-left paragraphs and centred text, list + numbering that continues across a plain paragraph and a list's own start number (documentation + silent); decoration colour in SVG exports (drawn with the text's fill, like run fills); Korean + keeps breaking by syllable and Han runs aren't split by a dictionary (documentation silent). 0.5 h. +3. **Auto layout and sections depth**: *landed:* on-canvas padding/gap handles (also wrapped-row + and grid column/row gaps, grid tracks outlined), min/max width/height (UI, Hug/Fill/Fixed, + hugging text wraps at its max), ignore auto layout (absolute position) with constraints, text + baseline alignment, grid tracks (fixed/hug/`fr`), grid cells and spans, wrap row gap UI, + wrap-aware drag-reorder (by row) and dragging into a grid cell, nested hug/fill settling in one + pass (wrapping fill text, three levels deep, grid rows), min/max redistribution, coherent + Fill/Hug rules, drag across nested auto layout frames (deepest one wins, insertion line, out to + the canvas or a plain frame). Arrow keys move auto layout children one place (along the flow + past hidden and absolute siblings, Up/Down between wrapped rows, to the neighbouring grid cell + (automatically placed children swap, anchored ones take or swap cells), and at the end of a + flow or across a row's axis into the neighbouring nested auto layout frame when the frame + around flows that way; one undo step, none when nothing can move). Visual QA of nested + hug/fill/fixed/min/max/wrap/grid samples against the documented rules (Fill turns a hugging + parent Fixed, min/max redistribution, wrap inside fill, absolute children following + constraints as hug frames grow). Sections: hide contents (name, outline and "Contents hidden" + on the canvas; contents skipped by rendering, hit testing and labels; dimmed in the layers + panel; Design panel checkbox and context menu), Wrap in new section, frames in sections get + canvas labels and are top-level frames (flows start there, navigation, Export frames to PDF), + "Navigate to" a section shows its frame shown last, else its first; ready for dev turns + "Changed" when the contents are edited, resolved with a reason; Compare changes shows what a + changed frame or section looked like when it was marked next to how it is now, with the + changed layers and their property changes. *Left:* whether `fr` rows of a fixed-height grid + should make room for fixed-size children (now `minmax(0, 1fr)`-like). 1–2 h. +4. **Components depth**: *landed:* component properties (edit/delete/bind) with the properties + panel, add variant, instance swap UI, per-layer overrides with reset, restore main, instance + structure protection; Assets panel with live thumbnails, search and drag-to-canvas (drops into + the frame or auto layout slot under the pointer); exposed nested instances (their properties + show on the main's instances); component descriptions (edited on the main, shown on + instances); local libraries (publish a file's components, sets and styles with per-asset + versions, enable library files, place library components and apply library styles as + read-only linked copies, "Library updates available" with overrides kept on update); slots + (a frame of a main made a slot property with preferred components; instances fill it with any + content of their own, kept across main updates, variant switches, copy/paste and detach, with + the main's default content until edited; reset slot; slot badges in the layers panel and on the + canvas; fill menu in the properties panel; drops from the Assets panel and the canvas land in + slots); nested overrides kept when the main moves or swaps a nested instance, and on nested + instances copied out of an instance (re-linked to their own main); canvas drags can no longer + pull layers out of an instance; library variables (publishing includes the file's variable + collections with their modes, aliases and scopes, versioned per variable and collection, without + variables hidden from publishing; files using the library see its collections read-only under + the library's name in the Variables editor and the variable pickers; binding, aliasing or setting + a library collection's mode on a frame copies the variables in as read-only linked copies, so the + file renders without the library; library components keep their variable bindings and modes; + "Library updates available" covers changed variables and bound layers follow). Explicit variable + modes in the Design panel: a frame's mode per collection, Auto (inherited) or its own, with + enabled libraries' collections listed under the library's name (picking a mode copies the + collection in). Instance boolean and text properties bound to boolean and string variables + (following modes, renames, reset and detach); layout grid settings in the Design panel whose + count, size, margin/offset and gutter bind to number variables; fills, strokes, effects and + layout grids reordered by dragging their rows, their variable bindings following. Components at + scale, QA'd with a synthetic design system (about 300 components: buttons, inputs, cards, navs + and badges as sets of size × state × type × theme variants, boolean/text/instance swap/slot + properties, instances nested four deep, text and paint styles, light/dark variables bound across + it) used 2,000 times with overrides at every level: main edits keep every override and match a + full rebuild exactly; values from variables (a theme switch, a bound corner radius) are no longer + recorded as overrides; detaching a variable on an instance layer is an override that survives main + edits; exposed nested instances stay exposed and swapped nested instances keep their layer names + through main edits; layers a swapped nested instance's main adds keep their ids and overrides. + Instance swap properties have preferred values (offered first). Alt-drop from the Assets panel + onto an instance swaps it (same-set instance preferred, highlighted while dragging); instances + pasted into other files follow their mains (library copies shared, published components used + from their library, other mains left behind as a restorable missing main). The Assets panel lays + out only the tiles in view and renders thumbnails as they show (cached, re-rendered only for + changed components). A behaviour suite from the public documentation checks overrides (what an + instance takes, what it refuses: geometry, constraints and resizing of its layers, its auto + layout direction), variant switches (overrides stay where both variants agreed; text layers keep + theirs), swaps (by layer name; from the Assets panel only text overrides), the top-left default + variant, detach, reset and push. *Left:* QA with real-world (not synthetic) libraries. 1 h. +5. **Prototyping depth**: *landed:* on-canvas connections with drag-to-connect, interaction editor, + flows, smart animate, overlays, scroll/fixed/sticky, change-to and set-variable in the presenter. + Custom bezier and custom spring easings with editor fields; device frames (phone, tablet, + laptop, desktop bodies drawn in code) around the presented frame; overlays animate out with + their opening transition reversed; several selected layers edit their interactions together + (or replace mixed ones); scrolling inside device frames (the frame sized to the device's screen, + clipped or scrolling by wheel and drag, fixed layers pinned to the screen by their constraints, + nested "Scroll to", preserve scroll position); a resizable preview pane over the canvas that + plays the selected frame and follows edits live; device rotation (phones and tablets turn to + landscape: saved in the file, imported from REST files, drawn turned in the presenter and the + pane); presentation options (fit, fit width, fill, 100%, with Z to cycle them and R to + restart, the device frame shown or hidden, kept between presentations; what overflows the + window pans); scroll momentum (drags fling and slow down the same at any frame rate, the wheel + coasts, a drag past the ends stretches and springs back); responsive presentation (the presented + frame resized to the device's screen, or the window without a device, its constraints and auto + layout responding live; in the Options menu and the Prototype panel, saved in the file); device + colours (each model's colour variants, bodies drawn in code) and custom device sizes (a bare + screen with width and height fields, imported from REST files); flow descriptions in the + presenter's flow picker and under each flow; conditional actions (if / else if / else over + variable expressions) and set-variable-mode actions run in the presenter (from REST files or + commands). Conditionals are built in the interaction editor: an interaction's actions are a list + (add, reorder by dragging a row's grip, also inside conditional blocks, or from its menu, remove, + open one to edit it; one undo step per move), "Add action ▸ Conditional" adds an if whose + condition comes from a builder (a variable from the variable picker, an operator, a value or + another variable) or is typed as an expression (`count >= 3 and not done`, checked when entered, + what's wrong shown under the field), each block with its own actions (the same editors, + nested), plus "Else if" and "Else"; set variable values can be expressions (`count + 1`, + `"Hi " + name`); expressions read a variable's value in a given mode of its collection + (`accent[Dark]`, also from REST files' variable mode lookups) and the mode a collection is in + where the interaction runs (`mode(Theme) == "Dark"`, following set variable mode actions); every + edit is an undo step (`proto.action.*`, `proto.conditional.*`). Hotspot hints: a click where + nothing reacts flashes the screen's hotspots (an Options menu toggle, on by default); the + presenter's Inspect panel shows the hovered layer's properties. Overlays opened through commands + close on a click outside unless told not to, as in the editor. A behaviour suite written from + the public documentation covers triggers, actions, overlays, animations, easing, scrolling and + interactive components ([prototyping-parity](prototyping-parity.md)); its fixes: drag-driven + animations go their way and follow the pointer, key combinations, overlay timers from when they + open, instances play their variant's interactions, physical springs, smart animate overshoot, + fixed layers on top; scroll to takes an offset. *Left:* QA against larger real-world + prototypes. 1–2 h. +6. **Interop**: *landed:* image crop handles; REST API file JSON import (`file.open` / `file.import` + / CLI: pages, layer tree, geometry, paints, strokes, effects, text runs, vectors from geometry + paths, auto layout, constraints, grids, components, sets, instances with overrides, component + properties, styles; image fills from an `images` folder or placeholders; warnings for the rest), + now with prototyping (interactions with their triggers, actions, transitions and easing, flows, + scrolling, the prototype device; conditional actions (if / else if / else over expressions), + set variable with variable references and expressions, set variable mode, all run by the + presenter) and variables (the local variables JSON passed along, next to the file or embedded: + collections, modes, aliases, scopes, layer bindings and explicit modes). + Export panel: a row per export setting (typed or preset sizes like `2x` / `512w` / `512h`, PNG, + JPG, WebP, SVG and PDF, suffix, advanced options: contents only, include bounding box, and for + SVG outline text, id attributes and simplified strokes), a preview, and "Export N layers" (`file.exportLayers`) in the + panel and the Export dialog. Paste from other apps: images, SVG and text (the newest clipboard + content wins over our own copy; pasted SVG lands centred in the view). SVG import is editable and + checked against a corpus of synthetic files (expected pixels worked out by hand, layer structure, + export and re-import): groups named from their ids with transforms baked into the layers; + rectangles (corner radius), ellipses and lines kept as those shapes; gradients with exact + handles, `reflect`/`repeat` and focal points; patterns as image fills; clip paths and masks as + vector, luminance and alpha masks; shadow and blur filter chains as drop shadows, inner shadows + and layer blurs; blend modes; text as text layers with runs, anchors and stacked lines; images + and nested SVG images; import warnings for what's left out; linked files never read; hostile + input fuzzed. SVG export keeps alpha and luminance masks. PDF export: one page per frame in points (Export frames to + PDF, the Export panel, `figcraft-cli export -o x.pdf`), vectors, gradients and strokes as + vectors, real selectable text in subset fonts with clickable hyperlinks, images embedded once, + opacity and blend modes, clip content, alpha/luminance/vector masks, shadows and layer and + background blurs as images; checked by rendering the PDFs back against PNG exports. PDF print + options in the Export panel (per PDF export setting), the Export dialog, `file.export`, + `file.exportLayers`, `file.exportFramesPdf` and `figcraft-cli export`: a bleed showing what lies + past the frame (its fills reach out, its children are clipped at the bleed), crop and bleed + marks outside it, MediaBox / BleedBox / TrimBox, and CMYK colour (a documented, deterministic + naive conversion; images and rasterised effects too). + *Left:* .fig import (best effort), a CMYK output intent (no CMYK ICC profile can ship in the + repository), SVG filters other than shadows and blurs (left out with a warning). 8–16 h. +7. **Look & feel and interaction-fidelity pass**: *landed:* keyboard shortcuts audited against the + public shortcut reference: one keymap (`figcraft_engine::keymap`) drives the menus, the canvas, + the control channel and MCP, with macOS and Windows/Linux keys where they differ (mask, bring to + front/back, distribute, tidy up, layout grids, pixel preview), Text-only and canvas-only bindings + (Cmd/Ctrl+K: link while typing, Actions on the canvas), Cmd/Ctrl+/ for Actions, `+`/`-` zoom, + Shift+0/1/2 from any layout, Cmd/Ctrl+0, hold Z for the zoom tool (Alt zooms out, drag an area), + two quick digits for an exact opacity, boolean keys (Alt+Shift+U/S/I/E), remove fill/stroke + (Alt+/, Shift+/), outlines on Shift+Cmd+O, pixel grid on Shift+' (and Cmd+'), Shift+E / Shift+D and the other panel keys, Ctrl+Tab between files, Shift+B (and B) for the paint bucket in vector edit mode; the Keyboard + shortcuts panel docks at the bottom with a tab per group and search, keycaps per platform. + A shortcut parity suite (`shortcut_parity_tests.rs`) checks every documented shortcut on both + platforms (129 rows: 128 bound, including moving the rotation origin with Alt+R and placing an + object with Enter from the toolbar; 1 moved off a documented conflict) and pins the 48 undocumented + bindings; others' cursors (`Alt+Cmd+\`), list levels while editing (Cmd/Ctrl+] and [), the + keyboard box selection on Alt+Space / Ctrl+Space, arrows that pan with nothing selected, + Control to suspend snapping on macOS and Space/N/C/F while presenting landed with it + ([ui-parity](ui-parity.md#keyboard-shortcuts)). + *Left:* tool by tool, panel by panel against the incumbent (cursors, modifier keys while + dragging, hover states, menus, popovers); per-keyboard-layout shortcut tables. 40–60 h. +8. **Collaboration (local):** comments landed: pins pinned to frames and layers that move with them + (to other pages too) and stay where they were when the layer is deleted, your pins dragged onto + another frame, crowded pins clustered when zoomed out (a click zooms them apart), Shift+C to show + or hide them; threads where you edit and delete your own messages, emoji reactions, @mentions + with a picker of the names used in the file (your name from Preferences), read and unread state, + resolve, and "Copy link" (`figcraft://comment/`: pasting or `comment.open` goes to the page, + zooms to the frame and opens the thread); the Comments panel with search (text, names, frame), + filters (open, resolved, yours, unread), sorting by date or page, each thread's frame and page, + and Up/Down to step through threads; comments on the presented frame (Comments button or + Shift+C: read and reply). All of it through `comment.*` commands. Version history + landed (named versions saved in the `.figcraft` file; restore, rename, duplicate, delete; panel; + view a version on the canvas read-only before restoring it; autosave checkpoints every 30 + minutes, folded in the panel). A behaviour suite written from the public documentation covers + both: restoring keeps the comments and first keeps the replaced design as an autosave, a titled + autosave becomes a named version, merging a branch records the main file before and after, and + threads made in a live session can be replied to and resolved. Real-time collaboration is designed (`docs/collab-protocol.md`: + WebSocket sessions the app can host itself, server-ordered per-property changes, live cursors) + and its core landed (`crates/collab`, no networking yet): client-scoped node ids, every saved + layer field and the document-level data (styles, variables, comments, images…) as synced + properties, diffs after a command that cost the size of the edit, fractional sibling positions, + a session server that orders and validates batches (cycles, deleted layers, id spaces, sizes) + and a client that shows its own edits at once and replays them over others'; derived data + (auto layout, instances, bindings) is repaired by the author after concurrent edits; binary + messages (a moved layer is 43 bytes, a cursor 23) with a JSON form; a randomized multi-client + test over real commands converges. Live sessions run over the network (hosting from the app, + invite links, document sync, live cursors, selections, avatars, follow). Undo is per person in + a session (`edit.undo` takes back only your own steps, leaving what others changed since; + creations and deletions invert; a gesture or command group stays one step); refused edits roll + back with a toast; a dropped connection keeps editing and resumes as the same client with what + it missed (or a snapshot), then sends what was made offline. Randomized sessions with undo, + redo and dropped connections converge, and a checker confirms no undo reverts someone else's + change. + Plugins landed: sandboxed WebAssembly plugins (a metered wasmi + interpreter with a memory cap and no WASI, also in the browser) whose API is the command + registry; each run is one undo step and is rolled back when it fails (error, trap, endless loop, + memory); a `figcraft-plugin.json` manifest declares menu commands with parameters and read or + write access with an optional command allowlist, and files, other documents, the undo history, + dialogs and other plugins are always off limits. Plugins ▸ Development ▸ Import plugin from + manifest… (kept in the preferences, the module re-read on every run), the installed plugins in + the Plugins menu with a parameters form drawn from the manifest, Run last plugin (⌥⌘P), Manage + plugins, and `plugin.*` commands for scripts, the CLI and MCP; a Rust SDK and two example + plugins (Tidy names, Swatch grid) in `plugins/`, built by `cargo xtask plugins`. Then plugin + panels (ABI v2: fields and buttons drawn with FigCraft's widgets, the plugin called back for + each button or changed value as one undo step, `plugin.ui` / `plugin.event` / `plugin.close` + for scripts and MCP) and per-plugin settings kept with the preferences. Then widgets (API 3: + a plugin describes a layer of its own, validated and built as a frame with locked layers; clicks + on its named parts call the plugin back in a fresh sandbox with the state kept in the layer, one + undo step each; `widget.insert` / `widget.click` / `widget.list`; a Counter example), panels of + several plugins side by side and the colour picker on plugin colour fields. Then a plugin + directory on `figcraft-server`: bundles published with the server key and checked in full, + browsed and installed from Plugins ▸ Manage plugins after a permissions confirmation, downloads + checked against their SHA-256, updates by version. *Left:* a public catalogue, publisher + accounts and signatures; richer widget events; the directory in the browser. 8–26 h. +9. **Hardening and performance**: *landed:* `cargo xtask bench` (synthetic 1k/10k/50k-layer + pages: engine, render and headless UI-frame timings). The canvas keeps its last image and + re-renders only what a pan uncovers or an edit touches: moving one layer at 10k layers re-renders + in ~0.2 ms instead of ~8 ms (0.8 vs 24 ms at 50k zoomed out), a 40 px pan in ~0.7 ms instead of + ~7.5 ms. The layers panel lays out only the rows in view (every screen expanded at 50k: 39 → + 0.6 ms a frame); text layouts are shared instead of re-formatted per lookup; overflowing text is + culled by its glyphs. At 10k: full 1440×900 renders 7–15 ms, single-layer edits ~10 µs, hit + tests ~26 µs. Zoomed out (50% and below) the canvas draws levels of detail: layers under 2 px + land as their averaged colour, text under 4 px is greeked into bars, small rectangles and clip + layers skip the path pipeline (exports and closer zooms stay exact): a whole 50k-layer page + renders in ~8 ms instead of ~50 ms. `.figcraft` at 50k layers saves in ~0.09 s instead of ~1 s + and loads in ~0.16 s instead of ~0.86 s (JSON streamed to and from the document without a + value tree, layer fields at their defaults left out: 63 → 22 MB of JSON, old files still + open). The retained canvas is held to a full render: a seeded random walk (edits, undo/redo, + pans and zooms over nested, rotated and clipping frames, groups, masks, booleans, auto layout, + effects, text, instances, variables and styles) checks every step, and found clip edges on a + pixel boundary letting a column through in some renders. Drags show live on the canvas (every + finished render is shown, not only the newest one asked for), and a button held still no + longer rebuilds the preview and re-renders every frame. + Components at scale (`cargo xtask bench`, synthetic design system of about 300 + components used by 2,000 instances): edits of mains reach their copies directly instead of + rebuilding every instance (value, size and layer added/removed edits; other edits rebuild each + affected instance once, inner mains first), bound variables no longer touch unchanged layers on + every edit, auto layout re-flows only where sizes or places changed: recolouring or resizing a + main's layer used ~900 times 188/226 → 4/18 ms, adding a layer to it 430 → 21 ms, a label style + in all 72 button variants (1,800 copies) 450 → 57 ms, switching a variant, reset, detach, push + and paste ~450 → under 3 ms, accepting a library update 460 → 64 ms; the Assets panel with 2,000 + assets 3.4 → 1.3 ms a frame. Accessibility: the custom-painted + controls name themselves for screen readers (AccessKit on desktop: role, name, value and state + for tools, layer rows with their type, level, selection and hidden/locked state, fields, + dropdowns, tabs, menu items, and the canvas, which describes the selection); keyboard navigation: + F6 / Shift+F6 move between the toolbar, layers, canvas and properties, Tab / Shift+Tab move + within a region with a focus ring, Enter or Space activate, the arrows go along the toolbar and + through the layer rows, menus and tool flyouts opened from the keyboard take it and give it back, + Escape returns to the canvas (where Tab selects the next layer again instead of wandering into + the tab strip); reduced motion follows the system's setting or a preference; secondary and + accent text meet WCAG AA in both themes. + An idle window draws no frames: it redrew ~240 times a second + (the window title was re-sent every frame, and each viewport command asks for another), ~33% of + a core in a debug build, and now sits at 0 frames and ~0% CPU; the toast, the autosave + checkpoint and the presenter's "after delay" timers wake it at their deadline, the interaction + editor's curve preview plays when it shows, changes or is hovered, and a screenshot keeps frames + coming only until it arrives (`ui.inspect` `framesDrawn` and `repaintCauses` show what asked for + frames; headless tests hold it). *Left:* a real-world file corpus, Windows/Linux/browser + QA, runs with real screen readers (NVDA, Narrator, VoiceOver, Orca), the AccessKit tree in the + browser (eframe's web backend doesn't send it yet). Landed since: F2 rename from the keyboard, + Shift+F10 context menus at the focused layer row or the canvas selection, keyboard box selection + (F8). 27–44 h. + +## Inventory: what exists today + +Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count there was stale: 963 `#[test]` functions as of this re-measure. + +- **Architecture:** 15 crates with enforced layering (`cargo xtask layers`): geom, color, doc, + layout, text, pathops, plugin, collab, render, format, svg, engine, ui-egui (+ mcp, pdf). Every action + is a command (449 command ids); undo is unlimited via structural sharing (`Arc` node snapshots); gestures are + one undo step; `command.batch` runs several commands as one step. +- **Document model:** pages, frames, groups, sections, rectangles, ellipses (arcs/rings), polygons, + stars, lines/arrows, vector networks, text with styled runs, boolean groups, components, component + sets (variants), instances with per-layer overrides, slices; fills (solid, linear/radial/angular/ + diamond gradients, images with fill/fit/crop/tile + adjustments), multiple strokes with + inside/center/outside alignment, per-side weights, dashes, caps/arrowheads, joins; effects (drop + and inner shadow with spread that offsets the outline exactly: ellipses, stars, vectors, text; + inner shadows over the fills and under the children and strokes; drop shadows hidden behind what + the layer covers, translucent fills included, unless shown behind transparent areas; layer blur; background blur + that sees the whole backdrop, inside clipping frames, faded groups and masks too; both blurs + uniform or progressive (the radius ramps between two points, dragged on the canvas); noise in + mono, duo and multi colour with grain size, density and blend mode; texture with clip to shape; + glass with light angle and intensity, refraction, depth, dispersion, frost and splay; the same in + PNG, PDF and SVG exports, SVG embedding renders of noise grains, texture, progressive and + background blur and glass); corner radii with + smoothing; constraints; auto layout (horizontal, vertical, wrap, grid; hug/fill/fixed; min/max; + absolute children); layout grids; export settings; prototype interactions, flows and transitions; + paint/text/effect/grid styles; variables with collections, modes and aliases. +- **Renderer:** CPU (tiny-skia) scene renderer used for canvas, export and the web: isolated layers + for opacity/blend/effects, clip content, masks, booleans (linesweeper), text (harfrust shaping, + skrifa outlines, font fallback), aligned strokes with caching. +- **Engine:** pointer tools (move/marquee/deep select/resize/rotate/Alt-duplicate with smart-guide + snapping, frame/section/slice/shapes/line/arrow with Shift/Alt, text, pen, pencil, hand, + eyedropper), Cmd/Ctrl-click and Cmd/Ctrl-marquee into nested layers, Shift-marquee toggling, + Select layer (every layer under the click), measuring (Alt-hover distances from the selection to + layers, parents and ruler guides, Cmd+Alt for nested layers, from a guide being dragged to the layers of its frame, extension lines, one box inside the + other, overlaps, live distances to the neighbours while Alt-dragging, Shift+Alt for auto layout + padding and gaps; labels that never overlap at any zoom), text editing (caret, selection, words, + lines), auto layout reorder by dragging, drop into frames, align/distribute/tidy, z-order, + flip/rotate, the scale tool (panel, and canvas handles that scale everything about its anchor + point or from the centre with Alt, with a live factor, one undo step per drag), booleans, flatten, + outline stroke, components (create, instances, sync with overrides, reset, detach, push changes, + variants, properties, slots), styles and variables, prototype commands, find/replace, batch rename, + copy/paste (internal + SVG/text), file new/open/save/import/export (native `.figcraft`, SVG + in/out, PNG/JPG/WebP), version snapshots. +- **UI (egui, swappable):** the incumbent's 2026 layout: tab strip that is also the window's title + bar (frameless on Windows/Linux with drawn window buttons, drag, double-click maximise and edge + resizing; macOS window buttons inset into it), icon rail, file header, pages and + layers tree (rename, visibility/lock, drag to reorder/reparent), Assets panel, floating bottom + toolbar with flyouts and mode switcher, Design / Prototype / Inspect panels (Dev Mode: properties with their styles and variables, annotations + with pinned properties and categories drawn as coloured callouts on the canvas (filtered by category), + a ready-for-dev list, a focus view on one frame with previous / next, Compare changes since a frame + was marked ready (before / after side by side or overlaid, property-level diffs), CSS/HTML/React/SwiftUI/Compose + code where components take their properties as parameters and instances are calls, components + connected to their code components showing the real snippet, read-only plugins), colour picker + (HSB square, hue/alpha, hex, gradient stops, image fills), main menu generated from the catalog, + context menu, Actions palette (⌘K), keyboard shortcuts from one keymap per platform (a tabbed, + searchable Keyboard shortcuts panel), keyboard navigation (F6 between regions, Tab within them, a + focus ring) and screen-reader names for the custom controls (AccessKit), presentation player with + transitions, light and dark themes, home screen, About/Preferences/shortcuts/variables dialogs. +- **Automation:** desktop control channel (`--control `: execute commands, inspect document/UI, + synthetic pointer/keys/typing, menu invocation, screenshots, renders). +- **Tests:** 963 `#[test]` functions (2026-10-10) including junk-parameter sweeps over every command (and every vector-edit command while editing), importer fuzzing and render regressions. + +## Revision history + +| Date | Change | Summary | +|---|---|---| +| 2026-10-15 | minor | M4: Outline stroke preserves even-odd stroke holes. Pixel comparisons cover alignment, dashes, individual side weights, vector paths and stroke-only opacity; native save/reopen, Undo/Redo and instance opacity inheritance/overrides are covered by `outline_stroke`. Existing estimates unchanged; filled-shape compositing needs separate work. | +| 2026-10-14 | minor | Russian catalog (M9), reviewed and approved by VanTan: all 2,165 current entries, Русский in Preferences and regional ru tags. Russian integer plurals use one/few/many; contributor display name VanTan added. Existing estimates unchanged. | +| 2026-10-13 | minor | Vector edit mode regions: the filled area under the pointer is hatched (clear of points and segments, holes left out, spacing by octave folding fitted to measurements at several zoom levels, fixed to the shape); click, Shift-click and drag select and move areas with their points (drawn unselected); a click on an empty spot clears them and stays in edit mode (see [ui-parity](ui-parity.md)) | +| 2026-10-13 | minor | Plugin log limits (#191): cap each UTF-8 line at 8 KiB, total log bytes at 1 MiB and lines at 1,000 per run/callback before copying guest memory or forwarding to the host. Over-budget runs fail and roll back document edits; callbacks get fresh budgets. Added sandbox boundary/forwarding tests and an engine rollback regression. Percentages and hours unchanged. | +| 2026-10-13 | minor | Component playground (M3): with an instance, a component or a component set selected, the Inspect tab shows a Playground: a preview of the component and a control for each variant, boolean and text property. What is tried there changes the preview and the code in the Code section, never the file (no undo step, the canvas instance stays as it is); Reset goes back to the component's or the instance's own values. `component.playground {id?, properties?, language?, path?}` gives the same to scripts: the properties with their values, the code, and a PNG on request (`playground_tests`). Left: instance-swap and slot properties in the playground (the command takes an instance swap; the panel has no control for it), a code connect template's snippet with the tried values. Numbers unchanged. | +| 2026-10-13 | minor | Auto layout, on-canvas handles of automatic gaps (M2): the handle between spread rows or columns (and between items with an auto gap) shows the distance the layout gives the gap instead of the stored value, and dragging it makes the gap a fixed one from there, as typing a gap in the panel does; one undo step (`gap_handle_tests`). Before, the drag changed a value that had no effect. The documentation is silent on dragging an automatic gap. Numbers unchanged. | +| 2026-10-13 | minor | Variables inside styles (M3): a shared style can bind variables to fields of its value (`style.bind {style, field, variable?}`: a paint style's colours and gradient stops, an effect style's colour, radius, spread and offsets, a grid style's size, count, gutter and offset, a text style's font size, line height, letter spacing, family and style). Every layer using the style shows those fields resolved in its own modes and stays linked when a variable or a mode changes; the style keeps them resolved in the default modes. A style made from a layer keeps that layer's bindings; a new value drops bindings to what is gone. The style editor binds from its property rows (paint colour, shadow colour, blur radius, grid size) and detaches from the variable's chip; the style picker marks styles that use variables. `.fig` export and import carry the bindings on the style, REST layers hand theirs to the style they use. Left: binding text style fields and the other effect and grid fields from the editor (commands only), library variables in the picker, a style's library copy. Numbers unchanged. | +| 2026-10-13 | minor | Effects on groups, inner shadows and background blur (M3): a group is no outline of its own, so its inner shadows are cast inwards from around what its layers draw together (their pixels, over them: no shade at a seam between two layers) and its background blur and glass show the backdrop through the outlines of its layers (under them). Canvas and PNG; SVG embeds a group with an inner shadow as a render and its background blur as an image under the layers; PDF places both as images, the blur matching the PNG. Where the group holds one shape the pixels match that shape, or a frame filled like it, carrying the effect (`group_effects_tests` in render, svg and pdf). Offsets and spreads of a group's inner shadow go by whole pixels. Numbers unchanged. | +| 2026-10-13 | minor | Text made by commands on a dark page (M1): in the app, `text.create` and `node.create {type: text}` that name no fill give white text on a dark page, by the same rule as the Text tool (the page's colour decides, also inside a frame); a named `fill`, `fills` or `fillStyle` is kept. Sessions without an interface (CLI, MCP, exports) still make black text everywhere. Numbers unchanged. | +| 2026-10-13 | minor | Frames and groups, a one-step convert (M1): `object.convertToGroup` and `object.convertToFrame` (the Design panel's type menu on a frame or a group) change the layer in place, its layers staying where they are on the page. A frame turned into a group loses what only a frame has (auto layout, with the positions it gave kept and fill children fixed; fills, strokes, corner radius, clipping, layout grids, guides, scrolling) and takes its layers' box; a group turned into a frame keeps its box, with no fill and no clipping. Instances, main components, layers inside instances and empty frames are refused with the reason, all or nothing; one undo step. Numbers unchanged. | +| 2026-10-13 | minor | Prototyping, delays on mouse triggers and manual overlay offsets (M5): mouse enter, leave, down and up wait `delayMs` before their actions run (once while waiting; dropped when the screen or overlay they were on goes away; a pointer that leaves early doesn't cancel, the documentation is silent), and a manual overlay sits at `offset: [x, y]` from the top-left of the layer that opens it (without one, right below it, as before). Delay and X/Y fields in the interaction editor, the player on its own clock, REST import (`delay`, `overlayRelativePosition`), `.fig` import and export (the export's field for mouse delays is unchecked against other software). Numbers unchanged. | +| 2026-10-13 | minor | Auto layout, wrapped-line alignment (M2): a wrapping layout's lines (rows of a horizontal one, columns of a vertical one) are packed where the cross alignment says (start, centre, end; before, they always sat at the start), stretch when every child fills across the flow, or take the free space between them (`contentAlign: spaceBetween`, the Design panel's Row/Column gap mode, the alignment pad lighting the whole cross axis). Read from REST JSON and `.fig`, written to `.fig`, `align-content` in CSS, an instance override with the other alignment settings. 13 hand-computed layout cases and command tests. Numbers unchanged. | +| 2026-10-13 | minor | Starter templates and a first run that explains itself: eight templates built in code through the command layer (mobile app, landing page, dashboard, design system starter, wireframe kit, social post sizes, presentation slides, icon set starter; original and synthetic, deterministic, about 10–190 ms each to build), `template.list` and `file.newFromTemplate` for the app, CLI and MCP, File ▸ New from template…, a Templates tab on the home screen (cards with thumbnails rendered from the documents, search, kind filter, keyboard and screen-reader names) in place of the Community placeholder, a row of templates on Recents while there are no recent files, a Getting started card that can be closed and comes back from Help ▸ Getting started (`help.gettingStarted`), and a first run that opens on the home screen. The Drafts placeholder is gone (the app has no drafts folder). Essentials: discoverability ×0.92 → ×0.94 (templates and first-run tips exist; no Community files, no tour, no user evidence), 77.8% × 0.92 × 0.94 × 0.88 = 59.2%, ~57% → ~59%; essentials hours ~60–110 h → ~55–100 h (the onboarding and templates item is done); menu breadth stays 211/211 (the two new menu items are FigCraft's own and not counted); every other number unchanged | +| 2026-10-12 | minor | Everyday editing partials closed (gap 4): number fields scrub a step a point (×10 with Shift, ×0.1 with Alt, on past the screen edge where the platform reports it), step with the arrow keys by the nudge preferences, take brackets, units and relative edits (`+10`, `*2`, `50%`), commit on Tab and nothing when left untouched, and on Mixed apply relative edits to each layer in one undo step; outline mode's Include hidden layers and Include object bounds (`view.outlinesHiddenLayers`, `view.outlinesObjectBounds`), drawn and clickable; Hide when publishing from the Assets panel (`component.hideWhenPublishing`); a cursor for every tool, modifier and hover target (rotate turned to its corner, pen states, eyedropper, comment, copy, not-allowed), with guides and handles keeping theirs; panel widths with limits, double-click reset and the preferences (`view.panelWidths`); a file or page made in the dark theme has a #1E1E1E page (sessions without an interface keep #F5F5F5) and text drawn on a dark page is white. Menu breadth 211/211. UI/UX fidelity 58% and the interaction pass 40–60 h unchanged at this rounding: the side-by-side comparison is what remains | +| 2026-10-12 | minor | Text metrics, first measured fidelity data (gap 3): our example documents, exported and re-saved by other software, were read back and every text layer's stored box, line heights, baselines and glyph positions compared with ours. Three rules explained every row and were applied: a line's height is rounded to whole pixels (halves up; Auto, % and px alike; the baseline keeps its fraction inside the rounded line); glyph advances and kerning are each rounded to 1/1024 em before scaling by the font size; letter spacing is not added after a line's last visible glyph. Before: 24 of 131 text boxes of the design-system example matched (118 in width, 24 in height); after: 131 of 131, and all 230 self-sizing text boxes across the 7 documents (Inter only, 11–120 px, letter spacing −6% to 8%, one wrapped paragraph). Glyph positions within 0.001 px: 223 of 1,832 → 1,781; the rest are kerning pairs at weights where the installed font files differ slightly, at most 0.05 px. Frames and other layers matching in size and position: 115 of 231 → 208; the other 23 hang on one grid frame whose row positions came back as written. Not measured: other font families, fractional px line heights, centred or right-aligned text with letter spacing, lists, mixed sizes on a line. Wording only; no number moves | +| 2026-10-12 | minor | `.fig` export (gap 1): written files were checked by opening them in other software: layers, fills, gradients, strokes, shadows, vectors, components and text draw as in FigCraft; effects such as glass and noise, images and prototype interactions are still unchecked there, and variables are not written. Wording only; no number moves | +| 2026-10-12 | minor | A public `figcraft-server` (open hosting): with `open_publish = true` anyone publishes a file without the server key and gets its edit and view links plus an owner token (128 bits, shown once, stored hashed) that lists, makes and revokes its links, renames it or removes it (`collab.manageFile`); limits per address (connections at once and a minute, messages a second, publishes an hour, files and MiB kept; IPv6 by /64; `X-Forwarded-For` believed only from `trusted_proxy`; tables capped at 10,000 addresses) and for everyone (`max_total_mb`), each refusal saying why; files published without the key expire after `idle_days` unopened (the date shown to everyone in the file); moderation (`files list --sort size|age|idle`, `block-ip`, `reports`, a Report button and `collab.report`); the operator's contact and terms shown in the Share dialog; `GET /healthz` on the same port; `packaging/server/install.sh` for a Debian/Ubuntu machine behind Caddy and a nightly backup timer. Protocol additions within version 2, gated like signals (older apps get clear refusals). Hosting infrastructure rather than a Figma feature: percentages and hours unchanged | +| 2026-10-12 | minor | Experimental `.fig` export (gap 1): the whole document written from the same public format notes as the import, verified only by round trips through our own importer. Import/export breadth 84% → 86%, ready 57% → 58%; file formats 59% → 60%; file exchange with Figma users stays ×0.85 (nothing shows another app opens the files); mainstream hours ~130–244 h → ~120–229 h (`.fig` export 15–30 h → checking it elsewhere 5–15 h); feature breadth (~84%), ready for real work (~53%), essentials (~57%), mainstream (~49%), beta and full parity hours unchanged at this rounding | +| 2026-10-12 | minor | Keyboard shortcut parity suite and bindings (gap 4): a table test with one row per shortcut in the public documentation, macOS and Windows/Linux keys each (129 rows: 126 bound, 110 before; 1 moved off a documented conflict; 2 missing; 2 kept by a platform), plus 48 pinned undocumented bindings so every default binding is in the table; newly bound: A (Frame), Shift+' (pixel grid), Alt+Cmd+\ (others' cursors, new `view.multiplayerCursors`), Ctrl+F6, Shift+Cmd+U (link), Cmd/Ctrl+] and [ (list levels while editing, new `text.listIndent` / `text.listOutdent`), Cmd/Ctrl+Backspace (ungroup), Alt+Space / Ctrl+Space (keyboard box selection, Cmd/Ctrl+arrows draw it out), suggest auto layout, go to main component, arrows that pan with nothing selected (new `view.pan`), Control suspending snapping on macOS, Space/N/C/F while presenting; symbols another layout types with Shift count without it; no default binding uses a system-reserved chord (tested). Menu breadth 207/207 (View ▸ Multiplayer cursors). UI/UX fidelity 58% and the interaction pass 40–60 h unchanged at this rounding | +| 2026-10-12 | minor | Simplified Chinese draft catalog (gap 5): `zh-Hans.toml` translates every UI string (1,371 keys; plurals give only `other`, and `plural_one` never picks "one"), command label (524) and menu wording (29), machine-drafted in a concise UI style with full-width punctuation and marked as a draft awaiting native review; Preferences ▸ Language lists 简体中文, System maps zh, zh-CN, zh-SG and zh-Hans to it, and Traditional Chinese (zh-Hant, zh-TW, zh-HK, zh-MO) has no catalog and stays English (or the next OS language), never Simplified. A Chinese UI draws Han in a Chinese face first (Microsoft YaHei, PingFang SC, Noto Sans CJK SC), ahead of the craft-fonts build's Japanese BIZ UDPGothic, and changing the UI language lays the CJK faces out again (switching from Japanese to Chinese no longer keeps the Japanese face in front). String counts re-measured: 1,371 UI keys, 524 command labels and 29 menu wordings (1,924 entries); the French, German, Brazilian Portuguese, Japanese and Korean drafts have 1,899 of them (the 25 newer strings read English). Localization 36% → 39%, 31–54 h → 29–51 h; ready for real work unchanged (~53%); full parity ~267–527 h → ~265–524 h | +| 2026-10-12 | minor | Latin American Spanish overlay (gap 5): `es-419.toml` lists only the 87 entries (61 UI strings, 26 command labels) whose wording differs in Latin American usage (agregar for añadir, presionar for pulsar, configuración for settings, Enter for the Intro key, control for a gamepad, resorte for a spring) and reads `es.toml`, then English, for the rest (a regional catalog overlays its language's); same tú register as Spanish; a draft awaiting native review. Preferences ▸ Language lists Español (Latinoamérica); System maps es-419, es-MX, es-AR, es-CO and the other Latin American and US Spanish tags to it, es-ES and es to Spanish. Localization 35% → 36%, 32–55 h → 31–54 h; ready for real work unchanged (~53%); full parity ~268–528 h → ~267–527 h | +| 2026-10-12 | minor | Prototyping tail (gap 15, the part that doesn't need video): key/gamepad triggers take controller buttons (`Gamepad A` … `Gamepad Home`, the standard layout's 17 buttons, from a Controller menu beside the key field; the REST import maps controller triggers instead of skipping them); the player reads controllers on Windows (Windows.Gaming.Input through the `windows` crate the app already used; no new dependency) and in the browser (the Gamepad API), polling only while the prototype has such a trigger and the window has the focus, and the control channel presses buttons (`ui.input {gamepad}`); macOS, Linux and FreeBSD read no controllers yet. Widgets react to clicks in a presented prototype (the canvas's callback and sandbox limits, on the prototype's copy: the file and its history don't change, restarting starts over). `proto.evaluate` computes an expression where a layer's interaction would run (MCP); the mode lookup and set-variable-mode rules are cases in a behaviour suite (they already held). Prototyping breadth 90% → 92%, ready unchanged at 74%, 3–6 h → 2–5 h; area hours 120–260 → 119–259 (core 33–75 → 32–74), Features (depth) 29–68 h → 28–67 h; percentages unchanged at this rounding; full parity ~276–538 h → ~275–537 h | +| 2026-10-12 | minor | Plugin directory on `figcraft-server` (gap 6): the server keeps plugin bundles (manifest, module, up to 32 assets; an uncompressed length-prefixed format named by its SHA-256) published with the server key (`figcraft-cli plugin publish`, `plugin.directory.publish`), checked in full before they are kept (size cap, manifest and plugin API version, a version newer than the one kept, the module compiled but never run, plain asset names) and capped (`max_plugins`, `max_plugin_mb`, `max_plugins_mb`); anyone who reaches the server lists, searches and fetches them (hash-checked again on read, a shared download budget); the operator moderates with `figcraft-server plugins [remove]`. The app's Plugins ▸ Manage plugins gains a Browse tab: a server's plugins with Install / Update / Remove, an install confirmation that shows the permissions asked for, the source and the SHA-256, the download checked against the listed hash before anything is read, plugins kept per user in hash-named folders; `plugin.directory.list` / `install` (needs `confirm: true`) / `publish` / `unpublish` for scripts and MCP. Not in the browser build yet; no public catalogue, publisher accounts, signatures, ratings or background update checks. Plugins & widgets 55%/32% → 60%/35%, 10–30 h → 8–26 h; Ecosystem dimension 34% → 38%; weighted totals unchanged at this rounding; full parity ~281–548 h → ~279–544 h | +| 2026-10-12 | minor | Japanese and Korean draft catalogs and CJK UI fonts (gap 5): `ja.toml` and `ko.toml` translate every UI string (1,351 keys; plurals give only `other`, and `plural_one` never picks "one" in either), command label (519) and menu wording (29), machine-drafted and marked as drafts awaiting native review; Preferences ▸ Language lists 日本語 and 한국어, System picks ja* and ko*. CJK UI text renders: once the UI language, the open file's names or comments, typed text or the language picker needs Japanese kana, Hangul or Han, the desktop app appends one installed face per script to egui's fallbacks (Windows Yu Gothic UI / Meiryo / Malgun Gothic / Microsoft YaHei, macOS Hiragino Sans / Apple SD Gothic Neo / PingFang SC, Linux Noto Sans CJK; the craft-fonts build's BIZ UDPGothic first), reading only that face's file and sharing its bytes with egui (English users load none: startup and memory unchanged); the web shows Japanese and Korean only when built with craft-fonts' Noto Sans CJK (`FIGCRAFT_EMBED_CJK`), else it keeps them out of the picker and System. Localization 30% → 35% (drafts, not reviewed: graded low), 40–65 h → 32–55 h; ready for real work ~52% → ~53% (52.9 → 53.1 by the dimension table); full parity ~284–554 h → ~276–544 h; beta ~162–292 h → ~155–280 h (Japanese and Korean leave the beta list; their native reviews need people) | +| 2026-10-12 | minor | Plugin widgets and the panel tail (gap 6, plugin API 3; v1 and v2 plugins unchanged): a plugin marked as a widget puts a layer of its own on the canvas from Plugins ▸ Widgets or `widget.insert`: it describes the layer (frames with auto layout, text, rectangles, ellipses, images it carries) and FigCraft validates the description (size, layer count, depth, numbers, colours, text and image sizes) and builds it as a frame with locked layers inside; a click on a part the plugin named (on the canvas with the Move tool, a pointing hand over it, or `widget.click`) calls the plugin back in a fresh, metered sandbox with the widget's state, as one undo step, and the widget is redrawn; the state lives in the widget's own layer data, so widgets survive save and reload, copying, undo, export and live sessions; no plugin can write that data or call `widget.*`. `widget.list` lists them. A Counter example widget. Plugin panels of different plugins open side by side, and a colour field's swatch opens the colour picker (the plugin hears of the colour when a drag ends). Plugins & widgets 45%/28% → 55%/32%, 15–40 h → 10–30 h; Ecosystem dimension 28% → 34%; weighted totals unchanged at this rounding; full parity ~286–558 h → ~281–548 h | +| 2026-10-12 | minor | French, German and Brazilian Portuguese draft catalogs (gap 5): every UI string, command label and menu wording (1,899 entries) machine-drafted in each and marked as drafts awaiting native review, falling back to English per key (new strings need only English and Spanish); 0 counts as singular in French and Brazilian Portuguese; System picks the first OS language with a catalog (pt-PT and pt read pt-BR); long translations elide with a tooltip in dropdowns, two-column labels and the home cards. Localization 20% → 30%, 45–75 h → 40–65 h; ready for real work unchanged (~52%; 52.6 → 52.9); full parity ~289–564 h → ~284–554 h; beta ~170–305 h → ~162–292 h (Figma's languages left on the way to beta: Japanese and Korean, 7–12 h) | +| 2026-10-12 | minor | Cursor chat and spotlight in live sessions: `/` opens a chat bubble at your pointer that everyone sees as you type and that fades a few seconds after the last change; a spotlight (your avatar's menu, `collab.spotlight`) makes everyone follow your view and page, latecomers included, until they move their own. Both are ephemeral presence the host validates and rate-limits, passed only to clients that read them. Multiplayer & sharing breadth 15% → 17%, ready unchanged (5%), 45–90 h → 42–84 h; the Collaboration dimension's hours 55–110 → 52–104 h (the dimension hours still add up to the total); feature breadth (~84%) and ready for real work (~51%) unchanged at this rounding; full parity ~301–587 h → ~298–581 h | +| 2026-10-12 | minor | Layers and Assets panels behaviour suite written from the public documentation (6 engine tests with about 50 table rows and 5 panel tests driven through the UI; about 30 rows that the code before the fixes contradicts) and its fixes: several layers dropped together in the layers panel land together (layers from apart split around the drop point); Collapse layers keeps the selection's rows open (it closed everything); the eye and padlock stay under the pointer (they vanished as it reached them, so clicks could miss) and dragging across them sets several layers in one undo step; several layers selected open a Rename layers dialog (match as text or a regular expression, Rename to, current name and number buttons, a start value, preview) instead of renaming the top one; `object.batchRename` counts `$n` up and `$N` down (they were swapped), pads with `$nnn`, takes `start`, regular expressions with `$1`…`$9`, `` $` `` / `$'` and codes in the replacement; pages are dragged to reorder, a blank page named with a hyphen is a divider (never opened, holds no layers), duplicates are named "Copy of …", the last page that isn't a divider can't be deleted; the Assets panel files components by page, frame and slash names in folders that fold away (`component.list` gives each `folder`); a leading `.` or `_` now keeps a component out of the library even after it was published, and removing it brings the component back. Pinned where the documentation is silent: Alt-click on an arrow, numbering in stacking order, hidden layers and locked frames taking drops, locking deselects. Layers panel 98%/82% → 99%/85%; percentages and full parity hours unchanged at this rounding | +| 2026-10-12 | minor | Vector multi-edit and the vector edit tail, with a behaviour suite written from the public documentation (5 tests, about 30 table rows and steps, and a junk-parameter sweep of every vector command in multi-edit mode): several vector layers are edited at once (Enter, double-click or `vector.edit.begin {ids}`; a press on another layer's point, handle or segment makes it the active layer; Shift-click, marquee and lasso select across layers; dragging, the arrows, Delete, delete and heal, mirroring, smoothing, corner radius and point caps and joins reach every layer's selected points in one undo step; the pen, bend, paint bucket, cut and eraser work in the active layer); several Bezier handles are selected with Shift-click and dragged together (`vector.edit.moveHandles`); a sharp point next to a curved segment rounds (the curve is cut where the arc meets it and keeps its shape); a point can have its own stroke cap and join (`vector.edit.strokeCap`, `vector.edit.strokeJoin`, the Point section of the Design panel; canvas, PNG, SVG and PDF, outline stroke); the Pen outside edit mode connects to one of its own points and ends the path, and continues a selected open path from an end point. Left: corner smoothing beyond rectangles, the transform box around points of several layers, per-point caps and joins on dashed, variable-width and brush strokes. Shapes & vector 98%/72% → 99%/73%, 2–4 h → 1–3 h; Figma Draw extras 80%/55% → 88%/60%, 2–5 h → 1–3 h (gap 9: pen pressure on macOS and Linux tablets left). Totals recomputed by the doc's method, which rows since the last recompute had left stale: the area table's weighted breadth is 83.7% (~83%; core 94.1%, ~94%) and ready 63.1% (~63%; core 72.1%, ~72%), hours 133–284 (core 38–83); Features (depth) 61% → 63%, 46–94 h → 34–76 h; ready for real work 52.6% (~52%); mainstream weekly areas 71.0% → 72.1% (× 0.9 × 0.9 × 0.85 = 49.7%, ~49%, rounded down); essentials unchanged (~57%); full parity ~306–595 h → ~294–572 h (on top of the rows merged since: ~301–587 h → ~289–564 h; area-table totals recomputed on the merged table and rounded down: breadth ~84%, ready ~63%, core ~94%/~72%, mainstream weekly areas 72.4%, ~49%) | +| 2026-10-12 | minor | Dev resources (gap 16): links for developers (a URL and a name) on any layer: `devResource.add` / `edit` / `remove` / `list`, undoable, saved and synced; an instance also lists its main component's (and that component's set's) links read-only with where they come from, and layers inside an instance show their main layer's; URLs are checked as web links (http or https, a host, no spaces, at most 2,048 characters) and never fetched (a link added without a name is named after its URL), opened only when clicked; the Inspect tab lists, adds, edits and removes them; `get_node` and `inspect_document` return them as `devResources`. The editor extension stays out of scope (the MCP server serves agents and editors). Dev Mode breadth 93% → 96%, ready 70% → 72%, 1–3 h → 0–1 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~304–593 h → ~303–591 h | +| 2026-10-12 | minor | Extended collections' tail (gap 14): variables a library adds to a collection this file extends now count as a library update of their own (listed by name without a copy yet) and arrive inheriting when accepted, instead of waiting for the library's next other update; a library collection that is itself an extended collection can be extended again, values flowing down the chain as each file accepts updates with every level's overrides kept (checked end to end). Styles & variables breadth 94% → 95%, ready unchanged (76%), 3–5 h → 2–4 h; gap 14 1–2 h → 0–1 h (real-library checks left); feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~305–594 h → ~304–593 h | +| 2026-10-12 | minor | A branch's own version history and finer list merges (gap 17): versions saved or autosaved on a branch tab go into the branch (viewed, restored, renamed, deleted and duplicated there, saved in the file with it); merging adds a version to the main file's history and the merged or archived branch keeps its own; fills, strokes and effects merge item by item, conflicting only when the same item changed both ways, an item was removed on one side or both sides added different items. Comments, versions, find, misc breadth 85% → 87%, ready 60% → 61%, 2–5 h → 1–4 h; gap 17 1–3 h → 1–2 h; feature breadth (~82%) and ready for real work (~51%) unchanged at this rounding; full parity ~306–595 h → ~305–594 h | +| 2026-10-12 | minor | Headline percentages recomputed from the area table by its own method (weights × written scores): feature breadth ~82% → ~84% (core editor ~93% → ~94%), weighted ready ~61% → ~63%, core ready ~70% → ~72%, mainstream weekly areas 71.3% → 72.3% and mainstream ~49% → ~50%; several earlier rows had moved the area scores without carrying them into the totals. Hours are unchanged (left for the next full re-measure) | +| 2026-10-12 | minor | Text lists, hanging punctuation and decoration styles, with a behaviour suite written from the public documentation (7 layout tests and 7 command tests, about 60 table rows): lists are set per paragraph (the list buttons, shortcuts and Type settings act on the paragraphs the selection touches, the whole layer when it isn't being edited), nest up to 8 levels (Tab and Shift+Tab; numbers go 1. a. i., bullets • ◦ ▪, counted per level), Enter on an empty item and Backspace at an item's start step out a level and then out of the list, typing `- `, `* ` or `1. ` at a paragraph's start starts one; list spacing (between items, in place of paragraph spacing), hanging lists (markers outside the box) and hanging punctuation (opening punctuation outside a start-aligned edge, closing punctuation outside an end-aligned one, taking no room when lines wrap); underline and strikethrough style (solid, dotted, wavy), thickness, underline offset, skip ink and colour. Canvas, PNG, SVG and PDF; `node.set` keys (`indentation`, `listSpacing`, `hangingPunctuation`, `hangingList`, `textDecorationStyle`/`Thickness`/`Offset`/`SkipInk`/`Color`), inspect (`lineTypes`, `lineIndentations`), CSS (`text-decoration-style`/`-thickness`/`-color`, `text-underline-offset`, `text-decoration-skip-ink`) and the REST import (`lineTypes` with `lineIndentations` per paragraph, which collapsed to one list style before). Text 87%/68% → 90%/70%, 5–10 h → 3–7 h; mainstream weekly areas 71.0% → 71.2%; other percentages unchanged at this rounding; full parity ~306–595 h → ~304–592 h | +| 2026-10-12 | minor | Comments and version history behaviour suite written from the public documentation (15 tests, about 80 table rows plus stepwise checks; 4 tests failed before the fixes) and its fixes: in a live session, threads made in the session couldn't be replied to, edited, reacted to, resolved, moved, deleted or opened (their ids, from their creator's id space, were refused as out of range); restoring a version brought back that version's comments, losing newer threads and replies (comments now stay, those on layers the version lacks staying where they were); restoring now keeps the replaced design in the history as an autosave first, unless a version of it was just saved or restored; giving an autosave a title makes it a named version (it could be dropped to make room); merging a branch adds the main file as it was and then a `Merged “”` version to its history. Pinned where the documentation is silent: pins go to the innermost frame under them, copies, duplicates and duplicated pages come without comments, replying reopens a resolved thread, versions stay with whoever saved them in a live session (a shared history needs accounts). Left: a branch's own version history, comparing two versions. Comments, versions, find, misc 85%/60% → 85%/62%, 2–5 h → 2–4 h; mainstream weekly areas 71.0% → 71.1%; percentages unchanged at this rounding; full parity ~306–595 h → ~306–594 h | +| 2026-10-11 | minor | Canvas navigation, rulers, guides, layout grids, outline mode and measuring behaviour suite written from the public documentation (18 tests, about 120 table rows and checks) and its fixes: the pixel grid shows from 400% (it waited for 800%); guides belong to the page or a top-level frame (a guide dropped over a nested frame goes to the frame holding it, and a frame moved into another keeps its guides but they stop showing, snapping and measuring); a guide can be selected, deleted with Delete, nudged with the arrows and removed from the context menu (`guide.select`, `guide.remove` without an index); Alt-dragging a guide drops a copy and holding Alt while dragging one measures to the nearest edges in the frame it crosses; columns and rows count Auto (as many fixed-size ones as fit); layout grids don't show or snap on a rotated frame; in outline mode shapes are clicked on their outlines, reaching layers behind. Left: outline mode's Include hidden layers and Include object bounds. Canvas & navigation 97%/72% → 98%/74%, 2–5 h → 1–4 h; feature breadth (~80%), ready for real work (~49%), core editor (~68%), essentials (~57%) and mainstream (~46%) unchanged at this rounding; full parity ~320–615 h → ~319–614 h | +| 2026-10-11 | minor | Export and inspect behaviour suite written from the public documentation (139 cases in 19 tests; 47 failed before the fixes) and its fixes: an export covers what the layer paints, so a drop shadow adds room only where it falls, strokes on boxes and ellipses reach their real width (they added up to four times as much), hidden layers take no room and a mask group exports the mask's area; a slice exports everything under it; slashes in layer names export into folders; Copy as PNG copies the whole selection. Added: the export settings' contents only and include bounding box, and for SVG outline text (off writes ``), unique id attributes and simplified strokes (stroke attributes where exact); Inspect colour formats (hex, RGB, CSS, HSL, HSB), CSS in rem, a Blend row, positions measured from the frame around a layer (groups aren't frames), CSS for rotation, blend modes, gradient directions and kinds, text alignment, case and decoration; icons and images in the asset list (`file.exportLayers {assets}`). Import/export 80%/50% → 82%/53%; Dev Mode 90%/64% → 93%/70%; file formats 55% → 56%; mainstream weekly areas 68.1% → 68.4%; feature breadth (~80%), ready for real work (~49%), essentials (~57%), mainstream (~46%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | `.fig` import's tail (gap 1): vectors open as editable vector networks (points, curved segments, filled areas with their winding, per-point corner radius and handle mirroring, end caps from the end points), falling back to outlines when the network is missing or damaged; sticky notes, shapes with text, connectors, tables, code blocks, slides and similar layers import as the closest frames, text layers and vectors with a warning instead of being dropped; set-variable, set-variable-mode and conditional prototype actions come across. New parsers fuzzed on fixtures generated in code. File formats 55% → 58%, 8–15 h → 5–10 h; import/export breadth 80% → 82%, ready 50% → 54%; feature and area totals and ready for real work (~49%) unchanged; beta ~173–310 h → ~170–305 h; full parity ~321–618 h → ~318–613 h | +| 2026-10-11 | minor | Shapes and vector behaviour suite written from the public documentation (10 shape tests, 6 vector tests and 3 handle tests: about 70 table rows plus stepwise checks) and its fixes: rounded vector points are drawn and exported (the radius was stored only; a vector's corner radius now rounds every point); a boolean group looks like its top layer, a subtraction like the bottom one (always the bottom); Alt draws a line from its centre; stars keep to 3–60 points; delete and heal fits the curve through the deleted point; Shift-clicking a selected point at a corner of the points' box takes it out (it scaled); outline stroke turns the stroke's colour style into the fill's and keeps opacity and effects; the miter field shows the miter angle (it showed the ratio under that name); individual corners only for rectangles and frames; Option/Alt+Shift+F flattens; edit-mode keys reach the edit tools through MCP `key` (Delete removed the layer). Added: corner radius and star ratio handles on the canvas, Count and Ratio fields for polygons and stars, cut tool drags that divide a shape into layers, paint bucket drags across areas. Shapes & vector 97%/68% → 98%/72%, 1–2 h → 2–4 h (the suite found gaps the estimate didn't count: corner smoothing beyond rectangles, a join or cap per point). Totals recomputed by the doc's method, which earlier rows had left stale: the area table's weighted figures are breadth ~82% (core ~93%) and ready ~61% (core ~69%), not the ~80%/~58% (~91%/~68%) carried since #130; Features (depth) follows the weighted ready, 57% → 61%; ready for real work ~49% → ~51%; mainstream ~46% → ~48% (weekly areas 67.8% → 69.4%); the selection suite's area 2 hours (−3/−5) hadn't reached the totals, and neither it nor the text suite had reached the Features (depth) row: Features 60–115 h → 56–109 h, full parity ~321–618 h → ~319–615 h (−3/−5 for area 2, +1/+2 for area 4) | +| 2026-10-11 | minor | Paint and effects behaviour suite written from the public documentation (14 paint and 9 effects tests with table rows for fills, opacity, every blend mode by its formula, pass-through and Normal containers, gradient handles and stops, image scale modes and adjustments, colour models, the colour profile, shadows, blurs, effect order and exports; 3 command tests) and its fixes: groups and frames set to Normal blend their children among themselves first, and clipping frames no longer isolate their children (blend modes inside a clipping frame met only the frame's children, not its fill or what is behind it) — canvas, SVG (`isolation`; clips put on what blends, since a clipped group isolates) and PDF (isolated groups); plus darker is drawn (it was multiply) on the canvas and in PNG exports; drop shadows hide behind what the layer covers, translucent fills included, unless shown behind transparent areas, in SVG too (it never cut them out) and the SVG filter region reaches as far as the effects; Fill images cover the box to its edges (upscaled ones faded out at the edge); "Normal" names Normal, not pass-through, in commands and the blend menu (the menu's Normal set pass-through). Added: a blend menu for each paint in the colour picker. Paint 66% → 70%, 4–7 h → 3–6 h; effects 72% → 75%, 3–6 h → 2–5 h; percentages unchanged at this rounding; full parity ~321–618 h → ~319–616 h | +| 2026-10-11 | minor | Frames, groups, sections and masks behaviour suite written from the public documentation (15 frame, group and section tests, 6 mask tests) and its fixes: a layer a mask hides can't be clicked there (clicks reach what's behind; Select layer agrees), a mask group's box and exports are the mask's (plus any layers under it), sections stay on the page or in sections (moves, drags, pastes, grouping and new sections in frames refused or kept on the page), drawing a frame or section around layers puts those fully inside it in it, resizing a section leaves its contents where they are (and a frame resized with Cmd/Ctrl held keeps its layers' sizes too), and X/Y of layers in groups are measured in the frame (groups have no coordinate space). Frames, groups, sections, masks 95%/75% → 97%/80%, 2–4 h → 1–3 h; percentages unchanged at this rounding; full parity ~323–621 h → ~322–620 h | +| 2026-10-11 | minor | Styles and variables behaviour suite written from the public documentation (18 command-level tests) and its fixes: editing a styled fill, stroke, effect or layout grid list (directly, a paint row, an added fill or a bound variable) unlinks that style instead of being overwritten by the next style edit; applying a style replaces variables bound to what it sets; text styles go on text layers only; deleting a variable (or its collection) unbinds the layers using it, which keep their last value, and aliases to it take its values; bindings are type-checked (a colour variable can't drive a width) and number variables fill text content; aliases are checked when a variable is created. Added: duplicate, move (reorder, into a folder) and rename folder for styles, with a Local styles list grouped by type and folder (click to edit, right-click, drag to reorder); duplicate mode, set as default and move column for modes; per-platform code syntax (Web, iOS, Android) used by generated CSS and the variables JSON; font weight, paragraph spacing/indent and text content scopes for number variables. Found missing: variables inside styles. Styles & variables 92%/72% → 94%/76%, 1–2 h → 3–5 h; percentages unchanged at this rounding; full parity ~323–621 h → ~325–624 h | +| 2026-10-11 | minor | Prototyping behaviour suite written from the public documentation (7 model tests, 12 player tests with 21 gestures) and its fixes: on drag starts only for a drag its animation's way and the animation follows the pointer, key combinations (`Shift+K`) fire (and are recorded and imported), an overlay's after-delay timer starts when it opens, instances of interactive components play their variant's interactions and "Change to" changes the right instance, spring presets are physical springs that play until they settle, smart animate overshoots with spring and back easings, fixed layers draw above scrolling content; added scroll to offsets. Prototyping 88%/70% → 90%/74%, 5–9 h → 3–6 h; mainstream weekly areas 68.1% → 68.4%, mainstream ~46% → ~47%; other percentages unchanged at this rounding; full parity ~320–615 h → ~318–612 h | +| 2026-10-11 | minor | Components behaviour suite written from the public documentation (10 tests, 45 table cases) and its fixes: instances now keep overrides of auto layout padding, gap and alignment, layout grids, export settings, prototype interactions and every text property (resizing, paragraph spacing, lists…; these were lost when the main changed); moving, rotating, resizing or re-constraining layers inside an instance, and its auto layout direction, are refused (the instance itself resizes); switching variants keeps an override only where both variants had the same value (text layers keep theirs); swapping from the Assets panel keeps only text overrides; the default variant is the top-left one (instances, Assets thumbnails, code); hidden exposed instances hide their properties; pushing changes from a nested layer goes to the outer main. Added: variant property reordering and duplicate combination warnings. Components 95%/68% → 96%/72%, 6–12 h → 4–9 h; with #133's selection and transform change, core editor ready ~68% → ~69% and mainstream ~46% → ~47% (weekly areas 68.1% → 68.8%); full parity ~320–615 h → ~315–607 h (the totals now also carry #133's area 2 hours) | +| 2026-10-11 | minor | Embedded image colour profiles: placed and opened images are converted once, when decoded, from their own colour space into the file's (sRGB or Display P3): ICC profiles in PNG (`iCCP`), JPG (APP2), WebP and TIFF, matrix/curve and table profiles in RGB or grey (moxcms, already built for the `image` crate), and a PNG's `sRGB`, `gAMA` and `cHRM` chunks; images without a profile, or with one that can't be used (CMYK, damaged, over 4 MB), are taken as sRGB as before. The original file is kept (PDF exports still embed it); pixel exports carry the converted pixels with the file's profile. The image fill's details show the source profile (Unknown — treated as sRGB when there's none) and `image.place` reports it as `imageProfile`. Paint breadth 87% → 90%, ready 66% → 67%, 4–7 h → 3–6 h; feature breadth (~80%), ready for real work (~49%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | Layers panel keyboard and stroke variables: F2 renames the selected layer (also from a layer row with the keyboard, which gets it back; Escape there cancels), Shift+F10 (`view.contextMenu`) opens the context menu at the focused row or below the canvas selection and the menu takes the keyboard, F8 (`select.area`) draws a selection box for keyboard-only users (arrows move it, Shift+arrows size it, Enter selects like a marquee; agents pass a box), long lists checked at 10k layers (revealing a layer, expanding or collapsing everything and arrow keys take at most 1.3 ms a frame in the release UI bench; the revealed row takes the keyboard); colour styles on strokes were already complete, and each side's stroke weight now binds to a number variable (`strokeTopWeight`…, bindable fields in the Design panel and REST import). Layers panel 95%/78% → 98%/82%, 2–4 h → 1–2 h; Strokes 90%/72% → 95%/75%, 1–3 h → 1–2 h; mainstream depth 67.8% → 68.1%; feature breadth (~80%), ready for real work (~49%), essentials (~57%) and mainstream (~46%) unchanged at this rounding; full parity ~321–618 h → ~320–615 h | +| 2026-10-11 | minor | Text behaviour suite written from the public documentation (13 text cases, 5 command tests) and its fixes: CJK lines break between ideographs, kana and Hangul and before and after Latin words without breaking inside them, with the line-start and line-end rules (closing punctuation, small kana and the prolonged sound mark never start a line, opening brackets never end one); a style's line height, baseline and decoration come from its own font, so lines with CJK or emoji drawn by fallback fonts keep the spacing of the others; paragraph indent applies to start-aligned text only; carets, Delete and Backspace step over whole grapheme clusters (emoji sequences, flags, combining marks), which also shape with one font; double-click and word moves take one script's run in text without spaces. Added: vertical trim (cap height to baseline; `leadingTrim`, Type settings, REST import, inspect, CSS `text-box`). Text 85%/64% → 87%/68%, 7–13 h → 5–10 h; percentages unchanged at this rounding; full parity ~323–621 h → ~321–618 h | +| 2026-10-11 | minor | Selection and transform behaviour suite written from the public documentation (5 selection tests with 38 table rows, 11 transform tests) and its fixes: Alt-resizing shrinks about the centre too (it could only grow); groups, frames with Scale or stretch constraints and several layers with different rotations scale each layer along its own axes, keeping its rotation and flip (children turned 90° grew along the wrong axis, nested turned groups too); several layers dragged past an edge mirror and flip; a flipped layer reads back the rotation typed; resizes land on whole pixels with the pixel grid on; a press inside a layer smaller than its handles moves it and the nearest handle wins (the first in reach did), with its handles hidden under 16 px; locked layers selected in the layers panel get no handles and aren't dragged with the rest; the background of a top-level frame with layers starts a marquee inside it (it moved the frame); Select inverse skips hidden layers like Select all. Added: equal spacing snapping with labelled gaps, Cmd/Ctrl while resizing a frame ignores its constraints. Selection & transform 95%/68% → 97%/72%, 6–10 h → 3–5 h; core editor ready ~67% → ~68%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~345–666 h → ~342–661 h | +| 2026-10-11 | minor | `.fig` import, best effort (gap 1): `.fig` files open (best effort): the zip, its deflate/zstd chunks and the kiwi schema are decoded generically and mapped by field name onto the REST importer, so pages, the layer tree, geometry, paints (styles resolved), strokes, effects, text with styled runs, vectors (as outlines), auto layout (grid included), constraints, components, sets, instances (expanded from their main with overrides, derived layout and property values), styles, variables with modes and bindings, prototyping and image fills from the archive come across; anything else is a warning. Import/export breadth 65% → 80%, ready 35% → 50%, 30–60 h → 8–15 h; file formats 40% → 55%, 30–60 h → 8–15 h; feature breadth ~79% → ~80% (core ~90% → ~91%), areas ready ~57% → ~58% (core ~67% → ~68%); ready for real work ~47% → ~49%; essentials ~53% → ~57% (opening files people send ×0.80 → ×0.88); mainstream ~43% → ~46% (weekly areas 66.8% → 67.8%, file exchange ×0.80 → ×0.85); beta ~195–355 h → ~173–310 h; full parity ~345–666 h → ~322–620 h | +| 2026-10-11 | minor | Auto layout behaviour suite written from the public documentation (64 layout cases, 8 command tests) and its fixes: a frame is never smaller than its padding, only the frame's inside stroke counts when strokes are included (and new auto layout includes them), auto gaps stop at 0, grid `fr` tracks behave like the CSS `fr` unit (a track never gets less than its content), canvas stacking is drawn, exported and hit (it was stored only). Added: auto gaps evenly and around, vertical wrap, grid cell alignment, strokes and canvas stacking controls. Auto layout 95%/68% → 98%/74%, 6–12 h → 3–6 h; percentages unchanged at this rounding; full parity ~348–672 h → ~345–666 h | +| 2026-10-11 | minor | Display P3 honest minimum (gap 18): the colour picker of a Display P3 file warns when a colour is outside sRGB (Outside sRGB, with what the canvas and exports do), and View ▸ Soft-proof sRGB (`view.proofSrgb`) stripes the canvas pixels sRGB can't show. Why the canvas still shows sRGB: wgpu 30 can present Display P3 or extended-range sRGB surfaces (Metal, DX12, Vulkan, WebGPU canvases), but eframe/egui-wgpu 0.36 configure an 8-bit sRGB surface with no option for the format or colour space, and egui's renderer writes sRGB-encoded UI colours; showing P3 needs those two upstream (or a patched egui-wgpu). Hardware 13–27 h → 12–26 h; gap 18 6–12 h → 5–11 h; percentages, feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~362–696 h | +| 2026-10-11 | minor | Several windows (gap 18): File ▸ New window, a tab's Open in new window or a tab dragged off the tab strip opens files in another window with its own tabs, panels and canvas, sharing the files, preferences, libraries and clipboard; each window draws only when it needs to (idle: ~0% CPU), closing one closes its files after asking about unsaved changes (the main window too, which takes over another window instead of quitting; the last window quits), and the control channel and MCP open, list, focus and capture windows (`window.new`, `window.list`, `window.focus`, `window` on any request); the screen-reader tree covers the main window only (eframe 0.36), so with a screen reader running files stay tabs; one window in the browser. Hardware 55% → 60%, 13–27 h → 10–21 h; gap 18 6–12 h → 4–8 h (P3 on screen left); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~363–697 h → ~360–691 h | +| 2026-10-11 | minor | Localization layer landed (gap 5): per-language catalogs baked in at build time with English text as keys and fallback, placeholders and plurals, command labels by id, Preferences ▸ Language (System follows the OS or the browser) applied live and `pref.language`; English catalog and a complete Spanish draft awaiting native review. Localization 10% → 20%, 60–100 h → 45–75 h; ready for real work unchanged (~47%); full parity ~363–697 h → ~348–672 h; beta ~200–365 h → ~195–355 h | +| 2026-10-11 | minor | Hosting `figcraft-server`: the desktop app and `figcraft-cli` open `wss://` links (rustls with Mozilla's root certificates, always verified; `FIGCRAFT_CA_FILE` adds a private authority), releases ship the server for Linux x86_64 and aarch64, it stops gracefully on SIGTERM or Ctrl+C, and `docs/server.md` has a deployment guide (systemd unit, Caddy, nginx, Docker); percentages and hours unchanged until the next re-measure | +| 2026-10-11 | minor | Plugin panels and settings landed (gap 6, ABI v2; v1 plugins unchanged): a plugin shows a panel of notes, fields (text, number, checkbox, menu, list, colour) and buttons drawn with FigCraft's widgets; a button or a changed value calls it back with every value (same limits per callback, one undo step per callback that edits, rolled back on failure), its module staying loaded while the panel is open; `plugin.ui` / `plugin.event` / `plugin.close` drive panels headless; per-plugin settings (64 KB) kept with the preferences; the Swatch grid example gained a palette panel that remembers the last palette. Plugins & widgets breadth 35% → 45%, ready 20% → 28%, 25–55 h → 15–40 h; ecosystem 20% → 28%; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~373–712 h → ~363–697 h | +| 2026-10-11 | minor | Canvas fixes (gap 13 closed): boolean groups are clicked where their result is (a click in a subtracted hole reaches what's behind; with the group or an operand selected its operands are hit, so each can still be picked), a layer dragged inside a group stays in the group (frames and auto layout still take other drops), View ▸ Frame outlines and View ▸ Slices draw on the canvas, and the idle tests wait for the render thread instead of counting frames. Shapes & vector 1–3 h → 1–2 h (vector QA with real files left); percentages, feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | Code connect and Dev Mode plugins (gap 16): a component or set links to its code component per language (import path, name, a template with `{property}` and `{property ? "a" : "b"}` placeholders); `inspect.code`, `inspect.componentCode` and the Inspect panel show that snippet with the instance's values (also inside generated layer trees), edited in Connect to code with a live preview; links are saved, synced and undoable, and travel as `figcraft.connect.json` (`codeConnect.export` / `codeConnect.import`). Plugins that only read run from the Inspect tab. Dev Mode breadth 80% → 90%, ready 62% → 64%, 4–8 h → 1–3 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~378–722 h → ~375–717 h | +| 2026-10-11 | minor | Stretch and scatter brushes and pen pressure landed (gap 9): eight brushes drawn in code (four stretched along the path, four scattered with spacing, size, rotation and scatter jitter from a seed) in the stroke settings, drawn everywhere strokes are and flattened by outline stroke; the Pencil records pen pressure as the stroke's width profile (Windows pens, browser styluses; macOS and Linux tablets report none) and can draw with a brush; Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–9 h; Strokes 2–4 h → 1–3 h; Hardware 50% → 55%, 15–30 h → 13–27 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~378–722 h → ~358–687 h | +| 2026-10-11 | minor | Text on a path and pattern fills landed (gap 9): text follows a path or shape (start, side, alignment; edited in place; canvas, PNG, SVG and PDF; SVG `` imports as text on a path) and pattern fills tile a layer (grid or half-shifted rows or columns, scale, spacing, alignment; live as the source changes; SVG ``, PDF tiling patterns); Figma Draw extras breadth 45% → 65%, ready 30% → 45%, 10–18 h → 5–10 h; Text 8–14 h → 7–13 h; feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~375–715 h | +| 2026-10-11 | minor | Team libraries on `figcraft-server` landed (gap 10): a file's library is published to the server under the file and enabled in other files by a share link (per-asset versions; updates noticed in the background and accepted with overrides kept), the server counts which files use which components, and a whole library can be swapped for another by names; Team libraries breadth 40% → 70%, ready 25% → 40%, 10–20 h → 4–8 h; collaboration 12% → 13%, 55–110 h → 49–98 h; feature breadth (~79%) and ready for real work (~47%) unchanged; full parity ~380–725 h → ~375–715 h | +| 2026-10-11 | minor | Branches landed (gap 17): File ▸ Create branch… copies the file into a branch tab that remembers its base; Review and merge shows both sides' changes (layers with Inspect-style property changes, styles, variables, comments) and the conflicts (the same property changed on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merging is one undo step and archives the branch; Update from main; branches are saved in the `.figcraft` file. Comments, versions, find, misc breadth 80% → 85%, ready 58% → 60%, 4–8 h → 2–5 h; collaboration 12% → 13%; menu breadth 204/204 (the catalog gained File ▸ Create branch…); feature breadth (~79%) and ready for real work (~47%) unchanged at this rounding; full parity ~380–725 h → ~378–722 h | +| 2026-10-11 | minor | Animated GIF fills and TIFF images (gap 12, without video and HEIC): GIF fills keep the original file, show the first frame on the canvas and in exports and play in the presenter and the preview (frames decoded on first play, at most 500 frames and 128 MB; always looping; the window wakes only for a GIF's next frame); TIFF images open, import, place and paste. Paint breadth 85% → 87%, 5–9 h → 4–7 h; gap 12 6–12 h → 5–10 h (video playback and HEIC left, both needing non-Rust decoders); feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | The last three catalogued menu items got commands (gap 21): File ▸ Open recent (recent files kept in the preferences, at most 20, a file that's gone greyed in the menu and dropped when picked, Clear recent), View ▸ Outlines ▸ Show outlines for selection and View ▸ Mask outlines: menu breadth 98.5% → 100% (203/203); Canvas & navigation breadth 95% → 97%, 3–6 h → 2–5 h; feature breadth (~79%), ready for real work (~47%) and full parity hours unchanged at this rounding | +| 2026-10-11 | minor | Extended collections, variables import/export and gradient stop and variant property bindings landed (gap 14): Styles & variables breadth 85% → 92%, ready 70% → 72%, 4–8 h → 1–2 h; feature breadth unchanged (~79%); ready for real work unchanged (~47%); full parity ~385–730 h → ~380–725 h | +| 2026-10-11 | minor | Collaboration wording after the standalone server: `figcraft-server` keeps many files, persisted as a snapshot plus a log of what changed (compacted, checksummed, damaged files skipped and reported), restarts lose nothing confirmed and apps resume where they were; the app publishes its file to it; share links per file let people edit or only view (the server refuses a viewer's edits), are revocable and stored as hashes; TLS comes from a reverse proxy (the desktop app has none of its own yet); percentages and hours unchanged until the next re-measure | +| 2026-10-11 | minor | Live boolean editing, Design panel arc fields, the shape builder on strokes and open paths and one-sided variable widths landed (gap 13): Shapes & vector breadth 92% → 97%, ready 66% → 68%, 8–14 h → 1–3 h; feature breadth ~78% → ~79%; ready for real work unchanged (~47%); full parity ~390–745 h → ~385–730 h | +| 2026-10-11 | minor | Wording only: live sessions (#104), then the browser joining them and `figcraft://` invite links opening the desktop app; percentages and hours unchanged until the next re-measure | +| 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 | minor | Readiness-by-audience table: hours to ~95% for full (~400–750 h), mainstream (~180–330 h) and essentials (~85–160 h). Full number confirmed as an additive weighted sum; unchanged | +| 2026-10-10 | minor | Added the essentials user number (~53%: core features 77.8% × 0.92 launch/stability × 0.92 discoverability × 0.8 opening files people send) | +| 2026-10-10 | minor | Added the mainstream practitioner number (~43%: weekly areas 66.5% × 0.9 interaction × 0.9 stability × 0.8 file exchange) and user-evidence count (none). Checked that ready for real work (~46%) is built from the written dimension weights and scores; unchanged | +| 2026-10-10 | minor | Stage checked against the alpha gate: stays alpha | +| 2026-10-10 | major | Created from the ROADMAP's parity rubric, honest assessment and area narrative. Full re-measure against Figma desktop 126.10.7: added multiplayer, plugins, team libraries, Figma Draw extras and AI as weighted areas; split breadth from ready-for-real-work; core depth re-graded ~79% → ~67%; hours calibrated from git history; full parity 115–195 h → 420–790 h | From 4c494b804f29a06b396dcc270cac15b3437acfb1 Mon Sep 17 00:00:00 2001 From: ahmedhosnypro Date: Fri, 16 Oct 2026 03:16:41 +0300 Subject: [PATCH 03/38] Fix the system theme always light on Linux and the BSDs winit 0.30 reports no system theme there (EventLoop::system_theme is None), so the "system" preference resolved to light every time. When the windowing stack gives no theme, the interface now follows the freedesktop colour scheme instead: read once and watched live through the settings portal (org.freedesktop.appearance / color-scheme) in the desktop services (Services::system_dark), so GNOME's and KDE Plasma 6's appearance setting switches the app without a restart. Where the OS theme is reported (macOS, Windows, web) it still wins, and without any answer the light fallback stands. --- Cargo.lock | 1 + apps/figcraft/Cargo.toml | 6 ++ apps/figcraft/src/services.rs | 78 ++++++++++++++++++++++- crates/ui-egui/src/lib.rs | 12 +++- crates/ui-egui/src/theme_startup_tests.rs | 43 ++++++++++++- docs/target-app-parity.md | 3 +- 6 files changed, 136 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aead39d1..b2f9d9d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1427,6 +1427,7 @@ dependencies = [ "webbrowser", "windows", "winresource", + "zbus", ] [[package]] diff --git a/apps/figcraft/Cargo.toml b/apps/figcraft/Cargo.toml index bafb6da1..b22650eb 100644 --- a/apps/figcraft/Cargo.toml +++ b/apps/figcraft/Cargo.toml @@ -31,6 +31,12 @@ env_logger = { version = "0.11", default-features = false, features = ["auto-col [target.'cfg(windows)'.dependencies] windows = { version = "0.62", default-features = false, features = ["UI_ViewManagement", "System_UserProfile", "Foundation_Collections", "Gaming_Input"] } +# The system colour scheme (org.freedesktop.portal.Settings): winit reports no theme on Linux and +# the BSDs, so the interface's "system" mode follows the portal's instead. zbus comes in through +# accesskit already; this only makes it direct. +[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] +zbus = { version = "5", default-features = false, features = ["async-io", "blocking-api"] } + # Finder double-clicks, Open With, Dock drops and `open -a FigCraft file.figcraft` arrive as a # kAEOpenDocuments Apple event, which winit 0.30 doesn't handle. This crate (also used by GridCraft, # EffectCraft and PhotoCraft) registers the handler behind a safe API (Objective-C interop needs diff --git a/apps/figcraft/src/services.rs b/apps/figcraft/src/services.rs index bd8a4204..60ae2559 100644 --- a/apps/figcraft/src/services.rs +++ b/apps/figcraft/src/services.rs @@ -3,7 +3,7 @@ use std::cell::RefCell; use std::path::{Path, PathBuf}; use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use figcraft_engine::{Prefs, Session}; use figcraft_ui_egui::{DialogFuture, FigApp, Services}; @@ -16,6 +16,8 @@ pub struct Desktop { pub prefs: Option, /// The system's reduce-motion setting (read in the background at startup). reduce_motion: Arc, + /// The system's colour scheme ([`watch_system_dark`]): 0 unknown, 1 dark, 2 light. + dark: Arc, /// The system's interface language (read at startup). language: Option, /// Recovery copies of unsaved files (written at once after a panic). @@ -54,7 +56,19 @@ impl Desktop { let reduce_motion = Arc::new(AtomicBool::new(false)); let flag = reduce_motion.clone(); std::thread::spawn(move || flag.store(system_reduce_motion(), Ordering::Relaxed)); - Self { clipboard: arboard::Clipboard::new().ok(), dir, prefs, reduce_motion, language: system_language(), recovery } + let dark = Arc::new(AtomicU8::new(DARK_UNKNOWN)); + // winit reports no system theme on Linux and the BSDs: the freedesktop colour scheme + // answers instead, read once and then watched in the background. + #[cfg(all(unix, not(target_os = "macos")))] + if let Err(e) = std::thread::Builder::new().name("figcraft-theme".into()).spawn({ + let dark = dark.clone(); + move || watch_system_dark(dark) + }) { + log::info!("colour scheme: can't start the watcher: {e}"); + } + #[cfg(not(all(unix, not(target_os = "macos"))))] + let _ = dark; + Self { clipboard: arboard::Clipboard::new().ok(), dir, prefs, reduce_motion, dark, language: system_language(), recovery } } } @@ -136,6 +150,59 @@ fn command_says(program: &str, args: &[&str], want: &str) -> bool { .is_some_and(|o| String::from_utf8_lossy(&o.stdout).trim() == want) } +/// [`Desktop::dark`]: the colour scheme is unknown, dark, or light. +const DARK_UNKNOWN: u8 = 0; +const DARK_DARK: u8 = 1; +const DARK_LIGHT: u8 = 2; + +/// The desktop's colour scheme through the freedesktop settings portal +/// (`org.freedesktop.appearance` / `color-scheme`: 1 dark, 2 light), read once and then watched +/// for changes. Without a session bus or a portal the scheme stays unknown and the UI keeps its +/// light fallback. +#[cfg(all(unix, not(target_os = "macos")))] +fn watch_system_dark(dark: Arc) { + const PORTAL: &str = "org.freedesktop.portal.Desktop"; + const SETTINGS: &str = "org.freedesktop.portal.Settings"; + const APPEARANCE: &str = "org.freedesktop.appearance"; + let store = |v: u32| match v { + 1 => dark.store(DARK_DARK, Ordering::Relaxed), + 2 => dark.store(DARK_LIGHT, Ordering::Relaxed), + _ => {} + }; + let Ok(conn) = zbus::blocking::connection::Builder::session().and_then(|b| b.build()) else { + log::info!("colour scheme: no session bus: the system theme isn't followed"); + return; + }; + let read = || -> zbus::Result { + conn.call_method(Some(PORTAL), "/org/freedesktop/portal/desktop", Some(SETTINGS), "ReadOne", &(APPEARANCE, "color-scheme"))? + .body() + .deserialize() + }; + match read() { + Ok(v) => store(v), + Err(e) => log::info!("colour scheme: can't read the system setting: {e}"), + } + let watch = || -> zbus::Result<()> { + let rule = + zbus::MatchRule::builder().msg_type(zbus::message::Type::Signal).sender(PORTAL)?.interface(SETTINGS)?.member("SettingChanged")?.build(); + for msg in zbus::blocking::MessageIterator::for_match_rule(rule, &conn, Some(8))? { + let msg = msg?; + let body = msg.body(); + let Ok((namespace, key, value)) = body.deserialize::<(String, String, zbus::zvariant::Value<'_>)>() else { continue }; + if namespace == APPEARANCE + && key == "color-scheme" + && let zbus::zvariant::Value::U32(v) = value + { + store(v); + } + } + Ok(()) + }; + if let Err(e) = watch() { + log::info!("colour scheme: can't watch the system setting: {e}"); + } +} + /// Show `path` in the file manager: selected in Finder and Explorer, its folder elsewhere. fn reveal(path: &Path) -> std::io::Result<()> { #[cfg(target_os = "macos")] @@ -278,6 +345,13 @@ impl Services for Desktop { fn reduce_motion(&self) -> bool { self.reduce_motion.load(Ordering::Relaxed) } + fn system_dark(&self) -> Option { + match self.dark.load(Ordering::Relaxed) { + DARK_DARK => Some(true), + DARK_LIGHT => Some(false), + _ => None, + } + } fn gamepad_held(&mut self) -> u32 { crate::gamepad::held() } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 20a8a77c..e0749a97 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -239,6 +239,11 @@ pub trait Services { fn reduce_motion(&self) -> bool { false } + /// The system asks for the dark interface (the freedesktop colour scheme): for platforms whose + /// windowing stack doesn't report a theme (winit gives none on Linux and the BSDs). + fn system_dark(&self) -> Option { + None + } /// After a panic in a frame: keep recovery copies of unsaved files. fn emergency_save(&mut self, _session: &Session) {} /// Keep the preferences now (the installed plugins or their settings changed), as the app @@ -1125,9 +1130,10 @@ impl FigApp { } let dark = match self.session.prefs.theme.as_str() { "dark" => true, - // The active visuals were set by us last frame (possibly for a manual theme). - // Read the integration's OS theme instead; without one, keep the light fallback. - "system" => ctx.system_theme() == Some(egui::Theme::Dark), + // The active visuals were set by us last frame (possibly for a manual theme). Read the + // integration's OS theme; winit reports none on Linux and the BSDs, where the platform + // services' colour scheme answers instead. Without either, keep the light fallback. + "system" => ctx.system_theme().map(|t| t == egui::Theme::Dark).or_else(|| self.services.system_dark()).unwrap_or(false), _ => false, }; self.ui.dark = dark; diff --git a/crates/ui-egui/src/theme_startup_tests.rs b/crates/ui-egui/src/theme_startup_tests.rs index 6f2b13e2..2f0d33c4 100644 --- a/crates/ui-egui/src/theme_startup_tests.rs +++ b/crates/ui-egui/src/theme_startup_tests.rs @@ -4,7 +4,7 @@ use egui::{Context, RawInput, Theme}; use figcraft_engine::{Prefs, Session}; use serde_json::json; -use crate::{FigApp, NoServices, theme::Tokens}; +use crate::{FigApp, NoServices, Services, theme::Tokens}; fn frame(ctx: &Context, app: &mut FigApp, system: Option) { let mut output = ctx.run_ui(RawInput { system_theme: system, ..Default::default() }, |ui| app.ui(ui)); @@ -83,3 +83,44 @@ fn unavailable_system_theme_uses_the_existing_light_fallback() { frame(&ctx, &mut app, None); assert_eq!(Tokens::get(&ctx), Tokens::light()); } + +/// Services whose colour scheme answer stands in for the platform's (winit gives no theme on Linux +/// and the BSDs, where the freedesktop colour scheme takes over). +struct Scheme(Option); +impl Services for Scheme { + fn system_dark(&self) -> Option { + self.0 + } +} + +#[test] +fn without_a_system_theme_the_colour_scheme_services_decide() { + for (dark, expected) in [(Some(true), Tokens::dark()), (Some(false), Tokens::light())] { + let ctx = Context::default(); + let mut app = FigApp::new(Session::empty(), Box::new(Scheme(dark))); + app.session.execute("view.themeSystem", &json!({})).unwrap(); + frame(&ctx, &mut app, None); + assert_eq!(Tokens::get(&ctx), expected); + assert_eq!(app.ui.dark, expected.dark); + } +} + +#[test] +fn the_os_theme_wins_over_the_colour_scheme_services() { + for (os, scheme) in [(Theme::Light, Some(true)), (Theme::Dark, Some(false))] { + let ctx = Context::default(); + let mut app = FigApp::new(Session::empty(), Box::new(Scheme(scheme))); + app.session.execute("view.themeSystem", &json!({})).unwrap(); + frame(&ctx, &mut app, Some(os)); + assert_eq!(Tokens::get(&ctx).dark, os == Theme::Dark); + } +} + +#[test] +fn manual_themes_ignore_the_colour_scheme_services() { + let ctx = Context::default(); + let mut app = FigApp::new(Session::empty(), Box::new(Scheme(Some(false)))); + app.session.execute("view.themeDark", &json!({})).unwrap(); + frame(&ctx, &mut app, None); + assert_eq!(Tokens::get(&ctx), Tokens::dark()); +} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index e15ddaed..4a6cec25 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # FigCraft parity with Figma -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (system theme follows the freedesktop colour scheme on Linux and the BSDs; reviewed Russian catalog and integer plurals; vector edit mode regions: hover hatch, selection and drag; plugin log byte/count limits (#191); core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; starter templates, a Templates tab and Getting started tips on the home screen: essentials ~57% → ~59%; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages, numbers unchanged but menu breadth 211/211; text box sizes measured against documents re-saved by other software: whole-pixel line heights, advances on a 1024-per-em grid, letter spacing between glyphs only, numbers unchanged; .fig export checked by opening written files in other software, numbers unchanged; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%; last major: full re-measure against Figma desktop 126.10.7) · **Target:** Figma Design (desktop app 126.10.7, macOS) The authoritative assessment of how close FigCraft is to Figma Design. [`ROADMAP.md`](../ROADMAP.md) summarizes it; [`gaps.md`](gaps.md) is the ranked work list; the measured menu checklist is @@ -628,6 +628,7 @@ Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count th | Date | Change | Summary | |---|---|---| +| 2026-10-16 | minor | Fix: the "system" interface theme followed the OS only where the windowing stack reports a theme; winit gives none on Linux and the BSDs, so the desktop app always drew light there. The interface now falls back to the freedesktop colour scheme (`org.freedesktop.portal.Settings`, read once and watched through a D-Bus signal, the `Services::system_dark` platform services), so system mode follows GNOME's and KDE Plasma 6's appearance setting live; the OS theme keeps winning where winit does report one (macOS, Windows, web), and without any answer the light fallback stands. Numbers unchanged. | | 2026-10-14 | minor | Russian catalog (M9), reviewed and approved by VanTan: all 2,165 current entries, Русский in Preferences and regional ru tags. Russian integer plurals use one/few/many; contributor display name VanTan added. Existing estimates unchanged. | | 2026-10-13 | minor | Vector edit mode regions: the filled area under the pointer is hatched (clear of points and segments, holes left out, spacing by octave folding fitted to measurements at several zoom levels, fixed to the shape); click, Shift-click and drag select and move areas with their points (drawn unselected); a click on an empty spot clears them and stays in edit mode (see [ui-parity](ui-parity.md)) | | 2026-10-13 | minor | Plugin log limits (#191): cap each UTF-8 line at 8 KiB, total log bytes at 1 MiB and lines at 1,000 per run/callback before copying guest memory or forwarding to the host. Over-budget runs fail and roll back document edits; callbacks get fresh budgets. Added sandbox boundary/forwarding tests and an engine rollback regression. Percentages and hours unchanged. | From d55781e56e4b63f8cdb3f9b5728c77c45cb10d8d Mon Sep 17 00:00:00 2001 From: ahmedhosnypro Date: Fri, 16 Oct 2026 10:58:06 +0300 Subject: [PATCH 04/38] Read the portal's colour scheme as the D-Bus variant it is ReadOne replies with a variant body (signature v); deserializing it straight to u32 (signature u) is a signature mismatch that zvariant rejects, so the initial colour scheme never arrived and system mode kept its light fallback. Take the Value and read the uint32 out of it. --- apps/figcraft/src/services.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/figcraft/src/services.rs b/apps/figcraft/src/services.rs index 60ae2559..fa95e123 100644 --- a/apps/figcraft/src/services.rs +++ b/apps/figcraft/src/services.rs @@ -174,9 +174,13 @@ fn watch_system_dark(dark: Arc) { return; }; let read = || -> zbus::Result { - conn.call_method(Some(PORTAL), "/org/freedesktop/portal/desktop", Some(SETTINGS), "ReadOne", &(APPEARANCE, "color-scheme"))? - .body() - .deserialize() + let reply = conn.call_method(Some(PORTAL), "/org/freedesktop/portal/desktop", Some(SETTINGS), "ReadOne", &(APPEARANCE, "color-scheme"))?; + let body = reply.body(); + let value = body.deserialize::()?; + match value { + zbus::zvariant::Value::U32(v) => Ok(v), + _ => Ok(0), + } }; match read() { Ok(v) => store(v), From 66dd31ad9fb5b600959b32aa364e05552a4307ca Mon Sep 17 00:00:00 2001 From: AganzeFelicite Date: Fri, 16 Oct 2026 11:07:33 +0200 Subject: [PATCH 05/38] M7: find fonts activated through Adobe Fonts on macOS Fonts activated through Adobe Fonts rendered in the fallback font without a warning: Creative Cloud keeps them in its own folder as hidden files named by number, which the font scan never read. The macOS scan now includes ~/Library/Application Support/Adobe/CoreSync/plugins/livetype, and files without an extension are indexed when they start with a font signature. Fonts are read in place. Closes #298 --- ROADMAP.md | 4 +++- crates/text/src/fontdb.rs | 16 ++++++++++++++-- crates/text/src/fontdb_tests.rs | 29 +++++++++++++++++++++++++++++ docs/cli.md | 4 ++++ docs/roadmap.md | 3 ++- 5 files changed, 52 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index c3c12085..d6159904 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents) -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (bounded canvas pan overscan; community fixes from #478 up and a control-channel fix (#589); bounded canvas pan overscan; Typography spacing icons support scrubbing (#373); font scans skip named pipes; system theme on first run; recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading (#207), SVG import budgets (#190), Leptos code (#223), full screen (#227), server snapshots readable again (#188), owner-only preferences (#200), ruzstd 0.9.1 (#182), layer cycle repair (#205), batch rollback, Direct3D 12 on Windows, plugin metering (#192); vector edit mode regions: hover hatch, selection and drag; SVG/PDF exports refuse over-depth documents (#208); bounded MCP/control framing, connections and queues (#201); plugin log limits (#191); live-session revert guard (#198); attached MCP no longer replays after a lost reply (#194); repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (Adobe Fonts found on macOS (#298); bounded canvas pan overscan; community fixes from #478 up and a control-channel fix (#589); bounded canvas pan overscan; Typography spacing icons support scrubbing (#373); font scans skip named pipes; system theme on first run; recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading (#207), SVG import budgets (#190), Leptos code (#223), full screen (#227), server snapshots readable again (#188), owner-only preferences (#200), ruzstd 0.9.1 (#182), layer cycle repair (#205), batch rollback, Direct3D 12 on Windows, plugin metering (#192); vector edit mode regions: hover hatch, selection and drag; SVG/PDF exports refuse over-depth documents (#208); bounded MCP/control framing, connections and queues (#201); plugin log limits (#191); live-session revert guard (#198); attached MCP no longer replays after a lost reply (#194); repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7) FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow, native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through @@ -131,6 +131,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). ## Progress log +- **2026-10-16** · Adobe Fonts found on macOS (M7, #298): fonts activated through Adobe Fonts rendered in the fallback font without a warning, because Creative Cloud keeps them in its own folder as hidden files named by number, which the font scan never read. The macOS scan now includes `~/Library/Application Support/Adobe/CoreSync/plugins/livetype`, and files without an extension are indexed when they start with a font signature. Fonts are read in place. Regression test in the font database tests; docs/cli.md lists the folder. Numbers unchanged. - **2026-10-14** · Corner handles win over the corner-radius handles (M1, #292): a press on the inner half of a corner handle's square picked the radius handle 10 px in, so the drag neither resized nor (dragging outward) rounded anything. Inside the drawn square the corner now resizes both sides (in ratio with Shift or the link); further in the radius handle still answers. Regression test in the transform behaviour suite. Numbers unchanged. - **2026-10-14** · Home screen recent files as cards (M7): each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged. - **2026-10-13** · Vector edit mode regions (M4): with the Move tool, the filled area under the pointer is hatched with "/" lines in the selection colour, clipped by the area's own fill rule so holes stay clear; the hatch goes near points (3.5 px) and segments (2 px), and its spacing follows octave folding (8 px at every power-of-two zoom), fixed to the shape. A click selects the area with its points (they move with it but keep the unselected look), Shift adds or removes one, a press on a selected area drags the whole selection as one undo step, and a click on an empty spot clears the areas and stays in edit mode. `vector.edit.select {regions}` and `vector.edit.state` (`regions`, `regionVertices`, `regionHover`); `vector_regions_tests` and `regions_tests`. Numbers unchanged. @@ -313,6 +314,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). | Date | Change | Summary | |---|---|---| +| 2026-10-16 | minor | Adobe Fonts found on macOS (M7, #298): fonts activated through Adobe Fonts rendered in the fallback font without a warning, because Creative Cloud keeps them in its own folder as hidden files named by number, which the font scan never read. The macOS scan now includes `~/Library/Application Support/Adobe/CoreSync/plugins/livetype`, and files without an extension are indexed when they start with a font signature. Fonts are read in place. Regression test in the font database tests; docs/cli.md lists the folder. Numbers unchanged | | 2026-10-16 | minor | Community fixes from #478 up (batches #591, #598, #602; 69 PRs) and a control-channel fix: a line that isn't JSON now ends a `--control` connection, so a web page can't drive the app through a browser request (#589). Highlights: page copies keep navigation, flows, prototype settings and guides; Find and Replace keeps line endings and case-insensitive matches; text hyperlinks survive import and export; recent colours record only what was picked; MCP screenshots respect `maxSize`; Swedish catalog (UI strings), Czech plurals; bounded canvas overscan while panning; Korean fallback at the run's weight; many vector-edit, guide, page and .fig round-trip fixes. Numbers unchanged | | 2026-10-15 | minor | Drop files anywhere to open them (M7): dragging files over the window shows a "Drop to open" (home) or "Drop to import" (canvas) overlay. On the home screen each supported file opens as a document of its own and other files are turned away with a toast; on the canvas the files import at the drop point. Both wait behind a small card that is on screen before a large file blocks the window. A canvas import stays with the document and page that received the drop even if the tab or page changes meanwhile, and is cancelled with a message if that document or page is gone. Numbers unchanged. | | 2026-10-15 | minor | M7: system font discovery skips entries observed as non-regular files before opening them, avoiding the reproduced named-pipe stall. Regular-file symlinks remain supported; directory depth, font limits and priorities are unchanged. Numbers unchanged. | diff --git a/crates/text/src/fontdb.rs b/crates/text/src/fontdb.rs index d5ff4190..34d4775e 100644 --- a/crates/text/src/fontdb.rs +++ b/crates/text/src/fontdb.rs @@ -694,7 +694,11 @@ fn system_font_dirs() -> Vec { v.push("/System/Library/Fonts".into()); v.push("/Library/Fonts".into()); if let Some(h) = std::env::var_os("HOME") { - v.push(std::path::PathBuf::from(h).join("Library/Fonts")); + let h = std::path::PathBuf::from(h); + v.push(h.join("Library/Fonts")); + // Adobe Fonts activates its fonts from here (hidden files named by number), without + // installing them into a font folder. + v.push(h.join("Library/Application Support/Adobe/CoreSync/plugins/livetype")); } } #[cfg(target_os = "windows")] @@ -753,7 +757,7 @@ fn scan_dir(dir: &std::path::Path, depth: usize, out: &mut BTreeMap bool { + use std::io::Read; + let mut magic = [0u8; 4]; + std::fs::File::open(path).and_then(|mut f| f.read_exact(&mut magic)).is_ok() && matches!(&magic, &[0, 1, 0, 0] | b"OTTO" | b"true" | b"ttcf") +} + /// The parts of a font file the scan reads: headers, table directories and `name` tables, at their /// offsets in a zeroed buffer of the file's length (reading whole files was most of the scan). fn index_bytes(path: &std::path::Path, len: usize) -> Option> { diff --git a/crates/text/src/fontdb_tests.rs b/crates/text/src/fontdb_tests.rs index ea762be6..636309d3 100644 --- a/crates/text/src/fontdb_tests.rs +++ b/crates/text/src/fontdb_tests.rs @@ -179,3 +179,32 @@ fn user_font_dirs_preserve_non_utf8_paths() { assert_eq!(dirs[0].as_os_str().as_bytes(), b"/home/\xff/.fonts"); assert_eq!(dirs[1].as_os_str().as_bytes(), b"/data/\xfe/fonts"); } + +#[test] +fn the_scan_reads_font_files_without_an_extension() { + // #298: Adobe Fonts keeps the fonts it activates in hidden files named by number. Any installed + // font stands in for one (none on CI); a file that isn't a font is left alone. + let Some(font) = system_font_dirs().into_iter().find_map(|d| { + std::fs::read_dir(d) + .ok()? + .flatten() + .map(|e| e.path()) + .find(|p| p.extension().is_some_and(|x| x.eq_ignore_ascii_case("ttf") || x.eq_ignore_ascii_case("otf"))) + }) else { + eprintln!("skipping: no installed font to stand in for an activated one"); + return; + }; + let dir = std::env::temp_dir().join(format!("figcraft-font-bare-{}", std::process::id())); + std::fs::create_dir_all(dir.join(".r")).unwrap(); + std::fs::copy(&font, dir.join(".r/.12345")).unwrap(); + std::fs::write(dir.join(".r/entitlements"), b"").unwrap(); + let mut found = BTreeMap::new(); + scan_dir(&dir, 0, &mut found); + let _ = std::fs::remove_dir_all(&dir); + let families: Vec<&String> = found.values().map(|(f, _)| f).collect(); + assert_eq!(found.values().map(|(_, v)| v.len()).sum::(), faces_in(&std::fs::read(&font).unwrap()).len(), "{families:?}"); + assert!(!families.is_empty(), "{} is read as a font", font.display()); + assert!(found.values().flat_map(|(_, v)| v).all(|p| p.path.ends_with(".r/.12345"))); + #[cfg(target_os = "macos")] + assert!(system_font_dirs().iter().any(|d| d.ends_with("Adobe/CoreSync/plugins/livetype")), "Adobe Fonts' folder is scanned"); +} diff --git a/docs/cli.md b/docs/cli.md index 9f5df40e..ccd2bb77 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -19,6 +19,10 @@ On Linux and other Unix systems except macOS, user fonts are read from `$XDG_DATA_HOME/fonts`, or `$HOME/.local/share/fonts` when `XDG_DATA_HOME` is unset, empty or relative. The legacy `$HOME/.fonts` directory is also scanned. +On macOS, fonts activated through Adobe Fonts are read where Creative Cloud keeps them +(`~/Library/Application Support/Adobe/CoreSync/plugins/livetype`), so they're available while +they're active there. Files without an extension are read when they start like a font file. + The collaboration server is a binary of its own, `figcraft-server` (`cargo build --release -p figcraft-server`): it keeps files with share links and serves their live sessions, and its `key`, `files`, `links`, `reports`, `block-ip` and `check` subcommands manage them (a server can diff --git a/docs/roadmap.md b/docs/roadmap.md index 9e9eaec9..de8c7ff1 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # FigCraft roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (community fixes from #478 up and a control-channel fix (#589); recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading, SVG import budgets, Leptos code, full screen, server snapshots, owner-only preferences, ruzstd 0.9.1, layer cycle repair, batch rollback, Direct3D 12 on Windows, plugin metering; vector edit mode regions: hover hatch, selection and drag; repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; `.fig` import, best effort; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (Adobe Fonts found on macOS (#298); community fixes from #478 up and a control-channel fix (#589); recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading, SVG import budgets, Leptos code, full screen, server snapshots, owner-only preferences, ruzstd 0.9.1, layer cycle repair, batch rollback, Direct3D 12 on Windows, plugin metering; vector edit mode regions: hover hatch, selection and drag; repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; `.fig` import, best effort; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~79%) · **Target:** Figma Design (desktop app 126.10.7) Forward-looking plan. Where FigCraft stands today: [`ROADMAP.md`](../ROADMAP.md) and [`target-app-parity.md`](target-app-parity.md); the full ranked list: [`gaps.md`](gaps.md). Hours @@ -73,6 +73,7 @@ blockers. | Date | Change | Summary | |---|---|---| +| 2026-10-16 | minor | Adobe Fonts found on macOS (M7, #298): fonts activated through Adobe Fonts rendered in the fallback font without a warning, because Creative Cloud keeps them in its own folder as hidden files named by number, which the font scan never read. The macOS scan now includes `~/Library/Application Support/Adobe/CoreSync/plugins/livetype`, and files without an extension are indexed when they start with a font signature. Fonts are read in place. Regression test in the font database tests; docs/cli.md lists the folder. Numbers unchanged | | 2026-10-16 | minor | Community fixes from #478 up (batches #591, #598, #602; 69 PRs) and a control-channel fix: a line that isn't JSON now ends a `--control` connection, so a web page can't drive the app through a browser request (#589). Highlights: page copies keep navigation, flows, prototype settings and guides; Find and Replace keeps line endings and case-insensitive matches; text hyperlinks survive import and export; recent colours record only what was picked; MCP screenshots respect `maxSize`; Swedish catalog (UI strings), Czech plurals; bounded canvas overscan while panning; Korean fallback at the run's weight; many vector-edit, guide, page and .fig round-trip fixes. Numbers unchanged | | 2026-10-14 | minor | M7: the Small caps button in Type settings showed a box where the UI font has no small-capital `ɢ` (builds without Inter use egui's fonts); its sample is now a capital and a smaller capital, drawn from ordinary letters every UI font has. Numbers unchanged | | 2026-10-14 | minor | M7: Home screen recent files as cards: each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged. | From 9397d2ca0300bb1672eecc658d32f78ff2d8aafd Mon Sep 17 00:00:00 2001 From: timrrrr <54307723+timrrrr@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:16:26 +0300 Subject: [PATCH 06/38] M1: crop image fills during Command/Ctrl resize (#702) --- ROADMAP.md | 4 +- contributors/people.toml | 3 + crates/engine/src/tools.rs | 33 ++++ .../engine/src/transform_behaviour_tests.rs | 181 ++++++++++++++++++ docs/ui-parity.md | 4 +- 5 files changed, 223 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index c3c12085..82d48ca2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents) -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (bounded canvas pan overscan; community fixes from #478 up and a control-channel fix (#589); bounded canvas pan overscan; Typography spacing icons support scrubbing (#373); font scans skip named pipes; system theme on first run; recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading (#207), SVG import budgets (#190), Leptos code (#223), full screen (#227), server snapshots readable again (#188), owner-only preferences (#200), ruzstd 0.9.1 (#182), layer cycle repair (#205), batch rollback, Direct3D 12 on Windows, plugin metering (#192); vector edit mode regions: hover hatch, selection and drag; SVG/PDF exports refuse over-depth documents (#208); bounded MCP/control framing, connections and queues (#201); plugin log limits (#191); live-session revert guard (#198); attached MCP no longer replays after a lost reply (#194); repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (Command/Ctrl image crop; bounded canvas pan overscan; community fixes from #478 up and a control-channel fix (#589); bounded canvas pan overscan; Typography spacing icons support scrubbing (#373); font scans skip named pipes; system theme on first run; recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading (#207), SVG import budgets (#190), Leptos code (#223), full screen (#227), server snapshots readable again (#188), owner-only preferences (#200), ruzstd 0.9.1 (#182), layer cycle repair (#205), batch rollback, Direct3D 12 on Windows, plugin metering (#192); vector edit mode regions: hover hatch, selection and drag; SVG/PDF exports refuse over-depth documents (#208); bounded MCP/control framing, connections and queues (#201); plugin log limits (#191); live-session revert guard (#198); attached MCP no longer replays after a lost reply (#194); repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7) FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow, native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through @@ -131,6 +131,8 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). ## Progress log +- **2026-10-16** · Command/Ctrl-resize with the Move tool crops image fills on a single non-text layer outside auto layout, keeping their scale and position and the original image bytes. Fill/Fit/Crop, rotated layers and image fills, undo and native save/reopen are covered by regression tests. Text, auto-layout contexts, Tile and the Scale tool keep their existing behaviour. Estimates unchanged. + - **2026-10-14** · Corner handles win over the corner-radius handles (M1, #292): a press on the inner half of a corner handle's square picked the radius handle 10 px in, so the drag neither resized nor (dragging outward) rounded anything. Inside the drawn square the corner now resizes both sides (in ratio with Shift or the link); further in the radius handle still answers. Regression test in the transform behaviour suite. Numbers unchanged. - **2026-10-14** · Home screen recent files as cards (M7): each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged. - **2026-10-13** · Vector edit mode regions (M4): with the Move tool, the filled area under the pointer is hatched with "/" lines in the selection colour, clipped by the area's own fill rule so holes stay clear; the hatch goes near points (3.5 px) and segments (2 px), and its spacing follows octave folding (8 px at every power-of-two zoom), fixed to the shape. A click selects the area with its points (they move with it but keep the unselected look), Shift adds or removes one, a press on a selected area drags the whole selection as one undo step, and a click on an empty spot clears the areas and stays in edit mode. `vector.edit.select {regions}` and `vector.edit.state` (`regions`, `regionVertices`, `regionHover`); `vector_regions_tests` and `regions_tests`. Numbers unchanged. diff --git a/contributors/people.toml b/contributors/people.toml index bc8cf71b..1d6cd8b7 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -44,3 +44,6 @@ sync_github_name = true [people.volkovskey] real_name = "Andrii Volkov" + +[people.timrrrr] +display_name = "Timur" diff --git a/crates/engine/src/tools.rs b/crates/engine/src/tools.rs index 3d3b9acb..fea703b9 100644 --- a/crates/engine/src/tools.rs +++ b/crates/engine/src/tools.rs @@ -4,6 +4,7 @@ use std::collections::BTreeMap; +use figcraft_color::{ImageScaleMode, PaintKind}; use figcraft_doc::geometry::select_target; use figcraft_doc::*; use figcraft_geom::{Affine, Point, Rect, Vec2}; @@ -1558,6 +1559,38 @@ impl Session { if flip_y { t *= Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, nh]); } + // Text reflow and auto layout can recompute this box after the preview. + let crop_images = ev.cmd + && !matches!(doc.get(*id)?.kind, NodeKind::Text(_)) + && !std::iter::once(*id).chain(doc.ancestors(*id)).any(|a| doc.node(a).is_some_and(|n| n.layout.is_some())); + if crop_images { + // Crop the layer's image fills without moving their pixels on the page. + // Work from the interaction snapshot, just like the geometry on every move. + let n = doc.get(*id)?; + let (w, h) = (n.width.max(1e-6), n.height.max(1e-6)); + let to_old = + Affine::scale_non_uniform(1.0 / w, 1.0 / h) * world.inverse() * t * Affine::scale_non_uniform(nw.max(0.01), nh.max(0.01)); + let mut fills = n.fills.clone(); + for paint in &mut fills { + let PaintKind::Image(img) = &mut paint.kind else { continue }; + // A repeating image cannot be represented by a single Crop placement. + if img.scale_mode == ImageScaleMode::Tile { + continue; + } + let Some(data) = doc.images.get(&img.image) else { continue }; + let (iw, ih) = (f64::from(data.width), f64::from(data.height)); + let crop = if img.scale_mode == ImageScaleMode::Crop { + img.effective_crop(iw, ih, w, h) + } else { + let (rw, rh) = img.rotated_size(iw.max(1.0), ih.max(1.0)); + let s = if img.scale_mode == ImageScaleMode::Fit { (w / rw).min(h / rh) } else { (w / rw).max(h / rh) }; + [w / (rw * s), 0.0, 0.0, h / (rh * s), (rw * s - w) / (2.0 * rw * s), (rh * s - h) / (2.0 * rh * s)] + }; + img.crop = Some((Affine::new(crop) * to_old).as_coeffs()); + img.scale_mode = ImageScaleMode::Crop; + } + doc.get_mut(*id)?.fills = fills; + } doc.set_world_transform(*id, t)?; if kept.is_empty() { crate::cmd::props::resize_node(doc, *id, nw.max(0.01), nh.max(0.01))?; diff --git a/crates/engine/src/transform_behaviour_tests.rs b/crates/engine/src/transform_behaviour_tests.rs index 47270a42..250b7da1 100644 --- a/crates/engine/src/transform_behaviour_tests.rs +++ b/crates/engine/src/transform_behaviour_tests.rs @@ -98,6 +98,187 @@ const ALT: Mods = Mods { shift: false, alt: true, cmd: false }; const BOTH: Mods = Mods { shift: true, alt: true, cmd: false }; const CMD: Mods = Mods { shift: false, alt: false, cmd: true }; +/// An asymmetric synthetic image: every column is different, so recentering is visible. +fn image_layer(s: &mut Session) -> NodeId { + use base64::Engine as _; + let image = image::RgbaImage::from_fn(200, 100, |x, y| image::Rgba([x as u8, y as u8, 120, 255])); + let mut bytes = std::io::Cursor::new(Vec::new()); + image.write_to(&mut bytes, image::ImageFormat::Png).unwrap(); + id(&ex( + s, + "file.import", + json!({"data": base64::engine::general_purpose::STANDARD.encode(bytes.into_inner()), "name": "crop.png", "x": 0, "y": 0}), + )) +} + +#[test] +fn command_resize_crops_image_pixels_and_round_trips() { + let mut s = session(); + let layer = image_layer(&mut s); + let original = s.doc().unwrap().clone(); + let before = figcraft_render::render_node(&original, layer, 1.0).unwrap(); + let steps = undo_steps(&s); + // Public crop documentation: Command (macOS) / Ctrl (elsewhere) quickly crops an image. + // Moving the right edge must keep the left half's pixels, not recenter or scale them. + s.pointer(&ev(PointerKind::Down, Point::new(200.0, 50.0), CMD)).unwrap(); + for x in [150.0, 100.0] { + s.pointer(&ev(PointerKind::Move, Point::new(x, 50.0), CMD)).unwrap(); + } + s.pointer(&ev(PointerKind::Up, Point::new(100.0, 50.0), CMD)).unwrap(); + assert_eq!(size(&s, layer), (100.0, 100.0)); + let after = figcraft_render::render_node(s.doc().unwrap(), layer, 1.0).unwrap(); + for y in 0..100usize { + assert_eq!(&after.data()[y * 400..(y + 1) * 400], &before.data()[y * 800..y * 800 + 400], "row {y}: the image stays put"); + } + assert_eq!(s.doc().unwrap().images, original.images, "cropping keeps the source bytes"); + assert_eq!(undo_steps(&s), steps + 1); + let cropped = s.doc().unwrap().clone(); + ex(&mut s, "edit.undo", json!({})); + assert_eq!(s.doc().unwrap(), &original); + ex(&mut s, "edit.redo", json!({})); + assert_eq!(s.doc().unwrap(), &cropped); + let path = std::env::temp_dir().join(format!("figcraft-command-crop-{}.figcraft", std::process::id())); + ex(&mut s, "file.saveAs", json!({"path": path})); + let mut reopened = session(); + ex(&mut reopened, "file.open", json!({"path": path})); + assert_eq!(reopened.doc().unwrap().node(layer), cropped.node(layer)); + assert_eq!(reopened.doc().unwrap().images, cropped.images); + assert_eq!(figcraft_render::render_node(reopened.doc().unwrap(), layer, 1.0).unwrap().data(), after.data()); + std::fs::remove_file(path).unwrap(); +} + +#[test] +fn command_crop_keeps_image_placement_in_the_layers_axes() { + use figcraft_doc::color::{ImageScaleMode, PaintKind}; + use figcraft_geom::Affine; + // Each row exercises a distinct placement: Fill, Fit, and an existing affine Crop. + // Geometry follows the existing resize rules; image pixels stay fixed in page space. + let rows = [ + ("fill", None, 0.0, Point::new(0.0, 50.0), Point::new(40.0, 50.0), CMD, (40.0, 0.0, 160.0, 150.0)), + ("fit", None, 30.0, Point::new(100.0, 0.0), Point::new(100.0, 20.0), CMD, (0.0, 20.0, 200.0, 130.0)), + ("fill", None, 0.0, Point::new(0.0, 50.0), Point::new(240.0, 50.0), CMD, (240.0, 0.0, 40.0, 150.0)), + ( + "crop", + Some([0.8, 0.1, -0.2, 0.9, 0.1, 0.05]), + 30.0, + Point::new(200.0, 150.0), + Point::new(150.0, 112.5), + Mods { alt: true, ..CMD }, + (50.0, 37.5, 100.0, 75.0), + ), + ]; + for (mode, crop, angle, from, to, mods, (x, y, w, h)) in rows { + let mut s = session(); + let layer = image_layer(&mut s); + let mut fills = s.doc().unwrap().node(layer).unwrap().fills.clone(); + let PaintKind::Image(img) = &mut fills[0].kind else { panic!("image fill") }; + img.scale_mode = match mode { + "fit" => ImageScaleMode::Fit, + "crop" => ImageScaleMode::Crop, + _ => ImageScaleMode::Fill, + }; + img.crop = crop; + set(&mut s, &[layer], json!({"height": 150, "rotation": angle, "fills": fills})); + let doc = s.doc().unwrap(); + let world = doc.world_transform(layer); + let n = doc.node(layer).unwrap(); + let old_crop = crop.unwrap_or(if mode == "fit" { [1.0, 0.0, 0.0, 1.5, 0.0, -0.25] } else { [2.0 / 3.0, 0.0, 0.0, 1.0, 1.0 / 6.0, 0.0] }); + let old_mapping = Affine::new(old_crop) * Affine::scale_non_uniform(1.0 / n.width, 1.0 / n.height) * world.inverse(); + drag(&mut s, world * from, world * to, mods); + let doc = s.doc().unwrap(); + let n = doc.node(layer).unwrap(); + assert!(near(n.width, w) && near(n.height, h), "{mode}: crop box size"); + assert!((doc.world_transform(layer) * Point::ZERO - world * Point::new(x, y)).hypot() < 1e-6, "{mode}: crop box anchor"); + let PaintKind::Image(img) = &n.fills[0].kind else { panic!("image fill") }; + assert_eq!(img.scale_mode, ImageScaleMode::Crop, "{mode}"); + let mapping = + Affine::new(img.crop.unwrap()) * Affine::scale_non_uniform(1.0 / n.width, 1.0 / n.height) * doc.world_transform(layer).inverse(); + for p in [Point::ZERO, Point::new(80.0, 60.0), Point::new(170.0, 90.0)] { + assert!((mapping * p - old_mapping * p).hypot() < 1e-6, "{mode}: same page point samples the same source pixel"); + } + } +} + +#[test] +fn ordinary_resize_and_scale_keep_image_fill_settings() { + for (tool, cmd, ctrl) in [("move", false, false), ("move", false, true), ("scale", true, false)] { + let mut s = session(); + let layer = image_layer(&mut s); + let fills = s.doc().unwrap().node(layer).unwrap().fills.clone(); + ex(&mut s, "tool.set", json!({"tool": tool})); + let to = Point::new(100.0, 50.0); + for (kind, p) in [(PointerKind::Down, Point::new(200.0, 100.0)), (PointerKind::Move, to), (PointerKind::Up, to)] { + let mut event = ev(kind, p, Mods { cmd, ..NONE }); + event.ctrl = ctrl; + s.pointer(&event).unwrap(); + } + assert_eq!(s.doc().unwrap().node(layer).unwrap().fills, fills, "{tool}, cmd={cmd}, ctrl={ctrl}"); + assert_eq!(size(&s, layer), (100.0, 50.0)); + } +} + +#[test] +fn command_crop_preserves_each_image_fill_and_leaves_tiles() { + use figcraft_doc::color::{Color, ImageScaleMode, Paint, PaintKind}; + let mut s = session(); + let layer = image_layer(&mut s); + let image = s.doc().unwrap().node(layer).unwrap().fills[0].clone(); + let mut rotated = image.clone(); + rotated.opacity = 0.4; + let PaintKind::Image(img) = &mut rotated.kind else { panic!("image fill") }; + img.rotation = 1; + let mut tile = image.clone(); + let PaintKind::Image(img) = &mut tile.kind else { panic!("image fill") }; + img.scale_mode = ImageScaleMode::Tile; + let mut expected = vec![Paint::solid(Color::from_rgba8(20, 40, 60, 255)), rotated, image, tile]; + set(&mut s, &[layer], json!({"fills": expected})); + drag(&mut s, Point::new(200.0, 50.0), Point::new(100.0, 50.0), CMD); + for (i, crop) in [(1, [0.5, 0.0, 0.0, 0.25, 0.0, 0.375]), (2, [0.5, 0.0, 0.0, 1.0, 0.0, 0.0])] { + let PaintKind::Image(img) = &mut expected[i].kind else { panic!("image fill") }; + img.scale_mode = ImageScaleMode::Crop; + img.crop = Some(crop); + } + assert_eq!(s.doc().unwrap().node(layer).unwrap().fills, expected); +} + +#[test] +fn command_resize_leaves_fills_when_text_or_auto_layout_recomputes_the_box() { + for case in ["text", "child", "frame"] { + let mut s = session(); + let image = image_layer(&mut s); + let fills = s.doc().unwrap().node(image).unwrap().fills.clone(); + let layer = match case { + "text" => id(&ex( + &mut s, + "text.create", + json!({"text": "hello world hello world hello world", "props": { + "width": 200, "height": 100, "fontSize": 24, "textAutoResize": "none", "fills": fills + }}), + )), + _ => { + let frame = id(&ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 200, "height": 100}))); + set(&mut s, &[frame], json!({"layout": "horizontal", "padding": 20})); + if case == "child" { + ex(&mut s, "node.reparent", json!({"ids": [image], "parent": frame})); + set(&mut s, &[image], json!({"minWidth": 150})); + image + } else { + set(&mut s, &[frame], json!({"fills": fills})); + frame + } + } + }; + ex(&mut s, "select.set", json!({"ids": [layer]})); + let doc = s.doc().unwrap(); + let world = doc.world_transform(layer); + let n = doc.node(layer).unwrap(); + let from = world * Point::new(n.width, n.height / 2.0); + let to = world * Point::new(10.0, n.height / 2.0); + drag(&mut s, from, to, CMD); + assert_eq!(s.doc().unwrap().node(layer).unwrap().fills, fills, "{case}: keep the existing resize behaviour"); + } +} + #[test] fn handle_drags_resize_by_the_documented_rules() { // A 100×50 box; (handle, pointer in the box's axes, modifiers, new box x0 y0 x1 y1, derivation). diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 62001715..faeb7430 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (fields select their text on focus; Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths) · **Target:** Figma Design UI3 (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (Command/Ctrl image crop; fields select their text on focus; Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths) · **Target:** Figma Design UI3 (desktop app 126.10.7) Fine canvas control is what Figma users feel first: handles, snapping, nudging, modifier keys, drag behaviour, numeric precision. This checklist covers the tools, gestures and panels. Status: @@ -88,6 +88,7 @@ interaction feel not yet compared. | Text on a path (Figma Draw) | done | put on from the Design panel or `text.onPath`, not by clicking a path with the Text tool; the path is edited by resizing only | | Stretch and scatter brushes (Figma Draw) | done | eight presets drawn in code; the Pencil's brush is set by `tool.pencilBrush`, not from the toolbar | | Image crop handles | done | | +| Command/Ctrl-resize to crop image fills | partial | The Move tool crops Fill/Fit/Crop image fills on a single non-text layer outside auto layout, keeping image scale and position. Text, auto-layout contexts and Tile retain their existing resize behaviour (`transform_behaviour_tests`) | ## Canvas @@ -166,6 +167,7 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look | Date | Change | Summary | |---|---|---| +| 2026-10-16 | minor | Command/Ctrl-resize crops image fills without moving or scaling their source pixels; one undo step, saved with the document. Single non-text layers outside auto layout; Tile excluded. Estimates unchanged. | | 2026-10-15 | minor | Hex fields take CSS colour names (`red` → `FF0000`): fills, strokes, effects, grids, variables, backgrounds and the colour picker | | 2026-10-15 | minor | Typography: drag the line-height, letter-spacing and paragraph-spacing icons to scrub their values; shared numeric-field rates, resize cursor and one Undo step per gesture (#373). | | 2026-10-15 | minor | A click or Tab into a one-line field selects all of its text (hex, inline, name, search and dialog fields); the first click into a fill's hex takes the keyboard | From 8a77ef6759158f97d1768a367726f50541adccf9 Mon Sep 17 00:00:00 2001 From: timrrrr <54307723+timrrrr@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:18:07 +0300 Subject: [PATCH 07/38] M1: preserve image fills with variable-bound resize dimensions --- ROADMAP.md | 3 +- crates/engine/src/tools.rs | 7 ++-- .../engine/src/transform_behaviour_tests.rs | 39 +++++++++++++++++++ docs/ui-parity.md | 5 ++- 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 82d48ca2..f9754a2d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents) -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (Command/Ctrl image crop; bounded canvas pan overscan; community fixes from #478 up and a control-channel fix (#589); bounded canvas pan overscan; Typography spacing icons support scrubbing (#373); font scans skip named pipes; system theme on first run; recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading (#207), SVG import budgets (#190), Leptos code (#223), full screen (#227), server snapshots readable again (#188), owner-only preferences (#200), ruzstd 0.9.1 (#182), layer cycle repair (#205), batch rollback, Direct3D 12 on Windows, plugin metering (#192); vector edit mode regions: hover hatch, selection and drag; SVG/PDF exports refuse over-depth documents (#208); bounded MCP/control framing, connections and queues (#201); plugin log limits (#191); live-session revert guard (#198); attached MCP no longer replays after a lost reply (#194); repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (variable-bound sizes retain existing resize behaviour during Command/Ctrl image crop; Command/Ctrl image crop; bounded canvas pan overscan; community fixes from #478 up and a control-channel fix (#589); bounded canvas pan overscan; Typography spacing icons support scrubbing (#373); font scans skip named pipes; system theme on first run; recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading (#207), SVG import budgets (#190), Leptos code (#223), full screen (#227), server snapshots readable again (#188), owner-only preferences (#200), ruzstd 0.9.1 (#182), layer cycle repair (#205), batch rollback, Direct3D 12 on Windows, plugin metering (#192); vector edit mode regions: hover hatch, selection and drag; SVG/PDF exports refuse over-depth documents (#208); bounded MCP/control framing, connections and queues (#201); plugin log limits (#191); live-session revert guard (#198); attached MCP no longer replays after a lost reply (#194); repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7) FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow, native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through @@ -131,6 +131,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). ## Progress log +- **2026-10-17** · Command/Ctrl image crop excludes variable-bound width/height (including `w`/`h` aliases), preserving the existing resize behaviour when normalization restores the bound size. Other bindings still allow crop; pixel and binding regressions cover both dimensions and ordinary resize. Estimates unchanged. - **2026-10-16** · Command/Ctrl-resize with the Move tool crops image fills on a single non-text layer outside auto layout, keeping their scale and position and the original image bytes. Fill/Fit/Crop, rotated layers and image fills, undo and native save/reopen are covered by regression tests. Text, auto-layout contexts, Tile and the Scale tool keep their existing behaviour. Estimates unchanged. - **2026-10-14** · Corner handles win over the corner-radius handles (M1, #292): a press on the inner half of a corner handle's square picked the radius handle 10 px in, so the drag neither resized nor (dragging outward) rounded anything. Inside the drawn square the corner now resizes both sides (in ratio with Shift or the link); further in the radius handle still answers. Regression test in the transform behaviour suite. Numbers unchanged. diff --git a/crates/engine/src/tools.rs b/crates/engine/src/tools.rs index fea703b9..fe502581 100644 --- a/crates/engine/src/tools.rs +++ b/crates/engine/src/tools.rs @@ -1559,14 +1559,15 @@ impl Session { if flip_y { t *= Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, nh]); } - // Text reflow and auto layout can recompute this box after the preview. + // Text reflow, size bindings and auto layout can recompute this box after the preview. + let n = doc.get(*id)?; let crop_images = ev.cmd - && !matches!(doc.get(*id)?.kind, NodeKind::Text(_)) + && !matches!(n.kind, NodeKind::Text(_)) + && !n.bound.keys().any(|field| matches!(field.as_str(), "width" | "height" | "w" | "h")) && !std::iter::once(*id).chain(doc.ancestors(*id)).any(|a| doc.node(a).is_some_and(|n| n.layout.is_some())); if crop_images { // Crop the layer's image fills without moving their pixels on the page. // Work from the interaction snapshot, just like the geometry on every move. - let n = doc.get(*id)?; let (w, h) = (n.width.max(1e-6), n.height.max(1e-6)); let to_old = Affine::scale_non_uniform(1.0 / w, 1.0 / h) * world.inverse() * t * Affine::scale_non_uniform(nw.max(0.01), nh.max(0.01)); diff --git a/crates/engine/src/transform_behaviour_tests.rs b/crates/engine/src/transform_behaviour_tests.rs index 250b7da1..146bdfd6 100644 --- a/crates/engine/src/transform_behaviour_tests.rs +++ b/crates/engine/src/transform_behaviour_tests.rs @@ -279,6 +279,45 @@ fn command_resize_leaves_fills_when_text_or_auto_layout_recomputes_the_box() { } } +#[test] +fn image_crop_respects_variable_bound_dimensions() { + for (field, value, from, to) in [ + ("width", 200, Point::new(200.0, 50.0), Point::new(100.0, 50.0)), + ("height", 100, Point::new(100.0, 100.0), Point::new(100.0, 50.0)), + ("w", 200, Point::new(200.0, 50.0), Point::new(100.0, 50.0)), + ("h", 100, Point::new(100.0, 100.0), Point::new(100.0, 50.0)), + ("opacity", 100, Point::new(200.0, 50.0), Point::new(100.0, 50.0)), + ] { + for mods in [CMD, NONE] { + let mut s = session(); + let layer = image_layer(&mut s); + ex(&mut s, "variable.createCollection", json!({"name": "Tokens"})); + ex(&mut s, "variable.create", json!({"collection": "Tokens", "name": "bound", "type": "number", "value": value})); + ex(&mut s, "variable.bind", json!({"ids": [layer], "field": field, "variable": "bound"})); + let before = s.doc().unwrap().clone(); + let pixels = figcraft_render::render_node(&before, layer, 1.0).unwrap(); + drag(&mut s, from, to, mods); + let doc = s.doc().unwrap(); + let n = doc.node(layer).unwrap(); + assert_eq!(n.bound, before.node(layer).unwrap().bound, "{field}: keep the binding"); + assert_eq!(doc.variables, before.variables, "{field}: do not edit the variable"); + let after = figcraft_render::render_node(doc, layer, 1.0).unwrap(); + if field != "opacity" { + assert_eq!((n.width, n.height), (200.0, 100.0), "{field}: the binding restores the size"); + assert!(after.data() == pixels.data(), "{field}, cmd={}: restored bounds must not stretch a partial crop", mods.cmd); + } else if mods.cmd { + assert_eq!((n.width, n.height), (100.0, 100.0)); + for y in 0..100usize { + assert_eq!(&after.data()[y * 400..(y + 1) * 400], &pixels.data()[y * 800..y * 800 + 400], "opacity binding still allows crop"); + } + } + if field != "opacity" || !mods.cmd { + assert_eq!(n.fills, before.node(layer).unwrap().fills, "{field}: keep the existing resize behaviour"); + } + } + } +} + #[test] fn handle_drags_resize_by_the_documented_rules() { // A 100×50 box; (handle, pointer in the box's axes, modifiers, new box x0 y0 x1 y1, derivation). diff --git a/docs/ui-parity.md b/docs/ui-parity.md index faeb7430..4eff3aac 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (Command/Ctrl image crop; fields select their text on focus; Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths) · **Target:** Figma Design UI3 (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (variable-bound sizes retain existing resize behaviour during Command/Ctrl image crop; Command/Ctrl image crop; fields select their text on focus; Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths) · **Target:** Figma Design UI3 (desktop app 126.10.7) Fine canvas control is what Figma users feel first: handles, snapping, nudging, modifier keys, drag behaviour, numeric precision. This checklist covers the tools, gestures and panels. Status: @@ -88,7 +88,7 @@ interaction feel not yet compared. | Text on a path (Figma Draw) | done | put on from the Design panel or `text.onPath`, not by clicking a path with the Text tool; the path is edited by resizing only | | Stretch and scatter brushes (Figma Draw) | done | eight presets drawn in code; the Pencil's brush is set by `tool.pencilBrush`, not from the toolbar | | Image crop handles | done | | -| Command/Ctrl-resize to crop image fills | partial | The Move tool crops Fill/Fit/Crop image fills on a single non-text layer outside auto layout, keeping image scale and position. Text, auto-layout contexts and Tile retain their existing resize behaviour (`transform_behaviour_tests`) | +| Command/Ctrl-resize to crop image fills | partial | The Move tool crops Fill/Fit/Crop image fills on a single non-text layer outside auto layout, keeping image scale and position. Text, variable-bound width/height, auto-layout contexts and Tile retain their existing resize behaviour (`transform_behaviour_tests`) | ## Canvas @@ -167,6 +167,7 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look | Date | Change | Summary | |---|---|---| +| 2026-10-17 | minor | Command/Ctrl image crop leaves fills unchanged when width or height has a variable binding (including `w`/`h` aliases); normalization may restore those dimensions after preview. Other bindings still allow crop. Regression covers pixels, binding preservation and ordinary resize. Estimates unchanged. | | 2026-10-16 | minor | Command/Ctrl-resize crops image fills without moving or scaling their source pixels; one undo step, saved with the document. Single non-text layers outside auto layout; Tile excluded. Estimates unchanged. | | 2026-10-15 | minor | Hex fields take CSS colour names (`red` → `FF0000`): fills, strokes, effects, grids, variables, backgrounds and the colour picker | | 2026-10-15 | minor | Typography: drag the line-height, letter-spacing and paragraph-spacing icons to scrub their values; shared numeric-field rates, resize cursor and one Undo step per gesture (#373). | From e409f9efc0c5ef98b430e4e66263021478bac046 Mon Sep 17 00:00:00 2001 From: timrrrr <54307723+timrrrr@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:48:50 +0300 Subject: [PATCH 08/38] M1: retain GitHub username for contributor credit --- contributors/people.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/contributors/people.toml b/contributors/people.toml index 1d6cd8b7..bc8cf71b 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -44,6 +44,3 @@ sync_github_name = true [people.volkovskey] real_name = "Andrii Volkov" - -[people.timrrrr] -display_name = "Timur" From 31d6566f3b02d915a16ecb559ac028193a3833d4 Mon Sep 17 00:00:00 2001 From: leandrodslv <205958219+leandrodslv@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:27:54 +0200 Subject: [PATCH 09/38] i18n(fr): land the native-speaker wording fixes from the draft review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #783 completed the French catalog to every current string, superseding the translations added in #681. This keeps only the ~50 UI strings and 8 command labels whose wording a native French speaker corrected during that review (keyboard focus, "Off" as "Désactivé", blend modes, stroke join, spotlight, the no-break space before : ; ? !…), applied on top of #783. The draft status is unchanged. Co-Authored-By: Claude Sonnet 5 --- crates/ui-egui/locales/fr.toml | 116 ++++++++++++++++----------------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/crates/ui-egui/locales/fr.toml b/crates/ui-egui/locales/fr.toml index 5edf1a62..ec080d67 100644 --- a/crates/ui-egui/locales/fr.toml +++ b/crates/ui-egui/locales/fr.toml @@ -148,7 +148,7 @@ status = "draft — needs native review" "Back to assets" = "Retour aux ressources" "Back to current" = "Revenir à l’actuelle" "Back to styles" = "Retour aux styles" -"Back to the canvas from a focused region" = "Revenir au canevas depuis une région active" +"Back to the canvas from a focused region" = "Revenir au canevas depuis une région qui a le focus" "Back to the current version" = "Revenir à la version actuelle" "Back to the values the font style implies" = "Revenir aux valeurs du style de police" "Background" = "Arrière-plan" @@ -184,7 +184,7 @@ status = "draft — needs native review" "Branch {name}" = "Branche {name}" "Branches" = "Branches" "Bring forward" = "Avancer" -"Bring the main file's changes into the branch" = "Apporte dans la branche les modifications du fichier principal" +"Bring the main file's changes into the branch" = "Apporter dans la branche les modifications du fichier principal" "Bring to front" = "Mettre au premier plan" "Bristle" = "Soies" "Browse" = "Parcourir" @@ -252,8 +252,8 @@ status = "draft — needs native review" "Collection" = "Collection" "Collections" = "Collections" "Color" = "Couleur" -"Color burn" = "Brûlage de couleur" -"Color dodge" = "Éclaircissement de couleur" +"Color burn" = "Densité couleur +" +"Color dodge" = "Densité couleur −" "Color format" = "Format de couleur" "Color profile" = "Profil colorimétrique" "Color styles" = "Styles de couleur" @@ -317,8 +317,8 @@ status = "draft — needs native review" "Corner radius" = "Rayon d’angle" "Corner smoothing" = "Lissage des angles" "Couldn't download the variables" = "Impossible de télécharger les variables" -"Couldn't reach {server}: showing what this computer remembers." = "Impossible de joindre {server} : affichage de ce dont cet ordinateur se souvient." -"Couldn't save automatically: {error}" = "Impossible d’enregistrer automatiquement : {error}" +"Couldn't reach {server}: showing what this computer remembers." = "Impossible de joindre {server} : affichage de ce dont cet ordinateur se souvient." +"Couldn't save automatically: {error}" = "Impossible d’enregistrer automatiquement : {error}" "Count" = "Nombre" "Create" = "Créer" "Create a variable collection first" = "Créez d’abord une collection de variables" @@ -446,14 +446,14 @@ status = "draft — needs native review" "Empty" = "Vide" "End" = "Fin" "End point" = "Point final" -"Ends the session; everyone keeps their copy" = "Met fin à la session ; chacun conserve sa copie" +"Ends the session; everyone keeps their copy" = "Met fin à la session ; chacun conserve sa copie" "Enter" = "Entrée" "Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "Saisissez l’adresse d’un figcraft-server et appuyez sur Parcourir pour voir les plugins qu’il propose." "Enter to save · Esc to cancel" = "Entrée pour enregistrer · Échap pour annuler" "Eraser" = "Gomme" "Esc" = "Échap" "Essential" = "Essentiels" -"Every conflict has a choice." = "Chaque conflit a un choix." +"Every conflict has a choice." = "Un choix est fait pour chaque conflit." "Every top-level frame already starts a flow" = "Chaque cadre de premier niveau démarre déjà un flux" "Exact opacity (4 then 5: 45%)" = "Opacité exacte (4 puis 5 : 45 %)" "Exclude" = "Exclure" @@ -471,9 +471,9 @@ status = "draft — needs native review" "Export variables" = "Exporter les variables" "Export {name}" = "Exporter {name}" "Export {n} layers" = { one = "Exporter {n} calque", other = "Exporter {n} calques" } -"Exported {file}" = "Exporté : {file}" +"Exported {file}" = "{file} exporté" "Exported {name}" = "{name} exporté" -"Exported {name}; {n} kinds of content were left out" = { one = "{name} exporté ; {n} type de contenu a été omis", other = "{name} exporté ; {n} types de contenu ont été omis" } +"Exported {name}; {n} kinds of content were left out" = { one = "{name} exporté ; {n} type de contenu a été omis", other = "{name} exporté ; {n} types de contenu ont été omis" } "Exported {n} files" = { one = "{n} fichier exporté", other = "{n} fichiers exportés" } "Export…" = "Exporter…" "Expose properties to instances" = "Exposer les propriétés aux instances" @@ -493,7 +493,7 @@ status = "draft — needs native review" "File name" = "Nom du fichier" "File not found" = "Fichier introuvable" "File published" = "Fichier publié" -"Files aren't end-to-end encrypted: the server's operator can read them." = "Les fichiers ne sont pas chiffrés de bout en bout : l’opérateur du serveur peut les lire." +"Files aren't end-to-end encrypted: the server's operator can read them." = "Les fichiers ne sont pas chiffrés de bout en bout : l’opérateur du serveur peut les lire." "Files nobody opens for {n} days are deleted." = { one = "Les fichiers que personne n’ouvre pendant {n} jour sont supprimés.", other = "Les fichiers que personne n’ouvre pendant {n} jours sont supprimés." } "Fill" = "Remplissage" "Fill (fr)" = "Remplir (fr)" @@ -571,14 +571,14 @@ status = "draft — needs native review" "Groups" = "Groupes" "Gutter" = "Gouttière" "Hand tool" = "Outil Main" -"Hanging list" = "Liste en retrait" +"Hanging list" = "Liste à retrait suspendu" "Hanging punctuation" = "Ponctuation en débord" "Hard light" = "Lumière dure" "Height" = "Hauteur" "Help" = "Aide" "Help and resources" = "Aide et ressources" "Help page" = "Page d’aide" -"Here: {names}" = "Présents : {names}" +"Here: {names}" = "Présents : {names}" "Hidden" = "Masqué" "Hide contents" = "Masquer le contenu" "Hide from publishing" = "Exclure de la publication" @@ -668,7 +668,7 @@ status = "draft — needs native review" "Joined" = "Connecté" "Joined {file}" = "Connecté à {file}" "Joining…" = "Connexion…" -"Join|stroke" = "Jointure" +"Join|stroke" = "Jonction" "Jul" = "juil." "Jun" = "juin" "Just now" = "À l’instant" @@ -693,7 +693,7 @@ status = "draft — needs native review" "Layer name" = "Nom du calque" "Layers" = "Calques" "Layers in an instance take their position from the main component" = "Les calques d’une instance prennent leur position du composant principal" -"Layout" = "Disposition" +"Layout" = "Mise en page" "Layout grid" = "Grille de mise en page" "Layout grid settings" = "Réglages de la grille de mise en page" "Layout grids" = "Grilles de mise en page" @@ -761,9 +761,9 @@ status = "draft — needs native review" "Maximize" = "Agrandir" "May" = "mai" "Measure to the selection" = "Mesurer jusqu’à la sélection" -"Merge into main" = "Fusionner dans le principal" +"Merge into main" = "Fusionner dans le fichier principal" "Merge regions" = "Fusionner les régions" -"Merge the branch into the main file (one undo step) and archive it" = "Fusionne la branche dans le fichier principal (une seule étape d’annulation) et l’archive" +"Merge the branch into the main file (one undo step) and archive it" = "Fusionner la branche dans le fichier principal (une seule étape d’annulation) et l’archiver" "Merged" = "Fusionnée" "Merged PRs" = "PR fusionnées" "Min H" = "H min." @@ -784,7 +784,7 @@ status = "draft — needs native review" "Mobile app" = "Application mobile" "Mode" = "Mode" "Model" = "Modèle" -"Models" = "Modèles" +"Models" = "Modèles d’IA" "Mono" = "Mono" "More" = "Plus" "More instance actions" = "Autres actions d’instance" @@ -797,8 +797,8 @@ status = "draft — needs native review" "Move column left" = "Déplacer la colonne à gauche" "Move column right" = "Déplacer la colonne à droite" "Move down" = "Descendre" -"Move in" = "Entrée" -"Move out" = "Sortie" +"Move in" = "Déplacer vers l’intérieur" +"Move out" = "Déplacer vers l’extérieur" "Move tools" = "Outils de déplacement" "Move up" = "Monter" "Moved to {name}" = "Déplacé vers {name}" @@ -813,7 +813,7 @@ status = "draft — needs native review" "New design file" = "Nouveau fichier de design" "Newest first" = "Plus récent d’abord" "Next frame" = "Cadre suivant" -"Next or previous control in the focused region" = "Contrôle suivant ou précédent dans la région active" +"Next or previous control in the focused region" = "Contrôle suivant ou précédent dans la région qui a le focus" "Next ready for dev" = "Prêt pour le dev suivant" "Next size option" = "Option de taille suivante" "No assets: no export settings, icons or images. Add export settings in the Design tab." = "Aucune ressource : ni réglage d’exportation, ni icône, ni image. Ajoutez des réglages d’exportation dans l’onglet Design." @@ -832,7 +832,7 @@ status = "draft — needs native review" "No contributor data was built into this copy." = "Cette copie ne contient aucune donnée de contributeurs." "No device" = "Aucun appareil" "No fonts match" = "Aucune police ne correspond" -"No join code here (the server is older than join codes, or this address can't be put in one): share the link instead." = "Pas de code d’accès ici (le serveur est antérieur aux codes d’accès, ou cette adresse ne tient pas dans un code) : partagez plutôt le lien." +"No join code here (the server is older than join codes, or this address can't be put in one): share the link instead." = "Pas de code d’accès ici (le serveur est antérieur aux codes d’accès, ou cette adresse ne tient pas dans un code) : partagez plutôt le lien." "No layer changes to show." = "Aucune modification de calque à afficher." "No libraries yet. Add a .figcraft file that was published as a library, or paste a team library's link." = "Aucune bibliothèque pour l’instant. Ajoutez un fichier .figcraft publié en tant que bibliothèque, ou collez le lien d’une bibliothèque d’équipe." "No list" = "Aucune liste" @@ -846,7 +846,7 @@ status = "draft — needs native review" "No number variables fit here." = "Aucune variable numérique ne convient ici." "No number variables yet. Create them in Local variables." = "Aucune variable numérique pour l’instant. Créez-en dans Variables locales." "No other pages" = "Aucune autre page" -"No paints" = "Aucune peinture" +"No paints" = "Aucun remplissage" "No plugins on {server}." = "Aucun plugin sur {server}." "No plugins yet. Import a development plugin from its figcraft-plugin.json." = "Aucun plugin pour l’instant. Importez un plugin en développement depuis son figcraft-plugin.json." "No preferred components" = "Aucun composant préféré" @@ -889,7 +889,7 @@ status = "draft — needs native review" "Numbered list" = "Liste numérotée" "Object" = "Objet" "Oct" = "oct." -"Off" = "Non" +"Off" = "Désactivé" "Offered first when filling the slot (any content is allowed)" = "Proposés en premier pour remplir l’emplacement (tout contenu est autorisé)" "Offered first when swapping the instance (any component is allowed)" = "Proposés en premier pour permuter l’instance (tout composant est autorisé)" "Offset" = "Décalage" @@ -900,7 +900,7 @@ status = "draft — needs native review" "One announcement in five common sizes: square, portrait, story, link and banner" = "Une annonce en cinq formats courants : carré, portrait, story, lien et bannière" "Online" = "En ligne" "Online files" = "Fichiers en ligne" -"Online sharing is off: files are shared on the local network only." = "Le partage en ligne est désactivé : les fichiers ne sont partagés que sur le réseau local." +"Online sharing is off: files are shared on the local network only." = "Le partage en ligne est désactivé : les fichiers ne sont partagés que sur le réseau local." "Only the first changed layers are listed." = "Seuls les premiers calques modifiés sont listés." "Only the first {n} changed layers are listed." = "Seuls les {n} premiers calques modifiés sont listés." "Only your threads" = "Vos fils uniquement" @@ -930,7 +930,7 @@ status = "draft — needs native review" "Order" = "Ordre" "Other" = "Autre" "Other apps may not open every .fig file exported here." = "D’autres applications risquent de ne pas ouvrir tous les fichiers .fig exportés ici." -"Others lose access and keep their copy; the file stays on the server for you" = "Les autres perdent l’accès et conservent leur copie ; le fichier reste sur le serveur pour vous" +"Others lose access and keep their copy; the file stays on the server for you" = "Les autres perdent l’accès et conservent leur copie ; le fichier reste sur le serveur pour vous" "Others type this under Share ▸ Join with a code" = "Les autres le saisissent dans Partager ▸ Rejoindre avec un code" "Outline stroke" = "Convertir le contour en tracé" "Outline text" = "Convertir le texte en tracés" @@ -973,7 +973,7 @@ status = "draft — needs native review" "Pen" = "Plume" "Pencil" = "Crayon" "People in this file" = "Personnes dans ce fichier" -"People join from anywhere with a code. The file is kept on {server} while you share it." = "On vous rejoint de partout avec un code. Le fichier est conservé sur {server} tant que vous le partagez." +"People join from anywhere with a code. The file is kept on {server} while you share it." = "Les personnes vous rejoignent de partout avec un code. Le fichier est conservé sur {server} tant que vous le partagez." "People on your network join with a code. Nothing goes through a server." = "Les personnes de votre réseau vous rejoignent avec un code. Rien ne passe par un serveur." "Phone" = "Téléphone" "Pick color" = "Prélever une couleur" @@ -1047,7 +1047,7 @@ status = "draft — needs native review" "Ratio" = "Rapport" "Reading the library…" = "Lecture de la bibliothèque…" "Ready for dev" = "Prêt pour le dev" -"Real name" = "Vrai nom" +"Real name" = "Nom réel" "Recent" = "Récents" "Recent files" = "Fichiers récents" "Recents" = "Récents" @@ -1080,9 +1080,9 @@ status = "draft — needs native review" "Remove text from path" = "Retirer le texte du tracé" "Remove the else and its actions" = "Retirer le « sinon » et ses actions" "Remove this condition and its actions" = "Retirer cette condition et ses actions" -"Remove this file from the server? It can't be shared or opened from there again. Your copy stays open here." = "Retirer ce fichier du serveur ? Il ne pourra plus être partagé ni ouvert depuis celui-ci. Votre copie reste ouverte ici." +"Remove this file from the server? It can't be shared or opened from there again. Your copy stays open here." = "Retirer ce fichier du serveur ? Il ne pourra plus être partagé ni ouvert depuis ce serveur. Votre copie reste ouverte ici." "Remove {name} from session" = "Retirer {name} de la session" -"Remove {name} from {server}?" = "Retirer {name} de {server} ?" +"Remove {name} from {server}?" = "Retirer {name} de {server} ?" "Removed" = "Supprimé" "Removed from the server" = "Retiré du serveur" "Removed since it was marked" = "Supprimé depuis le marquage" @@ -1113,7 +1113,7 @@ status = "draft — needs native review" "Reset to style" = "Revenir au style" "Reset to the inherited value" = "Revenir à la valeur héritée" "Resize by the nudge amount" = "Redimensionner du pas de décalage" -"Resize frames to the device's screen (or the window without a device): their constraints and auto layout respond" = "Redimensionne les cadres à l’écran de l’appareil (ou à la fenêtre sans appareil) : leurs contraintes et leur mise en page auto s’adaptent" +"Resize frames to the device's screen (or the window without a device): their constraints and auto layout respond" = "Redimensionner les cadres à l’écran de l’appareil (ou à la fenêtre sans appareil) : leurs contraintes et leur mise en page auto s’adaptent" "Resize to fit" = "Redimensionner au contenu" "Resizing" = "Redimensionnement" "Resolve" = "Résoudre" @@ -1186,7 +1186,7 @@ status = "draft — needs native review" "Select a frame to preview it" = "Sélectionnez un cadre pour le prévisualiser" "Select a layer to see its properties and code" = "Sélectionnez un calque pour voir ses propriétés et son code" "Select all" = "Tout sélectionner" -"Select all with same" = "Sélectionner tout ce qui a le même" +"Select all with same" = "Sélectionner par propriété commune" "Select layer" = "Sélectionner le calque" "Select layers and they space themselves as their content changes" = "Sélectionnez des calques et leur espacement s’ajuste quand leur contenu change" "Select layers to apply the style to" = "Sélectionnez les calques auxquels appliquer le style" @@ -1219,12 +1219,12 @@ status = "draft — needs native review" "Share with a code" = "Partager avec un code" "Share: live session, export" = "Partager : session en direct, exportation" "Shared" = "Partagé" -"Shared: copy its code from the file's menu" = "Partagé : copiez son code depuis le menu du fichier" -"Sharing a file needs the desktop app; here you can join one." = "Le partage d’un fichier nécessite l’application de bureau ; ici, vous pouvez en rejoindre un." -"Sharing on the local network (online sharing is off: no server is set in Preferences)." = "Partage sur le réseau local (le partage en ligne est désactivé : aucun serveur n’est défini dans les Préférences)." +"Shared: copy its code from the file's menu" = "Partagé : copiez son code depuis le menu du fichier" +"Sharing a file needs the desktop app; here you can join one." = "Le partage d’un fichier nécessite l’application de bureau ; ici, vous pouvez en rejoindre un." +"Sharing on the local network (online sharing is off: no server is set in Preferences)." = "Partage sur le réseau local (le partage en ligne est désactivé : aucun serveur n’est défini dans les Préférences)." "Sharing on your network · {people}" = "Partage sur votre réseau · {people}" "Sharing online · {people}" = "Partage en ligne · {people}" -"Sharing stopped: the file stays on the server, open only to you" = "Partage arrêté : le fichier reste sur le serveur, accessible à vous seul" +"Sharing stopped: the file stays on the server, open only to you" = "Partage arrêté : le fichier reste sur le serveur, accessible à vous seul" "Show" = "Afficher" "Show all templates" = "Afficher tous les modèles" "Show as grid" = "Afficher en grille" @@ -1281,7 +1281,7 @@ status = "draft — needs native review" "Spacing" = "Espacement" "Spam, stolen work, abuse…" = "Spam, travail volé, abus…" "Splay" = "Évasement" -"Spotlight me" = "Me mettre sous les projecteurs" +"Spotlight me" = "Faire suivre ma vue" "Spread" = "Étendue" "Square" = "Carré" "Standard" = "Standard" @@ -1303,7 +1303,7 @@ status = "draft — needs native review" "Stop following {name}" = "Ne plus suivre {name}" "Stop session" = "Arrêter la session" "Stop sharing" = "Arrêter le partage" -"Stop spotlighting" = "Quitter les projecteurs" +"Stop spotlighting" = "Arrêter de faire suivre ma vue" "Stop {n} {hex}" = "Arrêt {n} {hex}" "Stretch" = "Étirer" "Strikethrough" = "Barré" @@ -1357,7 +1357,7 @@ status = "draft — needs native review" "The destination document is unavailable. Nothing was imported." = "Le document de destination est indisponible. Rien n’a été importé." "The destination document was closed. Nothing was imported." = "Le document de destination a été fermé. Rien n’a été importé." "The destination page was removed. Nothing was imported." = "La page de destination a été supprimée. Rien n’a été importé." -"The file is deleted from the server: nobody can open or join it from there again, and this can't be undone. Copies that are open stay with whoever has them." = "Le fichier est supprimé du serveur : plus personne ne pourra l’ouvrir ni le rejoindre depuis celui-ci, et c’est irréversible. Les copies ouvertes restent à ceux qui les ont." +"The file is deleted from the server: nobody can open or join it from there again, and this can't be undone. Copies that are open stay with whoever has them." = "Le fichier est supprimé du serveur : plus personne ne pourra l’ouvrir ni le rejoindre depuis ce serveur, et c’est irréversible. Les copies ouvertes restent à ceux qui les ont." "The file now holds another library" = "Le fichier contient désormais une autre bibliothèque" "The file now holds another library (\"{name}\")" = "Le fichier contient désormais une autre bibliothèque (« {name} »)" "The files you shared or published online from this computer. They stay on their server until you remove them." = "Les fichiers que vous avez partagés ou publiés en ligne depuis cet ordinateur. Ils restent sur leur serveur jusqu’à ce que vous les retiriez." @@ -1372,16 +1372,16 @@ status = "draft — needs native review" "This branch can't be reviewed: {e}" = "Impossible de relire cette branche : {e}" "This branch was merged or archived." = "Cette branche a été fusionnée ou archivée." "This computer only" = "Cet ordinateur uniquement" -"This computer remembers the owner token, so the file can be managed from the home screen's Online list. Copy it to manage the file from somewhere else: it is a password for this file and won't be shown again." = "Cet ordinateur mémorise le jeton de propriétaire : le fichier se gère depuis la liste En ligne de l’écran d’accueil. Copiez-le pour gérer le fichier depuis un autre endroit : c’est un mot de passe pour ce fichier et il ne sera plus affiché." +"This computer remembers the owner token, so the file can be managed from the home screen's Online list. Copy it to manage the file from somewhere else: it is a password for this file and won't be shown again." = "Cet ordinateur mémorise le jeton de propriétaire : le fichier se gère depuis la liste En ligne de l’écran d’accueil. Copiez-le pour gérer le fichier depuis un autre endroit : c’est un mot de passe pour ce fichier et il ne sera plus affiché." "This condition is edited as an expression" = "Cette condition se modifie en tant qu’expression" "This file" = "Ce fichier" "This file has unsaved changes" = "Ce fichier contient des modifications non enregistrées" "This file is on {server}, open only to you. Share it to let others in with a new code." = "Ce fichier est sur {server}, accessible à vous seul. Partagez-le pour laisser entrer d’autres personnes avec un nouveau code." "This frame is new on the branch." = "Ce cadre est nouveau dans la branche." "This frame isn't on the branch." = "Ce cadre n’est pas dans la branche." -"This server is open to the public: no key needed." = "Ce serveur est ouvert au public : aucune clé nécessaire." -"This server needs its key: add ?key=… to the address (`figcraft-server key` prints it on the server's machine)." = "Ce serveur exige sa clé : ajoutez ?key=… à l’adresse (`figcraft-server key` l’affiche sur la machine du serveur)." -"This server takes files only with its key: see Advanced." = "Ce serveur n’accepte les fichiers qu’avec sa clé : voir Avancé." +"This server is open to the public: no key needed." = "Ce serveur est ouvert au public : aucune clé nécessaire." +"This server needs its key: add ?key=… to the address (`figcraft-server key` prints it on the server's machine)." = "Ce serveur exige sa clé : ajoutez ?key=… à l’adresse (`figcraft-server key` l’affiche sur la machine du serveur)." +"This server takes files only with its key: see Advanced." = "Ce serveur n’accepte les fichiers qu’avec sa clé : voir Avancé." "Tidy up" = "Ranger" "Tile" = "Mosaïque" "Tile type" = "Type de mosaïque" @@ -1427,7 +1427,7 @@ status = "draft — needs native review" "Unsaved changes: save first, or close again with {key} to discard" = "Modifications non enregistrées : enregistrez d’abord ou fermez à nouveau avec {key} pour les abandonner" "Up" = "Haut" "Update" = "Mettre à jour" -"Update from main" = "Mettre à jour depuis le principal" +"Update from main" = "Mettre à jour depuis le fichier principal" "Update from selection" = "Mettre à jour depuis la sélection" "Update plugin" = "Mettre à jour le plugin" "Updated {components} and {styles}" = "{components} et {styles} mis à jour" @@ -1488,7 +1488,7 @@ status = "draft — needs native review" "Wedge" = "Coin" "Weight" = "Graisse" "What changed since it was marked ready for dev" = "Ce qui a changé depuis le marquage prêt pour le dev" -"What is wrong with this file? The server's operator gets your report." = "Qu’est-ce qui ne va pas avec ce fichier ? L’opérateur du serveur reçoit votre signalement." +"What is wrong with this file? The server's operator gets your report." = "Qu’est-ce qui ne va pas avec ce fichier ? L’opérateur du serveur reçoit votre signalement." "What it's for" = "À quoi il sert" "What this changed from isn't known: it changed before FigCraft kept a copy of ready-for-dev frames. Resolve the change (or mark it ready again) to compare from now on." = "L’état d’origine de cette modification est inconnu : elle date d’avant que FigCraft ne conserve une copie des cadres prêts pour le dev. Résolvez la modification (ou marquez-la à nouveau comme prête) pour comparer à partir de maintenant." "What you try here changes this preview and the code below, not the file" = "Ce que vous essayez ici change cet aperçu et le code ci-dessous, pas le fichier" @@ -1510,9 +1510,9 @@ status = "draft — needs native review" "Yesterday" = "Hier" "You" = "Vous" "You'll get the shared file as a new tab and edit it together." = "Vous recevrez le fichier partagé dans un nouvel onglet et le modifierez ensemble." -"You're spotlighting: everyone follows your view" = "Vous êtes sous les projecteurs : tout le monde suit votre vue" +"You're spotlighting: everyone follows your view" = "Vous faites suivre votre vue : tout le monde vous suit" "Your copy of the file stays open" = "Votre copie du fichier reste ouverte" -"Your copy stays open; the file stays on the server" = "Votre copie reste ouverte ; le fichier reste sur le serveur" +"Your copy stays open; the file stays on the server" = "Votre copie reste ouverte ; le fichier reste sur le serveur" "Your name" = "Votre nom" "Your name in comments" = "Votre nom dans les commentaires" "Your system asks for reduced motion: animations stay off." = "Votre système demande de réduire les animations : elles restent désactivées." @@ -1662,11 +1662,11 @@ status = "draft — needs native review" "{name} ({tag}): {min} – {max}, default {default}. Double-click the slider to go back to the style's value; Escape while dragging cancels." = "{name} ({tag}) : {min} – {max}, par défaut {default}. Double-cliquez sur le curseur pour revenir à la valeur du style ; Échap pendant le glissement annule." "{name} cleared from the list" = "{name} effacé de la liste" "{name} from {path}" = "{name} depuis {path}" -"{name} is forgotten on this computer; the file stays on {server}" = "{name} est oublié sur cet ordinateur ; le fichier reste sur {server}" +"{name} is forgotten on this computer; the file stays on {server}" = "{name} est oublié sur cet ordinateur ; le fichier reste sur {server}" "{name} is on the server." = "{name} est sur le serveur." -"{name} is spotlighting" = "{name} est sous les projecteurs" +"{name} is spotlighting" = "{name} fait suivre sa vue" "{name} was removed from the server" = "{name} a été retiré du serveur" -"{name} · deleted on main" = "{name} · supprimé dans le principal" +"{name} · deleted on main" = "{name} · supprimé dans le fichier principal" "{name} · deleted on the branch" = "{name} · supprimé dans la branche" "{name} · not published" = "{name} · non publié" "{name} · on a server" = "{name} · sur un serveur" @@ -1766,7 +1766,7 @@ status = "draft — needs native review" "branch.merge" = "Fusionner la branche" "branch.open" = "Ouvrir la branche" "branch.review" = "Relire la branche" -"branch.updateFromMain" = "Mettre à jour depuis le principal" +"branch.updateFromMain" = "Mettre à jour depuis le fichier principal" "codeConnect.export" = "Exporter les liaisons au code" "codeConnect.import" = "Importer les liaisons au code" "codeConnect.remove" = "Délier du code" @@ -1790,7 +1790,7 @@ status = "draft — needs native review" "collab.report" = "Signaler le fichier à l’opérateur du serveur" "collab.serverInfo" = "Informations sur le serveur" "collab.share" = "Partager avec un code d’accès" -"collab.spotlight" = "Me mettre sous les projecteurs" +"collab.spotlight" = "Faire suivre ma vue" "collab.stop" = "Arrêter la session en direct" "command.batch" = "Exécuter plusieurs commandes" "comment.add" = "Ajouter un commentaire" @@ -2051,10 +2051,10 @@ status = "draft — needs native review" "select.inverse" = "Inverser la sélection" "select.layersAt" = "Calques en un point" "select.matching" = "Sélectionner les calques correspondants" -"select.next" = "Sélectionner l’élément suivant" +"select.next" = "Sélectionner l’élément suivant du même niveau" "select.none" = "Tout désélectionner" "select.parent" = "Sélectionner le parent" -"select.previous" = "Sélectionner l’élément précédent" +"select.previous" = "Sélectionner l’élément précédent du même niveau" "select.remove" = "Retirer de la sélection" "select.sameEffect" = "Sélectionner le même effet" "select.sameFill" = "Sélectionner le même remplissage" @@ -2141,7 +2141,7 @@ status = "draft — needs native review" "text.sizeDown" = "Réduire la taille de police" "text.sizeUp" = "Augmenter la taille de police" "text.strikethrough" = "Barré" -"text.style" = "Styler une plage de texte" +"text.style" = "Appliquer un style à une plage de texte" "text.trackingDown" = "Réduire l’interlettrage" "text.trackingUp" = "Augmenter l’interlettrage" "text.underline" = "Souligné" @@ -2224,7 +2224,7 @@ status = "draft — needs native review" "vector.edit.smooth" = "Lisser ou rendre anguleux les points" "vector.edit.state" = "État de la modification vectorielle" "vector.edit.strokeCap" = "Extrémité du contour au point" -"vector.edit.strokeJoin" = "Jointure du contour au point" +"vector.edit.strokeJoin" = "Jonction du contour au point" "vector.edit.tool" = "Outil de modification vectorielle" "vector.edit.transform" = "Transformer les points" "vector.edit.widthPoint" = "Point de largeur" @@ -2258,7 +2258,7 @@ status = "draft — needs native review" "view.multiplayerCursors" = "Curseurs des autres participants" "view.nextFrame" = "Zoomer sur le cadre suivant" "view.nextPage" = "Page suivante" -"view.nextRegion" = "Activer la région suivante" +"view.nextRegion" = "Placer le focus sur la région suivante" "view.outlines" = "Contours" "view.outlinesHiddenLayers" = "Inclure les calques masqués dans les contours" "view.outlinesObjectBounds" = "Inclure les limites des objets dans les contours" @@ -2269,7 +2269,7 @@ status = "draft — needs native review" "view.pixelPreview" = "Aperçu des pixels" "view.prevFrame" = "Zoomer sur le cadre précédent" "view.prevPage" = "Page précédente" -"view.previousRegion" = "Activer la région précédente" +"view.previousRegion" = "Placer le focus sur la région précédente" "view.proofSrgb" = "Épreuve écran sRGB" "view.fullscreen" = "Plein écran" "view.prototype" = "Panneau Prototype" From 4223031c5c287da7187409fa68bdc1f0eaa8d1e6 Mon Sep 17 00:00:00 2001 From: leandrodslv <205958219+leandrodslv@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:28:13 +0200 Subject: [PATCH 10/38] docs(contributors): credit leandrodslv by the name on their GitHub profile Co-Authored-By: Claude Sonnet 5 --- contributors/people.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contributors/people.toml b/contributors/people.toml index 8b14373e..5bdf5983 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -56,3 +56,6 @@ sync_github_name = true [people.pechnikov2026-star] sync_github_name = true + +[people.leandrodslv] +sync_github_name = true From 31da698dd5509d9f5af26add0e4999b6f04f7a1b Mon Sep 17 00:00:00 2001 From: YarilaFox <102650794+YarilaFox@users.noreply.github.com> Date: Tue, 20 Oct 2026 17:38:11 +0500 Subject: [PATCH 11/38] Fill selected layer when pasting an image --- ROADMAP.md | 1 + crates/engine/src/cmd/create.rs | 23 +++++++++++- crates/engine/src/cmd/edit.rs | 26 ++++++++++++- crates/ui-egui/src/menus.rs | 16 ++++++++ crates/ui-egui/src/paste_image_tests.rs | 49 +++++++++++++++++++++++++ docs/ui-parity.md | 7 +++- 6 files changed, 118 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 4e0d088d..7d85338a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -131,6 +131,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). ## Progress log +- **2026-10-20** · Pasting an image into a selected layer (M7): Cmd/Ctrl+V fills one selected shape, frame, text layer or boolean result with an image copied from another app or from FigCraft, keeping the layer's geometry and one undo step; with no selection or Paste here, it makes a separate image layer. Synthetic clipboard and oval export tests cover the paths. Numbers unchanged. - **2026-10-17** · Control channel while the window is minimised (M8, #720): a minimised or covered window answers every request that acts on the app's state (edits, `ui.inspect`, `ui.pointer`, `ui.key`, `ui.type`, `ui.set`, `app.quit`…) without drawing, and what needs a drawn frame (`ui.screenshot`, `ui.input`, `window.new`) is an error at once; before, those waited up to 60 s and held up every later request on the connection (an attached agent's `inspect_ui` or window screenshot stalled until the window came back). Measured on Windows 11, minimised: requests ~100 ms each (eframe wakes a minimised window at most ten times a second; ~2 ms on screen), a window screenshot fails in ~100 ms instead of 60 s, idle CPU 0. Numbers unchanged. - **2026-10-17** · Number variables on text content (M3, #629): the Typography section's variable picker also lists number variables for a text layer's content, as the engine already accepted (the text shows the number, e.g. 42.5, and follows the variable). Other text fields keep their own types. Numbers unchanged. - **2026-10-17** · Frames and sections convert into each other (M1, #719): `object.convertToSection` turns a frame on the page or in a section into a section in place (same box, name, fill and layers, which stay where they are; what only a frame has goes: auto layout, clipping, layout grids, guides, scrolling), and `object.convertToFrame` now also turns a section into a frame that keeps its fill and doesn't clip, so the look stays. Nested frames, instances, main components and sections holding sections are refused with the reason; one undo step each. Both are in the canvas context menu (Convert to section / Convert to frame) and in Spanish. Numbers unchanged. diff --git a/crates/engine/src/cmd/create.rs b/crates/engine/src/cmd/create.rs index 3a9f6d67..68375716 100644 --- a/crates/engine/src/cmd/create.rs +++ b/crates/engine/src/cmd/create.rs @@ -83,7 +83,7 @@ pub fn specs() -> Vec { spec!( "image.place", "Place image", - "{data (base64) | path, x?, y?, width?, height?, parent?, mode? (fill|fit|crop|tile)} → {id, image, width, height, imageProfile (its embedded colour profile; null: taken as sRGB)}", + "{data (base64) | path, x?, y?, width?, height?, parent?, target? (an existing shape to fill instead of making a layer), mode? (fill|fit|crop|tile)} → {id, image, width, height, imageProfile (its embedded colour profile; null: taken as sRGB)}", true, place_image ), @@ -372,9 +372,21 @@ fn read_path(path: &str) -> Result> { Err(crate::EngineError::Other(format!("{path}: files can't be read in the browser"))) } +/// Layers that can show an image inside their own outline. +pub fn accepts_image_fill(kind: &NodeKind) -> bool { + kind.is_shape() || kind.is_frame_like() || matches!(kind, NodeKind::Text(_) | NodeKind::BooleanOp { .. }) +} + /// Place an image as a rectangle with an image fill (Figma's image layer). pub fn place_image(s: &mut Session, p: &Value) -> Result { let cmd = "image.place"; + let target = opt_id(cmd, p, "target")?; + if let Some(id) = target { + let node = s.doc()?.node(id).ok_or(crate::EngineError::NoNode(id))?; + if !accepts_image_fill(&node.kind) { + return Err(bad(cmd, "`target` must be a layer that can have a fill")); + } + } let bytes = if let Some(b64) = string(p, "data") { use base64::Engine as _; let b64 = b64.split_once(',').map(|(_, d)| d.to_string()).unwrap_or(b64); @@ -390,6 +402,15 @@ pub fn place_image(s: &mut Session, p: &Value) -> Result { "tile" => ImageScaleMode::Tile, _ => ImageScaleMode::Fill, }; + if let Some(id) = target { + let doc = s.doc_mut()?; + let (hash, _, _) = figcraft_format::add_image(doc, bytes).map_err(|e| bad(cmd, e.to_string()))?; + let profile = figcraft_render::images::profile_name(&hash, doc.images.get(&hash)); + let node = doc.get_mut(id)?; + node.fills = vec![Paint::image(hash.clone(), mode)]; + node.styles.fill = None; + return Ok(json!({"id": id, "image": hash, "width": node.width, "height": node.height, "imageProfile": profile})); + } let page = s.page()?; let parent = crate::params::opt_id(cmd, p, "parent")?.unwrap_or(page); let ds = s.ds_mut()?; diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index a16fbb38..dc9c7b77 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -27,7 +27,7 @@ pub fn specs() -> Vec { spec!( "edit.paste", "Paste", - "{parent?, x?, y? (page coordinates of the pasted top-left)} a copied main component pastes as an instance of it within its file (cut ones, and pastes into other files, keep the component)", + "{parent?, x?, y? (page coordinates of the pasted top-left)} a single copied image fills a selected shape; a copied main component pastes as an instance of it within its file (cut ones, and pastes into other files, keep the component)", true, paste ), @@ -681,11 +681,33 @@ pub fn paste_into(s: &mut Session, parent: NodeId, at: Option<(f64, f64)>) -> Re Ok(out) } +fn copied_image(s: &Session) -> Option<(String, ImageData)> { + let [(_, nodes)] = s.clipboard.nodes.as_slice() else { return None }; + let [node] = nodes.as_slice() else { return None }; + let [paint] = node.fills.as_slice() else { return None }; + let figcraft_color::PaintKind::Image(image) = &paint.kind else { return None }; + s.clipboard.images.get(&image.image).map(|data| (image.image.clone(), data.clone())) +} + fn paste(s: &mut Session, p: &Value) -> Result { + let sel = s.selection(); + if get(p, "parent").is_none() + && get(p, "x").is_none() + && get(p, "y").is_none() + && let [target] = sel.as_slice() + && s.doc()?.node(*target).is_some_and(|node| super::create::accepts_image_fill(&node.kind)) + && let Some((hash, data)) = copied_image(s) + { + let doc = s.doc_mut()?; + doc.images.entry(hash.clone()).or_insert(data); + let node = doc.get_mut(*target)?; + node.fills = vec![figcraft_color::Paint::image(hash, figcraft_color::ImageScaleMode::Fill)]; + node.styles.fill = None; + return Ok(json!({"ids": [target]})); + } let page = s.page()?; // Figma pastes into the selected frame (if one is selected), at the same position when it fits, // else in the frame's centre; otherwise in place on the page (or the viewport centre). - let sel = s.selection(); let doc = s.doc()?; let copy_local = s.ds()?.uid == s.clipboard.source_doc; let target = id(p, "parent").or_else(|| { diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index e81a7c49..2002be0b 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -36,6 +36,22 @@ pub const UI_COMMANDS: &[&str] = &[ /// other way to read it). `at`: the page point to paste at (Paste here); `None` pastes in the /// middle of the view. pub fn paste(app: &mut FigApp, event_text: Option<&str>, at: Option<(f64, f64)>) { + let selected_layer = match app.session.selection().as_slice() { + [id] if at.is_none() + && app.session.doc().ok().and_then(|d| d.node(*id)).is_some_and(|n| figcraft_engine::cmd::create::accepts_image_fill(&n.kind)) => + { + Some(*id) + } + _ => None, + }; + if let Some(target) = selected_layer + && let Some((png, _, _)) = clipboard_png(app) + { + use base64::Engine as _; + let b64 = base64::engine::general_purpose::STANDARD.encode(png); + app.run("image.place", json!({"data": b64, "target": target})); + return; + } // Only HTML that holds readable design data takes this path; other HTML pastes as before. if let Some(html) = app.services.clipboard_get_html().filter(|h| figcraft_engine::cmd::edit::design_clipboard_buffer(h).is_some()) { let mut params = json!({"html": html}); diff --git a/crates/ui-egui/src/paste_image_tests.rs b/crates/ui-egui/src/paste_image_tests.rs index b6775f93..6fbc4101 100644 --- a/crates/ui-egui/src/paste_image_tests.rs +++ b/crates/ui-egui/src/paste_image_tests.rs @@ -58,6 +58,7 @@ impl Win { fn new() -> Self { let mut s = Session::new(); s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 100, "height": 50})).unwrap(); + s.execute("select.none", &json!({})).unwrap(); let ctx = egui::Context::default(); let clip = Rc::new(RefCell::new(Clip::default())); let app = FigApp::new(s, Box::new(Desktop(clip.clone()))); @@ -176,10 +177,57 @@ fn cmd_v_pastes_an_image_copied_elsewhere() { w.copy_image_elsewhere(); w.cmd_v(CMD); assert_eq!(w.named("Pasted image"), 1, "the image was pasted from the key's release"); + w.app.session.execute("select.none", &json!({})).unwrap(); w.cmd_v(CMD); assert_eq!(w.named("Pasted image"), 2, "and again"); } +#[test] +fn cmd_v_fills_a_selected_oval_without_adding_a_layer() { + let mut w = Win::new(); + let oval = + NodeId(w.app.session.execute("shape.ellipse", &json!({"x": 200, "y": 100, "width": 120, "height": 80})).unwrap()["id"].as_u64().unwrap()); + let before = w.app.session.doc().unwrap().node_count(); + let original_fill = w.app.session.doc().unwrap().node(oval).unwrap().fills.clone(); + w.copy_image_elsewhere(); + w.cmd_v(CMD); + + let doc = w.app.session.doc().unwrap(); + let node = doc.node(oval).unwrap(); + assert_eq!(doc.node_count(), before); + assert!(matches!(node.kind, figcraft_doc::NodeKind::Ellipse { .. })); + assert!(matches!(node.fills[0].kind, figcraft_color::PaintKind::Image(_))); + assert_eq!((node.width, node.height), (120.0, 80.0)); + assert_eq!(w.app.session.selection(), vec![oval]); + let png = figcraft_engine::cmd::file::export_bytes(doc, &[oval], figcraft_doc::ExportFormat::Png, 1.0).unwrap(); + let pixels = image::load_from_memory(&png).unwrap().to_rgba8(); + assert_eq!(pixels.get_pixel(0, 0)[3], 0, "the picture stays inside the oval"); + assert_eq!(pixels.get_pixel(60, 40)[3], 255, "the oval contains the picture"); + + w.app.session.undo().unwrap(); + assert_eq!(w.app.session.doc().unwrap().node(oval).unwrap().fills, original_fill); +} + +#[test] +fn cmd_v_fills_a_selected_oval_with_an_image_copied_in_figcraft() { + let mut w = Win::new(); + w.copy_image_elsewhere(); + w.cmd_v(CMD); + let source = w.app.session.selection()[0]; + crate::menus::invoke(&mut w.app, "edit.copy"); + + let oval = + NodeId(w.app.session.execute("shape.ellipse", &json!({"x": 200, "y": 100, "width": 120, "height": 80})).unwrap()["id"].as_u64().unwrap()); + let before = w.app.session.doc().unwrap().node_count(); + w.cmd_v(CMD); + + let doc = w.app.session.doc().unwrap(); + assert_eq!(doc.node_count(), before); + assert!(doc.contains(source)); + assert!(matches!(doc.node(oval).unwrap().fills[0].kind, figcraft_color::PaintKind::Image(_))); + assert_eq!(w.app.session.selection(), vec![oval]); +} + #[test] fn cmd_released_before_v_still_pastes_the_image() { let mut w = Win::new(); @@ -247,6 +295,7 @@ fn a_paste_event_from_another_key_does_not_block_the_next_cmd_v() { // Shift+Insert on Windows. w.frame(vec![Event::Paste("text".into())]); w.frame(vec![key(Key::Insert, false, Modifiers::SHIFT)]); + w.app.session.execute("select.none", &json!({})).unwrap(); w.copy_image_elsewhere(); w.cmd_v(CMD); assert_eq!(w.named("Pasted image"), 1); diff --git a/docs/ui-parity.md b/docs/ui-parity.md index d87103f0..3a53e747 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** minor (a rename selects the whole name, so typing replaces it; Escape or Enter after a pen path stays on the same vector (#760); IME composition kept on through idle frames of a text edit, candidates at the caret (#747); Selection colors for mixed selections and smart-selection spacing (#753); submenus with a hover delay and a safe triangle; the Actions palette's tabs, ranked search, highlight and recents; menu items that ask for a value; editor fixes from the community batches; fields select their text on focus; Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths) · **Target:** Figma Design UI3 (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-20 · **Change:** minor (pasting a copied image into one selected fillable layer; a rename selects the whole name, so typing replaces it; Escape or Enter after a pen path stays on the same vector (#760); IME composition kept on through idle frames of a text edit, candidates at the caret (#747); Selection colors for mixed selections and smart-selection spacing (#753); submenus with a hover delay and a safe triangle; the Actions palette's tabs, ranked search, highlight and recents; menu items that ask for a value; editor fixes from the community batches; fields select their text on focus; Typography spacing icons support scrubbing (#373); CSS colour names in hex fields; the Small caps sample in Type settings drawn from capitals; recent files as cards; vector edit mode: hovering, hatching, selecting and dragging filled regions; home screen: Templates tab, first-run templates and Getting started tips; number fields: scrub rates, arrow steps, arithmetic with brackets, relative edits of mixed values; outline mode's hidden layers and object bounds; Hide when publishing; cursors per tool and modifier; panel widths; dark pages for files made in the dark theme; drag threshold and double-clicks into text in groups; rotation origin on Alt+R and placing with Enter: no documented shortcut missing; keyboard shortcut parity suite and bindings; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and the vector edit tail; canvas navigation, guides and grids behaviour suite and fixes; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; layers panel keyboard: F2 rename, Shift+F10 context menu, F8 box selection; selection and transform behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; boolean hit testing on the result, group drags, frame outlines and slices; outlines for the selection, mask outlines; arc fields, live boolean editing, shape builder on strokes, one-sided widths) · **Target:** Figma Design UI3 (desktop app 126.10.7) Fine canvas control is what Figma users feel first: handles, snapping, nudging, modifier keys, drag behaviour, numeric precision. This checklist covers the tools, gestures and panels. Status: @@ -143,6 +143,10 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look - **Fixed (2026-10-17):** Shift+Cmd/Ctrl+V (Paste over selection) and Alt+Cmd/Ctrl+V (Paste properties) work on the desktop (they were bound but didn't act there, #716); Cmd/Ctrl+V pastes an image copied in another app (#663). Counts above unchanged (the suite checks bindings). +- **Fixed (2026-10-20):** Cmd/Ctrl+V makes an image fill when one fillable layer is selected (a + shape, frame, text or boolean result), whether the image came from the system clipboard or a + copied FigCraft image layer. With no selection it still creates an image layer; Paste here still + uses the requested position. The oval stays an oval and clips the image (`paste_image_tests`). - **Where the documentation disagrees with itself** (noted per row): pixel grid (Shift+' or Cmd+'), letter spacing (Alt or Cmd with < >; Cmd would be the font size keys, Alt bound), Windows remove fill (Alt+/ or Alt+,; Alt+, is letter spacing), the Layers panel on Windows (Alt+1 @@ -172,6 +176,7 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look | Date | Change | Summary | |---|---|---| +| 2026-10-20 | minor | A copied image pastes as the fill of one selected fillable layer, preserving its outline and size. With no selection, or with Paste here, the image remains a separate layer. Synthetic desktop clipboard tests cover both external and FigCraft copies, an oval's exported pixels, and Undo. Counts and hours unchanged | | 2026-10-20 | minor | Readable disabled and selected text (#890): the dim text colour (disabled buttons, placeholders, hints) is at least 3:1 against panels, fields and selected rows in both themes (it was 2.1:1 in the light theme), a disabled button is grey with that text instead of fading to 40%, and selected text (renaming a page or layer, any text box) takes the theme's readable green on the highlight (1.4:1 in the dark theme before, now 4.2:1 or more); checked with the colour contrast helper (`theme::tests`). Numbers unchanged | | 2026-10-20 | minor | Measuring while nudging (#894): with Alt held over another layer, the arrows nudge the selection (Shift: the big step) and the distances follow every nudge (Alt+arrows used to do nothing); while measuring, the selection's handles and size label hide and both layers keep their outlines (`measure_nudge_tests`). Numbers unchanged | | 2026-10-18 | minor | A rename (layers double-click, F2, File ▸ Rename, a page) selects the whole name in the frame its field first draws, so the first key replaces the name instead of being appended. Checked in the app on Windows over the control channel | From 6165d2ec85a0b139a73af82b8e9e1e8d3d75c6d2 Mon Sep 17 00:00:00 2001 From: YarilaFox <102650794+YarilaFox@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:12:47 +0500 Subject: [PATCH 12/38] M7: preserve bindings and design layers when pasting images --- crates/engine/src/cmd/create.rs | 10 ++++++-- crates/engine/src/cmd/edit.rs | 3 +-- crates/ui-egui/src/design_paste_tests.rs | 28 +++++++++++++++++++++ crates/ui-egui/src/menus.rs | 32 ++++++++++++------------ crates/ui-egui/src/paste_image_tests.rs | 17 +++++++++++++ 5 files changed, 70 insertions(+), 20 deletions(-) diff --git a/crates/engine/src/cmd/create.rs b/crates/engine/src/cmd/create.rs index 68375716..dc316056 100644 --- a/crates/engine/src/cmd/create.rs +++ b/crates/engine/src/cmd/create.rs @@ -377,6 +377,13 @@ pub fn accepts_image_fill(kind: &NodeKind) -> bool { kind.is_shape() || kind.is_frame_like() || matches!(kind, NodeKind::Text(_) | NodeKind::BooleanOp { .. }) } +/// Replace the fill list and detach variable bindings on paints that the image replaces. +pub(crate) fn replace_with_image_fill(node: &mut Node, image: String, mode: ImageScaleMode) { + node.fills = vec![Paint::image(image, mode)]; + node.bound.retain(|key, _| !matches!(super::lists::list_binding(key), Some((super::lists::BoundList::Fills, _, _)))); + node.styles.fill = None; +} + /// Place an image as a rectangle with an image fill (Figma's image layer). pub fn place_image(s: &mut Session, p: &Value) -> Result { let cmd = "image.place"; @@ -407,8 +414,7 @@ pub fn place_image(s: &mut Session, p: &Value) -> Result { let (hash, _, _) = figcraft_format::add_image(doc, bytes).map_err(|e| bad(cmd, e.to_string()))?; let profile = figcraft_render::images::profile_name(&hash, doc.images.get(&hash)); let node = doc.get_mut(id)?; - node.fills = vec![Paint::image(hash.clone(), mode)]; - node.styles.fill = None; + replace_with_image_fill(node, hash.clone(), mode); return Ok(json!({"id": id, "image": hash, "width": node.width, "height": node.height, "imageProfile": profile})); } let page = s.page()?; diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index dc9c7b77..93636cab 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -701,8 +701,7 @@ fn paste(s: &mut Session, p: &Value) -> Result { let doc = s.doc_mut()?; doc.images.entry(hash.clone()).or_insert(data); let node = doc.get_mut(*target)?; - node.fills = vec![figcraft_color::Paint::image(hash, figcraft_color::ImageScaleMode::Fill)]; - node.styles.fill = None; + super::create::replace_with_image_fill(node, hash, figcraft_color::ImageScaleMode::Fill); return Ok(json!({"ids": [target]})); } let page = s.page()?; diff --git a/crates/ui-egui/src/design_paste_tests.rs b/crates/ui-egui/src/design_paste_tests.rs index 35743a81..82c366c7 100644 --- a/crates/ui-egui/src/design_paste_tests.rs +++ b/crates/ui-egui/src/design_paste_tests.rs @@ -45,6 +45,34 @@ fn paste_takes_layers_copied_in_another_design_app() { assert_eq!(selected_names(&app), ["Copied layer"]); } +struct HtmlAndImage(String); + +impl Services for HtmlAndImage { + fn clipboard_get_html(&mut self) -> Option { + Some(self.0.clone()) + } + fn clipboard_get_image(&mut self) -> Option<(u32, u32, Vec)> { + Some((2, 2, vec![255; 16])) + } +} + +#[test] +fn editable_design_layers_take_priority_over_a_png_representation() { + let mut session = Session::new(); + let selected = session.execute("shape.ellipse", &json!({"x": 100, "y": 100, "width": 80, "height": 80})).unwrap()["id"].as_u64().unwrap(); + let mut app = FigApp::new(session, Box::new(HtmlAndImage(copied_html()))); + let before = app.session.doc().unwrap().node_count(); + + crate::menus::paste(&mut app, None, None); + + assert_eq!(selected_names(&app), ["Copied layer"]); + assert_eq!(app.session.doc().unwrap().node_count(), before + 1); + assert!(matches!( + app.session.doc().unwrap().node(figcraft_doc::NodeId(selected)).unwrap().fills[0].kind, + figcraft_color::PaintKind::Solid { .. } + )); +} + #[test] fn paste_here_puts_copied_design_layers_at_the_point() { let html = copied_html(); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 2002be0b..b3647a8f 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -36,22 +36,6 @@ pub const UI_COMMANDS: &[&str] = &[ /// other way to read it). `at`: the page point to paste at (Paste here); `None` pastes in the /// middle of the view. pub fn paste(app: &mut FigApp, event_text: Option<&str>, at: Option<(f64, f64)>) { - let selected_layer = match app.session.selection().as_slice() { - [id] if at.is_none() - && app.session.doc().ok().and_then(|d| d.node(*id)).is_some_and(|n| figcraft_engine::cmd::create::accepts_image_fill(&n.kind)) => - { - Some(*id) - } - _ => None, - }; - if let Some(target) = selected_layer - && let Some((png, _, _)) = clipboard_png(app) - { - use base64::Engine as _; - let b64 = base64::engine::general_purpose::STANDARD.encode(png); - app.run("image.place", json!({"data": b64, "target": target})); - return; - } // Only HTML that holds readable design data takes this path; other HTML pastes as before. if let Some(html) = app.services.clipboard_get_html().filter(|h| figcraft_engine::cmd::edit::design_clipboard_buffer(h).is_some()) { let mut params = json!({"html": html}); @@ -69,6 +53,22 @@ pub fn paste(app: &mut FigApp, event_text: Option<&str>, at: Option<(f64, f64)>) } return; } + let selected_layer = match app.session.selection().as_slice() { + [id] if at.is_none() + && app.session.doc().ok().and_then(|d| d.node(*id)).is_some_and(|n| figcraft_engine::cmd::create::accepts_image_fill(&n.kind)) => + { + Some(*id) + } + _ => None, + }; + if let Some(target) = selected_layer + && let Some((png, _, _)) = clipboard_png(app) + { + use base64::Engine as _; + let b64 = base64::engine::general_purpose::STANDARD.encode(png); + app.run("image.place", json!({"data": b64, "target": target})); + return; + } let sys_text = app.services.clipboard_get_text().or_else(|| event_text.map(str::to_string)).filter(|t| !t.is_empty()); let ours = sys_text.is_some() && sys_text == app.ui.last_copy; let (cx, cy) = at.or_else(|| app.session.ds().ok().map(|d| (d.view.cx, d.view.cy))).unwrap_or((0.0, 0.0)); diff --git a/crates/ui-egui/src/paste_image_tests.rs b/crates/ui-egui/src/paste_image_tests.rs index 6fbc4101..c659569c 100644 --- a/crates/ui-egui/src/paste_image_tests.rs +++ b/crates/ui-egui/src/paste_image_tests.rs @@ -208,6 +208,23 @@ fn cmd_v_fills_a_selected_oval_without_adding_a_layer() { assert_eq!(w.app.session.doc().unwrap().node(oval).unwrap().fills, original_fill); } +#[test] +fn pasting_an_image_detaches_the_replaced_fill_variable() { + let mut w = Win::new(); + let oval = + NodeId(w.app.session.execute("shape.ellipse", &json!({"x": 200, "y": 100, "width": 120, "height": 80})).unwrap()["id"].as_u64().unwrap()); + w.app.session.execute("variable.createCollection", &json!({"name": "Theme"})).unwrap(); + w.app.session.execute("variable.create", &json!({"collection": "Theme", "name": "brand", "type": "color", "value": "#FF0000"})).unwrap(); + w.app.session.execute("variable.bind", &json!({"ids": [oval.0], "field": "fill", "variable": "brand"})).unwrap(); + assert!(!w.app.session.doc().unwrap().node(oval).unwrap().bound.is_empty()); + + w.copy_image_elsewhere(); + w.cmd_v(CMD); + let node = w.app.session.doc().unwrap().node(oval).unwrap(); + assert!(matches!(node.fills[0].kind, figcraft_color::PaintKind::Image(_))); + assert!(node.bound.is_empty(), "the old colour variable must not remain on the image fill"); +} + #[test] fn cmd_v_fills_a_selected_oval_with_an_image_copied_in_figcraft() { let mut w = Win::new(); From 6c0ffe66770fc46e45a91e19de89263a08500f79 Mon Sep 17 00:00:00 2001 From: F3R9 Date: Thu, 22 Oct 2026 00:42:48 +0300 Subject: [PATCH 13/38] M9: make graphics-startup assertions independent of UI language --- ROADMAP.md | 3 ++- apps/figcraft/src/startup_tests.rs | 15 +++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index f43cd015..f19cc014 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents) -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (community batch #753: Selection colors, smart-selection spacing, a Present tab; language table re-measured from the catalogs; community fixes from #524 up (55 PRs: `.fig` import fidelity, paste from other apps, the Actions palette, editor fixes, server limits) and a boolean-op crash guard; number variables in the text content picker (#629); frames and sections convert into each other (#719); bounded canvas pan overscan; community fixes from #478 up and a control-channel fix (#589); bounded canvas pan overscan; Typography spacing icons support scrubbing (#373); font scans skip named pipes; system theme on first run; recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading (#207), SVG import budgets (#190), Leptos code (#223), full screen (#227), server snapshots readable again (#188), owner-only preferences (#200), ruzstd 0.9.1 (#182), layer cycle repair (#205), batch rollback, Direct3D 12 on Windows, plugin metering (#192); vector edit mode regions: hover hatch, selection and drag; SVG/PDF exports refuse over-depth documents (#208); bounded MCP/control framing, connections and queues (#201); plugin log limits (#191); live-session revert guard (#198); attached MCP no longer replays after a lost reply (#194); repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (community batch #753: Selection colors, smart-selection spacing, a Present tab; language table re-measured from the catalogs; community fixes from #524 up (55 PRs: `.fig` import fidelity, paste from other apps, the Actions palette, editor fixes, server limits) and a boolean-op crash guard; number variables in the text content picker (#629); frames and sections convert into each other (#719); bounded canvas pan overscan; community fixes from #478 up and a control-channel fix (#589); bounded canvas pan overscan; Typography spacing icons support scrubbing (#373); font scans skip named pipes; system theme on first run; recent files as cards with thumbnails on the home screen; community fixes: lost images reported on loading (#207), SVG import budgets (#190), Leptos code (#223), full screen (#227), server snapshots readable again (#188), owner-only preferences (#200), ruzstd 0.9.1 (#182), layer cycle repair (#205), batch rollback, Direct3D 12 on Windows, plugin metering (#192); vector edit mode regions: hover hatch, selection and drag; SVG/PDF exports refuse over-depth documents (#208); bounded MCP/control framing, connections and queues (#201); plugin log limits (#191); live-session revert guard (#198); attached MCP no longer replays after a lost reply (#194); repeat command recursion fix; the Layout panel's constrain-proportions link scales the other side, #220; core editor leftovers: component playground, auto gap handles, variables inside styles, inner shadows and background blur on groups, command-made text readable on dark pages, a one-step frame/group convert, delays on mouse triggers and manual overlay offsets, wrapped-line alignment; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; starter templates, a Templates tab and Getting started tips on the home screen; your online files; everyday editing partials: number fields, outline options, Hide when publishing, cursors, panel widths, dark pages; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; headline percentages recomputed from the area table: feature breadth ~84%, core ~94%, mainstream ~50%; text lists, hanging punctuation and decoration styles; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; export and inspect behaviour suite and fixes; .fig import: editable vector networks and more layer kinds; shapes and vector behaviour suite and fixes; paint and effects behaviour suite and fixes; frames, groups, sections and masks behaviour suite and fixes; styles and variables behaviour suite and fixes; prototyping behaviour suite and fixes; components behaviour suite and fixes; embedded image colour profiles; layers panel keyboard: F2 rename, Shift+F10 context menu, keyboard box selection, 10k-layer QA; per-side stroke weight variables; text behaviour suite: CJK line breaking, metrics, vertical trim; selection and transform behaviour suite and fixes; `.fig` import, best effort; Windows QA pass with eight fixes; auto layout behaviour suite and fixes; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings; feature breadth ~80%; ready ~49%) · **Target:** Figma Design (desktop app 126.10.7) FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow, native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through @@ -316,6 +316,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). | Date | Change | Summary | |---|---|---| +| 2026-10-22 | minor | Startup graphics-error tests assert translated hints rather than English substrings, so desktop recovery tests selecting a different UI language do not make them fail. Runtime behavior and readiness estimates unchanged. | | 2026-10-20 | minor | A document dropped on the canvas opens (#916): a `.figcraft`, `.fig` or file JSON dropped on the canvas opens as a file of its own, as on the home screen, in the browser too; it used to add its pages to the file in front. Images and SVG still import at the drop point, File ▸ Import still adds pages, and dragging only documents over the canvas says "Drop to open" (not in the browser, which names no file until the drop). Numbers unchanged | | 2026-10-20 | minor | Duplicating a main component makes an instance (#892): Duplicate and Alt-drag copied a main component as a second main; within its file the copy is now an instance of it, as Copy then Paste already was (a variant duplicated in its set stays a variant). Numbers unchanged. | | 2026-10-20 | minor | Readable disabled and selected text (#890): the dim text colour (disabled buttons, placeholders, hints) is at least 3:1 against panels, fields and selected rows in both themes (it was 2.1:1 in the light theme), a disabled button is grey with that text instead of fading to 40%, and selected text (renaming a page or layer, any text box) takes the theme's readable green on the highlight (1.4:1 in the dark theme before, now 4.2:1 or more); checked with the colour contrast helper (`theme::tests`). Numbers unchanged | diff --git a/apps/figcraft/src/startup_tests.rs b/apps/figcraft/src/startup_tests.rs index 0c92bd19..20910309 100644 --- a/apps/figcraft/src/startup_tests.rs +++ b/apps/figcraft/src/startup_tests.rs @@ -2,8 +2,15 @@ //! report it, instead of the app quitting silently (#261). use figcraft_ui_egui::dialogs::window_failed as message; +use figcraft_ui_egui::i18n::tr; const ERROR: &str = "WGPU error: Failed to create surface for any enabled backend: {}"; +const BACKEND_HINT: &str = "WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose."; +const DRIVER_HINT: &str = if cfg!(windows) { + "Updating the graphics driver often helps. To use OpenGL only, start FigCraft from a Command Prompt with:" +} else { + "Updating the graphics driver often helps. WGPU_BACKEND (vulkan, gl, metal) names another graphics API to try." +}; #[test] fn the_message_carries_the_error_and_where_to_report_it() { @@ -15,9 +22,9 @@ fn the_message_carries_the_error_and_where_to_report_it() { #[test] fn without_wgpu_backend_it_suggests_the_driver_and_another_graphics_api() { let text = message(ERROR, false); - assert!(text.contains("graphics driver"), "{text}"); + assert!(text.contains(tr(DRIVER_HINT)), "{text}"); assert!(text.contains("WGPU_BACKEND"), "another graphics API to try: {text}"); - assert!(!text.contains("unsetting"), "{text}"); + assert!(!text.contains(tr(BACKEND_HINT)), "{text}"); } #[cfg(windows)] @@ -30,6 +37,6 @@ fn on_windows_it_shows_how_to_start_with_opengl() { #[test] fn with_wgpu_backend_set_it_suggests_unsetting_it() { let text = message(ERROR, true); - assert!(text.contains("unsetting"), "{text}"); - assert!(!text.contains("graphics driver"), "the chosen backend is the likelier cause: {text}"); + assert!(text.contains(tr(BACKEND_HINT)), "{text}"); + assert!(!text.contains(tr(DRIVER_HINT)), "the chosen backend is the likelier cause: {text}"); } From b13e5c4ff3dee91fcff37607ed9348d5024f1a74 Mon Sep 17 00:00:00 2001 From: AganzeFelicite Date: Thu, 22 Oct 2026 09:00:37 +0200 Subject: [PATCH 14/38] M7: doc comments on the font scan's folders and scan_dir (#298) --- crates/text/src/fontdb.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/text/src/fontdb.rs b/crates/text/src/fontdb.rs index edfe5b60..32d34679 100644 --- a/crates/text/src/fontdb.rs +++ b/crates/text/src/fontdb.rs @@ -731,6 +731,7 @@ impl FontDb { } } +/// The folders this platform keeps installed and activated fonts in, walked by the font scan. fn system_font_dirs() -> Vec { #[allow(unused_mut)] let mut v: Vec = Vec::new(); @@ -794,6 +795,9 @@ fn unix_user_font_dirs(home: Option, data_home: Option)>) { if depth > 6 { return; From 0289890197a274d991995d46966b9f1c59ac2e79 Mon Sep 17 00:00:00 2001 From: aryans2611 Date: Fri, 23 Oct 2026 09:57:58 +0530 Subject: [PATCH 15/38] M6: mobile layout: one side panel at a time on phones, two-finger pan, scrolling toolbar Windows narrower than 760 pt show the Layers or Design panel over the canvas, opened from two touch-sized buttons. Two fingers pan as well as pinch. The main toolbar pill is capped to the canvas width and scrolls sideways (drag or wheel) when its buttons don't fit; the help button moves above it when they would overlap. The web page's viewport covers the safe area and stops touch scrolling and overscroll. Co-Authored-By: Claude Sonnet 5.5 --- apps/figcraft-web/index.html | 6 +-- crates/ui-egui/src/canvas.rs | 7 ++- crates/ui-egui/src/lib.rs | 62 ++++++++++++++++++++++--- crates/ui-egui/src/panel_width_tests.rs | 29 ++++++++++++ crates/ui-egui/src/toolbar.rs | 26 +++++++++-- 5 files changed, 116 insertions(+), 14 deletions(-) diff --git a/apps/figcraft-web/index.html b/apps/figcraft-web/index.html index 1a9969d5..f610aa81 100644 --- a/apps/figcraft-web/index.html +++ b/apps/figcraft-web/index.html @@ -2,12 +2,12 @@ - + FigCraft