openpencil/packages/scene-graph/AGENTS.md
Danila Poyarkov 37bc706a59
fix: lay out fill text and repeat() grid tracks (#866)
* fix(core): let fill text share an auto-layout row

Fill frames grow and shrink from a zero flex basis, so fill siblings split a row's free space. Fill text only got flexGrow, and its measure function capped it at its stored width, 100px for new text, so seven fill labels in a 280px row each kept 100px and overflowed. Without a measurer, the fallback pinned that width and set no grow at all. Fill text now uses the same zero basis as fill frames on both paths.

* fix(design-jsx): read repeat() and minmax() in grid tracks

Track lists were split on whitespace, so columns="repeat(7, 1fr)" became the tracks repeat(7, and 1fr), read as fixed 0px and 1px columns that collapsed the grid. Tokens inside parentheses now stay together, repeat() expands its tracks, minmax() grows like its maximum, and a track the grid cannot express sizes to its content instead of to 0.

* refactor(scene-graph): parse CSS grid tracks with postcss-value-parser

design-jsx read repeat() and minmax() with a hand-written tokenizer and regexes, while dom-css already parses CSS values with postcss-value-parser. Track lists are now parsed in @open-pencil/scene-graph/css on that library, where both packages can use it, and design-jsx calls it. dom-css's hand-written declaration of the library's types is replaced by the types the library ships, which the shared module needs. The Scene Graph guide records that CSS values are parsed there.
2026-10-05 14:06:07 +00:00

3.4 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 uses parentId and childIds. Frames do not clip by default.
  • Geometry is built on the matrices in packages/scene-graph/src/matrix.ts and packages/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.ts for nested values (fills, strokes, effects); never hand-copy node properties.
  • Instance children map to component children through componentId; runtime overrides use the structured InstanceOverrideState (self and descendants maps) in packages/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 in packages/core/src/layout/variables.ts, which the editor and the Figma API both call rather than reimplementing.
  • componentScale records an occurrence's coordinates relative to its component definition. Shared rescaling lives in packages/scene-graph/src/scaling/; Core's Figma API delegates there instead of scaling nodes itself.
  • Shared primitives that format packages need live here: color conversion and management under @open-pencil/scene-graph/color and text/layout direction under @open-pencil/scene-graph/text-direction.
  • CSS values that become scene-graph types, such as grid track lists, are parsed in @open-pencil/scene-graph/css on postcss-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_DATA in packages/scene-graph/src/plugin-data/fields.ts, with the Valibot schema that reads it; read and replace entries with readPluginData and withPluginData instead of matching pluginId and key by hand. The fields test checks that keys stay unique.
  • Vector network types live here; the reverse-engineered vectorNetworkBlob codecs live in packages/core/src/vector/.
  • packages/scene-graph/src/checkpoint.ts owns 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.
  • Stroke extends Fill, so a stroke is a paint plus its geometry; add a paint field to Fill and both get it, and copy it in copyFill, which copyStroke reuses (packages/scene-graph/src/copy.ts).