Solver behavior
Each solver runs an ordered list of strategies. Each strategy has a clear gate, so the first one that applies wins and the behavior is predictable. The result names the strategy that produced it; onTrace reports it for logging.
Shared inputs are SolveOptions:
Every solver returns a SolveResult: accepted, the new layout, the active item as it appears there (or the rejected candidate), the strategy, and shiftedSiblings (true when other items moved or resized). Inputs are never mutated.
Move
moveItem({ layout, itemId, position, options }). The requested position is the item's top-left. Intent is inferred from how the moved rectangle overlaps its siblings. Strategies, in order:
Ghosts are partitioned out before any strategy runs and merged back afterwards. A push that would displace a locked item, or a fixed-axis item anchored against the canvas edge, returns nothing and the solver proceeds to the next strategy. A requested rectangle that still overlaps a locked item after push-down is rejected without trying fallback-snap.
pushAndShrinkSiblings is exported for custom solvers; edgeAlignedSlots lists every canvas- or sibling-aligned slot an item could sit in.
Resize
resizeItem({ layout, itemId, edge, delta, rect, options }). Provide edge and delta for interactive resizing (the opposite edge stays anchored) or rect for programmatic resizing (edge then only informs snapping).
- The requested rectangle is rounded, clamped to bounds, and, when
snapis on and an edge is known, the moving edge snaps to nearby sibling edges (offset bygap) and the canvas edge. - If the result overlaps no solid sibling:
resize. - Otherwise neighbors the new rectangle newly collides with are trimmed on the side facing the resized item, down to their minimums:
resize-shrink-neighbors. Neighbors that already overlapped the original rect, ghosts, and locked items are not touched; a locked or ghost collision, or a trim belowminW/minH, refuses. rejected.
resizeRect (geometry) computes the constrained rectangle without considering siblings; resizeByShrinkingNeighbors is the trimming step on its own.
Place
placeItem({ layout, item, position, pointer, options }) inserts a NewGridItem (size plus optional position, policy, and data). Two forms.
Position form (position, or item.x/item.y; defaults to the padding corner). The top-left is the intent.
Pointer form (pointer). The item is centered on the pointer; this form is what a drop preview wants and it always accepts.
If item.id already exists in the layout, the existing item is replaced.
Transfer
transferItem({ source, target, itemId, pointer, size, options }) removes the item from source and places it in target using the pointer form. Unless size is given, the item's size is scaled by the ratio of the two canvases' inner areas (scaleSizeBetweenCanvases) and capped at the target's inner size. The result carries both layouts; on rejection both equal their inputs.
Snapping
Snap candidates for a move are built from sibling edges (x, right - w, x - gap - w, right + gap, and the same vertically) plus the canvas edges. The nearest candidate on each axis within snapDistance is tried first, then the mixed and original positions. Resize snapping considers the moving edge only. Set snap: false to disable alignment snapping for a single call; snapDistance: 0 has the same effect for every call.
Related
- Items and constraints for
policyand minimums. - Instrumentation for
TraceEvent. - Adding solver fixtures when a case behaves unexpectedly.