This commit is contained in:
Reudy 2026-09-26 19:13:02 +02:00
commit 0ad405c26e
78 changed files with 9127 additions and 0 deletions

82
src/lib/model/layout.ts Normal file
View file

@ -0,0 +1,82 @@
// 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;
}
export const GAP = 40;
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;
}