normal_track read the trak size table at a file-controlled u32 offset with unchecked additions,
which wrap on 32-bit targets such as wasm32 (a debug panic, or reads at the wrong offset). Its
reads are now checked, and an incomplete normal track is dropped entirely, never returned in part.
index_bytes was already bounded by fill(), which checks each range before the flagged additions
run; this makes those bounds explicit, still skipping damaged faces and reading later faces' name
tables without their outlines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
Builds on #1618. Variables::get and Variables::collection take an exact id before a name; stored mode ids go through Collection::mode_by_id (expressions, the prototype's mode switch); font family variables and the colour picker, prototype editor and panels read stored ids exactly. Ported from fix/stored-ids-exact.
The board connector router moves, unchanged, from the engine into figcraft-doc (route_points, RouteEnd) so the .fig importer can use it: an elbowed or curved connector saved without its path runs in right angles or one curve from the side of each layer it joins (the file's magnet, else the edge its point is on, else the side facing the other end). Straight connectors are unchanged. Gap 1.
* M6: figcraft-cli connect publish publishes code connect mappings to a design file
figcraft-cli connect publish <file.figcraft> [--mappings figcraft.connect.json] [--dry-run] links each entry's component or set by name through codeConnect.import, warns about and skips links that don't fit, saves the file in place through the engine's file.save, and exits 1 when nothing links. Gap 16.
* M6: the generated man pages include connect and connect publish
The engine notes when a drag has only moved the selected layers by one offset (Interaction::moved_by); the canvas keeps the outlines, box and slots from the drag's start document and shifts them, the selection spacing and the Design panel's values (but the position fields) come from that document, and the Export section's values are kept. Dragging 648 layers: UI pass 2.10 to 1.44 ms a frame.
The CSS export left out a colour calculation that resolves to nothing but still wrote an alias to
it as var(--name); where no block defines that property, whatever reads the alias is invalid.
CSS (and Tailwind) and Penpot now share one rule, Variables::flattener: an alias that leads to a
calculation some mode of its collection leaves out goes out as the value it shows in the mode,
or is left out when it shows nothing.
* M12: create the next board shape already connected (#1597)
A selected sticky or shape shows a + beside each side; hovering one previews where the next goes, clicking makes one of the same kind, size and colour there, joined by a connector, and starts typing in it, in one undo step. board.addConnected {id?, side?, edit?} does it from commands.
* M12: the connected-shape handle and command in every full catalog (#1597)
* M12: Shift+Cmd/Ctrl+Enter on a board shape adds the next one already connected (#1597)
inspect_fields::compact_examples_run_verbatim_from_the_docs split docs/mcp.md and docs/control-protocol.md on text ending in a newline, so on a Windows checkout with core.autocrlf (CRLF docs) it panicked and cargo xtask ci failed locally. The test now reads both with LF line ends. Found while gating on Windows.
Fonts name a family by its typographic name and by a legacy name per weight; fonts without typographic names are installed under the legacy ones only. FontDb::resolve finds either from the other, and matches names across case, spaces, hyphens and underscores; every lookup (import, paste, REST import, rendering, missing fonts, the font report) uses it. A weight-number style goes by that weight, and .fig and REST imports wait for the installed fonts to be indexed.
Why type_of gives references no type, what display shows, how resolve_memo bounds one resolve,
and what variable.create and variable.set check before anything changes.
A colour calculation that resolves to nothing in a mode is left out of that mode's token set, but
an alias to it still went out as a {name} reference to the missing token. Such an alias, and any
alias leading to one, now goes out as the colour it shows in the mode, or is left out with the
calculation's warning when it shows nothing. A calculation left out in only one mode of its
collection counts too: a reference to it from another collection would dangle whenever that
mode's theme is active.
A colour variable's value in a mode can now be {"alpha": {"variable", "opacity"}} - that
variable's colour with its alpha replaced - or {"mix": {"from", "to", "t"}}, a per-channel blend
of two colour variables or literals over the stored values (in the document's colour profile),
alpha included. Calculations resolve lazily like aliases, so bound layers, mode switches,
instances and undo follow their sources; they compose, and a loop, or a calculation that reads
more than 32 variables in its collection's modes (other collections in their default modes), is
refused when it is set. Operands are stored by id and read by exact id, as stored references
are. Files keep them; REST, CSS, Penpot and .fig exports write the colour they show in each mode;
deleting a source leaves its readers on the colour they showed; library import and swap
re-point their operands. The Variables panel shows them with their formula and can swap one for
an alias or detach it to its colour.
The .fig import gave a file one decoded value per byte of its compressed canvas (at least 20M, at
most 128M). Dense legitimate files decode past that, further since every scalar field counts, and
the whole import was refused as "not a valid FigCraft file: .fig file: message: too many values".
The budget is now four values per compressed byte; the floor and both ceilings (128M, 32M on 32-bit
targets) are unchanged, so a small file that unpacks past the floor is still refused and no file
decodes more values than the ceiling allowed before. Generated 21 MB and 15 MB canvases (220,044
and 108,012 layers) and a 49.7 MB one budgeted at the ceiling (530,106 layers) now open whole
without warnings; the 49.7 MB one's test peaks at about 9 GB.