In vector edit mode a point dragged onto another point of its path
snapped there but stayed a separate point, so a path couldn't be closed
or joined by dragging. A single dragged point dropped within the point
hit radius (6 px) of another point now merges into it, in the same undo
step as the move, and that point stays selected.
Closes#401
Copies written by plugins and tools carry their image bytes in the
message: a paint's image.dataBlob indexes the message's blobs. A bare
canvas (what the clipboard holds, see #647) has no images/, so these
fills came in as grey placeholders. The importer now falls back to the
embedded bytes, keyed by image hash, after the archive's images/.
Blobs over the image size cap and indexes past the blobs are skipped and
stay placeholders. Tests: a bare canvas carrying its image in a blob, and
a dataBlob past the blobs.
Found while testing crates/engine/src/cmd/edit.rs:1180
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The pages list took a fixed 40% of the left panel (#272). The line under
it now drags to make the list taller or shorter: at least three rows, and
always leaving the layers their header and three rows. A double-click puts
back the default. The main window keeps the height in the preferences, as
it does the panel widths, saved once the drag is over.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQxjkNSUJhTkTfY74aNeki
Flatten (Cmd+E, Alt+Shift+F) and Join selection (Cmd+J) built the result
from each layer's fill outline, so open paths, which have none, were
dropped: two pen paths flattened to "nothing to flatten", and next to a
closed shape they vanished. Vectors now bring their own network (open
paths, unfilled loops and each region's winding stay), mapped into the
parent's space; other layers still flatten to their outline.
Closes#377
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.
`document.json` was read under the 512 MB cap for any archive member, so a
large imported document saved but didn't open again: 900 000 layers save
as a 1.48 GB document.json, packed into 244 MB. It may now unpack to 16
times its packed size (counted up to the archive's own size), within
512 MB and 4 GB; images and other members keep 512 MB. A small archive
still can't unpack into gigabytes.
That saved file reopens in 13 s with a 5.9 GB peak.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQxjkNSUJhTkTfY74aNeki
A real 705 MB .fig file (401 644 nodes, 894 004 layers once instances are
expanded) didn't open: "message: too many values". Lifting that cap alone
made it import only its first 200 000 layers, and lifting that one took
41 GB of memory.
The global caps stay as they are. The .fig import gets budgets from the
size of its compressed canvas instead (counted up to the file's own size),
not from the message it unpacks to, since chunks inflate up to 512 MB:
- the kiwi value budget is one value per 2 bytes of the canvas, at least
the old 20 million (kiwi is only read for .fig files);
- the layer cap is one layer per 32 bytes of the canvas, at least the REST
import's 200 000 and at most 2 000 000. A small or crafted file can't
expand its instances into millions of layers, however far it unpacks; the
REST JSON import keeps 200 000.
Memory:
- The mapper no longer passes the document through `json!`, which serialises
its arguments: the whole tree was deep-copied twice.
- Vector networks are built once per distinct network and handed to the REST
importer beside the JSON (`vectorNetworkRef`), not written into it as JSON
objects for every instance that shows them.
- Decoded kiwi objects are trimmed, and each import stage's input is dropped
as soon as the next stage is built.
On that file: peak footprint 41 GB -> 11.3 GB, 67 s -> 25 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQxjkNSUJhTkTfY74aNeki
A library file is cached by its length and modification time. File systems keep the time in coarse ticks, so a same-length rewrite right after a read can keep the stamp, and library.updates then reports no change. A stamp now proves the file unchanged only once its modification time lies at least two seconds before the read; otherwise the file is read again.
Figma puts copied layers in the clipboard's HTML as a base64 .fig canvas
between (figma) markers. edit.pasteFromFigma reads it with the .fig import,
turns the layers into the session clipboard and pastes them as edit.paste
does (selected frame, in place or view centre), with the import's warnings.
The desktop app reads the clipboard's HTML (arboard) and Paste prefers a
Figma copy; the browser's paste event carries plain text only.
Tested on copies written by our own .fig export; not yet on real copies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ROADMAP.md, docs/localization-parity.md and docs/translating.md said
Swedish had 120 of the 588 command labels and menu wordings; with the
previous commit it has all of them. Worded without counts, so the docs
stay right as commands are added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The [commands] and [menu] rows held their command id as the text
("align.bottom" = "align.bottom"), which the menus and the command
palette would show as is. They now translate the English labels the
code gives those ids (spec! and m! in crates/engine), e.g.
"align.bottom" = "Justera nederkant". Also the interface strings that
are new on upstream main (23bdebd).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Straight connectors saved with a manual routing snapshot (connector and derived-frame version 1) now come in as an editable vector plus their label text layers, instead of an empty frame. Label placement composes the saved background and text transforms, and label paints and text styles use the existing import path. Connectors of other versions or line styles keep the existing fallback with their compatibility notes. At the layer depth or node limit the saved vector is kept without extra label layers, and skipped labels are reported once. Live endpoint bindings and routing are not retained.
Text layers, text styles and instance overrides in .fig files can carry list spacing, hanging list, hanging punctuation and leading trim. The importer now reads them from the highest layer with a valid value, so an invalid instance override row falls through to the valid value below it, while explicit zero, false and NONE values stay resets. Both text layers and text styles share one mapping. The native text data stores these options once per text node, so differing values on separate runs are not represented, and the experimental .fig exporter keeps its existing note that these settings are left out.
Text layers, styled runs and text styles in .fig files carry a skip-ink boolean for their decoration. The importer now reads it into the native decoration look: an omitted run value inherits the layer's, a local value overrides a linked shared text style without changing the style itself, and instance overrides keep an explicit false over a true main. A malformed value counts as unset. The exporter writes the actual value of the layer and of each styled run instead of a constant true, and a decoration look that differs from the default only in skip ink no longer triggers the unsupported-decoration note, so default native text exports its real false value.
Supported TEXT_PATH layers import as editable native text on their vector path, including mixed text styles, a start inside a later segment and reversed text direction. The importer converts the segment parameter stored in the file to the native distance along the complete path.
The mapping accepts a connected, unbranched path with one recognized start-segment marker and the default placement above the path. Damaged or unsupported path data keeps the text readable off its path and adds an import note. Other vertical placements, multiline text and ambiguous or disconnected networks are not converted. Underline and strikethrough remain a native path-rendering limitation and are reported. Public REST TEXT_PATH layers and the experimental .fig writer are unchanged.
The vector network decoder now keeps each segment's style id. Import reports a segment style that names a row of the layer's style table as metadata that was not interpreted, and one that names no row as a reference to a missing style entry, each at most once per file. The earlier note claimed that stroke styles were left out, which overstated what is known about the metadata. Segment geometry, paint and width are unchanged, and the style ids are not kept in the native vector model.
Slot properties of a component, the frames that reference them and the content that instances assign to them now come across from .fig files. A slot definition becomes a native slot property. The referencing frame of an expanded instance keeps following the frame of its main, and when the instance assigns slot content (including an explicitly empty slot) the frame carries that content as its own, independently owned children. Nested instances inside assigned content keep their own source links. An assignment on an outer instance wins over one made by an instance nested in the main, and a slot without an assignment keeps the default content of its main.
Assignments that are malformed, use the empty-GUID sentinel or point at a layer that is not slot content are skipped with an import note. Preferred component keys cannot be mapped to nodes of the file and are dropped with a note, as are slot settings the editor cannot represent. Public REST snapshots, the experimental .fig writer and property resources are not changed.
Grid columns and rows keep fixed pixel sizes, Hug sizing and fractional Fill weights when the minimum and maximum sizing definitions agree. Track GUIDs join the sizing records to the ordered tracks, so reordered records do not change cell placement. Unsupported min/max pairs, invalid sizes and duplicate records fall back with a reported note, and imports are bounded to 1,000 tracks per axis. Public REST snapshots ignore the private track metadata.
Found while testing crates/engine/src/cmd/arrange.rs:132
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A text layer that links a shared text style keeps its own, possibly stale, copy
of the typography. The style's typography now beats that copy, a case,
decoration, paragraph spacing or paragraph indent set on the layer itself beats
the style (so "original", "none" and zero clear the style's value), and an
override row of an instance beats both. An explicit "no style" on a text layer
unlinks it even when the older style field still names one.
Letter spacing given as a percentage of the font size stays a percentage. A text
property value on an instance no longer inherits the character styles written
for the main's text, which count other characters.
Override rows of an instance can name a layer of its main by the key the layer
had before the main was edited. The key now resolves to the layer's current id
when it is unambiguous and the current id does not exist already. Rows of an
enclosing instance win over those of the instances nested in it, whatever order
the wire lists them in, and an explicit "no style" on an override detaches the
style instead of falling back to the older field.
Paints bound to variables take the colours of the active modes when a document
loads, before the first edit, without touching other pending changes.
A stroke-only vector that imports as the outline of its stroke now moves the
stroke's shared style and colour variables to the fill it becomes, and vectors
whose single segments carry their own stroke style say so in a warning.
A .fig import now links the layers of an instance to the layers of the
component they came from by the ids the file stores, instead of leaving it to
the engine to guess from names and places. Renamed slots and layers, siblings
that share a name and swapped mains keep their source link and the override
they carry.
Components that live only on the file's hidden internal pages (cached copies of
library components) are kept in the hidden library store when a visible layer
uses them, so their instances stay instances. The read-only guard for library
copies now compares the copies themselves rather than the dirty set, which an
import leaves full until the first edit.
Rebasing an instance (after detaching an outer instance or restoring a main)
follows the source chain inside the right enclosing instance, so contextual
links survive it. The last change of a GUID wins and a removed change deletes
it. Ordinary file JSON never reads the private provenance.
Importing such a file into an open document, or pasting its layers into another one, copies the cached components along (import) or leaves them behind as a missing main (paste), so no link points at a layer of the target.
Clicking the + button to add a fill or stroke in the Design panel now
immediately opens the colour picker dialog for the newly added paint,
matching Figma's behaviour.
The canvas renderer and PDF export stroked text with the alignment
forced to centre, so Inside and Outside on text did nothing (SVG export
already honoured them). Glyph outlines are closed, so text strokes now
go through stroke_area like other shapes' strokes, with the same fill
rule: inside clips to the glyphs, outside to around them.
Opening or importing a .fig or REST JSON file, or an SVG that has notes,
from the app (File > Open or Import, a recent file, a drop, a paste, the
system's open event) now shows an Import report window with the
document's name, its page, layer and image counts and the compatibility
notes the importer collected, instead of leaving them only in the command
result. Commands sent over the control channel or MCP never open it.
file.open and file.import return the counts as `summary`; ui.inspect
reports the open report as `importReport`. The fixed notes the importers
emit are translated (German and Spanish) and shown with a capital first
letter; other notes, such as SVG's, show as written. The report goes
away with its document. Escape or Close dismisses it.
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.
Save as now offers .figcraft and .fig as separate file types. A .fig
destination, chosen in the dialog or passed to file.saveAs and file.save,
writes the existing experimental .fig export of the document that is shown
and reports how much was left out. The file's own path, unsaved changes,
versions, branches and undo history are not touched, and a shown earlier
version stays shown while the copy is written or a Save as choice is
pending.
Plain Save no longer writes over a file whose saved path ends in .fig;
earlier builds could write native data under that name. It asks for a
destination instead.
Figma's schema has leadingTrim on NodeChange (NONE, CAP_HEIGHT), and
the REST importer already reads style.leadingTrim, but the .fig mapping
never copied it, so text set to "Cap height to baseline" imported
untrimmed in its trimmed box. text_style now copies it with the other
text fields, from the layer or its text style.
Vector points carry their own stroke join in a .fig file. Import now keeps each point's join as the point's own override instead of dropping it for the layer's join.
Joins equal to the layer join no longer force styled stroke runs, and an absent layer join counts as miter. Unresolved point styles, unknown joins, and joins that dashed or variable-width strokes cannot draw each produce a warning.
Pasting or dropping a file runs file.import with the whole file as Base64
data, and every successful command was journaled with its parameters, so
each imported file stayed in memory a second time for the session. Byte
imports, including ones wrapped in command.batch, are no longer journaled.
Imports by path and all other commands are unchanged.
- Several ids: auto layout keys go to the frame-like layers, the others get the remaining keys
and are named in `warnings`. Refused only when no target can have auto layout.
- fontStyle is never refused: it draws in the nearest face, as a text layer resolves it.
`warnings` says so when that face's weight or slant differs from what the name asks for.
- set_properties passes `warnings` on to the agent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>