feat: add deterministic Diagram IR v2 authoring

This commit is contained in:
2026-09-03 10:05:29 +00:00
parent 305006c8cb
commit 50a07b90a9
31 changed files with 2108 additions and 98 deletions
@@ -0,0 +1,144 @@
import assert from "node:assert/strict";
import test from "node:test";
import type { DiagramPage } from "../../model/diagram-ir.js";
import { layoutPage } from "./layout-engine.js";
const PAGE: DiagramPage = {
id: "system",
title: "System",
layout: { type: "layered", direction: "horizontal", gridSize: 10, nodeGap: 40, layerGap: 80 },
nodes: [
{ id: "client", label: "Client", width: 120, height: 60 },
{ id: "worker", label: "Worker", width: 120, height: 60 },
{ id: "api", label: "API", width: 120, height: 60 },
{ id: "db", label: "Database", width: 120, height: 60 },
],
edges: [
{ id: "client-api", source: "client", target: "api" },
{ id: "worker-api", source: "worker", target: "api" },
{ id: "api-db", source: "api", target: "db" },
],
};
test("layered layout is deterministic, dependency-aware, and grid-aligned", () => {
const first = layoutPage(PAGE);
const second = layoutPage(PAGE);
assert.deepEqual(first, second);
const positions = new Map(first.nodes.map((node) => [node.id, node.geometry!]));
assert.ok(positions.get("client")!.x < positions.get("api")!.x);
assert.ok(positions.get("worker")!.x < positions.get("api")!.x);
assert.ok(positions.get("api")!.x < positions.get("db")!.x);
assert.notEqual(positions.get("client")!.y, positions.get("worker")!.y);
for (const geometry of positions.values()) {
assert.equal(geometry.x % 10, 0);
assert.equal(geometry.y % 10, 0);
}
});
test("layered layout avoids explicit-geometry siblings", () => {
const page: DiagramPage = {
id: "explicit",
title: "Explicit",
layout: { type: "layered", direction: "horizontal", gridSize: 10, nodeGap: 40, layerGap: 80 },
nodes: [
{ id: "a", label: "A", geometry: { x: 40, y: 80, width: 120, height: 60 } },
{ id: "b", label: "B" },
],
edges: [],
};
const laidOut = layoutPage(page);
const [a, b] = laidOut.nodes.map((node) => node.geometry!);
const overlaps = a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
assert.equal(overlaps, false);
});
test("manual layout preserves explicit geometry", () => {
const page: DiagramPage = {
id: "manual",
title: "Manual",
layout: { type: "manual" },
nodes: [{ id: "node", label: "Node", geometry: { x: 35, y: 45, width: 130, height: 70 } }],
edges: [],
};
assert.deepEqual(layoutPage(page), page);
});
test("grid and linear layouts avoid explicit siblings and honor grid/gap settings", () => {
for (const type of ["grid", "linear"] as const) {
const page: DiagramPage = {
id: `mixed-${type}`,
title: "Mixed",
layout: { type, direction: "horizontal", gridSize: 20, nodeGap: 60 },
nodes: [
{ id: "generated-one", label: "Generated one", width: 130, height: 60 },
{ id: "fixed", label: "Fixed", geometry: { x: 40, y: 80, width: 130, height: 60 } },
{ id: "generated-two", label: "Generated two", width: 130, height: 60 },
],
edges: [],
};
const laidOut = layoutPage(page);
const geometries = laidOut.nodes.map((node) => node.geometry!);
for (const geometry of geometries) {
assert.equal(geometry.x % 20, 0);
assert.equal(geometry.y % 20, 0);
}
for (let left = 0; left < geometries.length; left += 1) for (let right = left + 1; right < geometries.length; right += 1) {
const a = geometries[left];
const b = geometries[right];
const overlaps = a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
assert.equal(overlaps, false, `${type} layout overlapped mixed siblings`);
}
if (type === "linear") {
const generated = laidOut.nodes.filter((node) => node.id.startsWith("generated")).map((node) => node.geometry!).sort((a, b) => a.x - b.x);
assert.ok(generated[1].x - (generated[0].x + generated[0].width) >= 60);
}
}
});
test("automatic layout uses parent-relative coordinates and expands containers", () => {
const page: DiagramPage = {
id: "containers",
title: "Containers",
layout: { type: "grid", gridSize: 10, nodeGap: 40 },
nodes: [
{ id: "zone", label: "Zone", kind: "container", width: 120, height: 80 },
{ id: "one", label: "One", parentId: "zone", width: 120, height: 60 },
{ id: "two", label: "Two", parentId: "zone", width: 120, height: 60 },
],
edges: [],
};
const laidOut = layoutPage(page);
const byId = new Map(laidOut.nodes.map((node) => [node.id, node.geometry!]));
const zone = byId.get("zone")!;
for (const childId of ["one", "two"]) {
const child = byId.get(childId)!;
assert.ok(child.x >= 40 && child.y >= 40);
assert.ok(child.x + child.width + 40 <= zone.width);
assert.ok(child.y + child.height + 40 <= zone.height);
}
});
test("automatic layout positions generated sibling containers without overlap", () => {
for (const type of ["grid", "linear"] as const) {
const page: DiagramPage = {
id: "siblings",
title: "Siblings",
layout: { type, gridSize: 10, nodeGap: 40 },
nodes: [
{ id: "left", label: "Left", kind: "container" },
{ id: "left-child", label: "Left child", parentId: "left", width: 240 },
{ id: "right", label: "Right", kind: "container" },
{ id: "right-child", label: "Right child", parentId: "right", width: 240 },
],
edges: [],
};
const laidOut = layoutPage(page);
const byId = new Map(laidOut.nodes.map((node) => [node.id, node.geometry!]));
const left = byId.get("left")!;
const right = byId.get("right")!;
const overlaps = left.x < right.x + right.width && left.x + left.width > right.x && left.y < right.y + right.height && left.y + left.height > right.y;
assert.equal(overlaps, false, `${type} layout overlapped sibling containers`);
}
});