* fix(fig): read, render, and write Figma slots Instances of a component with a slot showed the component's default content instead of their own, and saving to .fig dropped slot properties, their settings, and every instance's content. Figma stores an instance's slot content as a frame on the internal canvas and assigns the slot property that frame's GUID. The reader follows the assignment while expanding the slot frame and pulls content frames into the dependency closure without making them layers. The scene graph gains a SLOT property type with its settings, a SLOT_CONTENT binding, and the rule that an assigned slot's content belongs to the instance, which component sync now leaves alone. The writer emits content frames on the internal canvas and binds slot frames through the parameter map, as Figma does. The occurrence and diagnostic types move from the interpreter to instance-overrides/occurrence.ts to keep it under the size limit. * fix(fig): keep slot content through swaps and missing content frames A slot assignment whose content frame the archive lacks no longer refuses the document: the reader reports it through onMissingSlotContent, like a missing component, and the slot keeps its component's content. Swapping an instance's component, which variant switches do, now carries the instance's own slot content to the new component's slot of the same name instead of dropping it, as Figma does. Component sync reads which slot a frame is from the component, whose bindings instance copies do not receive. Clipboard export numbers slot content after the other records on its dependency canvas, and the reader and writer share Figma's default slot value. * docs: note slot content kept across variant switches * fix(fig): pair clipboard text by record and drop dangling slot assignments The Figma clipboard paired text records with source text nodes by traversal order, but instance-owned slot content is written after the selected layers, so slotted text and the text after it swapped shaping data. Records are now paired with their nodes through their GUIDs. An instance assignment whose slot content frame is missing is dropped along with the reported diagnostic, so the slot keeps following its component instead of looking instance-owned. * ci: pull the slots fixture for unit tests * test: use GUID and Array.from in the slot tests * refactor(fig): group occurrence types and paths in one folder occurrence.ts and occurrence-path.ts became sibling prefixes when the occurrence types moved out of the interpreter; they now live in instance-overrides/occurrence/ as types.ts and path.ts.
6.7 KiB
Instance evaluation
flowchart TD
Layers[Owner layers: swaps, assignments, property claims] --> Route[Route structural layers to the instance they configure]
Route --> Expand[Expand each occurrence once with its effective component and bindings]
Expand --> Claims[Owners apply property claims onto the built subtree, inner to outer]
Claims --> Derived[Saved occurrence-derived data]
Derived --> Result[Effective values and provenance]
The model has three parts. An instance expands its component's subtree. Every owner contributes layers: a partial record at a path relative to that owner. Where layers overlap, the outermost owner wins.
A layer is structural when it carries a swap (overriddenSymbolID) or component-property
assignments; it selects what a nested instance expands. Everything else is a property layer.
Structural layers are routed down to the instance they address before it expands, so each
occurrence expands exactly once with its effective component and complete assignment list.
Property layers are applied by their declaring owner after its subtree is built, which keeps
their values in that owner's coordinate space and orders inner owners before outer ones.
Addressing
A property path is relative to its declaring owner. Ordinary containers may be traversed without adding an instance boundary; nested instances require an explicit segment.
Owner
+-- instance A -> Label (source L)
+-- instance B -> Label (source L)
[A, L] != [B, L] valid distinct addresses
[L] across an instance boundary not an implicit recursive search
Component GUIDs and component override keys can address the root. A binding-driven replacement can retain the source component's root identity without retaining its old descendant targets. A placed instance's explicit size remains separate from unscaled root-override size.
Provenance
| Origin | Interpretation |
|---|---|
| Component default | Inherited unless superseded. |
| Property assignment | Supplies a binding value; not equivalent to its default. |
| Explicit path override | Declared by an owner against a complete path. |
| Saved derived data | Effective geometry/typography; not automatically a user override. |
| Editor mutation | Recorded through the shared editing domains after materialization. |
default Label = "Badge"
+-- untouched instance -> inherits later component changes
+-- explicit "Badge" -> remains overridden despite equal text
Definitions with parentPropDefId inherit semantics within source ancestry while retaining
local property identity. Current typed varValue and PROP_REF parameter records are
normalized alongside older property forms.
Implemented: an outer owner's assignment supersedes an inner owner's explicit claim on the same field; unrelated fields of that claim are kept. A claim whose path passes through a swapped instance and resolved in the replaced component is stale and dropped. A missing target that did not resolve there either is reported, never remapped onto a similarly named replacement child.
Worked precedence example
Contribution text opacity
-------------------------- -------------- -------
Component default "Badge" 1.0
Intermediate explicit claim "Custom" 0.4
Outer text assignment "Assigned" --
Effective result "Assigned" 0.4
Retained intermediate claim -- 0.4
The outer assignment supersedes the intermediate text claim without erasing unrelated opacity. Within one owner, its assignments bind before its explicit claims, so an explicit child claim wins over that owner's own assignment to the same field.
BEFORE SWAP AFTER SWAP
Component A Component B
A/Label [explicit text claim] B/Icon
A/Icon B/Caption
A/Label does NOT become B/Caption merely because both contain text.
Names
Stored name alone is not proof of an explicit rename. Figma save captures distinguish
root-targeted name claims. Untouched replacement instances use the component name, or the
component-set name for variants. Initial stored names are not globally rewritten.
Known limitation: complex compositions of inherited renames, bindings, and structural
swaps still have unresolved oracle cases. Direct swapComponent() behavior must not be assumed
to describe every saved-record composition.
Derived text
Changing text or shaping properties invalidates inherited glyph caches unless a patch supplies replacement data. Later occurrence-derived glyph data can replace that invalidated cache. Paint-only changes do not alter glyph positions; every claim application applies the same validity rule. See materialization for rendering and editing boundaries.
Diagnostics
The library is strict by default: a missing or ambiguous target throws. InterpretInstanceOptions
lets a caller skip and report instead: onUnresolvedProperty and onUnresolvedAssignment for
records that address nodes the archive no longer contains, and onMissingComponent for an
instance of a deleted component, which then stays a childless instance with its saved
reference. A swap reports the same way when its target layer no longer exists, which Figma
retains as readily as a stale property override; an address that matches more than one record
is a wrong path rather than a stale one and stays fatal. Reports retain owner, effective
component context, complete path, and assignment payload where applicable.
Figma retains such records after deletions, so the application reader opts into all three
(readerSessionOptions in Core) and exposes the collected records through
readerDiagnostics(graph). Oracle tooling stays strict unless a flag names the concession.
Implementation and tests
- Interpreter
- Static source routing
- Occurrence path resolution
- Binding evaluation
- Text provenance
- Addressing tests
- Binding precedence tests
- Root-key tests
- Swap provenance tests
- Name capture provenance
Known limitation: field coverage and provenance transitions are not complete. Assignments
whose definition only exists on a detached ancestor are reported as unresolved rather than
remapped through detachedSymbolId lineage.