// Tree → world-space rectangles (units are PDF points; 1 CSS px = 1 pt in the // untransformed world layer). import { branchesOf, type Dir, type Slot, type Tree } from './tree'; export interface Size { width: number; height: number; } export interface Rect { x: number; y: number; width: number; height: number; } // Pages touch edge to edge; rows of mixed sizes stay centred, so it reads like a loose grid. export const GAP = 0; export function layout(tree: Tree, sizeOf: (id: string) => Size): Map { const rects = new Map(); let y = 0; for (const trunkId of tree.trunk) { const b = branchesOf(tree, trunkId); const rowIds = [trunkId, ...b.left, ...b.right]; const rowHeight = Math.max(...rowIds.map((id) => sizeOf(id).height)); const place = (id: string, x: number) => { const s = sizeOf(id); const r = { x, y: y + (rowHeight - s.height) / 2, width: s.width, height: s.height }; rects.set(id, r); return r; }; const trunk = place(trunkId, -sizeOf(trunkId).width / 2); let left = trunk.x; for (const id of b.left) left = place(id, left - GAP - sizeOf(id).width).x; let right = trunk.x + trunk.width; for (const id of b.right) { const r = place(id, right + GAP); right = r.x + r.width; } y += rowHeight + GAP; } return rects; } /** Where a ghost tile for `slot` sits, sized like `size`. */ export function slotRect(rects: Map, slot: Slot, size: Size): Rect | null { const a = rects.get(slot.anchor); if (!a) return null; return rectBeside(a, slot.dir, size); } export function rectBeside(a: Rect, dir: Dir, size: Size): Rect { const cx = a.x + a.width / 2; const cy = a.y + a.height / 2; switch (dir) { case 'up': return { x: cx - size.width / 2, y: a.y - GAP - size.height, ...size }; case 'down': return { x: cx - size.width / 2, y: a.y + a.height + GAP, ...size }; case 'left': return { x: a.x - GAP - size.width, y: cy - size.height / 2, ...size }; case 'right': return { x: a.x + a.width + GAP, y: cy - size.height / 2, ...size }; } } export function bounds(rects: Iterable): Rect | null { let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity; for (const r of rects) { x1 = Math.min(x1, r.x); y1 = Math.min(y1, r.y); x2 = Math.max(x2, r.x + r.width); y2 = Math.max(y2, r.y + r.height); } return x1 === Infinity ? null : { x: x1, y: y1, width: x2 - x1, height: y2 - y1 }; } export function intersects(a: Rect, b: Rect): boolean { return a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height; }