DocState::new already settles every opened document (native files, branches and live joins
included); the .fig/REST path keeps its settlement in link_instances for file.import into an
open document.
Frames on the page or in a section convert to sections (object.convertToSection) and sections back to frames (object.convertToFrame), in place, one undo step each, from the canvas context menu; nested frames, instances, main components and sections holding sections are refused.
Found while testing crates/engine/src/cmd/structure.rs:575
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/engine/src/cmd/edit.rs:499 (copy_to_clipboard, called from copy at :536 and cut at :542)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
`node.create` read its `parent` as `id(p, "parent").unwrap_or(page)`, so a `parent` that isn't a
layer id — a `{"$step": n}` nothing had resolved, a name, a negative number — quietly meant "no
parent" and the layer landed on the page, with the call reporting success. The same shape sat on
`image.place` and `instance.create`, on `index` (a string index appended where it was asked to
insert), and on the `ids` of every command that targets layers (an entry that wasn't an id was
dropped and the rest ran). A number that names nothing was reported; anything that isn't a number at
all was not.
`params::opt_id` and `params::opt_index` now say what the commands mean: missing and `null` are
"none", and anything else has to be a layer id (or a whole, non-negative number) and is named when it
isn't. `req_targets` refuses an `ids` entry that isn't an id, before falling back to the selection. A
number that names no layer still reports `no such node`, and a boolean is still a number to `f64_of`,
as it is for `x`, `width` and the rest.
This turns a silently wrong result into an error on purpose, so a caller that passed something that
isn't an id and relied on the fallback now hears about it.
Hovering a family in the Typography font dropdown previews it on the
canvas via text.preview; closing without picking restores the original,
clicking applies it as one undo step.
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>
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
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>
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.
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.
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.
A home-screen drop that carries only bytes queued file.open with data and name, which file.open rejected because it requires a path. file.open now also takes base64 data with a name and opens it as a new document without a save path.
Found while testing crates/engine/src/cmd/file.rs:1283
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
page.duplicate copies every layer with duplicate_all_into (navigation between
copied frames points at the copies) and builds the old-to-new list for flows
from that copy, instead of copying each layer separately.
Main's case-insensitive replace loop (from an earlier PR) took the place of the
loop this change edited, so the replacement text is normalised once, where
the parameter is read.