feat(diagram-engine): add_graph — arrow-ordered layout as a container

The layout vocabulary's biggest gap, after D2/TALA's 'containers are
first-class at every layout stage': hierarchical zones and arrow-ordered
graphs could not mix. draw_graph did whole-page flowcharts, containers did
nesting, and 'an architecture zone whose contents follow the data flow' was
inexpressible.

add_graph is a macro operation: nodes+edges go through the existing layered
pass (graph.ts — cycle breaking, longest-path layering, barycentre crossing
reduction) which emits ordinary container/box/link operations, and the
resulting block participates in the outer flexbox like any node. dir col/row
transposes the flow. No new layout code — the coordinate work was always
generic; what was missing was the entry point below page level.

Synthetic layer ids are namespaced by the graph's own id (g1__layer0), fixing
the collision that previously made a second graph per page impossible.
graph.ts gains parent/prefix/rootId options; draw_graph keeps its behaviour
as the page-level case of the same code path.

4 new tests: embedding in a flexbox column, two graphs per page, dir
transposition, unknown-endpoint errors. 565 unit tests green, 8 engine e2e
green. Acceptance: three-zone architecture diagram (person/cloud zone,
arrow-ordered pipeline zone with decision branches and a bold arrow,
cylinder/queue storage zone, cross-zone links) verified in the real editor.
This commit is contained in:
dayuan.jiang
2026-08-09 22:08:43 +09:00
parent d9cdfba3e1
commit 78e31ebb2a
5 changed files with 222 additions and 2 deletions

View File

@@ -61,6 +61,16 @@ export interface GraphEdge {
export interface GraphOptions {
/** "col" (default): layers stack downwards. "row": layers run left to right. */
flow?: "col" | "row"
/** Container to embed the graph in; absent means the page. */
parent?: string
/**
* Namespace for the synthetic layer-container ids. Without one, two graphs on one
* page would both emit `__layers`/`__layer0` and the second would be rejected as a
* duplicate id.
*/
prefix?: string
/** Id for the outer container itself; defaults to `${prefix}__layers`. */
rootId?: string
}
/** Distance between layers. */
@@ -305,12 +315,14 @@ export function graphToOperations(
// The flow axis is the OUTER container's direction; a layer runs across it.
const outerDir = flow
const layerDir = flow === "col" ? "row" : "col"
const root = `${LAYER_ID}s`
const ns = opts.prefix ?? ""
const root = opts.rootId ?? `${ns}${LAYER_ID}s`
const operations: Operation[] = [
{
op: "add_container",
id: root,
...(opts.parent ? { parent: opts.parent } : {}),
label: "",
dir: outerDir,
gap: LAYER_GAP,
@@ -344,7 +356,7 @@ export function graphToOperations(
operations.push(add(members[0], root))
return
}
const band = `${LAYER_ID}${i}`
const band = `${ns}${LAYER_ID}${i}`
operations.push({
op: "add_container",
id: band,