5. Layout engine

src/layout computes every position and size that is not stored (LAY-9, ADR-5).

src/layout computes every position and size that is not stored (LAY-9, ADR-5). The same code runs on the host, in the script worker, in the CLI and in the webview, so a frame laid out on the canvas is laid out the same in ud render and in use_figma reads.

5.1 What is stored and what is derived#

Node situation Stored Derived
Child of a page, a group, a section, or a frame without auto layout x, y, width, height, rotation, constraints absolute transform
Child in auto-layout flow sizing per axis, min/max, grow/align overrides x, y, and width/height on hug or fill axes
Absolutely positioned child of an auto-layout frame (LAY-5) x, y, size, constraints absolute transform
Auto-layout frame sizing per axis, fixed sizes where fixed size on hug or fill axes
Group, boolean operation children bounds (Figma: a group’s box is its children’s union)
Text sizing mode (auto width, auto height, fixed), fixed sizes where fixed size on auto axes (§5.6)

Constraints (LAY-1) are applied when a parent’s size changes through a command: node.resize on a frame runs the constraint solver and writes the children’s new stored x/y/width/height as events (unless Ctrl is held, which the gesture passes as ignoreConstraints). Constraints are therefore a command-time computation; auto layout is a read-time derivation.

5.2 Auto layout: flex#

For a frame with layoutMode HORIZONTAL or VERTICAL (optionally layoutWrap: "WRAP"), the engine follows Figma’s model, which is close to CSS flexbox with box-sizing: border-box:

  1. Inner box: frame size minus padding (and minus strokes when “include stroke in layout” is on, LAY-6).
  2. Measure each flow child on the main axis: fixed → its size; hug → its content size (recursively); fill → 0 for now. Text children measure through the text engine (§5.6).
  3. Lines: without wrap, one line. With wrap, break into lines when the next item plus itemSpacing exceeds the inner main size; counterAxisSpacing separates lines (LAY-2).
  4. Distribute per line: free space = inner main size − sum of measured sizes − gaps. Fill children share it equally (Figma has no flex weights). Min/max: a fill child whose share violates its min or max is frozen at the limit and the rest is redistributed, repeating until stable (the CSS “resolve flexible lengths” loop; at most n rounds for n fill children).
  5. Gap: a number, or "auto" (space between): the free space is split between items and fill children are not allowed (the command refuses fill with auto gap, as Figma does).
  6. Counter axis: fixed → its size; hug → content; fill → line’s cross size (the inner cross size without wrap). Alignment: the 9-point primaryAxisAlignItems × counterAxisAlignItems, plus BASELINE for horizontal layouts, which aligns first baselines taken from the text engine (LAY-2).
  7. Reverse z-order (itemReverseZIndex) changes paint order only, not positions.
  8. Hug parent: the frame’s own hug size is the content extent plus padding, clamped by its min/max.

Hug and fill on the same axis between parent and child is resolved as Figma does: setting a child to fill on an axis where its parent hugs changes the parent to fixed on that axis, as an extra event of the same command.

5.3 Auto layout: grid#

layoutMode: "GRID" (LAY-4) places flow children in tracks:

  • Tracks are fixed (px), fill (fr) or hug (largest content of the items in the track).
  • Explicit placement by row and column with spans; other children are auto-placed row by row into the first free cell, in sibling order.
  • Track sizing: fixed first, then hug from single-span items, then multi-span items grow hug tracks evenly, then fr tracks share what is left (CSS grid’s algorithm restricted to these three track kinds).
  • Items align within their cell by the same 9-point alignment, with fill meaning “stretch to the cell”.

5.4 Sizing, min and max#

Every node in flow has layoutSizingHorizontal and layoutSizingVertical in FIXED | HUG | FILL (LAY-3), the Plugin API names. minWidth, maxWidth, minHeight, maxHeight clamp the result of every sizing mode, including hug. A hug size is computed with the child’s own min/max applied first, then the parent’s distribution, then the clamp, in that order on every axis, so the result does not depend on traversal order.

5.5 Absolute children#

A child with layoutPositioning: "ABSOLUTE" is skipped by flow and laid out against the frame’s box by its constraints after the frame’s size is known. It does not contribute to a hug size (Figma’s rule).

5.6 Text auto-resize#

Text and layout depend on each other: a text’s height depends on its width, and its width may depend on the layout. The engine asks the text engine for measurements with an explicit available width:

textAutoResize In flow, main axis = width Height
WIDTH_AND_HEIGHT (auto width) hug: the widest line with no wrapping (max-content) sum of line heights
HEIGHT (auto height) width from fixed or fill; hug behaves as fixed at the stored width lines at that width
NONE / TRUNCATE (fixed) stored stored; overflow clipped or ellipsised (TXT-1)

A fill text in a horizontal hug row would be circular; it is measured at max-content, as Figma does. Text measurements are cached by (text node, resolved style hash, available width rounded to 1/64 px).

5.7 Incremental relayout#

A fold or a draft mutation marks nodes dirty. Dirtiness propagates:

  • up while the parent’s size depends on its children (hug on any axis, groups, booleans);
  • down to flow children when a frame’s inner size, padding, gap, alignment or direction changed, and to fill children of any frame whose size changed;
  • to every instance of a component whose layout changed (through instancesOf).

Relayout then runs from the highest dirty root, top-down, reusing cached measurements of clean subtrees. Each frame’s result is cached with the input key (inner width, inner height, style hash), so a parent that re-measures a clean child at the same width gets the cached answer. Dragging a node inside an auto-layout frame re-lays out that frame and its hug ancestors only.

5.8 Determinism#

  • Children are visited in fractional-index order, never in map order.
  • Only + − × ÷, min, max, abs and sqrt are used in layout; these are exactly specified by IEEE 754. Rotation uses core/math.ts’s own sin/cos (a port of fdlibm), because Math.sin is not required to give the same bits in every engine version.
  • Floating-point results are not rounded inside the engine; the pixel snapping of the renderer is separate.
  • test/layout/ runs every fixture in Node and, through the dev page, in Chromium, and compares the derived geometry bit for bit.