Nesting
A nested layout is a tree in which any node may carry a layout for its children. Containment and reflow stay a math problem: each container's layout is projected into the rectangle its parent assigned it. Nothing in the core knows or cares how you render the tree.
GridNode
GridNode is the normalized tree shape. You do not have to use it; see adapters.
A node's layout positions its children by id; the children list carries their own subtrees. gap is kept fixed when the container is projected and used when its children are solved. padding overrides layout.canvas.padding for rendering.
behavior flags:
flattenLayout
flattenLayout(root, rootRect, options) walks the tree and returns a FlatLayout: every node as a FlatItem with a rect in root coordinates, plus itemsById and childrenByParentId maps. Items are in paint order (parents before children), which is also z-order for hit testing.
For each container, the authored layout is projected into the container's rendered rect with renderLayoutForRect (chain projection with the node's gap). The projected entries become the children's sizing, their rects are offset by the parent's root rect, and the walk recurses.
Adapters
flattenLayout reads any tree shape through a GridTreeAdapter: getId, getChildren, getLayout, and optional getBehavior, getGap, getPadding. The default adapter is gridNodeAdapter. With your own adapter there is no conversion step, and FlatItem.node is your original node.
Queries
hitTest(flat, point): deepest item whose rect contains the point (last in paint order wins).findContainerAt(flat, point, { inset, sourceId }): deepest container that accepts children and contains the point.insetkeeps edge brushes from switching targets; the container the interaction started in wins without an inset.getAncestors,getDescendants: walk up or down.isInsideLockedSubtree,findFirstUnlockedAncestor,isDirectChildOfContained: the policy questions a drag controller asks before it starts.markLockedItems(items, flat): stamppolicy.movement: 'locked'on items whose node is locked so the solvers treat them as walls.
Solving inside a container
Solve in the container's rendered layout, then persist with toRenderedLayout, which rebases the canvas to the container's rendered size so the next render at that size is the identity.
To paint a preview while dragging, projectItemsToRoot(container, result.layout.items) converts the solver's items back to root rects using the same pipeline flattenLayout uses. Pass the full solver result so gap preservation sees every neighbor.
Moving between containers
transferItem works on two GridLayouts, so between containers it is: convert the root pointer into the target's local coordinates, scale the item's size with scaleSizeBetweenContainers, and pass the target container's layout as the target. See the transfer recipe.
Compaction
compactLayout(layout, { isRigid }) shrinks items vertically until a bounded canvas fits. Authored gaps between rows are preserved, flexible items shrink proportionally down to minH, and rigid items (fixed-height, or anything isRigid returns true for) keep their height. Horizontal geometry is untouched. fits is false when rigid heights, minimums, and gaps exceed the canvas; the returned layout is still the best effort.