Gap 10: a file counts the library instances it places, creates, pastes or duplicates and reports them like its detaches. The server counts what a file's totals grow past their highest in the current week and keeps a year of weeks (actions.json). library.usage {days} and Show usage give insertions and detachments over the past 30 days, 90 days or year. New variants are appended; an older server refuses them and Show usage says so.
A request line can carry timeoutMs beside id and method, capped at 600,000 ms; without it the wait stays 60 s and older apps ignore the field. figcraft-cli mcp --attach PORT --timeout SECONDS sends it with every request and reads the answers that much longer. A timeout's message says how long the request waited.
* M11: a widget opens its plugin's panel (plugin API 5)
Gap 6: a widget callback's ui_show opens the plugin's panel and keeps the module loaded; the panel's callbacks get the widget's id and state and redraw it in the same undo step. Older plugins' ui_show from a widget still opens nothing. The Counter example sets its count from a panel.
* M11: open_panel takes its opener as one value
Gap 14: variable.extendCollection and Extend collection on a collection's row in the Variables editor. Extensions follow edits of the collection they extend at once (normalising keeps them in step whenever variables change), extensions extend again, and a loop of extensions from a hostile file comes apart into plain collections.
* M4: the eyedropper with Cmd/Ctrl+Shift makes a colour style or variable of what it picks
* M4: the Create from color window's collection picker is its own function
library.check refused nothing while automation roots confined it: it
counted libraries it never asked about and remembered them as yours. It
now refuses before remembering any. library.updates and library.list
answered a refused check with the team library cached earlier, as if the
server had nothing newer; the refusal now reaches them instead of
falling back like an offline server. The MCP and control protocol docs
say that a root on a network share still reaches that file server.
The UI starts a request through its gate (a new Running state) just before running it and never runs one the transport already cancelled. At the timeout the reply says it was dropped and not applied, or that it is still running and will take effect; a reply that just made it is delivered. Only screenshots and input blame the window.
* M12: an ended vote's results say who voted for each object
* M12: a board keeps earlier votes' results, listed in Results and deleted one at a time
* M12: the vote Results popover is drawn by small helpers, and its stepper, rows and Clear are tested
The roots confined files only: a confined agent could still publish the
open document to any server, host it on the network or join a session.
The engine reaches the network only through the live link, which now
refuses a call made while the roots confine it (hosting, joining,
publishing, sharing, team libraries, plugin directories), and skips the
background team library checks. The MCP and control protocol docs say so.
* M12: stamps stick to the objects they land on
A stamp sticks to the topmost object under its centre (BoardItem::Stamp gains an optional on, left out when empty so older files open with stamps on nothing) and the board's normalize pass carries it when that object moves or turns, once when both move together. A stamp dragged onto another object sticks to that one, off everything to nothing; one whose object is deleted stays where it is. Gap 24.
* M12: the object a stamp will stick to is highlighted
While the stamp tool is over an object, or a stamp is dragged over one, the object it will stick to is outlined with the canvas's hover outline (Overlay::stamp_target, found by the same stamp_target as board.stamp and settle_stamps). A drag no longer drops what it moves into the auto layout of a sticky, shape with text or table: board objects take no drops; a stamp already inside its sticky moves with it once. Builds on #1671. Gap 24.
* M3: team library analytics count detached instances
Detaching an instance of a library component counts it in the file (libraries.detached, saved with it, capped). The background library check reports the counts after its fetch (ReportDetached); the server keeps them per file beside the file's usage report in usage.json (same caps) and answers Detached with them. Show usage and library.usage list detaches per component and per file (detached, detachesKnown). The new request and reply variants are appended, so every older library message keeps its bytes; an older server refuses the new requests, which the app ignores. Gap 10.
* M3: the detach test waits for the instance count too (the two arrive in separate reports)
Main merged #1652 (the Bosnian catalog) and added more strings; ROADMAP keeps main's rows with the count updated, and bs.toml translates the two new strings (a pasted table's size limit, connector labels without a snapshot), so it covers all 1,906 UI strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Tayw8nmxoLLmKv13jK93z
proto.linkNotice from the Actions palette, the control channel or MCP changed the preference in memory only until the app next saved its preferences. It now sets Session::prefs_unsaved, which the app saves at the end of the frame like installed plugins and live-session tokens; the notice's own Don't show this again goes the same way. Gap 15.
The bar over a selected connector adds start and end point menus with every line end, a line style menu (solid or dashed, weights 1 to 6) and a colour wheel opening the colour picker, beside the board colours and line type; every control names itself on hover. UI only, through board.setStyle and the stroke's dashes and weight.
Four strings main added since the last merge (pasted design layers and
their missing images, image fills and shared styles not in the file) get
Bosnian translations, so the catalog covers all 1,905 UI strings again;
the counts in ROADMAP.md and the localization parity doc follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Tayw8nmxoLLmKv13jK93z
* M6: plugin panels show images
An image control in a plugin panel carries a picture as base64 (at most 256 KB; panel specs up to 1 MB), drawn at its size up to a field's width and named by its label, which shows instead when the picture can't be decoded or is over 4,096 px on a side. Images can't be clicked or be command parameters. Panel and form captions no longer wrap.
* Render worker: in_flight counts the watchdog's late preview
The watchdog that previews a render running past PREVIEW_AFTER_MS could pass its still-rendering check, lose the race to the full render, and then hand over its preview and wake the UI after Worker::in_flight had gone false. An idle test that had settled then saw a 0 ns repaint whose cause egui reports a frame late (CI on #1639: 'asked for a repaint in 0ns: [app/files.rs:211]', the autosave tick of the previous frame). The watchdog now marks itself busy from before that check until it has handed over and woken (or skipped), and in_flight counts it; the full render's done markers are SeqCst so the watchdog sees a finished render before in_flight can read idle.
* Docs: drop a status item the merge listed twice
* M6: .fig connector labels without a saved snapshot are kept, centred on their line
A connector with its own label text but no saved snapshot imports as a frame holding its line (a vector with the connector's stroke and ends) and the label as text centred on the line's middle by length, in the connector's fill paints or black; its background isn't known and is left out, as the import report says. The connector branch of layer_filtered moves into connector_layer. Gap 1.
* Docs: drop a status item the merge listed twice
* M6: a pasted image the clipboard doesn't carry shows as missing and is named (#1646)
The placeholder an import stores for an image it lacks is now striped, not flat grey; the fill keeps the image's name; .fig and paste give a fixed, translated note per image; edit.pasteDesignLayers returns missingImages, the paste's message names those layers, and a paste with notes opens the import report.
* M6: the missing-image placeholder's stripes stay clean when scaled, and a .fig gives one note (#1646)
* M6: pasted layers keep the colours of their shared paint styles (#1638)
A clipboard links a style by the guid it has in its library, which it doesn't hold, and by its library asset, whose copy it does hold. Mapper::resource_id now takes a guid only when the file holds a node by it, else the asset, so .fig import and paste agree; a link to a style that really isn't there keeps the layer's stored colours, with a note.
* M6: Swedish catalog: keep both new import notes after the merge
* Docs: one copy of each row and status item, and a check that keeps it so
Merge resolutions that kept both sides had left progress-doc rows (the .fig row four times, co-editing, live cursors, Windows, M0.9) and status-line items (302 in all) twice or more. The rows are merged keeping every distinct fact, the newest wording winning (#1649's connector routing included); repeated status items are removed, the first kept. cargo xtask docs, run by ci, fails on a repeated table row or status item.
* Docs: remove the status items the merge of main brought back
Merging main into #1658 rebuilt the status lines from main's copies, which still listed items twice: ROADMAP 2, docs/roadmap.md 5, docs/gaps.md 43 and docs/target-app-parity.md 190, whose unclosed parenthesis also came back and hid them from cargo xtask docs. They are removed again, the first copy kept, and the parenthesis closed; every distinct item is still present against main.
The first text laid out asked the font database for its fallback faces
(the fonts a run's missing characters are drawn with), and each of the 29
fallback families went through `find`, which reads every face of a family.
On Linux with Noto Sans CJK that is all seven 16–20 MB files, twice (SC and
KR name the faces of the same collections, and the bytes were shared only
within one family); Hiragino Sans is ten files on macOS. Each text laid out
afterwards repeated the 29 lookups under the font database's write lock.
- `fallbacks_for` reads, per family, the face nearest the run's style and
only when it is nearer than the faces read already (`fallback_face`,
`nearer_pending`); a face that can't be read gives way to the next one.
Used by name, a family still reads all its faces (`find`).
- Faces of one file share its bytes across families (`file_bytes`, kept
while the file keeps its length).
- The fallback faces are worked out once per style until fonts change
(`FallbackLists`, by generation), not for every text laid out.
- Choosing a face by name before it is read counts a slant mismatch, italic
or oblique, as `best` does: DejaVu Sans names its slanted faces Oblique,
and a regular run's fallback stays Book.
Measured on Linux with Noto Sans CJK, DejaVu Sans and Inter installed: the
first text laid out read 41 font files (265 MiB, kept in memory) and parsed
41 faces; now 3 files (20 MiB) and 4 faces, 6 and 8 with a bold run too,
150–190 ms less file reading. The fallback lookups per text laid out took
about 45 µs and now 0.2 µs (lookup logic timed alone). The faces drawn are
the same except between two equally near the run's style.
Tests: fontdb_fallback_tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GmygZSTYRL3cVw7cJWMG3B