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/27] 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/27] 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/27] 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/27] 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 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 05/27] 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 06/27] 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 07/27] 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 08/27] 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 09/27] 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 10/27] 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 11/27] 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 1340e32b1694854300e4e469cf3692a3ad2a6e1c Mon Sep 17 00:00:00 2001 From: Salah1St <83543012+Salah1St@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:26:37 +0700 Subject: [PATCH 12/27] M4: Thai faces in the text fallback list Thai typed into a layer whose font has no Thai glyphs (Inter, which new text starts in) was drawn as missing-glyph boxes, on the desktop and on the web, even with a Thai face embedded or installed: FALLBACK_FAMILIES listed Latin, CJK, Hangul, Arabic and symbol families but no Thai one. Noto Sans Thai, Noto Sans Thai UI, Leelawadee UI, Thonburi and Tahoma go after Noto Sans Arabic; they are only consulted for characters the layer's own font lacks. Tests: the list has a Thai face for each platform, and with one installed or embedded every Thai cluster in a Latin layer, marks included, is drawn from it (skips without one). Co-Authored-By: Claude Opus 5.5 --- crates/text/src/behaviour_tests.rs | 29 +++++++++++++++++++++++++++++ crates/text/src/fontdb.rs | 7 ++++++- crates/text/src/fontdb_tests.rs | 12 ++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/crates/text/src/behaviour_tests.rs b/crates/text/src/behaviour_tests.rs index b3cb4884..ea6ac41d 100644 --- a/crates/text/src/behaviour_tests.rs +++ b/crates/text/src/behaviour_tests.rs @@ -265,6 +265,35 @@ fn hangul_comes_from_a_korean_font_when_one_is_installed() { } } +#[test] +fn thai_comes_from_a_thai_font_when_one_is_installed() { + // The default (Latin) face has no Thai. With a Thai face (Noto Sans Thai from craft-fonts or + // Linux, Leelawadee UI or Tahoma on Windows, Thonburi on macOS) the fallback draws every Thai + // cluster, marks included, instead of the missing-glyph box. Skips on a machine without one. + let Some(f) = face() else { return }; + if f.has_char('\u{0E01}') { + return; + } + let thai = ["Noto Sans Thai", "Noto Sans Thai UI", "Leelawadee UI", "Thonburi", "Tahoma"]; + if !thai.iter().any(|t| find_face(t, "Regular").is_some_and(|t| t.has_char('\u{0E01}'))) { + return; + } + // "ภาษาไทย" (the Thai language) and "น้ำ" (water: a tone mark over SARA AM). + let s = "Ab \u{0E20}\u{0E32}\u{0E29}\u{0E32}\u{0E44}\u{0E17}\u{0E22} \u{0E19}\u{0E49}\u{0E33}"; + let chars: Vec = s.chars().collect(); + let l = layout(&text(s, TextAutoResize::WidthAndHeight), None, None); + let is_thai = |g: &&Glyph| chars.get(g.char_start).is_some_and(|c| ('\u{0E00}'..='\u{0E7F}').contains(c)); + let glyphs: Vec<_> = l.glyphs.iter().filter(is_thai).collect(); + assert!(!glyphs.is_empty()); + for g in &glyphs { + assert!(g.gid != 0, "char {}: missing glyph in {} {}", g.char_start, g.face.family, g.face.style); + assert!(g.face.has_char('\u{0E01}'), "char {}: drawn by {}, which has no Thai", g.char_start, g.face.family); + } + let drawn: std::collections::BTreeSet = glyphs.iter().flat_map(|g| g.char_start..g.char_end).collect(); + let want: std::collections::BTreeSet = (3..chars.len()).filter(|i| chars[*i] != ' ').collect(); + assert_eq!(drawn, want, "every Thai char is in a drawn cluster"); +} + #[test] fn break_opportunities() { // (before, after, may break between, derivation) diff --git a/crates/text/src/fontdb.rs b/crates/text/src/fontdb.rs index 604f3eee..e6e7b2f2 100644 --- a/crates/text/src/fontdb.rs +++ b/crates/text/src/fontdb.rs @@ -406,7 +406,7 @@ fn names(f: &skrifa::FontRef) -> (Option, Option) { } /// Families tried, in order, for characters a text's own font lacks. -const FALLBACK_FAMILIES: [&str; 20] = [ +const FALLBACK_FAMILIES: [&str; 25] = [ "Inter", "Helvetica Neue", "Arial", @@ -425,6 +425,11 @@ const FALLBACK_FAMILIES: [&str; 20] = [ "Noto Sans CJK KR", "Noto Sans KR", "Noto Sans Arabic", + "Noto Sans Thai", + "Noto Sans Thai UI", + "Leelawadee UI", + "Thonburi", + "Tahoma", "Apple Symbols", "Segoe UI Symbol", ]; diff --git a/crates/text/src/fontdb_tests.rs b/crates/text/src/fontdb_tests.rs index f2456d28..9ae9db49 100644 --- a/crates/text/src/fontdb_tests.rs +++ b/crates/text/src/fontdb_tests.rs @@ -263,3 +263,15 @@ fn font_bytes_load_into_the_db() { assert_eq!(db().read().unwrap().face_count(), faces); } } + +#[test] +fn every_platform_has_a_thai_font_in_the_fallbacks() { + // Thai typed into a Latin layer (new text starts in Inter) is drawn by the first fallback that + // has it: Noto Sans Thai (craft-fonts, Linux), Leelawadee UI or Tahoma (Windows), Thonburi + // (macOS). They come after the Latin faces, which go on drawing Latin text. + let at = |family: &str| FALLBACK_FAMILIES.iter().position(|f| *f == family); + let latin = at("Segoe UI").unwrap(); + for thai in ["Noto Sans Thai", "Leelawadee UI", "Tahoma", "Thonburi"] { + assert!(at(thai).is_some_and(|i| i > latin), "{thai}"); + } +} From a51d3eed530f56d9684be649f375f66009bb7111 Mon Sep 17 00:00:00 2001 From: Salah1St <83543012+Salah1St@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:27:21 +0700 Subject: [PATCH 13/27] M7: Noto Sans Thai as a UI fallback when the build embeds it In the web build, Thai layer names, frame labels on the canvas, text in property fields and the layer search were drawn as missing-glyph boxes, even when the build embedded Noto Sans Thai: the UI's built-in fallback list (BUILT_IN in theme::install_fonts) didn't name it, and the browser has no system fonts to fall back on. Noto Sans Thai joins BUILT_IN. It is used only when the face is embedded, so builds without it are unchanged. Test: with it embedded, the UI fonts draw Thai, marks included (skips without it). Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/theme.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 611371ce..1cdfa5a1 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -304,7 +304,7 @@ pub fn install_fonts(ctx: &Context, system: bool, cjk: u8) { brand.extend(fonts.families.get(&FontFamily::Name("semibold".into())).cloned().unwrap_or_default()); fonts.families.insert(FontFamily::Name("brand".into()), brand); // CJK and other scripts from craft-fonts as fallbacks (Noto Sans CJK only with FIGCRAFT_EMBED_CJK). - const BUILT_IN: [&str; 5] = ["Inter", "BIZ UDPGothic", "Noto Sans CJK SC", "Noto Sans Arabic", "Noto Sans Georgian"]; + const BUILT_IN: [&str; 6] = ["Inter", "BIZ UDPGothic", "Noto Sans CJK SC", "Noto Sans Arabic", "Noto Sans Georgian", "Noto Sans Thai"]; let lang = crate::i18n::current().map_or("en", |l| l.code); // A Chinese UI draws Han in a Chinese face: the built-in Japanese one goes after the system faces. let late = |family: &str| family == "BIZ UDPGothic" && crate::cjk::language(lang) == crate::cjk::HAN; @@ -519,4 +519,26 @@ mod tests { } }); } + + #[test] + fn ui_font_draws_thai_with_the_built_in_thai_face() { + // The web build has no system fonts: Thai layer names, frame labels and field text are drawn + // by craft-fonts' Noto Sans Thai, a built-in fallback, when the build embeds it. + if !figcraft_text::CRAFT_FONTS.iter().any(|f| f.family == "Noto Sans Thai") { + eprintln!("built without craft-fonts' Noto Sans Thai; skipping"); + return; + } + let ctx = Context::default(); + install_fonts(&ctx, false, 0); + let input = + egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(100.0, 100.0))), ..Default::default() }; + ctx.run_ui(input, |_| {}).textures_delta.clear(); + // "ภาษาไทย" (the Thai language) and "น้ำ" (water: a tone mark over SARA AM). + let thai = "\u{0E20}\u{0E32}\u{0E29}\u{0E32}\u{0E44}\u{0E17}\u{0E22} \u{0E19}\u{0E49}\u{0E33}"; + ctx.fonts_mut(|f| { + for font in [regular(11.0), semibold(11.0)] { + assert!(f.has_glyphs(&font, thai), "UI font draws {thai:?}"); + } + }); + } } From b649035695fb9917faa6ac188f6d0f60c52a0639 Mon Sep 17 00:00:00 2001 From: Salah1St <83543012+Salah1St@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:28:29 +0700 Subject: [PATCH 14/27] M7: FIGCRAFT_DEFAULT_FONT names the font new text starts in TextStyle::default() hard-coded Inter, which has no Thai (nor Hebrew, Devanagari, ...). In a build for users who mostly write such a script, every new text layer started in a font that can't draw what they type: fallback faces hide the boxes, but the layer still says Inter and gets Inter's metrics and line height. Like FIGCRAFT_DEFAULT_SERVER, a build variable read with option_env! names another family; unset or empty means Inter. font_family is always written, so files open in the same font whichever build reads them. docs/releasing.md gets a "The default font" section. Co-Authored-By: Claude Opus 5.5 --- crates/doc/src/text.rs | 21 ++++++++++++++++++++- docs/releasing.md | 14 ++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/crates/doc/src/text.rs b/crates/doc/src/text.rs index 458426ad..e0956b9c 100644 --- a/crates/doc/src/text.rs +++ b/crates/doc/src/text.rs @@ -253,10 +253,17 @@ pub struct TextStyle { pub style_id: Option, } +/// The font new text starts in. A build can name another (`FIGCRAFT_DEFAULT_FONT="Noto Sans Thai"`, +/// for a distribution whose users mostly write a script Inter lacks); empty means Inter. +pub const DEFAULT_FONT: &str = match option_env!("FIGCRAFT_DEFAULT_FONT") { + Some(family) if !family.is_empty() => family, + _ => "Inter", +}; + impl Default for TextStyle { fn default() -> Self { Self { - font_family: "Inter".into(), + font_family: DEFAULT_FONT.into(), font_style: "Regular".into(), font_size: 12.0, line_height: LineHeight::Auto, @@ -783,6 +790,18 @@ mod case_tests { assert!(!valid_feature_tag("lig") && !valid_feature_tag("ligatures") && !valid_feature_tag("li\u{e9}")); } + #[test] + fn new_text_starts_in_the_builds_default_font() { + // Inter, unless the build names another with `FIGCRAFT_DEFAULT_FONT` (empty means Inter). + let named = option_env!("FIGCRAFT_DEFAULT_FONT").filter(|f| !f.is_empty()); + assert_eq!(DEFAULT_FONT, named.unwrap_or("Inter")); + let st = TextStyle::default(); + assert_eq!(st.font_family, DEFAULT_FONT); + // The family is always written, so a saved file opens in the same font in any build. + let saved = serde_json::to_value(&st).unwrap(); + assert_eq!(saved["font_family"], DEFAULT_FONT); + } + #[test] fn variations_are_backward_compatible_and_imply_weight() { // Files written before variable fonts load with no custom axes, and stay byte-identical. diff --git a/docs/releasing.md b/docs/releasing.md index 07492f6c..765c24b1 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -68,6 +68,20 @@ FIGCRAFT_DEFAULT_SERVER=wss://design.example.org/ cargo build --release -p figcr The release workflow leaves it unset. The public server's terms are in [`online-sharing-terms.md`](online-sharing-terms.md). +## The default font + +New text starts in Inter (`figcraft_doc::DEFAULT_FONT`). A distribution whose users mostly write a +script Inter lacks (Thai, say) can name another family with `FIGCRAFT_DEFAULT_FONT` when building; +unset or empty means Inter. Pick a family the build embeds from craft-fonts (the web build has no +other fonts) or one its users have installed: + +```sh +FIGCRAFT_DEFAULT_FONT="Noto Sans Thai" cargo build --release -p figcraft +``` + +Files record each text's family, so they open in the same font in any build. The release workflow +leaves it unset. + ## What gets built | Platform | Artifacts | Built on | From c4be9c1a34bb398f1c5203139bdeb2b20e8025cd Mon Sep 17 00:00:00 2001 From: Salah1St <83543012+Salah1St@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:34:43 +0700 Subject: [PATCH 15/27] M7: say what a default font family in files does and doesn't promise Review: a saved family renders only where that family is available. The docs and the test's comment now say the variable changes only what new text starts in, and that a file keeps its fonts in a build with another default, falling back like any missing family elsewhere. Co-Authored-By: Claude Opus 5.5 --- crates/doc/src/text.rs | 2 +- docs/releasing.md | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/doc/src/text.rs b/crates/doc/src/text.rs index e0956b9c..a9bf57ba 100644 --- a/crates/doc/src/text.rs +++ b/crates/doc/src/text.rs @@ -797,7 +797,7 @@ mod case_tests { assert_eq!(DEFAULT_FONT, named.unwrap_or("Inter")); let st = TextStyle::default(); assert_eq!(st.font_family, DEFAULT_FONT); - // The family is always written, so a saved file opens in the same font in any build. + // The family is always written, so a saved file keeps it in a build with another default. let saved = serde_json::to_value(&st).unwrap(); assert_eq!(saved["font_family"], DEFAULT_FONT); } diff --git a/docs/releasing.md b/docs/releasing.md index 765c24b1..8e96a25d 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -79,8 +79,10 @@ other fonts) or one its users have installed: FIGCRAFT_DEFAULT_FONT="Noto Sans Thai" cargo build --release -p figcraft ``` -Files record each text's family, so they open in the same font in any build. The release workflow -leaves it unset. +Files record each text's family, so the variable only changes what new text starts in: a file +saved by one build keeps its fonts in a build with another default. Like any other family, it +renders only where it is available (embedded or installed); elsewhere the text falls back to the +faces that are. The release workflow leaves it unset. ## What gets built From dd37c1763c73694d5927fad5544a16a59666a558 Mon Sep 17 00:00:00 2001 From: Salah1St <83543012+Salah1St@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:36:24 +0700 Subject: [PATCH 16/27] M4: the Thai fallback test checks the characters it draws Review: the test chose its Thai face, and checked each glyph's face, by U+0E01, which the sample doesn't contain. It now asks for a face that has every Thai character of the sample, and checks that each glyph's face has the characters of its cluster. Co-Authored-By: Claude Opus 5.5 --- crates/text/src/behaviour_tests.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/crates/text/src/behaviour_tests.rs b/crates/text/src/behaviour_tests.rs index ea6ac41d..201aa460 100644 --- a/crates/text/src/behaviour_tests.rs +++ b/crates/text/src/behaviour_tests.rs @@ -270,24 +270,26 @@ fn thai_comes_from_a_thai_font_when_one_is_installed() { // The default (Latin) face has no Thai. With a Thai face (Noto Sans Thai from craft-fonts or // Linux, Leelawadee UI or Tahoma on Windows, Thonburi on macOS) the fallback draws every Thai // cluster, marks included, instead of the missing-glyph box. Skips on a machine without one. - let Some(f) = face() else { return }; - if f.has_char('\u{0E01}') { - return; - } - let thai = ["Noto Sans Thai", "Noto Sans Thai UI", "Leelawadee UI", "Thonburi", "Tahoma"]; - if !thai.iter().any(|t| find_face(t, "Regular").is_some_and(|t| t.has_char('\u{0E01}'))) { - return; - } // "ภาษาไทย" (the Thai language) and "น้ำ" (water: a tone mark over SARA AM). let s = "Ab \u{0E20}\u{0E32}\u{0E29}\u{0E32}\u{0E44}\u{0E17}\u{0E22} \u{0E19}\u{0E49}\u{0E33}"; let chars: Vec = s.chars().collect(); + let is_thai = |c: &char| ('\u{0E00}'..='\u{0E7F}').contains(c); + let draws_all = |f: &Face| chars.iter().filter(|c| is_thai(c)).all(|c| f.has_char(*c)); + let Some(f) = face() else { return }; + if draws_all(&f) { + return; + } + let thai = ["Noto Sans Thai", "Noto Sans Thai UI", "Leelawadee UI", "Thonburi", "Tahoma"]; + if !thai.iter().any(|t| find_face(t, "Regular").is_some_and(|t| draws_all(&t))) { + return; + } let l = layout(&text(s, TextAutoResize::WidthAndHeight), None, None); - let is_thai = |g: &&Glyph| chars.get(g.char_start).is_some_and(|c| ('\u{0E00}'..='\u{0E7F}').contains(c)); - let glyphs: Vec<_> = l.glyphs.iter().filter(is_thai).collect(); + let glyphs: Vec<_> = l.glyphs.iter().filter(|g| chars.get(g.char_start).is_some_and(is_thai)).collect(); assert!(!glyphs.is_empty()); for g in &glyphs { assert!(g.gid != 0, "char {}: missing glyph in {} {}", g.char_start, g.face.family, g.face.style); - assert!(g.face.has_char('\u{0E01}'), "char {}: drawn by {}, which has no Thai", g.char_start, g.face.family); + let cluster = chars.get(g.char_start..g.char_end).unwrap_or(&[]); + assert!(cluster.iter().all(|c| g.face.has_char(*c)), "chars {}..{}: drawn by {}, which lacks some", g.char_start, g.char_end, g.face.family); } let drawn: std::collections::BTreeSet = glyphs.iter().flat_map(|g| g.char_start..g.char_end).collect(); let want: std::collections::BTreeSet = (3..chars.len()).filter(|i| chars[*i] != ' ').collect(); From f728b8f9fe9ec502bdf6f414a262d9667a0188a1 Mon Sep 17 00:00:00 2001 From: Salah1St <83543012+Salah1St@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:07:40 +0700 Subject: [PATCH 17/27] M7: test that text.create and the Text tool start in DEFAULT_FONT Review: the doc test serialised TextStyle directly. The engine test makes text with text.create (no fontFamily) and with a Text tool click, and checks each starts in figcraft_doc::DEFAULT_FONT and writes it with the text. Passes with FIGCRAFT_DEFAULT_FONT unset and set. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/text_behaviour_tests.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/engine/src/text_behaviour_tests.rs b/crates/engine/src/text_behaviour_tests.rs index 84315f1f..c24cc49a 100644 --- a/crates/engine/src/text_behaviour_tests.rs +++ b/crates/engine/src/text_behaviour_tests.rs @@ -229,3 +229,25 @@ fn a_drag_with_the_text_tool_makes_a_fixed_size_box() { let (b3, _) = text_tool((100.0, 100.0), (180.0, 101.0), "A"); assert_eq!((b3.width(), b3.height()), (80.0, line.height())); } + +#[test] +fn new_text_starts_in_the_builds_default_font() { + // `text.create` without a fontFamily and a click with the Text tool both start in + // `figcraft_doc::DEFAULT_FONT` (Inter, unless the build names another with + // `FIGCRAFT_DEFAULT_FONT`), and the family is written with the text. + use crate::tools::{PointerEvent, PointerKind, ToolId}; + let mut s = Session::new(); + let made = text(&mut s, "Text", json!({})); + s.set_tool(ToolId::Text); + s.pointer(&PointerEvent::at(PointerKind::Down, 300.0, 300.0, 1.0)).unwrap(); + s.pointer(&PointerEvent::at(PointerKind::Up, 300.0, 300.0, 1.0)).unwrap(); + crate::textedit::insert(&mut s, "Tool").unwrap(); + let typed = s.ds().unwrap().text_edit.as_ref().unwrap().id; + assert_ne!(made, typed); + let doc = s.doc().unwrap(); + for t in [made, typed] { + let td = doc.node(t).unwrap().kind.text().unwrap(); + assert_eq!(td.style.font_family, figcraft_doc::DEFAULT_FONT); + assert_eq!(serde_json::to_value(td).unwrap()["style"]["font_family"], figcraft_doc::DEFAULT_FONT); + } +} From 98b90b3c562a4728c5fa5bdacefad37577175591 Mon Sep 17 00:00:00 2001 From: CosmoNetinfo Date: Fri, 23 Oct 2026 16:49:31 +0200 Subject: [PATCH 18/27] Add Italian (it) interface language Adds crates/ui-egui/locales/it.toml: all 1747 UI strings (42 plural entries, one/other), 574 command labels and 31 menu wordings, marked as a draft awaiting native review like the other machine-drafted catalogs. Adds a resolve/plural test for Italian locales and lists Italian in docs/translating.md. Not compiled locally; the catalog was validated with a script (same keys as en.toml, identical placeholders and ellipses per entry). Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Fr2xzM8A8RiUwZWddELBCt --- crates/ui-egui/locales/it.toml | 2366 ++++++++++++++++++++++++++++++ crates/ui-egui/src/i18n_tests.rs | 12 + docs/translating.md | 2 +- 3 files changed, 2379 insertions(+), 1 deletion(-) create mode 100644 crates/ui-egui/locales/it.toml diff --git a/crates/ui-egui/locales/it.toml b/crates/ui-egui/locales/it.toml new file mode 100644 index 00000000..68d042f6 --- /dev/null +++ b/crates/ui-egui/locales/it.toml @@ -0,0 +1,2366 @@ +# FigCraft interface strings: Italian. +# +# Draft — needs native review: machine-drafted from the English catalog. Brand names and keyboard +# glyphs stay as they are. See docs/translating.md. + +[meta] +name = "Italiano" +status = "draft — needs native review" + +[ui] +"% of all commits" = "% di tutti i commit" +"(no longer published)" = "(non più pubblicato)" +"({n} warnings: some entries were skipped)" = { one = "({n} avviso: alcune voci sono state saltate)", other = "({n} avvisi: alcune voci sono state saltate)" } +"+ Create variable" = "+ Crea variabile" +".fig (experimental)" = ".fig (sperimentale)" +"1…9, 0" = "1…9, 0" +"@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}" = "@{login}: {prs} PR, {commits} commit, +{added} / −{deleted} righe (Δ {delta}), +{bin_added} / −{bin_deleted} asset binari, {first} – {last}" +"A collection of this file with these modes and variables: override their values per mode and add variables of its own" = "Una collezione di questo file con queste modalità e variabili: sovrascrivi i loro valori per modalità e aggiungi variabili proprie" +"A .figcraft file with this name already exists. Choose its full name to confirm replacement." = "Esiste già un file .figcraft con questo nome. Scegli il nome completo per confermare la sostituzione." +"A layer on its own aligns to its frame: select more layers, or one in a frame" = "Un livello da solo si allinea al proprio frame: seleziona più livelli, oppure uno dentro un frame" +"A responsive product page: hero, features, figures, a quote and a footer" = "Una pagina prodotto responsive: hero, funzionalità, figure, una citazione e un footer" +"A spring plays until it settles: its mass, stiffness and damping set how long" = "Una molla si muove finché non si assesta: massa, rigidità e smorzamento ne determinano la durata" +"A title, an agenda, a big figure, a chart and a closing slide" = "Un titolo, un'agenda, una cifra grande, un grafico e una slide di chiusura" +"A to Z" = "Dalla A alla Z" +"About" = "Informazioni" +"About FigCraft" = "Informazioni su FigCraft" +"About this server" = "Informazioni su questo server" +"Action" = "Azione" +"Actions" = "Azioni" +"Actions for {name}" = "Azioni per {name}" +"Actions…" = "Azioni…" +"Active" = "Attivo" +"Actual size (100%)" = "Dimensione reale (100%)" +"Add a category" = "Aggiungi una categoria" +"Add a comment" = "Aggiungi un commento" +"Add a description" = "Aggiungi una descrizione" +"Add a frame (F) or a shape (R) to get started" = "Aggiungi un frame (F) o una forma (R) per iniziare" +"Add a layer or component to tile first" = "Aggiungi prima un livello o un componente da affiancare" +"Add a note for developers" = "Aggiungi una nota per gli sviluppatori" +"Add action" = "Aggiungi azione" +"Add actions for when no condition holds" = "Aggiungi azioni per quando nessuna condizione è vera" +"Add an annotation for developers" = "Aggiungi un'annotazione per gli sviluppatori" +"Add another condition" = "Aggiungi un'altra condizione" +"Add auto layout" = "Aggiungi auto layout" +"Add background behind overlay" = "Aggiungi sfondo dietro la sovrapposizione" +"Add content to this slot" = "Aggiungi contenuto a questo slot" +"Add dev resource" = "Aggiungi risorsa per sviluppatori" +"Add effect" = "Aggiungi effetto" +"Add export setting" = "Aggiungi impostazione di esportazione" +"Add fill" = "Aggiungi riempimento" +"Add interaction" = "Aggiungi interazione" +"Add layout grid" = "Aggiungi griglia di layout" +"Add library" = "Aggiungi libreria" +"Add library file…" = "Aggiungi file libreria…" +"Add max height" = "Aggiungi altezza massima" +"Add max width" = "Aggiungi larghezza massima" +"Add min height" = "Aggiungi altezza minima" +"Add min width" = "Aggiungi larghezza minima" +"Add mode" = "Aggiungi modalità" +"Add new page" = "Aggiungi nuova pagina" +"Add or update collections from a variables JSON" = "Aggiungi o aggiorna collezioni da un JSON di variabili" +"Add properties to make instances configurable" = "Aggiungi proprietà per rendere le istanze configurabili" +"Add reaction" = "Aggiungi reazione" +"Add starting point" = "Aggiungi punto di partenza" +"Add stroke" = "Aggiungi contorno" +"Add variant" = "Aggiungi variante" +"Add variant with a new value" = "Aggiungi variante con un nuovo valore" +"Added" = "Aggiunto" +"Added the library \"{name}\"" = "Libreria \"{name}\" aggiunta" +"Adding to {name}" = "Aggiunta a {name}" +"Adjust" = "Regola" +"Adjustment presets" = "Preset di regolazione" +"Advanced" = "Avanzate" +"Advanced stroke" = "Contorno avanzato" +"After delay" = "Dopo un ritardo" +"After delay {ms}ms" = "Dopo un ritardo di {ms} ms" +"Align" = "Allinea" +"Align bottom" = "Allinea in basso" +"Align center" = "Allinea al centro" +"Align horizontal centers" = "Allinea i centri orizzontali" +"Align left" = "Allinea a sinistra" +"Align middle" = "Allinea a metà altezza" +"Align right" = "Allinea a destra" +"Align text baselines" = "Allinea le linee di base del testo" +"Align top" = "Allinea in alto" +"Align vertical centers" = "Allinea i centri verticali" +"Alignment" = "Allineamento" +"All" = "Tutti" +"All categories" = "Tutte le categorie" +"All comments" = "Tutti i commenti" +"All components" = "Tutti i componenti" +"All fonts" = "Tutti i font" +"All supported properties" = "Tutte le proprietà supportate" +"All the fonts this file uses are installed" = "Tutti i font usati da questo file sono installati" +"All variables" = "Tutte le variabili" +"Alpha" = "Alfa" +"Alphabetical" = "Alfabetico" +"Alt+drag" = "Alt+trascina" +"An analytics screen on a grid: figures, charts drawn from vectors and a table" = "Una schermata di analisi su una griglia: cifre, grafici disegnati con vettori e una tabella" +"An invite link that opens FigCraft and joins" = "Un link di invito che apre FigCraft e si unisce" +"Anchor" = "Ancora" +"Android" = "Android" +"Angular" = "Angolare" +"Animation" = "Animazione" +"Annotations" = "Annotazioni" +"Annotations shown on the canvas" = "Annotazioni mostrate sul canvas" +"Another server, or one that needs its key. The usual server is set in Preferences." = "Un altro server, oppure uno che richiede la sua chiave. Il server abituale si imposta nelle Preferenze." +"Any status" = "Qualsiasi stato" +"Anyone on your network with this code can edit the file, while this app stays open." = "Chiunque nella tua rete abbia questo codice può modificare il file, finché questa app resta aperta." +"Anyone with the edit link can change the file; the view link only lets people look. The links carry their secrets: share them like passwords. `figcraft-server links` makes more and revokes them." = "Chiunque abbia il link di modifica può cambiare il file; il link di visualizzazione permette solo di guardare. I link contengono i loro segreti: condividili come password. `figcraft-server links` ne crea altri e li revoca." +"Anyone with this code can edit the file." = "Chiunque abbia questo codice può modificare il file." +"Anyone with this code can look at the file, not change it." = "Chiunque abbia questo codice può vedere il file, ma non modificarlo." +"Appearance" = "Aspetto" +"Apply a component property" = "Applica una proprietà del componente" +"Apply variable" = "Applica variabile" +"Apply variable to text" = "Applica variabile al testo" +"Apply variable to visibility" = "Applica variabile alla visibilità" +"Apps" = "App" +"Apr" = "apr" +"Arc" = "Arco" +"Arc ratio" = "Rapporto dell'arco" +"Arc start" = "Inizio dell'arco" +"Arc sweep" = "Ampiezza dell'arco" +"Archive" = "Archivia" +"Archived" = "Archiviato" +"Arrange" = "Disponi" +"Arrow" = "Freccia" +"As typed" = "Come digitato" +"Asking the server…" = "Interrogo il server…" +"Assets" = "Asset" +"Aug" = "ago" +"Auto" = "Auto" +"Auto (space around)" = "Auto (spazio attorno)" +"Auto (space between)" = "Auto (spazio tra)" +"Auto (space evenly)" = "Auto (spazio uniforme)" +"Auto ({mode})" = "Auto ({mode})" +"Auto columns" = "Colonne automatiche" +"Auto gap" = "Spaziatura automatica" +"Auto height" = "Altezza automatica" +"Auto layout" = "Auto layout" +"Auto rows" = "Righe automatiche" +"Auto width" = "Larghezza automatica" +"Back to the FigCraft server" = "Torna al server FigCraft" +"Back to the font's value" = "Torna al valore del font" +"Back" = "Indietro" +"Back to Ready for dev" = "Torna a Pronto per lo sviluppo" +"Back to Ready for dev: the next change compares with how it is now" = "Torna a Pronto per lo sviluppo: la prossima modifica sarà confrontata con lo stato attuale" +"Back to assets" = "Torna agli asset" +"Back to current" = "Torna alla versione attuale" +"Back to styles" = "Torna agli stili" +"Back to the canvas from a focused region" = "Torna al canvas da un'area attiva" +"Back to the current version" = "Torna alla versione attuale" +"Back to the values the font style implies" = "Torna ai valori previsti dallo stile del font" +"Background" = "Sfondo" +"Background blur" = "Sfocatura dello sfondo" +"Basics" = "Nozioni di base" +"Before" = "Prima" +"Bend" = "Curva" +"Bevel" = "Smusso" +"Binary assets added" = "Asset binari aggiunti" +"Binary assets removed" = "Asset binari rimossi" +"Black & white" = "Bianco e nero" +"Bleed" = "Abbondanza" +"Bleed marks" = "Crocini di abbondanza" +"Bleed marks need a bleed" = "I crocini di abbondanza richiedono un'abbondanza" +"Blend" = "Fusione" +"Blend mode" = "Metodo di fusione" +"Blue" = "Blu" +"Blur" = "Sfocatura" +"Boolean" = "Booleano" +"Boolean groups" = "Gruppi booleani" +"Both directions" = "Entrambe le direzioni" +"Bottom" = "In basso" +"Bottom left radius" = "Raggio in basso a sinistra" +"Bottom padding" = "Margine interno inferiore" +"Bottom right radius" = "Raggio in basso a destra" +"Branch" = "Ramo" +"Branch archived" = "Ramo archiviato" +"Branch merged" = "Ramo unito" +"Branch name" = "Nome del ramo" +"Branch of {file}" = "Ramo di {file}" +"Branch of {file} · merged or archived" = "Ramo di {file} · unito o archiviato" +"Branch updated from main" = "Ramo aggiornato da main" +"Branch {name}" = "Ramo {name}" +"Branches" = "Rami" +"Bound to {name}. Click to detach the variable." = "Collegato a {name}. Clicca per scollegare la variabile." +"Bring forward" = "Porta avanti" +"Bring the main file's changes into the branch" = "Porta le modifiche del file principale nel ramo" +"Bring to front" = "Porta in primo piano" +"Bristle" = "Setola" +"Browse" = "Sfoglia" +"Brush" = "Pennello" +"Brush rotation jitter" = "Variazione casuale della rotazione del pennello" +"Brush scatter" = "Dispersione del pennello" +"Brush size jitter" = "Variazione casuale della dimensione del pennello" +"Brush spacing" = "Spaziatura del pennello" +"Bulleted list" = "Elenco puntato" +"C or Shift+C" = "C o Maiusc+C" +"C or ⇧C" = "C o ⇧C" +"CMYK colour" = "Colore CMYK" +"Can view" = "Può visualizzare" +"Checking the fonts installed on this computer…" = "Controllo dei font installati su questo computer…" +"Checking the server…" = "Controllo del server…" +"Clear from this list" = "Rimuovi da questo elenco" +"Code or invite link" = "Codice o link di invito" +"Color profile: {name}" = "Profilo colore: {name}" +"Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "Stili di colore e di testo, variabili Chiaro e Scuro, pulsanti, campi di input e schede" +"Convert to frame" = "Converti in frame" +"Convert to section" = "Converti in sezione" +"CSS custom properties…" = "Proprietà personalizzate CSS…" +"Can edit" = "Può modificare" +"Can't read this library: {e}" = "Impossibile leggere questa libreria: {e}" +"Cancel" = "Annulla" +"Canvas" = "Canvas" +"Canvas stacking" = "Sovrapposizione nel canvas" +"Cap height to baseline" = "Altezza delle maiuscole fino alla linea di base" +"Case" = "Maiuscole/minuscole" +"Case-sensitive forms" = "Forme sensibili alle maiuscole" +"Categories" = "Categorie" +"Center" = "Centro" +"Change alias" = "Cambia alias" +"Change resolved" = "Cambia lo stato di risolto" +"Change style" = "Cambia stile" +"Change to" = "Cambia in" +"Change to needs a layer inside a variant" = "\"Cambia in\" richiede un livello dentro una variante" +"Changed" = "Modificato" +"Changed on both sides: keep one" = "Modificato su entrambi i lati: mantienine uno" +"Changes on main" = "Modifiche su main" +"Changes on the branch" = "Modifiche sul ramo" +"Changes unknown" = "Modifiche sconosciute" +"Choose React, Leptos, SwiftUI or Compose above to connect this component to its code." = "Scegli React, Leptos, SwiftUI o Compose qui sopra per collegare questo componente al suo codice." +"Choose another profile to change it." = "Scegli un altro profilo per cambiarlo." +"Choose collection" = "Scegli collezione" +"Choose image" = "Scegli immagine" +"Choose mode" = "Scegli modalità" +"Choose variable" = "Scegli variabile" +"Circle" = "Cerchio" +"Clear recent" = "Svuota recenti" +"Click + to replace mixed content" = "Clicca + per sostituire il contenuto misto" +"Click anywhere on the canvas to leave one." = "Clicca ovunque sul canvas per lasciarne uno." +"Click or drag to merge · {alt} removes" = "Clicca o trascina per unire · {alt} rimuove" +"Click or drag to remove · {alt} merges" = "Clicca o trascina per rimuovere · {alt} unisce" +"Click to record a key" = "Clicca per registrare un tasto" +"Clip content" = "Ritaglia il contenuto" +"Clip to shape" = "Ritaglia sulla forma" +"Close" = "Chiudi" +"Close overlay" = "Chiudi sovrapposizione" +"Close preview" = "Chiudi anteprima" +"Close tab" = "Chiudi scheda" +"Close version history" = "Chiudi cronologia versioni" +"Close when clicking outside" = "Chiudi cliccando all'esterno" +"Close without saving" = "Chiudi senza salvare" +"Code" = "Codice" +"Code syntax" = "Sintassi del codice" +"Collapse" = "Comprimi" +"Collapse panel" = "Comprimi pannello" +"Collection" = "Collezione" +"Collections" = "Collezioni" +"Color" = "Colore" +"Color burn" = "Brucia colore" +"Color dodge" = "Scherma colore" +"Color format" = "Formato colore" +"Color profile" = "Profilo colore" +"Color styles" = "Stili di colore" +"Color values in this file are in this color space." = "I valori di colore in questo file sono in questo spazio colore." +"Color variables" = "Variabili di colore" +"Colour" = "Colore" +"Column" = "Colonna" +"Column gap" = "Spaziatura colonne" +"Column sizes" = "Dimensioni colonne" +"Column span" = "Estensione colonne" +"Columns" = "Colonne" +"Columns {count}" = "Colonne {count}" +"Comment" = "Commento" +"Comments" = "Commenti" +"Commits" = "Commit" +"Company" = "Azienda" +"Compare changes" = "Confronta modifiche" +"Compatibility notes" = "Note di compatibilità" +"Completed" = "Completato" +"Component" = "Componente" +"Component name" = "Nome del componente" +"Component property" = "Proprietà del componente" +"Component set" = "Set di componenti" +"Component sets" = "Set di componenti" +"Components" = "Componenti" +"Conditional" = "Condizionale" +"Confetti" = "Coriandoli" +"Conflicts" = "Conflitti" +"Connect to code" = "Collega al codice" +"Connected" = "Collegato" +"Connected to code: {from}" = "Collegato al codice: {from}" +"Connection lost, reconnecting (attempt {attempt})…" = "Connessione persa, riconnessione in corso (tentativo {attempt})…" +"Constrain proportions" = "Mantieni le proporzioni" +"Constraints" = "Vincoli" +"Content" = "Contenuto" +"Contents hidden" = "Contenuti nascosti" +"Contents only" = "Solo contenuti" +"Contextual alternates" = "Alternative contestuali" +"Contrast" = "Contrasto" +"Contributors" = "Collaboratori" +"Contributors: {people} · Commits: {commits}" = "Collaboratori: {people} · Commit: {commits}" +"Controller" = "Controller" +"Cool" = "Freddo" +"Copied as CSS" = "Copiato come CSS" +"Copied as PNG" = "Copiato come PNG" +"Copied as SVG" = "Copiato come SVG" +"Copied as text" = "Copiato come testo" +"Copied {value}" = "Copiato {value}" +"Copy" = "Copia" +"Copy as" = "Copia come" +"Copy as CSS" = "Copia come CSS" +"Copy as PNG" = "Copia come PNG" +"Copy as SVG" = "Copia come SVG" +"Copy code" = "Copia codice" +"Copy link" = "Copia link" +"Copy the {label}" = "Copia {label}" +"Copy {value}" = "Copia {value}" +"Corner radius" = "Raggio degli angoli" +"Corner smoothing" = "Smussatura degli angoli" +"Couldn't download the variables" = "Impossibile scaricare le variabili" +"Couldn't reach {server}: showing what this computer remembers." = "Impossibile raggiungere {server}: mostro ciò che questo computer ha in memoria." +"Couldn't read {name}." = "Impossibile leggere {name}." +"Couldn't save automatically: {error}" = "Impossibile salvare automaticamente: {error}" +"Count" = "Numero" +"Create" = "Crea" +"Create a variable collection first" = "Crea prima una collezione di variabili" +"Create a variable first" = "Crea prima una variabile" +"Create alias" = "Crea alias" +"Create branch" = "Crea ramo" +"Create collection" = "Crea collezione" +"Create component" = "Crea componente" +"Create component property" = "Crea proprietà del componente" +"Create components, styles or variables to publish them as a library." = "Crea componenti, stili o variabili per pubblicarli come libreria." +"Create property…" = "Crea proprietà…" +"Create style" = "Crea stile" +"Create style from the selected text" = "Crea stile dal testo selezionato" +"Created" = "Creato" +"Crop" = "Ritaglia" +"Crop marks" = "Crocini di taglio" +"Ctrl+arrows" = "Ctrl+frecce" +"Ctrl+click" = "Ctrl+clic" +"Ctrl+scroll" = "Ctrl+scorrimento" +"Current name" = "Nome attuale" +"Current version" = "Versione attuale" +"Cursor chat" = "Chat del cursore" +"Custom" = "Personalizzato" +"Custom size" = "Dimensione personalizzata" +"Cut" = "Taglia" +"Dark" = "Scuro" +"Darken" = "Scurisci" +"Dash" = "Tratteggio" +"Dashboard" = "Dashboard" +"Date created" = "Data di creazione" +"Dec" = "dic" +"Decoration style" = "Stile della decorazione" +"Decoration" = "Decorazione" +"Deep select" = "Selezione profonda" +"Default" = "Predefinito" +"Default content" = "Contenuto predefinito" +"Default value (the top-left variant)" = "Valore predefinito (la variante in alto a sinistra)" +"Delay" = "Ritardo" +"Delete" = "Elimina" +"Delete category (its annotations stay)" = "Elimina categoria (le sue annotazioni restano)" +"Delete collection" = "Elimina collezione" +"Delete group" = "Elimina gruppo" +"Delete mode" = "Elimina modalità" +"Delete page" = "Elimina pagina" +"Delete preset" = "Elimina preset" +"Delete property" = "Elimina proprietà" +"Delete the style (layers keep their values)" = "Elimina lo stile (i livelli mantengono i propri valori)" +"Delete thread" = "Elimina discussione" +"Delete variable" = "Elimina variabile" +"Delete “{name}”? This can't be undone." = "Eliminare “{name}”? L'operazione non può essere annullata." +"Density" = "Densità" +"Depth" = "Profondità" +"Describe how and when to use this component" = "Descrivi come e quando usare questo componente" +"Describe what changed" = "Descrivi cosa è cambiato" +"Description" = "Descrizione" +"Description (optional)" = "Descrizione (facoltativa)" +"Design" = "Design" +"Design system starter" = "Starter per design system" +"Desktop" = "Desktop" +"Destination" = "Destinazione" +"Detach alias" = "Scollega alias" +"Detach instance" = "Scollega istanza" +"Detach property" = "Scollega proprietà" +"Detach style" = "Scollega stile" +"Detach styles" = "Scollega stili" +"Detach variable" = "Scollega variabile" +"Details" = "Dettagli" +"Dev Mode" = "Modalità sviluppo" +"Dev resources" = "Risorse per sviluppatori" +"Development" = "Sviluppo" +"Device" = "Dispositivo" +"Device at 100%" = "Dispositivo al 100%" +"Diamond" = "Rombo" +"Difference" = "Differenza" +"Dimensions" = "Dimensioni" +"Direction" = "Direzione" +"Disconnect from code" = "Scollega dal codice" +"Dispersion" = "Dispersione" +"Display name" = "Nome visualizzato" +"Dissolve" = "Dissolvenza" +"Distribute and tidy" = "Distribuisci e riordina" +"Distribute horizontal spacing" = "Distribuisci spaziatura orizzontale" +"Distribute vertical spacing" = "Distribuisci spaziatura verticale" +"Divider" = "Divisore" +"Don't truncate" = "Non troncare" +"Done" = "Fatto" +"Done editing" = "Modifica terminata" +"Dots" = "Punti" +"Dotted" = "Puntinato" +"Down" = "Giù" +"Drafts" = "Bozze" +"Drag the stroke to add a width point" = "Trascina il contorno per aggiungere un punto di larghezza" +"Draw" = "Disegna" +"Draw a frame" = "Disegna un frame" +"Drawing tools" = "Strumenti di disegno" +"Drop a .figcraft, .fig, .json, .svg or image file." = "Rilascia un file .figcraft, .fig, .json, .svg o un'immagine." +"Drop to import" = "Rilascia per importare" +"Drop to open" = "Rilascia per aprire" +"Drop shadow" = "Ombra esterna" +"Dual-licensed MIT OR Apache-2.0. Made by the ArtCraft team and community." = "Doppia licenza MIT OR Apache-2.0. Creato dal team e dalla community di ArtCraft." +"Duo" = "Duo" +"Duplicate" = "Duplica" +"Duplicate by dragging" = "Duplica trascinando" +"Duplicate mode" = "Duplica modalità" +"Duplicate page" = "Duplica pagina" +"Edit" = "Modifica" +"Edit as an expression" = "Modifica come espressione" +"Edit colour" = "Modifica colore" +"Edit comment" = "Modifica commento" +"Edit dev resource" = "Modifica risorsa per sviluppatori" +"Edit link" = "Link di modifica" +"Edit link copied" = "Link di modifica copiato" +"Edit style" = "Modifica stile" +"Edit variable" = "Modifica variabile" +"Edit with the builder" = "Modifica con il builder" +"Edited {when}" = "Modificato {when}" +"Effect settings" = "Impostazioni dell'effetto" +"Effect styles" = "Stili di effetto" +"Effects" = "Effetti" +"Ellipse" = "Ellisse" +"Ellipses" = "Ellissi" +"Else" = "Altrimenti" +"Else if" = "Altrimenti se" +"Embedded profile" = "Profilo incorporato" +"Empty" = "Vuoto" +"End" = "Fine" +"End point" = "Punto finale" +"Ends the session; everyone keeps their copy" = "Termina la sessione; ognuno conserva la propria copia" +"Enter" = "Invio" +"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "Inserisci l'indirizzo di un figcraft-server e premi Sfoglia per vedere i plugin che offre." +"Enter to save · Esc to cancel" = "Invio per salvare · Esc per annullare" +"Eraser" = "Gomma" +"Esc" = "Esc" +"Essential" = "Essenziale" +"Every conflict has a choice." = "Ogni conflitto ha una scelta." +"Every top-level frame already starts a flow" = "Ogni frame di primo livello avvia già un flusso" +"Exact opacity (4 then 5: 45%)" = "Opacità esatta (4 poi 5: 45%)" +"Exclude" = "Escludi" +"Excluded" = "Escluso" +"Excluded from layout" = "Escluso dal layout" +"Excludes" = "Esclude" +"Exclusion" = "Esclusione" +"Exit focus view (Esc)" = "Esci dalla vista focus (Esc)" +"Expand" = "Espandi" +"Export" = "Esporta" +"Export all" = "Esporta tutto" +"Export frames and layers as PNG, JPG, SVG or PDF" = "Esporta frame e livelli come PNG, JPG, SVG o PDF" +"Export page as PNG" = "Esporta pagina come PNG" +"Export settings" = "Impostazioni di esportazione" +"Export variables" = "Esporta variabili" +"Export {name}" = "Esporta {name}" +"Export {n} layers" = { one = "Esporta {n} livello", other = "Esporta {n} livelli" } +"Exported {file}" = "Esportato {file}" +"Exported {name}" = "Esportato {name}" +"Exported {name}; {n} kinds of content were left out" = { one = "Esportato {name}; {n} tipo di contenuto è stato escluso", other = "Esportato {name}; {n} tipi di contenuto sono stati esclusi" } +"Exported {n} files" = { one = "Esportato {n} file", other = "Esportati {n} file" } +"Export…" = "Esporta…" +"Expose properties to instances" = "Esponi le proprietà alle istanze" +"Exposure" = "Esposizione" +"Expression" = "Espressione" +"Extend collection" = "Estendi collezione" +"Extends \"{name}\": overridden values are marked" = "Estende \"{name}\": i valori sovrascritti sono evidenziati" +"Eye" = "Occhio" +"F" = "F" +"Fade" = "Sfuma" +"False" = "Falso" +"Feb" = "feb" +"Fetching from the server…" = "Recupero dal server…" +"FigCraft couldn't open its window: {error}" = "FigCraft non è riuscito ad aprire la sua finestra: {error}" +"FigCraft doesn't support every feature of these files yet. The notes list what was left out or changed in the imported copy; the original file is unchanged." = "FigCraft non supporta ancora tutte le funzionalità di questi file. Le note elencano ciò che è stato omesso o modificato nella copia importata; il file originale resta invariato." +"File" = "File" +"File name" = "Nome del file" +"File not found" = "File non trovato" +"File published" = "File pubblicato" +"Files aren't end-to-end encrypted: the server's operator can read them." = "I file non sono crittografati end-to-end: chi gestisce il server può leggerli." +"Files nobody opens for {n} days are deleted." = { one = "I file che nessuno apre da {n} giorno vengono eliminati.", other = "I file che nessuno apre da {n} giorni vengono eliminati." } +"Fill" = "Riempimento" +"Fill (fr)" = "Riempimento (fr)" +"Fill container" = "Riempi contenitore" +"Fill window" = "Riempi finestra" +"Fills" = "Riempimenti" +"Fill|sizing" = "Riempi" +"Filter and sort" = "Filtra e ordina" +"Find" = "Trova" +"Find and replace" = "Trova e sostituisci" +"Find layers" = "Trova livelli" +"First" = "Primo" +"First commit" = "Primo commit" +"First on top" = "Il primo in alto" +"Fit" = "Adatta" +"Fit device to window" = "Adatta il dispositivo alla finestra" +"Fit to contents" = "Adatta al contenuto" +"Fit to window" = "Adatta alla finestra" +"Fit width" = "Adatta alla larghezza" +"Fixed" = "Fisso" +"Fixed gap" = "Spaziatura fissa" +"Fixed size" = "Dimensione fissa" +"Flatten" = "Appiattisci" +"Flip" = "Rifletti" +"Flip horizontal" = "Rifletti orizzontalmente" +"Flip text to the other side" = "Rifletti il testo sull'altro lato" +"Flip vertical" = "Rifletti verticalmente" +"Flip width profile" = "Inverti il profilo di larghezza" +"Flow starting point" = "Punto di partenza del flusso" +"Flow starting points" = "Punti di partenza del flusso" +"Flows" = "Flussi" +"Focus view" = "Vista focus" +"Fold autosaves" = "Comprimi i salvataggi automatici" +"Folder: click to collapse" = "Cartella: clicca per comprimere" +"Folder: click to expand" = "Cartella: clicca per espandere" +"Follow" = "Segui" +"Follow {name}" = "Segui {name}" +"Following" = "Seguendo" +"Following {name} · {key} to stop" = "Stai seguendo {name} · {key} per smettere" +"Font" = "Font" +"Font not installed" = "Font non installato" +"Font size" = "Dimensione del font" +"Font style" = "Stile del font" +"Forget on this computer" = "Dimentica su questo computer" +"Four screens of a trail app with components and a prototype flow" = "Quattro schermate di un'app per sentieri con componenti e un flusso di prototipo" +"Fractions" = "Frazioni" +"Frame" = "Frame" +"Frame selection" = "Racchiudi la selezione in un frame" +"Frame tools" = "Strumenti frame" +"Frames" = "Frame" +"From the library \"{name}\": read-only" = "Dalla libreria \"{name}\": sola lettura" +"From {name}" = "Da {name}" +"From {server}" = "Da {server}" +"Frost" = "Brina" +"Gap" = "Spaziatura" +"Gap between columns" = "Spaziatura tra le colonne" +"Gap between items" = "Spaziatura tra gli elementi" +"Gap between rows" = "Spaziatura tra le righe" +"Getting started" = "Per iniziare" +"Give the address of a server open to the public, or a server's address with its key (`figcraft-server key` prints it)." = "Indica l'indirizzo di un server aperto al pubblico, oppure l'indirizzo di un server con la sua chiave (la stampa `figcraft-server key`)." +"Glass" = "Vetro" +"Go to main component" = "Vai al componente principale" +"Go to {name}" = "Vai a {name}" +"Grab bag" = "Varie" +"Gradient" = "Gradiente" +"Gray" = "Grigio" +"Green" = "Verde" +"Greyscale components and two example screens to sketch a flow with" = "Componenti in scala di grigi e due schermate di esempio con cui abbozzare un flusso" +"Grid" = "Griglia" +"Grid ({size})" = "Griglia ({size})" +"Grid styles" = "Stili di griglia" +"Grid {size}" = "Griglia {size}" +"Group" = "Gruppo" +"Group selection" = "Raggruppa selezione" +"Groups" = "Gruppi" +"Gutter" = "Intercolonna" +"Hand tool" = "Strumento mano" +"Hanging list" = "Elenco sporgente" +"Hanging punctuation" = "Punteggiatura sporgente" +"Hard light" = "Luce intensa" +"Height" = "Altezza" +"Help" = "Aiuto" +"Help and resources" = "Aiuto e risorse" +"Help page" = "Pagina di aiuto" +"Here: {names}" = "Qui: {names}" +"Hidden" = "Nascosto" +"Hide contents" = "Nascondi contenuti" +"Hide from publishing" = "Nascondi dalla pubblicazione" +"Hide section contents" = "Nascondi il contenuto della sezione" +"Hide these tips (Getting started in the Help menu shows them again)" = "Nascondi questi suggerimenti (Per iniziare nel menu Aiuto li mostra di nuovo)" +"Hide when publishing" = "Nascondi alla pubblicazione" +"Highlight layers on hover" = "Evidenzia i livelli al passaggio del mouse" +"Highlights" = "Luci" +"Hold Alt" = "Tieni premuto Alt" +"Hold Alt+Z" = "Tieni premuto Alt+Z" +"Hold Ctrl" = "Tieni premuto Ctrl" +"Hold Z" = "Tieni premuto Z" +"Hold ⌃ or ⌘" = "Tieni premuto ⌃ o ⌘" +"Hold ⌥" = "Tieni premuto ⌥" +"Hold ⌥Z" = "Tieni premuto ⌥Z" +"Home" = "Home" +"Horizontal" = "Orizzontale" +"Horizontal align" = "Allineamento orizzontale" +"Horizontal hex" = "Esagono orizzontale" +"Horizontal padding" = "Margine interno orizzontale" +"Horizontal spacing" = "Spaziatura orizzontale" +"Hosted by {operator}" = "Ospitato da {operator}" +"Hover a layer to see its properties" = "Passa sopra un livello per vederne le proprietà" +"Hover other layers to measure the distance" = "Passa sopra altri livelli per misurare la distanza" +"Hue" = "Tonalità" +"Hug" = "Aderisci" +"Hug contents" = "Aderisci al contenuto" +"Icon components on a 24 px grid with keylines and a size preview" = "Componenti icona su una griglia da 24 px con linee guida e anteprima delle dimensioni" +"Icon set starter" = "Starter per set di icone" +"If it keeps happening, please report it at {link} with this message." = "Se il problema persiste, segnalalo su {link} insieme a questo messaggio." +"If" = "Se" +"If {condition}" = "Se {condition}" +"Ignore auto layout (absolute position)" = "Ignora auto layout (posizione assoluta)" +"Image" = "Immagine" +"Images" = "Immagini" +"Import" = "Importa" +"Import path" = "Percorso di importazione" +"Import plugin from manifest" = "Importa plugin da manifest" +"Import plugin from manifest…" = "Importa plugin da manifest…" +"Import report" = "Resoconto di importazione" +"Import variables" = "Importa variabili" +"Imported document" = "Documento importato" +"Imported {name}" = "Importato {name}" +"Imported {n} new variables, updated {updated}" = { one = "Importata {n} nuova variabile, aggiornate {updated}", other = "Importate {n} nuove variabili, aggiornate {updated}" } +"Import…" = "Importa…" +"In an instance, this comes from the main component" = "In un'istanza, questo proviene dal componente principale" +"In the browser you can join sessions the desktop app hosts and files on a FigCraft server. A page opened over https reaches other computers only through wss:// links (a server behind TLS)." = "Nel browser puoi unirti alle sessioni ospitate dall'app desktop e ai file su un server FigCraft. Una pagina aperta tramite https raggiunge altri computer solo attraverso link wss:// (un server dietro TLS)." +"Include bounding box" = "Includi riquadro di delimitazione" +"Include id attribute" = "Includi attributo id" +"Included" = "Incluso" +"Included in layout" = "Incluso nel layout" +"Individual corners" = "Angoli singoli" +"Individual padding" = "Margini interni singoli" +"Individual strokes" = "Contorni singoli" +"Ink" = "Inchiostro" +"Inner shadow" = "Ombra interna" +"Inside" = "Dentro" +"Inspect" = "Ispeziona" +"Inspect the layer under the pointer: size, colours, typography" = "Ispeziona il livello sotto il puntatore: dimensioni, colori, tipografia" +"Install plugin" = "Installa plugin" +"Install" = "Installa" +"Installed {name}" = "Installato {name}" +"Installed" = "Installato" +"Installing…" = "Installazione…" +"Instance" = "Istanza" +"Instance properties" = "Proprietà dell'istanza" +"Instance swap" = "Scambio di istanza" +"Instance swap (no other component)" = "Scambio di istanza (nessun altro componente)" +"Instances" = "Istanze" +"Instant" = "Istantaneo" +"Intensity" = "Intensità" +"Interaction" = "Interazione" +"Interactions" = "Interazioni" +"Interface design and prototyping. A free, open-source, clean-room design tool written from scratch in Rust, part of the ArtCraft Crafting Apps." = "Progettazione e prototipazione di interfacce. Uno strumento di design gratuito e open source, sviluppato da zero in Rust con metodo clean-room, parte delle ArtCraft Crafting Apps." +"Interface size" = "Dimensione dell'interfaccia" +"Intersect" = "Interseca" +"Intersects" = "Interseca" +"Invalid color" = "Colore non valido" +"Invite link" = "Link di invito" +"Invite link copied" = "Link di invito copiato" +"It asks permission to:" = "Chiede il permesso di:" +"Jan" = "gen" +"Join" = "Unisciti" +"Join code copied" = "Codice di partecipazione copiato" +"Join code or invite link" = "Codice di partecipazione o link di invito" +"Join live session" = "Partecipa alla sessione live" +"Join us on Discord" = "Unisciti a noi su Discord" +"Join with a code" = "Partecipa con un codice" +"Joined" = "Unito" +"Joined {file}" = "Partecipazione a {file}" +"Joining…" = "Partecipazione…" +"Join|stroke" = "Giunzione" +"Jul" = "lug" +"Jun" = "giu" +"Just now" = "Adesso" +"Justify" = "Giustifica" +"Keep appearance" = "Mantieni l'aspetto" +"Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift." = "\"Mantieni l'aspetto\" converte ogni valore di colore in {profile} così i colori sembrano uguali. \"Mantieni i valori\" lascia i numeri invariati, quindi i colori cambiano." +"Keep tool selected after use" = "Mantieni lo strumento selezionato dopo l'uso" +"Keep values" = "Mantieni i valori" +"Key/Gamepad" = "Tasto/Gamepad" +"Key/Gamepad {key}" = "Tasto/Gamepad {key}" +"Keyboard shortcuts" = "Scorciatoie da tastiera" +"Kits" = "Kit" +"Landing page" = "Landing page" +"Language" = "Lingua" +"Lasso" = "Lazo" +"Last" = "Ultimo" +"Last change: {note}" = "Ultima modifica: {note}" +"Last commit" = "Ultimo commit" +"Last modified" = "Ultima modifica" +"Last on top" = "L'ultimo in alto" +"Layer blur" = "Sfocatura del livello" +"Layer name" = "Nome del livello" +"Layers" = "Livelli" +"Layers in an instance take their position from the main component" = "I livelli in un'istanza prendono la posizione dal componente principale" +"Layout" = "Layout" +"Layout grid" = "Griglia di layout" +"Layout grid settings" = "Impostazioni della griglia di layout" +"Layout grids" = "Griglie di layout" +"Leave session" = "Esci dalla sessione" +"Leaves" = "Foglie" +"Left" = "Sinistra" +"Left & right" = "Sinistra e destra" +"Left padding" = "Margine interno sinistro" +"Left side" = "Lato sinistro" +"Left side width" = "Larghezza del lato sinistro" +"Letter case" = "Maiuscole/minuscole" +"Letter spacing" = "Spaziatura tra lettere" +"Libraries" = "Librerie" +"Libraries in this file" = "Librerie in questo file" +"Library updates available" = "Aggiornamenti delle librerie disponibili" +"Library: click to collapse" = "Libreria: clicca per comprimere" +"Library: click to expand" = "Libreria: clicca per espandere" +"Library: its variables are read-only here; edit them in the library file and publish it" = "Libreria: qui le sue variabili sono di sola lettura; modificale nel file della libreria e pubblicalo" +"Light" = "Chiaro" +"Lighten" = "Schiarisci" +"Light|glass" = "Luce" +"Line" = "Linea" +"Line arrow" = "Freccia lineare" +"Line delta" = "Variazione righe" +"Line height" = "Interlinea" +"Linear" = "Lineare" +"Lines" = "Righe" +"Lines +/−" = "Righe +/−" +"Lines added" = "Righe aggiunte" +"Lines deleted" = "Righe eliminate" +"Link" = "Link" +"Link copied" = "Link copiato" +"Link docs, code or tickets for developers." = "Collega documentazione, codice o ticket per gli sviluppatori." +"Link frames on the Prototype tab, then present the flow" = "Collega i frame nella scheda Prototipo, poi presenta il flusso" +"Link this component to its code component: instances then show your real snippet with their values." = "Collega questo componente al suo componente nel codice: le istanze mostreranno il tuo snippet reale con i loro valori." +"List spacing" = "Spaziatura dell'elenco" +"List style" = "Stile dell'elenco" +"Live session" = "Sessione live" +"Live · {people}" = "Live · {people}" +"Local components" = "Componenti locali" +"Local network" = "Rete locale" +"Local styles" = "Stili locali" +"Local variables" = "Variabili locali" +"Lock/Unlock" = "Blocca/Sblocca" +"Locked" = "Bloccato" +"Lowercase" = "Minuscolo" +"Luminance" = "Luminanza" +"Luminosity" = "Luminosità" +"Main" = "Main" +"Main component deleted" = "Componente principale eliminato" +"Main menu" = "Menu principale" +"Make a component" = "Crea un componente" +"Make slot" = "Crea slot" +"Mar" = "mar" +"Margin" = "Margine" +"Mark all as read" = "Segna tutto come letto" +"Mark as resolved" = "Segna come risolto" +"Mark as unread" = "Segna come non letto" +"Mark as unresolved" = "Segna come non risolto" +"Mask" = "Maschera" +"Mass · Stiffness · Damping" = "Massa · Rigidità · Smorzamento" +"Match (empty: the whole name)" = "Corrispondenza (vuoto: l'intero nome)" +"Max H" = "Alt. max" +"Max W" = "Largh. max" +"Max lines" = "Righe massime" +"Maximize" = "Ingrandisci" +"May" = "mag" +"Measure to the selection" = "Misura fino alla selezione" +"Merge into main" = "Unisci in main" +"Merge regions" = "Unisci regioni" +"Merge the branch into the main file (one undo step) and archive it" = "Unisci il ramo nel file principale (un solo passaggio di annullamento) e archivialo" +"Merged" = "Unito" +"Merged PRs" = "PR uniti" +"Min H" = "Alt. min" +"Min W" = "Largh. min" +"Minimize" = "Riduci a icona" +"Mirror angle" = "Rifletti angolo" +"Mirror angle and length" = "Rifletti angolo e lunghezza" +"Mirroring" = "Riflessione" +"Missing" = "Mancante" +"Missing fonts" = "Font mancanti" +"Missing layer" = "Livello mancante" +"Missing style" = "Stile mancante" +"Missing variable" = "Variabile mancante" +"Miter" = "Spigolo vivo" +"Miter angle" = "Angolo dello spigolo" +"Mixed" = "Misto" +"Mixed fonts" = "Font misti" +"Mixed interactions: click + to replace them" = "Interazioni miste: clicca + per sostituirle" +"Mobile app" = "App mobile" +"Mode" = "Modalità" +"Model" = "Modello" +"Models" = "Modelli" +"Mono" = "Mono" +"More" = "Altro" +"More instance actions" = "Altre azioni per l'istanza" +"Mouse down" = "Pressione del mouse" +"Mouse enter" = "Ingresso del mouse" +"Mouse leave" = "Uscita del mouse" +"Mouse up" = "Rilascio del mouse" +"Move along the toolbar or the layer rows" = "Spostati lungo la barra degli strumenti o le righe dei livelli" +"Move" = "Sposta" +"Move column left" = "Sposta colonna a sinistra" +"Move column right" = "Sposta colonna a destra" +"Move down" = "Sposta giù" +"Move in" = "Sposta dentro" +"Move out" = "Sposta fuori" +"Move tools" = "Strumenti di spostamento" +"Move up" = "Sposta su" +"Moved to {name}" = "Spostato in {name}" +"Multi" = "Multi" +"Multiply" = "Moltiplica" +"Muted" = "Attenuato" +"Name" = "Nome" +"Name (A–Z)" = "Nome (A–Z)" +"Named instance" = "Istanza con nome" +"Navigate to" = "Vai a" +"Nested instance" = "Istanza annidata" +"New design file" = "Nuovo file di design" +"Newest first" = "Prima i più recenti" +"Next or previous control in the focused region" = "Controllo successivo o precedente nell'area attiva" +"Next frame" = "Frame successivo" +"Next ready for dev" = "Prossimo pronto per lo sviluppo" +"Next size option" = "Opzione di dimensione successiva" +"No assets: no export settings, icons or images. Add export settings in the Design tab." = "Nessun asset: nessuna impostazione di esportazione, icona o immagine. Aggiungi le impostazioni di esportazione nella scheda Design." +"No boolean variables fit here." = "Nessuna variabile booleana adatta qui." +"No boolean variables yet. Create them in Local variables." = "Ancora nessuna variabile booleana. Creale in Variabili locali." +"No branches. A branch is a copy of the file to try changes in, then review and merge." = "Nessun ramo. Un ramo è una copia del file in cui provare modifiche, da rivedere e unire poi." +"No category" = "Nessuna categoria" +"No changes" = "Nessuna modifica" +"No color styles" = "Nessuno stile di colore" +"No colour variables fit here." = "Nessuna variabile di colore adatta qui." +"No colour variables yet. Create them in Local variables." = "Ancora nessuna variabile di colore. Creale in Variabili locali." +"No comments yet" = "Ancora nessun commento" +"No compatibility warnings reported" = "Nessun avviso di compatibilità segnalato" +"No components yet. Select layers and press {keys}." = "Ancora nessun componente. Seleziona dei livelli e premi {keys}." +"No conflicts." = "Nessun conflitto." +"No contributor data was built into this copy." = "In questa copia non sono inclusi dati sui collaboratori." +"No device" = "Nessun dispositivo" +"No fonts match" = "Nessun font corrisponde" +"No files or templates match your search" = "Nessun file o modello corrisponde alla ricerca" +"No join code here (the server is older than join codes, or this address can't be put in one): share the link instead." = "Nessun codice di partecipazione qui (il server è più vecchio dei codici di partecipazione, oppure questo indirizzo non può essere inserito in uno): condividi invece il link." +"No layer changes to show." = "Nessuna modifica ai livelli da mostrare." +"No libraries yet. Add a .figcraft file that was published as a library, or paste a team library's link." = "Ancora nessuna libreria. Aggiungi un file .figcraft pubblicato come libreria, oppure incolla il link di una libreria del team." +"No list" = "Nessun elenco" +"No matching comments" = "Nessun commento corrispondente" +"No matching components" = "Nessun componente corrispondente" +"No matching layers" = "Nessun livello corrispondente" +"No matching styles" = "Nessuno stile corrispondente" +"No matching variables" = "Nessuna variabile corrispondente" +"No mirroring" = "Nessuna riflessione" +"No model credits were built into this copy." = "In questa copia non sono inclusi crediti dei modelli." +"No number variables fit here." = "Nessuna variabile numerica adatta qui." +"No number variables yet. Create them in Local variables." = "Ancora nessuna variabile numerica. Creale in Variabili locali." +"No other pages" = "Nessun'altra pagina" +"No paints" = "Nessun riempimento" +"No plugins on {server}." = "Nessun plugin su {server}." +"No plugins yet. Import a development plugin from its figcraft-plugin.json." = "Ancora nessun plugin. Importa un plugin di sviluppo dal suo figcraft-plugin.json." +"No preferred components" = "Nessun componente preferito" +"No preset" = "Nessun preset" +"No saved versions yet. Save a version to mark a point in your work you can come back to." = "Ancora nessuna versione salvata. Salva una versione per segnare un punto del tuo lavoro a cui poter tornare." +"No scrolling" = "Nessuno scorrimento" +"No shortcuts match" = "Nessuna scorciatoia corrisponde" +"No status" = "Nessuno stato" +"No string variables fit here." = "Nessuna variabile stringa adatta qui." +"No string variables yet. Create them in Local variables." = "Ancora nessuna variabile stringa. Creale in Variabili locali." +"No styles yet." = "Ancora nessuno stile." +"No styles yet. Click + to create one from the selection." = "Ancora nessuno stile. Clicca + per crearne uno dalla selezione." +"No templates match your search" = "Nessun modello corrisponde alla ricerca" +"No text styles yet. Click + to create one from the selected text." = "Ancora nessuno stile di testo. Clicca + per crearne uno dal testo selezionato." +"No variables fit here." = "Nessuna variabile adatta qui." +"No variables yet. Create them in Local variables." = "Ancora nessuna variabile. Creale in Variabili locali." +"Nobody else is here yet" = "Non c'è ancora nessun altro" +"Noise" = "Rumore" +"None" = "Nessuno" +"None." = "Nessuno." +"Normal" = "Normale" +"Not bound" = "Non collegato" +"Not in a live session" = "Non in una sessione live" +"Not in this font" = "Non presente in questo font" +"Not properties, kept as written: {names}" = "Non sono proprietà, mantenute come scritte: {names}" +"Not read yet" = "Non ancora letto" +"Not shared" = "Non condiviso" +"Note {n}" = "Nota {n}" +"Nothing changed since this was marked ready for dev." = "Nulla è cambiato da quando è stato segnato come pronto per lo sviluppo." +"Nothing here yet. Set a frame's status to list it." = "Ancora niente qui. Imposta lo stato di un frame per elencarlo." +"Nothing online yet. Use Share on a file to put it online and it is listed here." = "Ancora niente online. Usa Condividi su un file per metterlo online e comparirà qui." +"Nothing to export" = "Niente da esportare" +"Nov" = "nov" +"Now" = "Ora" +"Nudge (Shift: big nudge)" = "Sposta di poco (Maiusc: spostamento grande)" +"Nudge amount" = "Entità dello spostamento" +"Number" = "Numero" +"Number ↑" = "Numero ↑" +"Number ↓" = "Numero ↓" +"Numbered list" = "Elenco numerato" +"Object" = "Oggetto" +"Oct" = "ott" +"Off" = "Disattivato" +"Offered first when filling the slot (any content is allowed)" = "Proposto per primo quando si riempie lo slot (qualsiasi contenuto è ammesso)" +"Offered first when swapping the instance (any component is allowed)" = "Proposto per primo quando si scambia l'istanza (qualsiasi componente è ammesso)" +"Offset" = "Scostamento" +"Oldest first" = "Prima i più vecchi" +"On click" = "Al clic" +"On drag" = "Al trascinamento" +"On this page" = "In questa pagina" +"One announcement in five common sizes: square, portrait, story, link and banner" = "Un annuncio in cinque formati comuni: quadrato, verticale, storia, link e banner" +"Online" = "Online" +"Online files" = "File online" +"Online sharing is off: files are shared on the local network only." = "La condivisione online è disattivata: i file sono condivisi solo sulla rete locale." +"Only the first changed layers are listed." = "Sono elencati solo i primi livelli modificati." +"Only the first {n} changed layers are listed." = "Sono elencati solo i primi {n} livelli modificati." +"Only your threads" = "Solo le tue discussioni" +"Opacity" = "Opacità" +"Opacity & blend" = "Opacità e fusione" +"Opacity 10%…90%, 0: 100%" = "Opacità 10%…90%, 0: 100%" +"Open" = "Apri" +"Open .figcraft, .svg or images" = "Apri .figcraft, .svg o immagini" +"Open branch" = "Apri ramo" +"Open file" = "Apri file" +"Open files" = "Apri file" +"Open in focus view" = "Apri nella vista focus" +"Open in new window" = "Apri in una nuova finestra" +"Open invite links in FigCraft" = "Apri i link di invito in FigCraft" +"Open link" = "Apri link" +"Open link {url}" = "Apri link {url}" +"Open overlay" = "Apri sovrapposizione" +"Open prototype preview" = "Apri anteprima del prototipo" +"Open variables ({count})" = "Variabili aperte ({count})" +"Opened as a new document" = "Aperto come nuovo documento" +"Opened “{name}” in a new tab" = "“{name}” aperto in una nuova scheda" +"Opening the file…" = "Apertura del file…" +"Opening {n} files as new files" = { one = "Apertura come nuovo file", other = "Apertura di {n} file come nuovi file" } +"Open|comments" = "Aperti" +"Options" = "Opzioni" +"Orange" = "Arancione" +"Order" = "Ordine" +"Other apps may not open every .fig file exported here." = "Altre app potrebbero non aprire tutti i file .fig esportati qui." +"Other" = "Altro" +"Others lose access and keep their copy; the file stays on the server for you" = "Gli altri perdono l'accesso e conservano la propria copia; il file resta sul server per te" +"Others type this under Share ▸ Join with a code" = "Gli altri lo digitano in Condividi ▸ Partecipa con un codice" +"Outline stroke" = "Contorna tratto" +"Outline text" = "Contorna testo" +"Outlines" = "Contorni" +"Outside" = "Fuori" +"Overflow" = "Overflow" +"Overlay" = "Sovrapposizione" +"Overlay position" = "Posizione della sovrapposizione" +"Overridden here (the collection it extends has another value)" = "Sovrascritto qui (la collezione che estende ha un altro valore)" +"Overrides" = "Sovrascritture" +"Owner token" = "Token del proprietario" +"Owner token copied" = "Token del proprietario copiato" +"PDF print options" = "Opzioni di stampa PDF" +"PDF print settings" = "Impostazioni di stampa PDF" +"Padding" = "Margine interno" +"Page" = "Pagina" +"Page deleted" = "Pagina eliminata" +"Pages" = "Pagine" +"Pages added to the document" = "Pagine aggiunte al documento" +"Placed on the current page" = "Posizionato nella pagina corrente" +"Pages: {pages}" = "Pagine: {pages}" +"Paint bucket" = "Secchiello" +"Pan the canvas" = "Sposta il canvas" +"Pan with nothing selected (Shift: farther)" = "Sposta la vista senza selezione (Maiusc: più lontano)" +"Panels" = "Pannelli" +"Paper" = "Carta" +"Paragraph indent" = "Rientro del paragrafo" +"Paragraph indent applies to left-aligned text" = "Il rientro del paragrafo si applica al testo allineato a sinistra" +"Paragraph spacing" = "Spaziatura dei paragrafi" +"Pass through" = "Passa attraverso" +"Paste a link (https://…)" = "Incolla un link (https://…)" +"Paste a team library link (a view link)" = "Incolla il link di una libreria del team (un link di visualizzazione)" +"Paste here" = "Incolla qui" +"Paste to replace" = "Incolla per sostituire" +"Pasted design layers; {n} kinds of content were left out" = { one = "Livelli di design incollati; {n} tipo di contenuto è stato escluso", other = "Livelli di design incollati; {n} tipi di contenuto sono stati esclusi" } +"Pattern" = "Motivo" +"Pattern alignment" = "Allineamento del motivo" +"Pattern scale" = "Scala del motivo" +"Pattern source" = "Sorgente del motivo" +"Pen" = "Penna" +"Pencil" = "Matita" +"People in this file" = "Persone in questo file" +"People join from anywhere with a code. The file is kept on {server} while you share it." = "Le persone partecipano da ovunque con un codice. Il file è conservato su {server} finché lo condividi." +"People on your network join with a code. Nothing goes through a server." = "Le persone nella tua rete partecipano con un codice. Niente passa da un server." +"Phone" = "Telefono" +"Pick color" = "Scegli colore" +"Pick color (I)" = "Scegli colore (I)" +"Pick one font family first" = "Scegli prima una famiglia di font" +"Pick the Frame tool, then drag on the canvas or choose a device size" = "Scegli lo strumento Frame, poi trascina sul canvas o scegli le dimensioni di un dispositivo" +"Pin a property" = "Fissa una proprietà" +"Pinch" = "Pizzico" +"Pixel grid" = "Griglia di pixel" +"Pixel preview 1x" = "Anteprima pixel 1x" +"Pixel preview 2x" = "Anteprima pixel 2x" +"Place a shape or frame (its tool chosen)" = "Inserisci una forma o un frame (con il suo strumento scelto)" +"Place automatically" = "Posiziona automaticamente" +"Place image" = "Inserisci immagine" +"Plugin" = "Plugin" +"Plugins run in FigCraft's sandbox, without files, network or clock. Install only plugins you trust." = "I plugin vengono eseguiti nella sandbox di FigCraft, senza file, rete o orologio. Installa solo plugin di cui ti fidi." +"Plugins" = "Plugin" +"Plugins & widgets" = "Plugin e widget" +"Plus darker" = "Più scuro" +"Plus lighter" = "Più chiaro" +"Point" = "Punto" +"Points" = "Punti" +"Polygon" = "Poligono" +"Polygons" = "Poligoni" +"Port" = "Porta" +"Position" = "Posizione" +"Post" = "Invia" +"Preferences" = "Preferenze" +"Preferred" = "Preferito" +"Present" = "Presenta" +"Present or preview" = "Presenta o anteprima" +"Present this flow" = "Presenta questo flusso" +"Presentation" = "Presentazione" +"Presentation slides" = "Slide di presentazione" +"Presenting: comments" = "In presentazione: commenti" +"Presenting: cycle the size options" = "In presentazione: scorri le opzioni di dimensione" +"Presenting: full screen" = "In presentazione: schermo intero" +"Presenting: next frame" = "In presentazione: frame successivo" +"Presenting: previous frame" = "In presentazione: frame precedente" +"Presenting: restart" = "In presentazione: ricomincia" +"Preserve scroll position" = "Mantieni la posizione di scorrimento" +"Presets" = "Preset" +"Press a key…" = "Premi un tasto…" +"Playground" = "Playground" +"Preview" = "Anteprima" +"Preview this flow" = "Anteprima di questo flusso" +"Preview this frame" = "Anteprima di questo frame" +"Previous frame" = "Frame precedente" +"Previous ready for dev" = "Precedente pronto per lo sviluppo" +"Progressive" = "Progressivo" +"Properties" = "Proprietà" +"Prototype" = "Prototipo" +"Prototype preview" = "Anteprima del prototipo" +"Prototype settings" = "Impostazioni del prototipo" +"Publish" = "Pubblica" +"Publish changes" = "Pubblica modifiche" +"Publish to another server…" = "Pubblica su un altro server…" +"Publish to server" = "Pubblica sul server" +"Published version {v}" = "Versione {v} pubblicata" +"Published version {v} to the server" = "Versione {v} pubblicata sul server" +"Published version {v}: save the file so other files can use it" = "Versione {v} pubblicata: salva il file perché altri file possano usarla" +"Published · {people}" = "Pubblicato · {people}" +"Publishing to a server needs the desktop app; in the browser you can join a server's files" = "Per pubblicare su un server serve l'app desktop; nel browser puoi unirti ai file di un server" +"Publishing with the server's key." = "Pubblicazione con la chiave del server." +"Publishing…" = "Pubblicazione…" +"Push" = "Invia" +"Push changes to main component" = "Invia le modifiche al componente principale" +"Put text on path" = "Metti il testo su tracciato" +"R" = "R" +"Radial" = "Radiale" +"Radius" = "Raggio" +"Ratio" = "Rapporto" +"Reading the library…" = "Lettura della libreria…" +"Ready for dev" = "Pronto per lo sviluppo" +"Real name" = "Nome reale" +"Recent" = "Recenti" +"Recent files" = "File recenti" +"Recents" = "Recenti" +"Recovered {n} unsaved files after the app quit unexpectedly: save them to keep them" = { one = "Recuperato un file non salvato dopo la chiusura imprevista dell'app: salvalo per conservarlo", other = "Recuperati {n} file non salvati dopo la chiusura imprevista dell'app: salvali per conservarli" } +"Rectangle" = "Rettangolo" +"Rectangles" = "Rettangoli" +"Red" = "Rosso" +"Reduce motion" = "Riduci animazioni" +"Refraction" = "Rifrazione" +"Regular expression" = "Espressione regolare" +"Relative to your system's display scaling (100% is the system's own size). Document zoom is separate." = "Relativo alla scala dello schermo del sistema (100% è la dimensione del sistema). Lo zoom del documento è separato." +"Remove" = "Rimuovi" +"Remove all" = "Rimuovi tutto" +"Remove annotation" = "Rimuovi annotazione" +"Remove auto layout" = "Rimuovi auto layout" +"Remove dev resource" = "Rimuovi risorsa per sviluppatori" +"Remove export setting" = "Rimuovi impostazione di esportazione" +"Remove from recents" = "Rimuovi dai recenti" +"Remove from server" = "Rimuovi dal server" +"Remove from server…" = "Rimuovi dal server…" +"Remove guide" = "Rimuovi guida" +"Remove interaction" = "Rimuovi interazione" +"Remove library from this file" = "Rimuovi la libreria da questo file" +"Remove min and max" = "Rimuovi minimo e massimo" +"Remove min and max height" = "Rimuovi altezza minima e massima" +"Remove min and max width" = "Rimuovi larghezza minima e massima" +"Remove regions" = "Rimuovi regioni" +"Remove slot" = "Rimuovi slot" +"Remove starting point" = "Rimuovi punto di partenza" +"Remove stop" = "Rimuovi stop" +"Remove text from path" = "Rimuovi il testo dal tracciato" +"Remove the else and its actions" = "Rimuovi \"Altrimenti\" e le sue azioni" +"Remove this condition and its actions" = "Rimuovi questa condizione e le sue azioni" +"Remove this file from the server? It can't be shared or opened from there again. Your copy stays open here." = "Rimuovere questo file dal server? Non potrà più essere condiviso né aperto da lì. La tua copia resta aperta qui." +"Remove {name} from session" = "Rimuovi {name} dalla sessione" +"Remove {name} from {server}?" = "Rimuovere {name} da {server}?" +"Removed" = "Rimosso" +"Removed from the server" = "Rimosso dal server" +"Removed since it was marked" = "Rimosso da quando è stato segnato" +"Rename" = "Rinomina" +"Rename duplicated layers" = "Rinomina i livelli duplicati" +"Rename folder" = "Rinomina cartella" +"Rename group" = "Rinomina gruppo" +"Rename layers" = "Rinomina livelli" +"Rename mode" = "Rinomina modalità" +"Rename on the server" = "Rinomina sul server" +"Rename on the server…" = "Rinomina sul server…" +"Rename page" = "Rinomina pagina" +"Rename to" = "Rinomina in" +"Reorder selection" = "Riordina selezione" +"Replace all" = "Sostituisci tutto" +"Replace mixed export settings" = "Sostituisci le impostazioni di esportazione miste" +"Replace with" = "Sostituisci con" +"Replace with a new interaction" = "Sostituisci con una nuova interazione" +"Reply" = "Rispondi" +"Report" = "Segnala" +"Report this file…" = "Segnala questo file…" +"Reset" = "Ripristina" +"Reset adjustments" = "Ripristina regolazioni" +"Reset all changes" = "Ripristina tutte le modifiche" +"Reset all overrides on this layer" = "Ripristina tutte le sovrascritture su questo livello" +"Reset overrides" = "Ripristina sovrascritture" +"Reset slot" = "Ripristina slot" +"Reset this override" = "Ripristina questa sovrascrittura" +"Reset to style" = "Ripristina allo stile" +"Reset to the inherited value" = "Ripristina al valore ereditato" +"Resize by the nudge amount" = "Ridimensiona dell'entità dello spostamento" +"Resize frames to the device's screen (or the window without a device): their constraints and auto layout respond" = "Ridimensiona i frame allo schermo del dispositivo (o alla finestra senza dispositivo): vincoli e auto layout reagiscono" +"Resize to fit" = "Ridimensiona per adattare" +"Resizing" = "Ridimensionamento" +"Resolve" = "Risolvi" +"Resolve change" = "Risolvi la modifica" +"Resolved" = "Risolto" +"Resources" = "Risorse" +"Responsive" = "Responsive" +"Restart" = "Riavvia" +"Restore" = "Ripristina" +"Restore main component" = "Ripristina componente principale" +"Restore this version" = "Ripristina questa versione" +"Restored “{name}”" = "“{name}” ripristinato" +"Reverse the order" = "Inverti l'ordine" +"Reversed triangle" = "Triangolo rovesciato" +"Review and merge" = "Rivedi e unisci" +"Ribbon" = "Nastro" +"Right" = "Destra" +"Right padding" = "Margine interno destro" +"Right side" = "Lato destro" +"Right side width" = "Larghezza del lato destro" +"Rotate 90°" = "Ruota di 90°" +"Rotate 90° right" = "Ruota di 90° a destra" +"Rotate to landscape" = "Ruota in orizzontale" +"Rotate to portrait" = "Ruota in verticale" +"Rotation" = "Rotazione" +"Rotation jitter" = "Variazione casuale della rotazione" +"Round" = "Arrotondato" +"Rounded" = "Arrotondato" +"Row" = "Riga" +"Row gap" = "Spaziatura righe" +"Row sizes" = "Dimensioni righe" +"Row span" = "Estensione righe" +"Rows" = "Righe" +"Rows {count}" = "Righe {count}" +"Rulers" = "Righelli" +"Run" = "Esegui" +"Run {label}" = "Esegui {label}" +"Save files automatically" = "Salva i file automaticamente" +"Search templates" = "Cerca modelli" +"See all templates" = "Vedi tutti i modelli" +"Select layers and they space themselves as their content changes" = "Seleziona dei livelli e si distanzieranno da soli al variare del contenuto" +"SHA-256: {sha}…" = "SHA-256: {sha}…" +"Same as layer" = "Come il livello" +"Saturation" = "Saturazione" +"Save" = "Salva" +"Save as" = "Salva come" +"Save as preset…" = "Salva come preset…" +"Save version {key}" = "Salva versione {key}" +"Saved" = "Salvato" +"Saved {name}" = "Salvato {name}" +"Say something" = "Scrivi qualcosa" +"Scale" = "Scala" +"Scatter" = "Dispersione" +"Scoping: where pickers offer it" = "Ambito: dove i selettori lo propongono" +"Screen" = "Schermo" +"Scroll behavior" = "Comportamento di scorrimento" +"Scroll to" = "Scorri fino a" +"Scroll to layer" = "Scorri fino al livello" +"Scroll with parent" = "Scorri con il genitore" +"Search plugins" = "Cerca plugin" +"Search" = "Cerca" +"Search actions, layers and tools" = "Cerca azioni, livelli e strumenti" +"Search assets" = "Cerca asset" +"Search comments" = "Cerca commenti" +"Search files and templates" = "Cerca file e modelli" +"Search results" = "Risultati della ricerca" +"Search fonts" = "Cerca font" +"Search layers" = "Cerca livelli" +"Search pages" = "Cerca pagine" +"Search shortcuts" = "Cerca scorciatoie" +"Section" = "Sezione" +"Section color" = "Colore della sezione" +"Sections" = "Sezioni" +"Select a frame and add a starting point to present it as a flow." = "Seleziona un frame e aggiungi un punto di partenza per presentarlo come flusso." +"Select a frame to preview it" = "Seleziona un frame per vederne l'anteprima" +"Select a layer to see its properties and code" = "Seleziona un livello per vederne proprietà e codice" +"Select all" = "Seleziona tutto" +"Select all with same" = "Seleziona tutto con lo stesso" +"Select layer" = "Seleziona livello" +"Select layers to apply the style to" = "Seleziona i livelli a cui applicare lo stile" +"Select layers with this color" = "Seleziona i livelli con questo colore" +"Select overlapping shapes" = "Seleziona forme sovrapposte" +"Select points to edit their position, radius and mirroring" = "Seleziona dei punti per modificarne posizione, raggio e riflessione" +"Select slot" = "Seleziona slot" +"Selection colors" = "Colori della selezione" +"Selection" = "Selezione" +"Selection box, {w} by {h} at {x}, {y}: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Riquadro di selezione, {w} per {h} a {x}, {y}: le frecce lo spostano, Maiusc+frecce o {cmd}+frecce lo ridimensionano, Invio seleziona, Esc annulla" +"Selection box: arrows move it, Shift+arrows or {cmd}+arrows size it, Enter selects, Escape cancels" = "Riquadro di selezione: le frecce lo spostano, Maiusc+frecce o {cmd}+frecce lo ridimensionano, Invio seleziona, Esc annulla" +"Send backward" = "Porta indietro" +"Send report" = "Invia segnalazione" +"Send to back" = "Porta in fondo" +"Sep" = "set" +"Server address (ws://host:47900/)" = "Indirizzo del server (ws://host:47900/)" +"Server address" = "Indirizzo del server" +"Server for online sharing" = "Server per la condivisione online" +"Set as default" = "Imposta come predefinito" +"Set variable" = "Imposta variabile" +"Set variable mode" = "Imposta modalità della variabile" +"Set variable {name}" = "Imposta variabile {name}" +"Set {name} to {value}" = "Imposta {name} su {value}" +"Shadows" = "Ombre" +"Shape" = "Forma" +"Shape builder" = "Generatore di forme" +"Shape tools" = "Strumenti forma" +"Share" = "Condividi" +"Share this file" = "Condividi questo file" +"Share with a code" = "Condividi con un codice" +"Share: live session, export" = "Condividi: sessione live, esportazione" +"Shared" = "Condiviso" +"Shared: copy its code from the file's menu" = "Condiviso: copia il suo codice dal menu del file" +"Sharing a file needs the desktop app; here you can join one." = "Per condividere un file serve l'app desktop; qui puoi partecipare a uno." +"Sharing on the local network (online sharing is off: no server is set in Preferences)." = "Condivisione sulla rete locale (la condivisione online è disattivata: nelle Preferenze non è impostato alcun server)." +"Sharing on your network · {people}" = "Condivisione sulla tua rete · {people}" +"Sharing online · {people}" = "Condivisione online · {people}" +"Sharing stopped: the file stays on the server, open only to you" = "Condivisione interrotta: il file resta sul server, aperto solo a te" +"Show" = "Mostra" +"Show all templates" = "Mostra tutti i modelli" +"Show as grid" = "Mostra come griglia" +"Show as list" = "Mostra come elenco" +"Show autosaves" = "Mostra salvataggi automatici" +"Show behind transparent areas" = "Mostra dietro le aree trasparenti" +"Show comments (Shift+C)" = "Mostra commenti (Maiusc+C)" +"Show device frame" = "Mostra cornice del dispositivo" +"Show dimensions on objects" = "Mostra dimensioni sugli oggetti" +"Show hotspot hints" = "Mostra suggerimenti sulle aree interattive" +"Show in Explorer" = "Mostra in Esplora risorse" +"Show in Finder" = "Mostra nel Finder" +"Show in folder" = "Mostra nella cartella" +"Show only this on the canvas" = "Mostra solo questo sul canvas" +"Show open" = "Mostra aperti" +"Show section contents" = "Mostra il contenuto della sezione" +"Show this layer" = "Mostra questo livello" +"Show usage" = "Mostra utilizzo" +"Show/Hide" = "Mostra/Nascondi" +"Show/Hide UI" = "Mostra/Nascondi interfaccia" +"Side by side" = "Affiancati" +"Sidebar" = "Barra laterale" +"Simplify stroke" = "Semplifica tratto" +"Size" = "Dimensione" +"Size jitter" = "Variazione casuale della dimensione" +"Skip ink" = "Evita i discendenti" +"Slashed zero" = "Zero barrato" +"Slice" = "Slice" +"Slices" = "Slice" +"Slide in" = "Scorrimento in entrata" +"Slide out" = "Scorrimento in uscita" +"Slot" = "Slot" +"Slot content" = "Contenuto dello slot" +"Small capitals" = "Maiuscoletto" +"Small caps" = "Maiuscoletto" +"Smart animate" = "Animazione intelligente" +"Snap to pixel grid" = "Aggancia alla griglia di pixel" +"Social and slides" = "Social e slide" +"Social media" = "Social media" +"Social post sizes" = "Dimensioni dei post social" +"Soft light" = "Luce soffusa" +"Solid" = "Pieno" +"Some variants have the same property values: give each one a unique combination" = "Alcune varianti hanno gli stessi valori di proprietà: assegna a ciascuna una combinazione unica" +"Sort" = "Ordina" +"Sort by" = "Ordina per" +"Sort by date" = "Ordina per data" +"Sort by page" = "Ordina per pagina" +"Sort by {column}" = "Ordina per {column}" +"Space around" = "Spazio attorno" +"Space between" = "Spazio tra" +"Space drag" = "Trascinamento con Spazio" +"Space evenly" = "Spazio uniforme" +"Space+drag" = "Spazio+trascina" +"Spacing" = "Spaziatura" +"Spam, stolen work, abuse…" = "Spam, lavoro rubato, abusi…" +"Splay" = "Svasatura" +"Spotlight me" = "Metti me in evidenza" +"Spread" = "Diffusione" +"Square" = "Quadrato" +"Standard" = "Standard" +"Standard ligatures" = "Legature standard" +"Star" = "Stella" +"Stars" = "Stelle" +"Start" = "Inizio" +"Start ascending from / stop descending at" = "Inizia a salire da / smetti di scendere a" +"Start from a blank canvas" = "Inizia da un canvas vuoto" +"Start from a template" = "Inizia da un modello" +"Start on path" = "Inizio sul tracciato" +"Start on the Home screen" = "Avvia dalla schermata Home" +"Start point" = "Punto iniziale" +"Start sharing" = "Inizia a condividere" +"Start sharing with the Share button, then pass on the code it gives" = "Inizia a condividere con il pulsante Condividi, poi passa il codice che ti dà" +"Starting to share…" = "Avvio della condivisione…" +"Status" = "Stato" +"Sticky" = "Sticky" +"Stop" = "Interrompi" +"Stop following {name}" = "Smetti di seguire {name}" +"Stop session" = "Termina sessione" +"Stop sharing" = "Interrompi condivisione" +"Stop spotlighting" = "Smetti di mettere in evidenza" +"Stop {n} {hex}" = "Stop {n} {hex}" +"Stretch" = "Allunga" +"Strikethrough" = "Barrato" +"String" = "Stringa" +"Stroke" = "Contorno" +"Stroke style" = "Stile del contorno" +"Stroke weight" = "Spessore del contorno" +"Strokes" = "Contorni" +"Style" = "Stile" +"Styles" = "Stili" +"Stylistic set 1" = "Set stilistico 1" +"Stylistic set 2" = "Set stilistico 2" +"Stylistic set 3" = "Set stilistico 3" +"Stylistic set 4" = "Set stilistico 4" +"Stylistic set 5" = "Set stilistico 5" +"Subtract" = "Sottrai" +"Subtracts" = "Sottrae" +"Suffix" = "Suffisso" +"Suspend snapping while dragging" = "Sospendi l'aggancio durante il trascinamento" +"Swap instance" = "Scambia istanza" +"Swap library: use another library's assets of the same names" = "Scambia libreria: usa gli asset con gli stessi nomi di un'altra libreria" +"Swap orientation" = "Scambia orientamento" +"Swap overlay" = "Scambia sovrapposizione" +"Swap to {name}" = "Scambia con {name}" +"Swap width and height" = "Scambia larghezza e altezza" +"Swapped instance" = "Istanza scambiata" +"Swapped {n} assets" = { one = "Scambiato {n} asset", other = "Scambiati {n} asset" } +"Swapped {n} assets; {missed} had no match and still use the old library" = { one = "Scambiato {n} asset; mancate corrispondenze: {missed}, che usano ancora la vecchia libreria", other = "Scambiati {n} asset; mancate corrispondenze: {missed}, che usano ancora la vecchia libreria" } +"Sweep" = "Ampiezza" +"System" = "Sistema" +"Tab / Shift+Tab" = "Tab / Maiusc+Tab" +"Table" = "Tabella" +"Tablet" = "Tablet" +"Tabular numbers" = "Numeri tabulari" +"Take the values from the first selected layer" = "Prendi i valori dal primo livello selezionato" +"Taper" = "Rastremazione" +"Taper both ends" = "Rastrema entrambe le estremità" +"Tell the server's operator what is wrong with it" = "Spiega a chi gestisce il server cosa c'è che non va" +"Temperature" = "Temperatura" +"Template" = "Modello" +"Templates" = "Modelli" +"Terms" = "Termini" +"Text" = "Testo" +"Text align" = "Allineamento del testo" +"Text content" = "Contenuto del testo" +"Text style" = "Stile di testo" +"Text styles" = "Stili di testo" +"Texts" = "Testi" +"Texture" = "Texture" +"The canvas previews colors in sRGB; exported PNG, JPG and WebP files are tagged Display P3." = "Il canvas mostra i colori in sRGB; i file PNG, JPG e WebP esportati sono contrassegnati come Display P3." +"The destination document is unavailable. Nothing was imported." = "Il documento di destinazione non è disponibile. Non è stato importato nulla." +"The destination document was closed. Nothing was imported." = "Il documento di destinazione è stato chiuso. Non è stato importato nulla." +"The destination page was removed. Nothing was imported." = "La pagina di destinazione è stata rimossa. Non è stato importato nulla." +"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." = "Il file viene eliminato dal server: nessuno potrà più aprirlo o parteciparvi da lì, e l'operazione non può essere annullata. Le copie aperte restano a chi le ha." +"The file now holds another library" = "Il file contiene ora un'altra libreria" +"The file now holds another library (\"{name}\")" = "Il file contiene ora un'altra libreria (\"{name}\")" +"The files you shared or published online from this computer. They stay on their server until you remove them." = "I file che hai condiviso o pubblicato online da questo computer. Restano sul loro server finché non li rimuovi." +"The link carries the session's secret: share it only with people you invite." = "Il link contiene il segreto della sessione: condividilo solo con le persone che inviti." +"The name in your online files and on the server. The document inside keeps its own name." = "Il nome nei tuoi file online e sul server. Il documento al suo interno mantiene il proprio nome." +"The selection uses several text styles: click to apply one" = "La selezione usa più stili di testo: clicca per applicarne uno" +"The server deletes this file if nobody opens it before {date}." = "Il server elimina questo file se nessuno lo apre prima del {date}." +"The whole file" = "L'intero file" +"The window is too small for {size}%, so the interface is drawn at {drawn}%. Make the window larger to use {size}%." = "La finestra è troppo piccola per {size}%, quindi l'interfaccia è disegnata al {drawn}%. Ingrandisci la finestra per usare {size}%." +"Theme" = "Tema" +"There's no destination for that action" = "Non c'è nessuna destinazione per quell'azione" +"Thickness" = "Spessore" +"This branch can't be reviewed: {e}" = "Questo ramo non può essere rivisto: {e}" +"This branch was merged or archived." = "Questo ramo è stato unito o archiviato." +"This computer only" = "Solo questo computer" +"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." = "Questo computer ricorda il token del proprietario, quindi il file può essere gestito dall'elenco Online della schermata Home. Copialo per gestire il file da un altro posto: è una password per questo file e non verrà mostrato di nuovo." +"This condition is edited as an expression" = "Questa condizione viene modificata come espressione" +"This file" = "Questo file" +"This file has unsaved changes" = "Questo file ha modifiche non salvate" +"This file is too large to join here (up to {mb} MB)" = "Questo file è troppo grande per parteciparvi da qui (fino a {mb} MB)" +"This file uses fonts that aren't installed on this computer. Their text is drawn in a fallback font until you install them." = "Questo file usa font che non sono installati su questo computer. Il loro testo è disegnato con un font di riserva finché non li installi." +"This file is on {server}, open only to you. Share it to let others in with a new code." = "Questo file è su {server}, aperto solo a te. Condividilo per far entrare altri con un nuovo codice." +"This frame is new on the branch." = "Questo frame è nuovo nel ramo." +"This frame isn't on the branch." = "Questo frame non è nel ramo." +"This server is open to the public: no key needed." = "Questo server è aperto al pubblico: non serve nessuna chiave." +"This server needs its key: add ?key=… to the address (`figcraft-server key` prints it on the server's machine)." = "Questo server richiede la sua chiave: aggiungi ?key=… all'indirizzo (`figcraft-server key` la stampa sul computer del server)." +"This server takes files only with its key: see Advanced." = "Questo server accetta file solo con la sua chiave: vedi Avanzate." +"Tidy up" = "Riordina" +"Tile" = "Affianca" +"Tile type" = "Tipo di affiancamento" +"Tint" = "Tinta" +"Title" = "Titolo" +"Title case" = "Iniziali maiuscole" +"To share this library with your team, publish the file to a server (Share ▸ Publish to server…), then publish the library there." = "Per condividere questa libreria con il tuo team, pubblica il file su un server (Condividi ▸ Pubblica sul server…), poi pubblica lì la libreria." +"Toggle" = "Attiva/Disattiva" +"Toggle light/dark theme" = "Passa dal tema chiaro a quello scuro" +"Toggle visibility" = "Attiva/Disattiva visibilità" +"Tools" = "Strumenti" +"Top" = "In alto" +"Top & bottom" = "In alto e in basso" +"Top left radius" = "Raggio in alto a sinistra" +"Top padding" = "Margine interno superiore" +"Top right radius" = "Raggio in alto a destra" +"Transform" = "Trasforma" +"Triangle arrow" = "Freccia triangolare" +"Trigger" = "Trigger" +"True" = "Vero" +"Truncate text" = "Tronca testo" +"Truncate with an ellipsis" = "Tronca con i puntini di sospensione" +"Try again" = "Riprova" +"Try another search or filter." = "Prova un'altra ricerca o un altro filtro." +"Try another search, or browse templates." = "Prova un'altra ricerca, oppure sfoglia i modelli." +"Try another word, or look at every kind." = "Prova un'altra parola, oppure guarda tutti i tipi." +"Turn a layer into a component, reuse it, and change every copy at once" = "Trasforma un livello in componente, riutilizzalo e cambia tutte le copie in una volta" +"Two digits" = "Due cifre" +"Type" = "Tipo" +"Type settings" = "Impostazioni tipografiche" +"Type the join code you were given, or paste an invite link." = "Digita il codice di partecipazione che ti è stato dato, oppure incolla un link di invito." +"Typography" = "Tipografia" +"Underline" = "Sottolineato" +"Undo takes back only your own changes; what others changed since stays." = "Annulla ripristina solo le tue modifiche; quelle fatte da altri nel frattempo restano." +"Ungroup" = "Separa" +"Uniform" = "Uniforme" +"Union" = "Unione" +"Unions" = "Unioni" +"Unknown — treated as sRGB" = "Sconosciuto — trattato come sRGB" +"Unread" = "Non letto" +"Unresolve" = "Riapri" +"Unsaved changes" = "Modifiche non salvate" +"Unsaved changes in {names}. Close the window and lose them?" = "Modifiche non salvate in {names}. Chiudere la finestra e perderle?" +"Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "Modifiche non salvate: salva prima ({save}), oppure chiudi la scheda con {alt}+clic per scartarle" +"Unsaved changes: save first, or close again with {key} to discard" = "Modifiche non salvate: salva prima, oppure chiudi di nuovo con {key} per scartarle" +"Up" = "Su" +"Update plugin" = "Aggiorna plugin" +"Update" = "Aggiorna" +"Update from main" = "Aggiorna da main" +"Update from selection" = "Aggiorna dalla selezione" +"Updated {components} and {styles}" = "Aggiornati {components} e {styles}" +"Updated {name}" = "Aggiornato {name}" +"Updating the graphics driver often helps. To use OpenGL only, start FigCraft from a Command Prompt with:" = "Aggiornare il driver grafico spesso aiuta. Per usare solo OpenGL, avvia FigCraft dal Prompt dei comandi con:" +"Updating the graphics driver often helps. WGPU_BACKEND (vulkan, gl, metal) names another graphics API to try." = "Aggiornare il driver grafico spesso aiuta. WGPU_BACKEND (vulkan, gl, metal) indica un'altra API grafica da provare." +"Uploads this file to a FigCraft server and keeps this tab in sync with it. You get a link to edit and a link to view." = "Carica questo file su un server FigCraft e mantiene questa scheda sincronizzata con esso. Ricevi un link per modificare e un link per visualizzare." +"Uppercase" = "Maiuscolo" +"Use a fixed value" = "Usa un valore fisso" +"Use an expression, e.g. count + 1" = "Usa un'espressione, ad es. count + 1" +"Use as mask" = "Usa come maschera" +"Use auto layout" = "Usa auto layout" +"Use number keys for opacity" = "Usa i tasti numerici per l'opacità" +"Use the .figcraft or .fig extension." = "Usa l'estensione .figcraft o .fig." +"Use the layer's fill" = "Usa il riempimento del livello" +"Use your browser's zoom to change the interface size." = "Usa lo zoom del browser per cambiare la dimensione dell'interfaccia." +"Used in {files} · {instances}" = "Usato in {files} · {instances}" +"Username" = "Nome utente" +"Value" = "Valore" +"Uses variables: each layer shows them in its own mode" = "Usa variabili: ogni livello le mostra nella propria modalità" +"Variable" = "Variabile" +"Variable collections" = "Collezioni di variabili" +"Variable modes" = "Modalità delle variabili" +"Variable width" = "Larghezza variabile" +"Variables" = "Variabili" +"Variables JSON…" = "JSON delle variabili…" +"Variables store reusable values (colours, numbers, strings, booleans) with modes such as Light and Dark." = "Le variabili memorizzano valori riutilizzabili (colori, numeri, stringhe, booleani) con modalità come Chiaro e Scuro." +"Variant" = "Variante" +"Vector" = "Vettore" +"Vectors" = "Vettori" +"Version" = "Versione" +"Version history" = "Cronologia versioni" +"Version {version}" = "Versione {version}" +"Version {v} · {components} · {styles} · {variables}" = "Versione {v} · {components} · {styles} · {variables}" +"Vertical" = "Verticale" +"Vertical align" = "Allineamento verticale" +"Vertical hex" = "Esagono verticale" +"Vertical padding" = "Margine interno verticale" +"Vertical spacing" = "Spaziatura verticale" +"Vertical trim" = "Rifilatura verticale" +"View" = "Vista" +"View link" = "Link di visualizzazione" +"View link copied" = "Link di visualizzazione copiato" +"View only" = "Sola visualizzazione" +"View only: you see every change as it happens but can't edit. Leave to keep a copy you can edit." = "Sola visualizzazione: vedi ogni modifica mentre avviene ma non puoi modificare. Esci per conservare una copia modificabile." +"Viewing “{name}”" = "Visualizzazione di “{name}”" +"Violet" = "Viola" +"Visibility" = "Visibilità" +"Visible" = "Visibile" +"Vivid" = "Vivido" +"WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose." = "WGPU_BACKEND è impostata e indica l'API grafica da usare; rimuovendola FigCraft sceglie da sé." +"Warm" = "Caldo" +"Watch" = "Osserva" +"Wavy" = "Ondulato" +"Web" = "Web" +"Websites" = "Siti web" +"WebSocket URL" = "URL WebSocket" +"WebSocket URL copied" = "URL WebSocket copiato" +"Wedge" = "Spicchio" +"Weight" = "Peso" +"What you try here changes this preview and the code below, not the file" = "Ciò che provi qui cambia questa anteprima e il codice sotto, non il file" +"What changed since it was marked ready for dev" = "Cosa è cambiato da quando è stato segnato come pronto per lo sviluppo" +"What is wrong with this file? The server's operator gets your report." = "Cosa c'è che non va in questo file? Chi gestisce il server riceve la tua segnalazione." +"What it's for" = "A cosa serve" +"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." = "Non si sa da cosa sia cambiato: è cambiato prima che FigCraft conservasse una copia dei frame pronti per lo sviluppo. Risolvi la modifica (o segnalo di nuovo come pronto) per confrontare da ora in poi." +"While hovering" = "Durante il passaggio del mouse" +"While pressing" = "Durante la pressione" +"White" = "Bianco" +"Who runs it, its terms and how long it keeps files" = "Chi lo gestisce, i suoi termini e per quanto tempo conserva i file" +"Why it changed" = "Perché è cambiato" +"Widget" = "Widget" +"Widgets" = "Widget" +"Width" = "Larghezza" +"Width point" = "Punto di larghezza" +"Width profile" = "Profilo di larghezza" +"Wireframe kit" = "Kit wireframe" +"With the layers' export settings" = "Con le impostazioni di esportazione dei livelli" +"Wrap" = "A capo" +"Wrap in new section" = "Racchiudi in una nuova sezione" +"Yellow" = "Giallo" +"Yesterday" = "Ieri" +"You" = "Tu" +"You'll get the shared file as a new tab and edit it together." = "Riceverai il file condiviso in una nuova scheda e lo modificherete insieme." +"You're spotlighting: everyone follows your view" = "Stai mettendo in evidenza: tutti seguono la tua vista" +"Your copy of the file stays open" = "La tua copia del file resta aperta" +"Your copy stays open; the file stays on the server" = "La tua copia resta aperta; il file resta sul server" +"Your name" = "Il tuo nome" +"Your name in comments" = "Il tuo nome nei commenti" +"Your system asks for reduced motion: animations stay off." = "Il tuo sistema richiede movimento ridotto: le animazioni restano disattivate." +"Z" = "Z" +"Z to A" = "Dalla Z alla A" +"Zoom" = "Zoom" +"Zoom around the pointer" = "Zoom attorno al puntatore" +"Zoom device to fill window" = "Ingrandisci il dispositivo per riempire la finestra" +"Zoom in" = "Ingrandisci" +"Zoom out" = "Riduci" +"Zoom to 100%" = "Zoom al 100%" +"Zoom to 200%" = "Zoom al 200%" +"Zoom to 50%" = "Zoom al 50%" +"Zoom to fit" = "Zoom per adattare" +"Zoom to selection" = "Zoom sulla selezione" +"Zoom tool: click to zoom in, drag an area" = "Strumento zoom: clicca per ingrandire, trascina un'area" +"Zoom tool: zoom out" = "Strumento zoom: riduci" +"and {n} more" = { one = "e altro {n}", other = "e altri {n}" } +"bleed marks" = "crocini di abbondanza" +"by {author}" = "di {author}" +"call any command its access allows" = "chiama qualsiasi comando consentito dal suo accesso" +"call only: {commands}" = "chiama solo: {commands}" +"can edit the document" = "può modificare il documento" +"conditional prototype actions nested too deeply were skipped" = "le azioni condizionali del prototipo annidate troppo in profondità sono state saltate" +"conditional prototype actions with conditions that can't be evaluated were skipped" = "le azioni condizionali del prototipo con condizioni non valutabili sono state saltate" +"connector label background was skipped: style data is missing" = "lo sfondo dell'etichetta del connettore è stato saltato: mancano i dati di stile" +"connector label was skipped: background bounds are invalid" = "l'etichetta del connettore è stata saltata: i limiti dello sfondo non sono validi" +"connector label was skipped: background transform is invalid" = "l'etichetta del connettore è stata saltata: la trasformazione dello sfondo non è valida" +"connector label was skipped: composed text transform is invalid" = "l'etichetta del connettore è stata saltata: la trasformazione del testo composto non è valida" +"connector label was skipped: derived background bounds are missing" = "l'etichetta del connettore è stata saltata: mancano i limiti dello sfondo derivati" +"connector label was skipped: derived text bounds are missing" = "l'etichetta del connettore è stata saltata: mancano i limiti del testo derivati" +"connector label was skipped: text bounds are invalid" = "l'etichetta del connettore è stata saltata: i limiti del testo non sono validi" +"connector label was skipped: text style data is missing" = "l'etichetta del connettore è stata saltata: mancano i dati di stile del testo" +"connector label was skipped: text transform is invalid" = "l'etichetta del connettore è stata saltata: la trasformazione del testo non è valida" +"connector labels were skipped" = "le etichette dei connettori sono state saltate" +"connector layers import as static frame/vector snapshots with labels when present; live endpoint bindings are not retained" = "i livelli connettore vengono importati come istantanee statiche di frame/vettori con le etichette, se presenti; i collegamenti dinamici alle estremità non vengono mantenuti" +"connector snapshot vector data is missing; connector was left empty" = "mancano i dati vettoriali dell'istantanea del connettore; il connettore è stato lasciato vuoto" +"connectors whose ends can't be placed are empty" = "i connettori le cui estremità non possono essere posizionate sono vuoti" +"crop marks" = "crocini di taglio" +"damaged vector geometry was skipped" = "la geometria vettoriale danneggiata è stata saltata" +"damaged vector networks import as outlines" = "le reti vettoriali danneggiate vengono importate come contorni" +"development: {path}" = "sviluppo: {path}" +"document children other than pages were skipped" = "i figli del documento diversi dalle pagine sono stati saltati" +"duplicate grid track sizing entries use equal tracks" = "le voci duplicate di dimensionamento delle tracce della griglia usano tracce uguali" +"e.g. count + 1" = "ad es. count + 1" +"e.g. count >= 3 and not done" = "ad es. count >= 3 and not done" +"elbowed and curved connectors without a saved path import as straight lines" = "i connettori ad angolo e curvi senza un tracciato salvato vengono importati come linee rette" +"entries without a node type were skipped" = "le voci senza tipo di nodo sono state saltate" +"figcraft:// links from a browser, chat or email open this app and join the session." = "i link figcraft:// da un browser, una chat o un'email aprono questa app e partecipano alla sessione." +"filled areas of a vector with their own fill use the layer's fill" = "le aree riempite di un vettore con riempimento proprio usano il riempimento del livello" +"from {server}" = "da {server}" +"gamepad triggers without exactly one known controller button were skipped" = "i trigger del gamepad senza esattamente un pulsante noto del controller sono stati saltati" +"grid track list exceeds 1000 entries; the tracks use the default equal sizing" = "l'elenco delle tracce della griglia supera 1000 voci; le tracce usano il dimensionamento uniforme predefinito" +"grid track sizing is unsupported or malformed; affected tracks use equal sizing" = "il dimensionamento delle tracce della griglia non è supportato o è malformato; le tracce interessate usano dimensioni uguali" +"grid track sizing list exceeds 1000 entries; the tracks use the default equal sizing" = "l'elenco di dimensionamento delle tracce della griglia supera 1000 voci; le tracce usano il dimensionamento uniforme predefinito" +"hidden" = "nascosto" +"iOS" = "iOS" +"image filters are ambiguous because both filter containers are present; filter conversion was skipped" = "i filtri immagine sono ambigui perché sono presenti entrambi i contenitori di filtri; la conversione dei filtri è stata saltata" +"image filters use FigCraft's approximate renderer" = "i filtri immagine usano il renderer approssimato di FigCraft" +"instance swap properties whose default component isn't in the file were skipped" = "le proprietà di scambio istanza il cui componente predefinito non è nel file sono state saltate" +"instances inside their own main component were imported as frames" = "le istanze all'interno del proprio componente principale sono state importate come frame" +"instances of components that aren't in the file show no layers" = "le istanze di componenti non presenti nel file non mostrano livelli" +"instances of components that aren't in the file were imported as frames" = "le istanze di componenti non presenti nel file sono state importate come frame" +"instances that would nest a component inside itself were imported as frames" = "le istanze che anniderebbero un componente dentro se stesso sono state importate come frame" +"instances without a main component were imported as empty frames" = "le istanze senza componente principale sono state importate come frame vuoti" +"invalid internal .fig text path data was ignored" = "i dati interni non validi del tracciato di testo .fig sono stati ignorati" +"invalid or oversized vector networks import as outlines" = "le reti vettoriali non valide o troppo grandi vengono importate come contorni" +"invalid or oversized vector path data was skipped" = "i dati di tracciato vettoriale non validi o troppo grandi sono stati saltati" +"just now" = "adesso" +"key triggers without a supported key were skipped" = "i trigger da tasto senza un tasto supportato sono stati saltati" +"key up triggers import as key presses" = "i trigger di rilascio del tasto vengono importati come pressioni del tasto" +"layers without a position were placed at their parent's origin" = "i livelli senza posizione sono stati posti all'origine del loro genitore" +"layers without a type were skipped" = "i livelli senza tipo sono stati saltati" +"legacy image filter container is unsupported; filter conversion was skipped" = "il contenitore di filtri immagine legacy non è supportato; la conversione dei filtri è stata saltata" +"locked" = "bloccato" +"malformed image filters container was skipped" = "il contenitore di filtri immagine malformato è stato saltato" +"malformed image paintFilter container was skipped" = "il contenitore paintFilter dell'immagine malformato è stato saltato" +"malformed slot content assignment was skipped" = "l'assegnazione di contenuto dello slot malformata è stata saltata" +"no bleed" = "nessuna abbondanza" +"nobody else here yet" = "ancora nessun altro qui" +"non-default slot settings are not represented and were skipped" = "le impostazioni dello slot non predefinite non sono rappresentate e sono state saltate" +"now" = "ora" +"only in a text style or variable" = "solo in uno stile di testo o in una variabile" +"or" = "oppure" +"point stroke joins were preserved; dashed or variable-width strokes render with the layer join" = "le giunzioni dei punti del contorno sono state mantenute; i contorni tratteggiati o di larghezza variabile vengono resi con la giunzione del livello" +"preferred slot component keys cannot be resolved to file nodes and were skipped" = "le chiavi dei componenti preferiti dello slot non possono essere risolte in nodi del file e sono state saltate" +"prototype actions of unknown types were skipped" = "le azioni del prototipo di tipo sconosciuto sono state saltate" +"prototype actions without a destination in the file were skipped" = "le azioni del prototipo senza una destinazione nel file sono state saltate" +"prototype expressions on variables that aren't in the variables JSON were skipped" = "le espressioni del prototipo su variabili non presenti nel JSON delle variabili sono state saltate" +"prototype expressions with unsupported functions were skipped" = "le espressioni del prototipo con funzioni non supportate sono state saltate" +"prototype interactions without supported actions were skipped" = "le interazioni del prototipo senza azioni supportate sono state saltate" +"prototype navigations of unknown types were skipped" = "le navigazioni del prototipo di tipo sconosciuto sono state saltate" +"prototype transitions of unknown types play instantly" = "le transizioni del prototipo di tipo sconosciuto vengono eseguite istantaneamente" +"prototype triggers of unknown types were skipped" = "i trigger del prototipo di tipo sconosciuto sono stati saltati" +"put a widget on the canvas" = "inserire un widget sul canvas" +"read and edit the document" = "leggere e modificare il documento" +"read the document" = "leggere il documento" +"reads the document" = "legge il documento" +"set-variable actions on variables that aren't in the variables JSON were skipped" = "le azioni \"imposta variabile\" su variabili non presenti nel JSON delle variabili sono state saltate" +"set-variable actions with values that can't be evaluated were skipped" = "le azioni \"imposta variabile\" con valori non valutabili sono state saltate" +"set-variable actions with values that don't fit the variable were skipped" = "le azioni \"imposta variabile\" con valori non adatti alla variabile sono state saltate" +"set-variable actions without a value were skipped" = "le azioni \"imposta variabile\" senza valore sono state saltate" +"set-variable-mode actions on collections or modes that aren't in the variables JSON were skipped" = "le azioni \"imposta modalità variabile\" su collezioni o modalità non presenti nel JSON delle variabili sono state saltate" +"slot content assignment target is missing or is not slot content" = "la destinazione dell'assegnazione del contenuto dello slot manca o non è un contenuto di slot" +"slot content assignment uses the empty GUID sentinel and was skipped" = "l'assegnazione del contenuto dello slot usa il GUID sentinella vuoto ed è stata saltata" +"slot layers without a matching slot property were imported as frames" = "i livelli slot senza una proprietà slot corrispondente sono stati importati come frame" +"some vector points refer to point settings missing from the file; they use the layer's joins and plain corners" = "alcuni punti vettoriali fanno riferimento a impostazioni di punto mancanti nel file; usano le giunzioni del livello e angoli semplici" +"some vector segments refer to style settings missing from the file; they use the layer's stroke" = "alcuni segmenti vettoriali fanno riferimento a impostazioni di stile mancanti nel file; usano il contorno del livello" +"stale or invalid grid track sizing entries were ignored" = "le voci di dimensionamento delle tracce della griglia obsolete o non valide sono state ignorate" +"stars and polygons without point counts or geometry use the default point count" = "stelle e poligoni senza numero di punte o geometria usano il numero di punte predefinito" +"style settings of some vector segments aren't supported; those segments use the layer's stroke" = "le impostazioni di stile di alcuni segmenti vettoriali non sono supportate; quei segmenti usano il contorno del livello" +"text path underline or strikethrough decoration is not rendered" = "la sottolineatura o il barrato del testo su tracciato non viene reso" +"text style overrides refer to missing table entries" = "le sovrascritture dello stile di testo fanno riferimento a voci di tabella mancanti" +"the file has no pages" = "il file non ha pagine" +"the plugin" = "il plugin" +"the variables JSON has no `variableCollections` (expected the local variables response) and was ignored" = "il JSON delle variabili non ha `variableCollections` (era attesa la risposta delle variabili locali) ed è stato ignorato" +"this session only" = "solo questa sessione" +"unknown component property types were skipped" = "i tipi di proprietà del componente sconosciuti sono stati saltati" +"variable aliases to variables that aren't in the variables JSON (or of another type) were skipped" = "gli alias di variabile verso variabili non presenti nel JSON delle variabili (o di un altro tipo) sono stati saltati" +"variable bindings to properties the layer doesn't have were skipped" = "i collegamenti di variabile a proprietà che il livello non possiede sono stati saltati" +"variable bindings to variables that aren't in the variables JSON (library variables) were skipped" = "i collegamenti di variabile a variabili non presenti nel JSON delle variabili (variabili di libreria) sono stati saltati" +"variable bindings were skipped: the file JSON has no variable definitions (pass the local variables JSON as `variables`, or embed its `meta` next to the file JSON)" = "i collegamenti di variabile sono stati saltati: il JSON del file non ha definizioni di variabili (passa il JSON delle variabili locali come `variables`, oppure incorpora il suo `meta` accanto al JSON del file)" +"variable bindings whose variable type doesn't fit the property were skipped" = "i collegamenti di variabile il cui tipo non è adatto alla proprietà sono stati saltati" +"variable collections without modes were skipped" = "le collezioni di variabili senza modalità sono state saltate" +"variable mode lookups in prototype expressions that aren't a variable and one of its modes were skipped" = "le ricerche di modalità di variabile nelle espressioni del prototipo che non sono una variabile e una delle sue modalità sono state saltate" +"variable modes of collections that aren't in the file were skipped" = "le modalità di variabile di collezioni non presenti nel file sono state saltate" +"variable modes of collections that aren't in the variables JSON were skipped" = "le modalità di variabile di collezioni non presenti nel JSON delle variabili sono state saltate" +"variable values that don't match their type (or composed colours) were skipped" = "i valori di variabile che non corrispondono al loro tipo (o i colori composti) sono stati saltati" +"variables of unknown types were skipped" = "le variabili di tipo sconosciuto sono state saltate" +"variables whose collection isn't in the variables JSON were skipped" = "le variabili la cui collezione non è nel JSON delle variabili sono state saltate" +"variables without readable values were skipped" = "le variabili senza valori leggibili sono state saltate" +"vector points with unknown stroke joins use the layer join" = "i punti vettoriali con giunzioni di contorno sconosciute usano la giunzione del livello" +"vectors without geometry are empty" = "i vettori senza geometria sono vuoti" +"vectors without geometry are empty (export the file JSON with geometry=paths)" = "i vettori senza geometria sono vuoti (esporta il JSON del file con geometry=paths)" +"very long text was cut to 1,048,576 characters" = "un testo molto lungo è stato troncato a 1.048.576 caratteri" +"very long vector paths were skipped" = "i tracciati vettoriali molto lunghi sono stati saltati" +"video actions are not supported and were skipped" = "le azioni video non sono supportate e sono state saltate" +"video triggers are not supported and were skipped" = "i trigger video non sono supportati e sono stati saltati" +"yesterday" = "ieri" +"{instances} in {files}" = "{instances} in {files}" +"{language}: draft translation, waiting for a native speaker's review." = "{language}: traduzione provvisoria, in attesa della revisione di un madrelingua." +"{length} bleed" = "abbondanza di {length}" +"{month} {day}, {year}" = "{day} {month} {year}" +"{name} cleared from the list" = "{name} rimosso dall'elenco" +"{name} is forgotten on this computer; the file stays on {server}" = "{name} è dimenticato su questo computer; il file resta su {server}" +"{name} is on the server." = "{name} è sul server." +"{name} is spotlighting" = "{name} sta mettendo in evidenza" +"{name} is too large to open in the browser (over 512 MB)." = "{name} è troppo grande per essere aperto nel browser (oltre 512 MB)." +"{name}\nClick to apply to the selection" = "{name}\nClicca per applicare alla selezione" +"{name}\nDrag onto the canvas, or click to place in the middle of the view.\nHold Alt (Option) while dropping onto an instance to swap it." = "{name}\nTrascina sul canvas, oppure clicca per posizionare al centro della vista.\nTieni premuto Alt (Opzione) mentre rilasci su un'istanza per scambiarla." +"{name} (from {library})" = "{name} (da {library})" +"{name} (you)" = "{name} (tu)" +"{name} ({layers} layers)" = "{name} ({layers} livelli)" +"{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}, predefinito {default}. Fai doppio clic sul cursore per tornare al valore dello stile; Esc durante il trascinamento annulla." +"{name} from {path}" = "{name} da {path}" +"{name} was removed from the server" = "{name} è stato rimosso dal server" +"{name} · deleted on main" = "{name} · eliminato su main" +"{name} · deleted on the branch" = "{name} · eliminato sul ramo" +"{name} · not published" = "{name} · non pubblicato" +"{name} · on a server" = "{name} · su un server" +"{name} · published version {v}" = "{name} · versione {v} pubblicata" +"{name} · version {v} on the server" = "{name} · versione {v} sul server" +"{name}, in a live session" = "{name}, in una sessione live" +"{name}, online, open only to you" = "{name}, online, aperto solo a te" +"{name}, {kind}, {w} by {h}, selected" = "{name}, {kind}, {w} per {h}, selezionato" +"{n} added" = { one = "{n} aggiunto", other = "{n} aggiunti" } +"{n} autosave versions" = { one = "{n} versione di salvataggio automatico", other = "{n} versioni di salvataggio automatico" } +"{n} changed" = { one = "{n} modificato", other = "{n} modificati" } +"{n} columns" = { one = "{n} colonna", other = "{n} colonne" } +"{n} components" = { one = "{n} componente", other = "{n} componenti" } +"{n} components and styles have newer versions" = { one = "{n} componente o stile ha una versione più recente", other = "{n} componenti e stili hanno versioni più recenti" } +"{n} conflicts to resolve" = { one = "{n} conflitto da risolvere", other = "{n} conflitti da risolvere" } +"{n} days ago" = { one = "{n} giorno fa", other = "{n} giorni fa" } +"{n} files" = { one = "{n} file", other = "{n} file" } +"{n} hours ago" = { one = "{n} ora fa", other = "{n} ore fa" } +"{n} instances" = { one = "{n} istanza", other = "{n} istanze" } +"{n} layers" = { one = "{n} livello", other = "{n} livelli" } +"{n} layers as one file" = { one = "{n} livello come un unico file", other = "{n} livelli come un unico file" } +"{n} layers selected" = { one = "{n} livello selezionato", other = "{n} livelli selezionati" } +"{n} layers · {parts}" = { one = "{n} livello · {parts}", other = "{n} livelli · {parts}" } +"{n} matches" = { one = "{n} corrispondenza", other = "{n} corrispondenze" } +"{n} minutes ago" = { one = "{n} minuto fa", other = "{n} minuti fa" } +"{n} months ago" = { one = "{n} mese fa", other = "{n} mesi fa" } +"{n} more notes omitted from this report" = { one = "{n} altra nota omessa da questo resoconto", other = "{n} altre note omesse da questo resoconto" } +"{n} of {total}" = "{n} di {total}" +"{n} people joined" = { one = "{n} persona si è unita", other = "{n} persone si sono unite" } +"{n} preferred components" = { one = "{n} componente preferito", other = "{n} componenti preferiti" } +"{n} ready" = { one = "{n} pronto", other = "{n} pronti" } +"{n} removed" = { one = "{n} rimosso", other = "{n} rimossi" } +"{n} replies" = { one = "{n} risposta", other = "{n} risposte" } +"{n} rows" = { one = "{n} riga", other = "{n} righe" } +"{n} styles" = { one = "{n} stile", other = "{n} stili" } +"{n} text layers" = { one = "{n} livello di testo", other = "{n} livelli di testo" } +"{n} variables" = { one = "{n} variabile", other = "{n} variabili" } +"{n} weeks ago" = { one = "{n} settimana fa", other = "{n} settimane fa" } +"{n} years ago" = { one = "{n} anno fa", other = "{n} anni fa" } +"{n}d" = "{n}g" +"{n}h" = "{n}h" +"{n}m" = "{n}m" +"{n}w" = "{n}sett" +"{page}, nothing selected" = "{page}, nessuna selezione" +"{percent}%" = "{percent}%" +"{profile} (current)" = "{profile} (attuale)" +"{property} is its value; {property ? \"a\" : \"b\"} picks a string. Other {…} stays as written." = "{property} è il suo valore; {property ? \"a\" : \"b\"} sceglie una stringa. Qualsiasi altro {…} resta come scritto." +"{server} can't say what became of your files (it is older than this app)." = "{server} non sa dire che fine abbiano fatto i tuoi file (è più vecchio di questa app)." +"{server} · No longer on the server" = "{server} · Non più sul server" +"{server} · {shared} · Opened {date}" = "{server} · {shared} · Aperto {date}" +"{tool} hold {key}" = "{tool} tieni premuto {key}" +"{value} isn't a colour" = "{value} non è un colore" +"{when} · edited" = "{when} · modificato" +"← ↑ → ↓" = "← ↑ → ↓" +"← → ↑ ↓" = "← → ↑ ↓" +"←" = "←" +"→, Space or N" = "→, Spazio o N" +"↵" = "↵" +"⇥ / ⇧⇥" = "⇥ / ⇧⇥" +"⌘ arrows" = "⌘ frecce" +"⌘ click" = "⌘ clic" +"⌘ scroll" = "⌘ scorrimento" +"⌥ drag" = "⌥ trascina" + +[commands] +"align.bottom" = "Allinea in basso" +"align.hcenter" = "Allinea i centri orizzontali" +"align.left" = "Allinea a sinistra" +"align.right" = "Allinea a destra" +"align.top" = "Allinea in alto" +"align.vcenter" = "Allinea i centri verticali" +"annotation.add" = "Aggiungi annotazione" +"annotation.addCategory" = "Aggiungi categoria di annotazione" +"annotation.categories" = "Categorie di annotazione" +"annotation.edit" = "Modifica annotazione" +"annotation.editCategory" = "Modifica categoria di annotazione" +"annotation.list" = "Elenca annotazioni" +"annotation.remove" = "Rimuovi annotazione" +"annotation.removeCategory" = "Elimina categoria di annotazione" +"app.actions" = "Azioni" +"app.preferences" = "Preferenze" +"arrange.back" = "Porta in fondo" +"arrange.backward" = "Porta indietro" +"arrange.forward" = "Porta avanti" +"arrange.front" = "Porta in primo piano" +"arrange.packH" = "Compatta in orizzontale" +"arrange.packV" = "Compatta in verticale" +"arrange.reorder" = "Riordina selezione" +"arrange.roundToPixels" = "Arrotonda ai pixel" +"arrange.spacing" = "Spaziatura" +"arrange.tidy" = "Riordina" +"bool.exclude" = "Escludi selezione" +"bool.flatten" = "Appiattisci booleano" +"bool.intersect" = "Interseca selezione" +"bool.subtract" = "Sottrai selezione" +"bool.union" = "Unisci selezione" +"branch.archive" = "Archivia ramo" +"branch.create" = "Crea ramo" +"branch.list" = "Elenca rami" +"branch.merge" = "Unisci ramo" +"branch.open" = "Apri ramo" +"branch.review" = "Rivedi ramo" +"branch.updateFromMain" = "Aggiorna da main" +"codeConnect.export" = "Esporta collegamenti al codice" +"codeConnect.import" = "Importa collegamenti al codice" +"codeConnect.remove" = "Scollega dal codice" +"codeConnect.set" = "Collega al codice" +"collab.cursor" = "Mostra cursore a" +"collab.cursorChat" = "Chat del cursore" +"collab.diff" = "Mostra modifiche di sincronizzazione" +"collab.files" = "I tuoi file online" +"collab.follow" = "Segui" +"collab.forgetFile" = "Dimentica file online" +"collab.host" = "Avvia sessione live" +"collab.join" = "Partecipa alla sessione live" +"collab.leave" = "Esci dalla sessione live" +"collab.manageFile" = "Gestisci file pubblicato" +"collab.openFile" = "Apri file online" +"collab.peers" = "Persone in questo file" +"collab.publish" = "Pubblica sul server" +"collab.recoverFile" = "Recupera file online" +"collab.registerLinks" = "Apri i link di invito in FigCraft" +"collab.removePerson" = "Rimuovi dalla sessione" +"collab.report" = "Segnala il file a chi gestisce il server" +"collab.serverInfo" = "Informazioni sul server" +"collab.share" = "Condividi con un codice di partecipazione" +"collab.spotlight" = "Metti me in evidenza" +"collab.stop" = "Termina sessione live" +"command.batch" = "Esegui più comandi" +"comment.add" = "Aggiungi commento" +"comment.delete" = "Elimina commento" +"comment.edit" = "Modifica commento" +"comment.link" = "Copia link al commento" +"comment.list" = "Elenca commenti" +"comment.markRead" = "Segna commento come letto" +"comment.move" = "Sposta commento" +"comment.next" = "Commento successivo" +"comment.open" = "Apri commento" +"comment.people" = "Persone nei commenti" +"comment.react" = "Reagisci al commento" +"comment.reply" = "Rispondi al commento" +"comment.resolve" = "Risolvi commento" +"comment.zoom" = "Zoom sui commenti" +"component.addProperty" = "Aggiungi proprietà del componente" +"component.addVariant" = "Aggiungi variante" +"component.bindProperty" = "Collega livello alla proprietà" +"component.combineVariants" = "Combina come varianti" +"component.create" = "Crea componente" +"component.createMultiple" = "Crea più componenti" +"component.deleteProperty" = "Elimina proprietà del componente" +"component.detach" = "Scollega istanza" +"component.editProperty" = "Modifica proprietà del componente" +"component.exposeInstance" = "Esponi istanza annidata" +"component.goToMain" = "Vai al componente principale" +"component.hideWhenPublishing" = "Nascondi alla pubblicazione" +"component.list" = "Elenca componenti" +"component.makeSlot" = "Crea slot" +"component.playground" = "Playground dei componenti" +"component.properties" = "Proprietà del componente" +"component.pushChanges" = "Invia le modifiche al componente principale" +"component.removeSlot" = "Rimuovi slot" +"component.resetInstance" = "Ripristina tutte le modifiche" +"component.resetOverride" = "Ripristina sovrascrittura" +"component.restoreMain" = "Ripristina componente principale" +"dev.compare" = "Confronta modifiche" +"dev.focus" = "Vista focus" +"dev.resolveChange" = "Risolvi la modifica" +"dev.unfocus" = "Esci dalla vista focus" +"devResource.add" = "Aggiungi risorsa per sviluppatori" +"devResource.edit" = "Modifica risorsa per sviluppatori" +"devResource.list" = "Elenca risorse per sviluppatori" +"devResource.remove" = "Rimuovi risorsa per sviluppatori" +"distribute.horizontal" = "Distribuisci spaziatura orizzontale" +"distribute.vertical" = "Distribuisci spaziatura verticale" +"doc.home" = "Home" +"doc.list" = "Elenca documenti" +"doc.nextTab" = "Scheda successiva" +"doc.previousTab" = "Scheda precedente" +"doc.switch" = "Cambia documento" +"edit.copy" = "Copia" +"edit.copyAsCss" = "Copia come CSS" +"edit.copyAsPng" = "Copia come PNG" +"edit.copyAsSvg" = "Copia come SVG" +"edit.copyAsText" = "Copia come testo" +"edit.copyLink" = "Copia link alla selezione" +"edit.copyProperties" = "Copia proprietà" +"edit.cut" = "Taglia" +"edit.delete" = "Elimina" +"edit.duplicate" = "Duplica" +"edit.findReplace" = "Trova e sostituisci" +"edit.paste" = "Incolla" +"edit.pasteDesignLayers" = "Incolla livelli di design" +"edit.pasteOver" = "Incolla sopra la selezione" +"edit.pasteProperties" = "Incolla proprietà" +"edit.pasteReplace" = "Incolla per sostituire" +"edit.pickColor" = "Scegli colore" +"edit.redo" = "Ripeti" +"edit.repeat" = "Ripeti l'ultimo comando" +"edit.setDefaultProperties" = "Imposta proprietà predefinite" +"edit.undo" = "Annulla" +"effect.add" = "Aggiungi effetto" +"effect.move" = "Sposta effetto" +"effect.remove" = "Rimuovi effetto" +"effect.set" = "Imposta effetto" +"export.add" = "Aggiungi impostazione di esportazione" +"export.remove" = "Rimuovi impostazione di esportazione" +"export.update" = "Modifica impostazione di esportazione" +"file.close" = "Chiudi scheda" +"file.colorProfile" = "Profilo colore" +"file.duplicate" = "Duplica file" +"file.export" = "Esporta" +"file.exportFramesPdf" = "Esporta frame in PDF" +"file.exportLayers" = "Esporta livelli" +"file.exportSvg" = "Esporta selezione come SVG" +"file.import" = "Importa" +"file.moveTab" = "Sposta scheda" +"file.new" = "Nuovo file di design" +"file.newFromTemplate" = "Nuovo da modello" +"file.open" = "Apri" +"file.openRecent" = "Apri recente" +"file.placeImage" = "Inserisci immagine" +"file.recover" = "Recupera file" +"file.rename" = "Rinomina file" +"file.revert" = "Ripristina l'ultima versione salvata" +"file.save" = "Salva" +"file.saveAs" = "Salva come" +"file.saveVersion" = "Salva nella cronologia versioni" +"file.versionHistory" = "Mostra cronologia versioni" +"fill.add" = "Aggiungi riempimento" +"fill.move" = "Sposta riempimento" +"fill.pattern" = "Riempimento a motivo" +"fill.remove" = "Rimuovi riempimento" +"fill.set" = "Imposta riempimento" +"frame.create" = "Crea frame" +"frame.presets" = "Elenca preset di frame" +"frame.swapOrientation" = "Scambia orientamento" +"grid.add" = "Aggiungi griglia di layout" +"grid.move" = "Sposta griglia di layout" +"grid.remove" = "Rimuovi griglia di layout" +"guide.add" = "Aggiungi guida" +"guide.clear" = "Rimuovi tutte le guide" +"guide.move" = "Sposta guida" +"guide.remove" = "Rimuovi guida" +"guide.select" = "Seleziona guida" +"help.gettingStarted" = "Per iniziare" +"help.about" = "Informazioni su FigCraft" +"help.page" = "Pagina di aiuto" +"help.shortcuts" = "Scorciatoie da tastiera" +"image.adjust" = "Regola immagine" +"image.deletePreset" = "Elimina preset di immagine" +"image.place" = "Inserisci immagine" +"image.presets" = "Preset di immagine" +"image.savePreset" = "Salva preset di immagine" +"inspect.assets" = "Ispeziona asset" +"inspect.code" = "Codice del livello" +"inspect.componentCode" = "Codice del componente" +"inspect.properties" = "Ispeziona proprietà" +"inspect.readyForDev" = "Pronto per lo sviluppo" +"instance.create" = "Crea istanza" +"instance.exposed" = "Istanze annidate esposte" +"instance.place" = "Inserisci istanza" +"instance.resetSlot" = "Ripristina slot" +"instance.setProperties" = "Imposta proprietà dell'istanza" +"instance.slotInsert" = "Inserisci nello slot" +"instance.swap" = "Scambia istanza" +"instance.swapAt" = "Scambia istanza nel punto" +"layers.collapseAll" = "Comprimi livelli" +"layout.add" = "Aggiungi auto layout" +"layout.remove" = "Rimuovi auto layout" +"layout.setTrack" = "Imposta dimensione traccia della griglia" +"layout.suggest" = "Suggerisci auto layout" +"libraries.open" = "Librerie" +"libraries.publish" = "Pubblica libreria…" +"library.acceptUpdates" = "Accetta aggiornamenti della libreria" +"library.add" = "Aggiungi libreria" +"library.aliasVariable" = "Alias di variabile di libreria" +"library.applyStyle" = "Applica stile di libreria" +"library.bindVariable" = "Collega variabile di libreria" +"library.extendCollection" = "Estendi collezione di libreria" +"library.import" = "Importa componente di libreria" +"library.importVariable" = "Importa variabile di libreria" +"library.list" = "Elenca librerie" +"library.place" = "Inserisci componente di libreria" +"library.publish" = "Pubblica libreria" +"library.publishToServer" = "Pubblica libreria sul server" +"library.remove" = "Rimuovi libreria" +"library.setVariableMode" = "Imposta modalità della variabile di libreria" +"library.swap" = "Scambia libreria" +"library.swapAt" = "Scambia istanza con componente di libreria" +"library.updates" = "Aggiornamenti della libreria" +"library.usage" = "Utilizzo della libreria" +"measure.distances" = "Misura distanze" +"measure.padding" = "Misura margini interni dell'auto layout" +"node.create" = "Crea livello" +"node.get" = "Ottieni livello" +"node.reorder" = "Riordina livello" +"node.reparent" = "Sposta livello dentro" +"node.set" = "Imposta proprietà" +"object.batchRename" = "Rinomina in blocco" +"object.convertToFrame" = "Converti in frame" +"object.convertToGroup" = "Converti in gruppo" +"object.convertToSection" = "Converti in sezione" +"object.flipH" = "Rifletti orizzontalmente" +"object.flipV" = "Rifletti verticalmente" +"object.frameSelection" = "Racchiudi la selezione in un frame" +"object.group" = "Raggruppa selezione" +"object.hideOthers" = "Nascondi altri livelli" +"object.mask" = "Usa come maschera" +"object.moveToPage" = "Sposta nella pagina" +"object.opacityKeys" = "Opacità dai tasti numerici" +"object.rasterize" = "Rasterizza selezione" +"object.releaseMask" = "Rilascia maschera" +"object.removeFill" = "Rimuovi riempimento" +"object.removeFrame" = "Rimuovi frame" +"object.removeStroke" = "Rimuovi contorno" +"object.rename" = "Rinomina" +"object.resizeToFit" = "Ridimensiona per adattare" +"object.rotate180" = "Ruota di 180°" +"object.rotateLeft" = "Ruota di 90° a sinistra" +"object.rotateRight" = "Ruota di 90° a destra" +"object.setThumbnail" = "Imposta come miniatura" +"object.swapFillStroke" = "Scambia riempimento e contorno" +"object.toggleLock" = "Blocca/Sblocca selezione" +"object.toggleVisible" = "Mostra/Nascondi selezione" +"object.ungroup" = "Separa selezione" +"object.wrapInSection" = "Racchiudi in una nuova sezione" +"page.add" = "Aggiungi pagina" +"page.delete" = "Elimina pagina" +"page.duplicate" = "Duplica pagina" +"page.list" = "Elenca pagine" +"page.move" = "Sposta pagina" +"page.rename" = "Rinomina pagina" +"page.select" = "Vai alla pagina" +"page.setBackground" = "Sfondo della pagina" +"paint.documentColors" = "Colori del documento" +"plugin.close" = "Chiudi pannello del plugin" +"plugin.directory.install" = "Installa plugin dalla directory" +"plugin.directory.list" = "Sfoglia la directory dei plugin" +"plugin.directory.publish" = "Pubblica plugin nella directory" +"plugin.directory.unpublish" = "Rimuovi plugin dalla directory" +"plugin.event" = "Usa controllo del pannello del plugin" +"plugin.import" = "Importa plugin da manifest…" +"plugin.list" = "Elenca plugin" +"plugin.remove" = "Rimuovi plugin" +"plugin.run" = "Esegui plugin" +"plugin.ui" = "Pannello del plugin" +"plugins.manage" = "Gestisci plugin…" +"plugins.runLast" = "Esegui l'ultimo plugin" +"pref.autoSave" = "Salva i file automaticamente" +"pref.highlightHover" = "Evidenzia i livelli al passaggio del mouse" +"pref.keepTool" = "Mantieni lo strumento selezionato dopo l'uso" +"pref.language" = "Lingua dell'interfaccia" +"pref.nudge" = "Entità dello spostamento" +"pref.numberKeysOpacity" = "Usa i tasti numerici per l'opacità" +"pref.publishServer" = "Server per la condivisione online" +"pref.reduceMotion" = "Riduci animazioni" +"pref.renameDuplicates" = "Rinomina i livelli duplicati" +"pref.showDimensions" = "Mostra dimensioni sugli oggetti" +"pref.startOnHome" = "Avvia dalla schermata Home" +"pref.uiScale" = "Dimensione dell'interfaccia" +"pref.userName" = "Il tuo nome nei commenti" +"proto.action.add" = "Aggiungi azione del prototipo" +"proto.action.move" = "Riordina azione del prototipo" +"proto.action.remove" = "Rimuovi azione del prototipo" +"proto.action.set" = "Modifica azione del prototipo" +"proto.closePreview" = "Chiudi anteprima del prototipo" +"proto.conditional.addBlock" = "Aggiungi \"altrimenti se\" / \"altrimenti\"" +"proto.conditional.removeBlock" = "Rimuovi \"se\" / \"altrimenti se\" / \"altrimenti\"" +"proto.conditional.setCondition" = "Modifica condizione" +"proto.evaluate" = "Valuta espressione del prototipo" +"proto.flow.add" = "Aggiungi punto di partenza del flusso" +"proto.flow.remove" = "Rimuovi flusso" +"proto.flow.rename" = "Rinomina flusso" +"proto.interaction.add" = "Aggiungi interazione" +"proto.interaction.clear" = "Rimuovi tutte le interazioni" +"proto.interaction.remove" = "Rimuovi interazione" +"proto.interaction.set" = "Modifica interazione" +"proto.present" = "Presenta" +"proto.presentOptions" = "Opzioni di presentazione" +"proto.preview" = "Apri anteprima del prototipo" +"proto.settings" = "Impostazioni del prototipo" +"section.toggleContents" = "Nascondi/Mostra il contenuto della sezione" +"select.add" = "Aggiungi alla selezione" +"select.all" = "Seleziona tutto" +"select.area" = "Seleziona area" +"select.byName" = "Seleziona livelli per nome" +"select.children" = "Seleziona figli" +"select.inverse" = "Inverti selezione" +"select.layersAt" = "Livelli in un punto" +"select.matching" = "Seleziona livelli corrispondenti" +"select.next" = "Seleziona fratello successivo" +"select.none" = "Deseleziona tutto" +"select.parent" = "Seleziona genitore" +"select.previous" = "Seleziona fratello precedente" +"select.remove" = "Rimuovi dalla selezione" +"select.sameEffect" = "Seleziona stesso effetto" +"select.sameFill" = "Seleziona stesso riempimento" +"select.sameFont" = "Seleziona stesso font" +"select.sameInstance" = "Seleziona stessa istanza" +"select.sameProperties" = "Seleziona stesse proprietà" +"select.sameStroke" = "Seleziona stesso contorno" +"select.sameText" = "Seleziona stesse proprietà del testo" +"select.set" = "Seleziona" +"selection.applyColorStyle" = "Applica stile di colore della selezione" +"selection.colors" = "Colori della selezione" +"selection.selectColor" = "Seleziona livelli con il colore" +"selection.setPaint" = "Cambia colore della selezione" +"shape.arrow" = "Freccia" +"shape.ellipse" = "Ellisse" +"shape.line" = "Linea" +"shape.polygon" = "Poligono" +"shape.rectangle" = "Rettangolo" +"shape.star" = "Stella" +"shapeBuilder.merge" = "Unisci regioni" +"shapeBuilder.mode" = "Modalità generatore di forme" +"shapeBuilder.regions" = "Regioni del generatore di forme" +"shapeBuilder.remove" = "Rimuovi regioni" +"stroke.add" = "Aggiungi contorno" +"stroke.brush" = "Pennello" +"stroke.move" = "Sposta contorno" +"stroke.remove" = "Rimuovi contorno" +"stroke.set" = "Imposta contorno" +"stroke.widthProfile" = "Profilo di larghezza" +"style.apply" = "Applica stile" +"style.bind" = "Collega variabile nello stile" +"style.create" = "Crea stile" +"style.delete" = "Elimina stile" +"style.detach" = "Scollega stile" +"style.duplicate" = "Duplica stile" +"style.list" = "Elenca stili" +"style.move" = "Sposta stili" +"style.renameFolder" = "Rinomina cartella di stili" +"style.update" = "Modifica stile" +"text.alignCenter" = "Centra" +"text.alignJustify" = "Giustifica" +"text.alignLeft" = "Allinea a sinistra" +"text.alignRight" = "Allinea a destra" +"text.bold" = "Grassetto" +"text.bulletList" = "Elenco puntato" +"text.case" = "Maiuscole/minuscole" +"text.caseLower" = "Minuscolo" +"text.caseOriginal" = "Originale" +"text.caseSmallCaps" = "Maiuscoletto" +"text.caseTitle" = "Iniziali maiuscole" +"text.caseUpper" = "Maiuscolo" +"text.create" = "Testo" +"text.decoration" = "Decorazione" +"text.edit.begin" = "Modifica testo" +"text.edit.caret" = "Imposta cursore" +"text.edit.end" = "Termina modifica del testo" +"text.edit.insert" = "Digita testo" +"text.edit.key" = "Tasto del testo" +"text.edit.preedit" = "Componi testo (IME)" +"text.edit.selectLine" = "Seleziona riga" +"text.edit.selectWord" = "Seleziona parola" +"text.edit.selection" = "Selezione del testo" +"text.endPreview" = "Termina anteprima del testo" +"text.fonts" = "Elenca font" +"text.italic" = "Corsivo" +"text.lineHeightDown" = "Riduci interlinea" +"text.lineHeightUp" = "Aumenta interlinea" +"text.link" = "Crea link" +"text.listIndent" = "Aumenta rientro elenco" +"text.listOutdent" = "Riduci rientro elenco" +"text.listStyle" = "Stile dell'elenco" +"text.loadFont" = "Carica file di font" +"text.maxLines" = "Righe massime" +"text.missingFonts" = "Font mancanti" +"text.numberList" = "Elenco numerato" +"text.onPath" = "Testo su tracciato" +"text.paragraphIndent" = "Rientro del paragrafo" +"text.paragraphSpacing" = "Spaziatura dei paragrafi" +"text.pathOptions" = "Opzioni del tracciato di testo" +"text.preview" = "Anteprima della modifica del testo" +"text.refit" = "Adatta il testo ai font" +"text.removeFromPath" = "Rimuovi il testo dal tracciato" +"text.rescanFonts" = "Riesamina i font installati" +"text.set" = "Imposta testo" +"text.setFeatures" = "Funzionalità OpenType" +"text.setVariations" = "Assi del font variabile" +"text.sizeDown" = "Riduci dimensione del font" +"text.sizeUp" = "Aumenta dimensione del font" +"text.strikethrough" = "Barrato" +"text.style" = "Applica stile a un intervallo di testo" +"text.trackingDown" = "Riduci spaziatura tra lettere" +"text.trackingUp" = "Aumenta spaziatura tra lettere" +"text.underline" = "Sottolineato" +"text.weightDown" = "Riduci peso del font" +"text.weightUp" = "Aumenta peso del font" +"tool.arrow" = "Freccia" +"tool.comment" = "Commento" +"tool.ellipse" = "Ellisse" +"tool.frame" = "Frame" +"tool.hand" = "Mano" +"tool.line" = "Linea" +"tool.move" = "Sposta" +"tool.pen" = "Penna" +"tool.pencil" = "Matita" +"tool.pencilBrush" = "Pennello matita" +"tool.place" = "Inserisci con lo strumento" +"tool.polygon" = "Poligono" +"tool.rectangle" = "Rettangolo" +"tool.scale" = "Scala" +"tool.scaleAnchor" = "Ancora di scala" +"tool.section" = "Sezione" +"tool.set" = "Scegli strumento" +"tool.shapeBuilder" = "Generatore di forme" +"tool.slice" = "Slice" +"tool.star" = "Stella" +"tool.text" = "Testo" +"transform.move" = "Sposta" +"transform.moveTo" = "Sposta in" +"transform.nudge" = "Sposta di poco" +"transform.nudgeSize" = "Ridimensiona di poco" +"transform.resize" = "Ridimensiona" +"transform.rotate" = "Ruota" +"transform.rotationOrigin" = "Sposta origine di rotazione" +"transform.scale" = "Scala" +"transform.setRotationOrigin" = "Imposta origine di rotazione" +"variable.addMode" = "Aggiungi modalità" +"variable.bind" = "Collega variabile" +"variable.create" = "Crea variabile" +"variable.createCollection" = "Crea collezione di variabili" +"variable.delete" = "Elimina variabile" +"variable.deleteCollection" = "Elimina collezione" +"variable.duplicate" = "Duplica variabile" +"variable.export" = "Esporta variabili" +"variable.import" = "Importa variabili" +"variable.list" = "Elenca variabili" +"variable.moveMode" = "Sposta modalità" +"variable.removeMode" = "Elimina modalità" +"variable.renameCollection" = "Rinomina collezione" +"variable.renameMode" = "Rinomina modalità" +"variable.resetOverride" = "Ripristina sovrascrittura della variabile" +"variable.set" = "Imposta valore della variabile" +"variable.setMode" = "Imposta modalità della variabile" +"variable.update" = "Modifica variabile" +"vector.create" = "Vettore" +"vector.deleteHeal" = "Elimina e ricuci selezione" +"vector.edit.addVertex" = "Aggiungi punto" +"vector.edit.begin" = "Modifica vettore" +"vector.edit.bend" = "Curva segmento" +"vector.edit.bounds" = "Limiti dei punti selezionati" +"vector.edit.connect" = "Collega punti" +"vector.edit.cornerRadius" = "Raggio dell'angolo del punto" +"vector.edit.cut" = "Taglia tracciato" +"vector.edit.delete" = "Elimina punti" +"vector.edit.end" = "Modifica terminata" +"vector.edit.erase" = "Cancella" +"vector.edit.lasso" = "Selezione a lazo" +"vector.edit.mirroring" = "Riflessione delle maniglie" +"vector.edit.moveHandles" = "Sposta maniglie" +"vector.edit.moveVertices" = "Sposta punti" +"vector.edit.nudge" = "Sposta punti di poco" +"vector.edit.paint" = "Secchiello" +"vector.edit.pen" = "Punto penna" +"vector.edit.penEnd" = "Termina tracciato penna" +"vector.edit.position" = "Posizione del punto" +"vector.edit.resize" = "Ridimensiona punti" +"vector.edit.rotate" = "Ruota punti" +"vector.edit.select" = "Seleziona punti" +"vector.edit.selectAll" = "Seleziona tutti i punti" +"vector.edit.setHandle" = "Imposta maniglia" +"vector.edit.smooth" = "Ammorbidisci o appuntisci punti" +"vector.edit.state" = "Stato di modifica del vettore" +"vector.edit.strokeCap" = "Terminazione del contorno del punto" +"vector.edit.strokeJoin" = "Giunzione del contorno del punto" +"vector.edit.tool" = "Strumento di modifica vettoriale" +"vector.edit.transform" = "Trasforma punti" +"vector.edit.widthPoint" = "Punto di larghezza" +"vector.flatten" = "Appiattisci" +"vector.join" = "Unisci selezione" +"vector.offset" = "Contorno parallelo" +"vector.outlineStroke" = "Contorna tratto" +"vector.reverse" = "Inverti direzione del tracciato" +"vector.simplify" = "Semplifica vettore" +"vector.smoothJoin" = "Unisci selezione in modo uniforme" +"version.autosave" = "Versione di salvataggio automatico" +"version.delete" = "Elimina versione" +"version.duplicate" = "Duplica versione" +"version.exitPreview" = "Torna alla versione attuale" +"version.list" = "Elenca versioni" +"version.preview" = "Visualizza versione" +"version.rename" = "Rinomina versione" +"version.restore" = "Ripristina versione" +"view.assets" = "Pannello degli asset" +"view.comments" = "Commenti" +"view.contextMenu" = "Mostra menu contestuale" +"view.design" = "Pannello Design" +"view.designPrototype" = "Passa tra Design e Prototipo" +"view.devMode" = "Modalità sviluppo" +"view.frameOutlines" = "Contorni dei frame" +"view.get" = "Ottieni vista" +"view.layers" = "Pannello dei livelli" +"view.layoutGrids" = "Griglie di layout" +"view.leftSidebar" = "Mostra/Nascondi barra laterale sinistra" +"view.maskOutlines" = "Contorni delle maschere" +"view.multiplayerCursors" = "Cursori multiutente" +"view.nextFrame" = "Zoom sul frame successivo" +"view.nextPage" = "Pagina successiva" +"view.nextRegion" = "Sposta il focus sull'area successiva" +"view.outlines" = "Contorni" +"view.outlinesHiddenLayers" = "Includi i livelli nascosti nei contorni" +"view.outlinesObjectBounds" = "Includi i limiti degli oggetti nei contorni" +"view.outlinesSelection" = "Contorni della selezione" +"view.pan" = "Sposta la vista" +"view.panelWidths" = "Ripristina larghezze dei pannelli" +"view.pixelGrid" = "Griglia di pixel" +"view.pixelPreview" = "Anteprima pixel" +"view.prevFrame" = "Zoom sul frame precedente" +"view.prevPage" = "Pagina precedente" +"view.previousRegion" = "Sposta il focus sull'area precedente" +"view.proofSrgb" = "Soft proof sRGB" +"view.fullscreen" = "Schermo intero" +"view.prototype" = "Pannello Prototipo" +"view.rulers" = "Righelli" +"view.set" = "Imposta vista" +"view.slices" = "Slice" +"view.snapGeometry" = "Aggancia alla geometria" +"view.snapObjects" = "Aggancia agli oggetti" +"view.snapPixel" = "Aggancia alla griglia di pixel" +"view.themeDark" = "Tema scuro" +"view.themeLight" = "Tema chiaro" +"view.themeSystem" = "Segui il tema del sistema" +"view.toggleUi" = "Mostra/Nascondi interfaccia" +"view.zoom100" = "Zoom al 100%" +"view.zoom200" = "Zoom al 200%" +"view.zoom50" = "Zoom al 50%" +"view.zoomAt" = "Strumento zoom" +"view.zoomFit" = "Zoom per adattare" +"view.zoomIn" = "Ingrandisci" +"view.zoomOut" = "Riduci" +"view.zoomSelection" = "Zoom sulla selezione" +"widget.click" = "Clicca widget" +"widget.insert" = "Inserisci widget" +"widget.list" = "Elenca widget" +"window.focus" = "Porta la finestra in primo piano" +"window.new" = "Nuova finestra" +"template.list" = "Elenca modelli" + +[menu] +"bool.exclude" = "Escludi" +"bool.flatten" = "Appiattisci" +"bool.intersect" = "Interseca" +"bool.subtract" = "Sottrai" +"bool.union" = "Unisci" +"file.duplicate" = "Duplica" +"file.import" = "Importa da file…" +"file.rename" = "Rinomina" +"libraries.open" = "Apri librerie" +"select.sameEffect" = "Effetto" +"select.sameFill" = "Riempimento" +"select.sameFont" = "Font" +"select.sameInstance" = "Istanza" +"select.sameProperties" = "Proprietà" +"select.sameStroke" = "Contorno" +"select.sameText" = "Proprietà del testo" +"text.alignCenter" = "Centra testo" +"text.alignJustify" = "Giustifica testo" +"text.alignLeft" = "Allinea testo a sinistra" +"text.alignRight" = "Allinea testo a destra" +"view.assets" = "Asset" +"view.design" = "Design" +"view.layers" = "Livelli" +"view.outlines" = "Mostra contorni" +"view.outlinesHiddenLayers" = "Includi livelli nascosti" +"view.outlinesObjectBounds" = "Includi limiti degli oggetti" +"view.outlinesSelection" = "Mostra contorni della selezione" +"view.prototype" = "Prototipo" +"view.themeDark" = "Scuro" +"view.themeLight" = "Chiaro" +"view.themeSystem" = "Segui il sistema" diff --git a/crates/ui-egui/src/i18n_tests.rs b/crates/ui-egui/src/i18n_tests.rs index 046212c1..3a99e814 100644 --- a/crates/ui-egui/src/i18n_tests.rs +++ b/crates/ui-egui/src/i18n_tests.rs @@ -361,6 +361,18 @@ fn system_languages_pick_the_first_one_with_a_catalog() { assert_eq!(code("fr", Some("de-DE")), "fr", "a chosen language beats the system's"); } +#[test] +fn italian_is_picked_for_italian_locales() { + let code = |pref: &str, system: Option<&str>| LANGS[resolve(pref, system)].code; + for tag in ["it", "it-IT", "it-CH", "it_IT.UTF-8"] { + assert_eq!(code("system", Some(tag)), "it", "{tag}"); + } + let it = LANGS.iter().position(|l| l.code == "it").unwrap(); + assert_eq!(LANGS[it].name, "Italiano"); + assert!(LANGS[it].draft); + assert!(!plural_one(it, 0) && plural_one(it, 1) && !plural_one(it, 2)); +} + #[test] fn swedish_is_picked_for_swedish_locales() { let code = |pref: &str, system: Option<&str>| LANGS[resolve(pref, system)].code; diff --git a/docs/translating.md b/docs/translating.md index eaa59755..c67a6e7d 100644 --- a/docs/translating.md +++ b/docs/translating.md @@ -17,7 +17,7 @@ Simplified. The catalogs: English (`en`, the source), and Spanish (`es`, with the Latin American overlay `es-419`), French (`fr`), German (`de`), Dutch (`nl`), Brazilian Portuguese (`pt-BR`), Japanese (`ja`), Korean (`ko`), Simplified Chinese (`zh-Hans`), Czech (`cs`), Indonesian (`id`), Finnish -(`fi`), Polish (`pl`) and Bengali (`bn`), machine-drafted and waiting for native speakers' review. Georgian +(`fi`), Polish (`pl`), Bengali (`bn`) and Italian (`it`), machine-drafted and waiting for native speakers' review. Georgian (`ka`) was written with AI assistance and reviewed by a native speaker, and stays a draft until a maintainer changes its status. Russian (`ru`, Русский) was reviewed and approved by VanTan; its catalog keeps Frame, Dev Mode and Smart animate as familiar From 77540d8c73dae8e6178c0f301b6759d77d38bb30 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 24 Oct 2026 09:52:53 +0700 Subject: [PATCH 19/27] #921: a pasted image is added on top of the selected layer's fills; the others keep their variables --- crates/engine/src/cmd/create.rs | 12 ++++++------ crates/ui-egui/src/paste_image_tests.rs | 22 ++++++++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/crates/engine/src/cmd/create.rs b/crates/engine/src/cmd/create.rs index 71a9b822..934bac5e 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?, 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)}", + "{data (base64) | path, x?, y?, width?, height?, parent?, target? (an existing layer: the image is added on top of its fills 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 ), @@ -378,10 +378,10 @@ 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, _, _)))); +/// Add an image fill on top of the layer's fills: the others keep their place and variable +/// bindings; a linked fill style no longer describes the list, so the link goes. +fn add_image_fill(node: &mut Node, image: String, mode: ImageScaleMode) { + node.fills.push(Paint::image(image, mode)); node.styles.fill = None; } @@ -415,7 +415,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)?; - replace_with_image_fill(node, hash.clone(), mode); + add_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/ui-egui/src/paste_image_tests.rs b/crates/ui-egui/src/paste_image_tests.rs index c659569c..a76eae79 100644 --- a/crates/ui-egui/src/paste_image_tests.rs +++ b/crates/ui-egui/src/paste_image_tests.rs @@ -196,7 +196,10 @@ fn cmd_v_fills_a_selected_oval_without_adding_a_layer() { 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(_))); + // Added on top: the oval's own fill stays under it. + assert_eq!(node.fills.len(), original_fill.len() + 1); + assert_eq!(node.fills[..original_fill.len()], original_fill[..]); + assert!(matches!(node.fills.last().unwrap().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(); @@ -209,7 +212,7 @@ fn cmd_v_fills_a_selected_oval_without_adding_a_layer() { } #[test] -fn pasting_an_image_detaches_the_replaced_fill_variable() { +fn pasting_an_image_keeps_the_other_fills_variables() { 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()); @@ -221,12 +224,14 @@ fn pasting_an_image_detaches_the_replaced_fill_variable() { 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"); + assert!(matches!(node.fills.last().unwrap().kind, figcraft_color::PaintKind::Image(_))); + assert!(matches!(node.fills[0].kind, figcraft_color::PaintKind::Solid { .. })); + assert!(!node.bound.is_empty(), "the colour fill under the image keeps its variable"); } +/// An image layer copied in FigCraft pastes as a layer, a selected shape or not. #[test] -fn cmd_v_fills_a_selected_oval_with_an_image_copied_in_figcraft() { +fn an_image_layer_copied_in_figcraft_pastes_as_a_layer() { let mut w = Win::new(); w.copy_image_elsewhere(); w.cmd_v(CMD); @@ -235,14 +240,15 @@ fn cmd_v_fills_a_selected_oval_with_an_image_copied_in_figcraft() { let oval = NodeId(w.app.session.execute("shape.ellipse", &json!({"x": 200, "y": 100, "width": 120, "height": 80})).unwrap()["id"].as_u64().unwrap()); + let fills = w.app.session.doc().unwrap().node(oval).unwrap().fills.clone(); 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_eq!(doc.node_count(), before + 1); 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]); + assert_eq!(doc.node(oval).unwrap().fills, fills); + assert_ne!(w.app.session.selection(), vec![oval]); } #[test] From 6595974a52844f6091a86d53c687c82aef34804d Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 24 Oct 2026 09:52:54 +0700 Subject: [PATCH 20/27] #921: an image layer copied in FigCraft pastes as a layer; another app's image fills a selected layer only when it is newer than our copy --- crates/engine/src/cmd/edit.rs | 25 ++----------------------- crates/ui-egui/src/menus.rs | 24 ++++++++---------------- 2 files changed, 10 insertions(+), 39 deletions(-) diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index e1d6273f..60a96b31 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 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)", + "{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)", true, paste ), @@ -688,32 +688,11 @@ 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)?; - super::create::replace_with_image_fill(node, hash, figcraft_color::ImageScaleMode::Fill); - 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 715f8c12..dede7cb7 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -57,22 +57,6 @@ 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)); @@ -83,6 +67,14 @@ pub fn paste(app: &mut FigApp, event_text: Option<&str>, at: Option<(f64, f64)>) } use base64::Engine as _; let b64 = base64::engine::general_purpose::STANDARD.encode(png); + // One selected layer that can have fills takes the image as its top fill. + if let [target] = app.session.selection()[..] + && at.is_none() + && app.session.doc().ok().and_then(|d| d.node(target)).is_some_and(|n| figcraft_engine::cmd::create::accepts_image_fill(&n.kind)) + { + app.run("image.place", json!({"data": b64, "target": target})); + return; + } // Centred in the view; Paste here puts its top-left at the point, as it does layers. let (x, y) = if at.is_some() { (cx, cy) } else { ((cx - f64::from(w) / 2.0).round(), (cy - f64::from(h) / 2.0).round()) }; app.run("file.import", json!({"data": b64, "name": "Pasted image.png", "x": x, "y": y})); From 839e9fa4bc2c3aa551a11b00d5c77d22d8992356 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 24 Oct 2026 09:59:26 +0700 Subject: [PATCH 21/27] #623: the colour scheme watcher wakes the window on a change, reads GNOME's 0 (no preference) as light, and falls back to Read on portals before 1.17 --- apps/figcraft/src/main.rs | 23 ++++++------ apps/figcraft/src/services.rs | 68 ++++++++++++++++++++--------------- 2 files changed, 53 insertions(+), 38 deletions(-) diff --git a/apps/figcraft/src/main.rs b/apps/figcraft/src/main.rs index 0b71149a..633b6998 100644 --- a/apps/figcraft/src/main.rs +++ b/apps/figcraft/src/main.rs @@ -149,21 +149,23 @@ fn main() -> eframe::Result { // System fonts: index them in the background, then wake the UI (an idle window draws no frames) // to lay text out again with them. let ctx_slot: Arc>> = Arc::new(Mutex::new(None)); - let fonts_slot = ctx_slot.clone(); + // Wakes the window (once it is there) from another thread. + let wake = |slot: &Arc>>| { + let slot = slot.clone(); + move || { + if let Some(c) = slot.lock().unwrap_or_else(std::sync::PoisonError::into_inner).as_ref() { + c.request_repaint(); + } + } + }; + let fonts_wake = wake(&ctx_slot); std::thread::spawn(move || { figcraft_text::scan_system_fonts(); - if let Some(c) = fonts_slot.lock().unwrap_or_else(std::sync::PoisonError::into_inner).as_ref() { - c.request_repaint(); - } + fonts_wake(); }); let mut session = Session::empty(); // Live sessions: the network threads wake the window when something arrives. - let wake_slot = ctx_slot.clone(); - session.live.install(Box::new(figcraft_collab_net::NetLink::new(Arc::new(move || { - if let Some(c) = wake_slot.lock().unwrap_or_else(std::sync::PoisonError::into_inner).as_ref() { - c.request_repaint(); - } - })))); + session.live.install(Box::new(figcraft_collab_net::NetLink::new(Arc::new(wake(&ctx_slot))))); for f in &args.files { if let Err(e) = session.execute("file.open", &json!({"path": f})) { log::error!("{f}: {e}"); @@ -175,6 +177,7 @@ fn main() -> eframe::Result { // The preferences first: they hold the online files a recovered document may be one of. let recovery = recovery::Shared::default(); let svc = services::Desktop::load(recovery.clone()); + svc.follow_system_dark(wake(&ctx_slot)); if let Some(prefs) = svc.prefs.clone() { session.prefs = prefs; } diff --git a/apps/figcraft/src/services.rs b/apps/figcraft/src/services.rs index cee8765b..668100f7 100644 --- a/apps/figcraft/src/services.rs +++ b/apps/figcraft/src/services.rs @@ -57,18 +57,22 @@ impl Desktop { let flag = reduce_motion.clone(); std::thread::spawn(move || flag.store(system_reduce_motion(), Ordering::Relaxed)); 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. + Self { clipboard: arboard::Clipboard::new().ok(), dir, prefs, reduce_motion, dark, language: system_language(), recovery } + } + + /// winit reports no system theme on Linux and the BSDs: there the freedesktop colour scheme + /// answers instead, read once and then watched in the background, calling `wake` when it + /// changes (an idle window draws no frames). Elsewhere this does nothing. + pub fn follow_system_dark(&self, wake: impl Fn() + Send + 'static) { #[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) + let dark = self.dark.clone(); + move || watch_system_dark(&dark, &wake) }) { 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 } + let _ = wake; } } @@ -156,33 +160,44 @@ 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. +/// (`org.freedesktop.appearance` / `color-scheme`: 1 dark, 2 light, 0 no preference, which is +/// light), read once and then watched for changes, each one calling `wake`. 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) { +fn watch_system_dark(dark: &AtomicU8, wake: &dyn Fn()) { + use zbus::zvariant::Value; 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), - _ => {} + // `Read` (portals before 1.17, which lack `ReadOne`) wraps the value in one more variant. + let scheme = |v: &Value| match v { + Value::U32(x) => *x, + Value::Value(inner) => match **inner { + Value::U32(x) => x, + _ => 0, + }, + _ => 0, + }; + let store = |v: u32| { + let new = match v { + 1 => DARK_DARK, + 2 => DARK_LIGHT, + _ => DARK_UNKNOWN, + }; + if dark.swap(new, Ordering::Relaxed) != new { + wake(); + } }; 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 { - let reply = conn.call_method(Some(PORTAL), "/org/freedesktop/portal/desktop", Some(SETTINGS), "ReadOne", &(APPEARANCE, "color-scheme"))?; + let read = |method: &str| -> zbus::Result { + let reply = conn.call_method(Some(PORTAL), "/org/freedesktop/portal/desktop", Some(SETTINGS), method, &(APPEARANCE, "color-scheme"))?; let body = reply.body(); - let value = body.deserialize::()?; - match value { - zbus::zvariant::Value::U32(v) => Ok(v), - _ => Ok(0), - } + Ok(scheme(&body.deserialize::()?)) }; - match read() { + match read("ReadOne").or_else(|_| read("Read")) { Ok(v) => store(v), Err(e) => log::info!("colour scheme: can't read the system setting: {e}"), } @@ -192,12 +207,9 @@ fn watch_system_dark(dark: Arc) { 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); + let Ok((namespace, key, value)) = body.deserialize::<(String, String, Value<'_>)>() else { continue }; + if namespace == APPEARANCE && key == "color-scheme" { + store(scheme(&value)); } } Ok(()) From 2912222c53299ae67c9acacaf7db9f7a87a9057e Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 24 Oct 2026 10:02:53 +0700 Subject: [PATCH 22/27] #607: the outline-stroke tests join the engine's one integration test binary (tests/it) --- crates/engine/tests/it/main.rs | 2 + crates/engine/tests/it/outline_stroke.rs | 172 ++++++++++++++++++++++ crates/engine/tests/outline_stroke.rs | 174 ----------------------- 3 files changed, 174 insertions(+), 174 deletions(-) create mode 100644 crates/engine/tests/it/outline_stroke.rs delete mode 100644 crates/engine/tests/outline_stroke.rs diff --git a/crates/engine/tests/it/main.rs b/crates/engine/tests/it/main.rs index a97e3434..34a12847 100644 --- a/crates/engine/tests/it/main.rs +++ b/crates/engine/tests/it/main.rs @@ -6,6 +6,8 @@ mod empty_text_box_undo; mod library_style_acceptance; #[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] mod list_indent_response; +#[allow(clippy::unwrap_used)] +mod outline_stroke; #[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] mod scope_reset; #[allow(clippy::unwrap_used)] diff --git a/crates/engine/tests/it/outline_stroke.rs b/crates/engine/tests/it/outline_stroke.rs new file mode 100644 index 00000000..9efb58c9 --- /dev/null +++ b/crates/engine/tests/it/outline_stroke.rs @@ -0,0 +1,172 @@ +//! Outline stroke keeps the renderer's holes and existing property ownership. + +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/crates/engine/tests/outline_stroke.rs b/crates/engine/tests/outline_stroke.rs deleted file mode 100644 index 477f77fe..00000000 --- a/crates/engine/tests/outline_stroke.rs +++ /dev/null @@ -1,174 +0,0 @@ -//! 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()); - } -} From 86e39cc9c053a8746ee26cc6f3199b3d8c410ea5 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 24 Oct 2026 10:04:32 +0700 Subject: [PATCH 23/27] #703: a Cmd/Ctrl-resize crop whose coefficients aren't finite leaves the image fill as it is --- crates/engine/src/tools.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/tools.rs b/crates/engine/src/tools.rs index ca078eb5..493df125 100644 --- a/crates/engine/src/tools.rs +++ b/crates/engine/src/tools.rs @@ -1711,7 +1711,12 @@ impl Session { 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()); + // A degenerate (zero-scale) layer has no inverse: its image keeps its placement. + let crop = (Affine::new(crop) * to_old).as_coeffs(); + if !crop.iter().all(|c| c.is_finite()) { + continue; + } + img.crop = Some(crop); img.scale_mode = ImageScaleMode::Crop; } doc.get_mut(*id)?.fills = fills; From caeab4e89e519cea6b5c905f363db384e1a04065 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 24 Oct 2026 10:09:27 +0700 Subject: [PATCH 24/27] #703: an explicit size edit detaches a binding made under the short names w and h too, as it does width and height --- crates/engine/src/cmd/styles.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/styles.rs b/crates/engine/src/cmd/styles.rs index dd93bac3..cb85ed5a 100644 --- a/crates/engine/src/cmd/styles.rs +++ b/crates/engine/src/cmd/styles.rs @@ -1010,8 +1010,8 @@ fn bound_field(n: &Node, field: &str) -> Option { let cl = &n.child_layout; let pad = |i: usize| layout.and_then(|l| l.padding.get(i).copied()); Some(match field { - "width" => format!("{}", n.width), - "height" => format!("{}", n.height), + "width" | "w" => format!("{}", n.width), + "height" | "h" => format!("{}", n.height), "minWidth" => format!("{:?}", cl.min_w), "maxWidth" => format!("{:?}", cl.max_w), "minHeight" => format!("{:?}", cl.min_h), From a44bba56a07ab565c142cd1a37c39bc88c81bc3f Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 24 Oct 2026 10:09:28 +0700 Subject: [PATCH 25/27] #703: Cmd/Ctrl-resize crops a layer whose size was bound to a variable as well: the resize detaches the binding (#809), so the new size sticks --- crates/engine/src/tools.rs | 4 +-- .../engine/src/transform_behaviour_tests.rs | 34 +++++++++---------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/crates/engine/src/tools.rs b/crates/engine/src/tools.rs index 493df125..ac60a758 100644 --- a/crates/engine/src/tools.rs +++ b/crates/engine/src/tools.rs @@ -1683,11 +1683,11 @@ impl Session { if flip_y { t *= Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, nh]); } - // Text reflow, size bindings and auto layout can recompute this box after the preview. + // Text reflow and auto layout can recompute this box after the preview (a size bound + // to a variable lets go of it: the resize is an explicit edit). let n = doc.get(*id)?; let crop_images = ev.cmd && !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. diff --git a/crates/engine/src/transform_behaviour_tests.rs b/crates/engine/src/transform_behaviour_tests.rs index 146bdfd6..3e724244 100644 --- a/crates/engine/src/transform_behaviour_tests.rs +++ b/crates/engine/src/transform_behaviour_tests.rs @@ -279,14 +279,16 @@ fn command_resize_leaves_fills_when_text_or_auto_layout_recomputes_the_box() { } } +/// A resize is an explicit edit of the size: a size bound to a variable lets go of it (#809), so +/// Cmd/Ctrl crops a bound layer's image as any other's; other bindings stay. #[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)), +fn image_crop_detaches_a_bound_size_and_crops() { + for (field, value, from, to, (w, h)) in [ + ("width", 200, Point::new(200.0, 50.0), Point::new(100.0, 50.0), (100usize, 100usize)), + ("height", 100, Point::new(100.0, 100.0), Point::new(100.0, 50.0), (200, 50)), + ("w", 200, Point::new(200.0, 50.0), Point::new(100.0, 50.0), (100, 100)), + ("h", 100, Point::new(100.0, 100.0), Point::new(100.0, 50.0), (200, 50)), + ("opacity", 100, Point::new(200.0, 50.0), Point::new(100.0, 50.0), (100, 100)), ] { for mods in [CMD, NONE] { let mut s = session(); @@ -299,19 +301,15 @@ fn image_crop_respects_variable_bound_dimensions() { 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!(n.bound.is_empty(), field != "opacity", "{field}: a size binding goes, others stay"); 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"); + assert_eq!((n.width, n.height), (w as f64, h as f64), "{field}"); + if mods.cmd { + let after = figcraft_render::render_node(doc, layer, 1.0).unwrap(); + for y in 0..h { + assert_eq!(&after.data()[y * w * 4..(y + 1) * w * 4], &pixels.data()[y * 800..y * 800 + w * 4], "{field}: the image stays put"); } - } - if field != "opacity" || !mods.cmd { + } else { assert_eq!(n.fills, before.node(layer).unwrap().fills, "{field}: keep the existing resize behaviour"); } } From a5afa004b41c72cfec089ef8ced0217b61d48217 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 24 Oct 2026 13:02:41 +0700 Subject: [PATCH 26/27] #1224: Italian drops two strings the close dialog no longer uses --- crates/ui-egui/locales/it.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/ui-egui/locales/it.toml b/crates/ui-egui/locales/it.toml index 68d042f6..3e34ae0a 100644 --- a/crates/ui-egui/locales/it.toml +++ b/crates/ui-egui/locales/it.toml @@ -249,7 +249,6 @@ status = "draft — needs native review" "Close tab" = "Chiudi scheda" "Close version history" = "Chiudi cronologia versioni" "Close when clicking outside" = "Chiudi cliccando all'esterno" -"Close without saving" = "Chiudi senza salvare" "Code" = "Codice" "Code syntax" = "Sintassi del codice" "Collapse" = "Comprimi" @@ -1443,7 +1442,6 @@ status = "draft — needs native review" "Unread" = "Non letto" "Unresolve" = "Riapri" "Unsaved changes" = "Modifiche non salvate" -"Unsaved changes in {names}. Close the window and lose them?" = "Modifiche non salvate in {names}. Chiudere la finestra e perderle?" "Unsaved changes: save first ({save}), or close the tab with {alt}-click to discard" = "Modifiche non salvate: salva prima ({save}), oppure chiudi la scheda con {alt}+clic per scartarle" "Unsaved changes: save first, or close again with {key} to discard" = "Modifiche non salvate: salva prima, oppure chiudi di nuovo con {key} per scartarle" "Up" = "Su" From 4a07c6a4852af8e834b40d98f65c4c14f6334209 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Mon, 26 Oct 2026 08:53:00 +0700 Subject: [PATCH 27/27] #1224: Italian drops "Contents only", which the export settings no longer use --- crates/ui-egui/locales/it.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/ui-egui/locales/it.toml b/crates/ui-egui/locales/it.toml index 3e34ae0a..8f86b145 100644 --- a/crates/ui-egui/locales/it.toml +++ b/crates/ui-egui/locales/it.toml @@ -294,7 +294,6 @@ status = "draft — needs native review" "Constraints" = "Vincoli" "Content" = "Contenuto" "Contents hidden" = "Contenuti nascosti" -"Contents only" = "Solo contenuti" "Contextual alternates" = "Alternative contestuali" "Contrast" = "Contrasto" "Contributors" = "Collaboratori"