* fix(figma-api): lay out pending edits before geometry reads Scripts read x, y, width, height, transforms, and bounds as they were before the script ran until the tool finished and laid out its changes. Figma lays out on read, so a hugging parent reports its new size right after a child is added. The graph now records the scope of edits outside layout application, and the geometry getters lay that scope out first through the same runner tools use after a call. One recorder exists per graph; a new FigmaAPI starts it afresh because the editor lays out its own edits. * fix(layout): size each axis on its own, as Figma does Fill is stored on the child, as layoutGrow along the parent's primary axis and STRETCH across it, with a grid laid out like a row; primaryAxisSizing and counterAxisSizing only fix or hug. A shared layoutSizing helper in scene-graph reads and writes per-axis sizing, and layout, the Figma API, the properties panel, design JSX, DOM/CSS export, and .pen import use it. This fixes grid children filling both axes when set to fill one, auto-layout children that stretch but kept a fixed size, the Figma API writing Fill to a frame's own sizing (which .fig export dropped), and JSX and HTML exports losing grid and cross-axis fill. Behavior was checked against live Figma, and imported layouts were compared with the geometry stored in material3.fig and nuxtui.fig. * fix(layout): keep pending edits until laid out and opt out of inherited stretch A new FigmaAPI cleared the edits recorded on its graph, so after a script failed before its tool laid out its changes, the next script read stale geometry; edits now stay recorded until a read lays them out, and return to the record if that layout throws. Setting a child to Fixed or Hug across a parent that stretches every child left it filling; it now opts out with MIN, as frame presets do.
4.7 KiB
4.7 KiB
Scene Graph
Framework-neutral document model. Owns node types, primitives, geometry, matrices, copy/snap/undo helpers, variables, instances, hit testing, and checkpoint recovery.
- Nodes live in a flat
Map<string, SceneNode>; runtime hierarchy usesparentIdandchildIds. Frames do not clip by default. - Walk up the tree with
SceneGraph.closest()orisDescendant(), which stop on a parent cycle in bad data, rather than a hand-writtenparentIdloop (packages/scene-graph/tests/basic/graph.test.ts). - Change a live graph's hierarchy through
reparentNode,reorderChild,insertChildAt, orupdateNode, which emit the events collaboration syncs; assignchildIdsdirectly only on a graph copy or nodes created in the same edit (sortInstanceChildreninpackages/scene-graph/src/instances/sync.ts). - Geometry is built on the matrices in
packages/scene-graph/src/matrix.tsandpackages/scene-graph/src/coordinate.ts(getWorldMatrix,getAxisAlignedWorldBounds,getNodeLocalMatrix). Add new transform, bounds, or inverse helpers here beside them; do not interpret ancestor rotations or reflections independently elsewhere. LINE pivots remain at the origin; other nodes rotate around their centers. - Bounds accumulation goes through the helpers in
packages/scene-graph/src/geometry.ts(computeBounds,computeAbsoluteBounds,computeVisualBounds); do not add another min/max loop. - Reparenting preserves child world positions; groups preserve them too. Sort children geometrically before creating auto-layout. Consumers such as layer trees must react to reparenting rather than retaining stale child references.
- Use the copy helpers in
packages/scene-graph/src/copy.tsfor nested values (fills, strokes, effects); never hand-copy node properties. - Instance children map to component children through
componentId; runtime overrides use the structuredInstanceOverrideState(selfanddescendantsmaps) inpackages/scene-graph/src/instance-overrides.ts. - A bound numeric field records its unit conversion in
variableBindingScales; preserve it through clone, copy, and transfer so a binding keeps meaning in its new scope. Core reconciles bindings and layout together inpackages/core/src/layout/variables.ts, which the editor and the Figma API both call rather than reimplementing. componentScalerecords an occurrence's coordinates relative to its component definition. Shared rescaling lives inpackages/scene-graph/src/scaling/; Core's Figma API delegates there instead of scaling nodes itself.- Shared primitives that format packages and collaboration need live here: color conversion and management under
@open-pencil/scene-graph/color, text/layout direction under@open-pencil/scene-graph/text-direction, and fractional sibling order keys under@open-pencil/scene-graph/order-keys(packages/scene-graph/tests/order-keys.test.ts). - A node's per-axis sizing (Figma's
layoutSizingHorizontal/Vertical) is read and written throughpackages/scene-graph/src/layout-sizing.ts. Fill is stored on the child as Figma stores it,layoutGrowalong the parent's primary axis (a grid's is horizontal) andlayoutAlignSelf: 'STRETCH'across it;primaryAxisSizing/counterAxisSizingonly fix or hug. Layout, the Figma API, the properties panel, and exporters call it instead of mapping axes themselves (tests/engine/figma/api/layout/sizing.test.ts). - CSS values that become scene-graph types, such as grid track lists, are parsed in
@open-pencil/scene-graph/cssonpostcss-value-parser; design-jsx and dom-css call it instead of splitting or matching CSS by hand (packages/scene-graph/tests/css). - Every plugin-data key OpenPencil writes is a field of
OPEN_PENCIL_PLUGIN_DATAinpackages/scene-graph/src/plugin-data/fields.ts, with the Valibot schema that reads it and its role:contenttravels with library assets and counts towards their hash (contentPluginData),formatrepeats a node field for files,bookkeepingstays behind. Read and replace entries withreadPluginDataandwithPluginDatainstead of matchingpluginIdandkeyby hand. The fields test checks that keys stay unique. - Vector network types live here; the reverse-engineered
vectorNetworkBlobcodecs live inpackages/core/src/vector/. packages/scene-graph/src/checkpoint.tsowns transaction checkpoint recovery, including hierarchy and indexes; Core's atomic tool execution relies on it.- Export named types and primitives (
Color,Vector,Rect,SceneNode,Effect,Fill,Stroke) from the public entry; downstream packages reuse them instead of respelling shapes. StrokeextendsFill, so a stroke is a paint plus its geometry; add a paint field toFilland both get it, and copy it incopyFill, whichcopyStrokereuses (packages/scene-graph/src/copy.ts).