Neighbouring pages now touch, so the tree reads like a grid. Rows with pages of different sizes are still centred, so it isn't a strict grid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsorPV3JeJRoZ1mD1pdvtL
83 lines
2.5 KiB
TypeScript
83 lines
2.5 KiB
TypeScript
// 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<string, Rect> {
|
|
const rects = new Map<string, Rect>();
|
|
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<string, Rect>, 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>): 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;
|
|
}
|