mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-02 01:20:23 +08:00
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:
109
tests/unit/diagram-engine-embed-graph.test.ts
Normal file
109
tests/unit/diagram-engine-embed-graph.test.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { restructureDiagram } from "@/lib/diagram-engine"
|
||||
|
||||
/**
|
||||
* add_graph: arrow-driven layout as a CONTAINER, not just a page-level tool.
|
||||
*
|
||||
* D2/TALA's core claim is that containers are first-class at every layout stage —
|
||||
* hierarchical zones and arrow-ordered graphs mix in one diagram. Before this, the
|
||||
* engine was split: draw_graph did whole-page flowcharts, containers did nesting, and
|
||||
* "a poster column with a small flowchart in it" was inexpressible.
|
||||
*/
|
||||
|
||||
const rectOf = (xml: string, id: string) => {
|
||||
const m = xml.match(
|
||||
new RegExp(
|
||||
`id="${id}"[^>]*>\\s*<mxGeometry[^>]*width="([\\d.]+)" height="([\\d.]+)"`,
|
||||
),
|
||||
)
|
||||
if (!m) throw new Error(`no geometry for ${id}`)
|
||||
return { w: Number(m[1]), h: Number(m[2]) }
|
||||
}
|
||||
|
||||
const FLOW = {
|
||||
nodes: [
|
||||
{ id: "a", label: "request" },
|
||||
{ id: "b", label: "validate", shape: "decision" },
|
||||
{ id: "c", label: "process" },
|
||||
{ id: "d", label: "reject" },
|
||||
],
|
||||
edges: [
|
||||
{ source: "a", target: "b" },
|
||||
{ source: "b", target: "c", label: "ok" },
|
||||
{ source: "b", target: "d", label: "no" },
|
||||
],
|
||||
}
|
||||
|
||||
describe("add_graph", () => {
|
||||
it("embeds an arrow-ordered graph inside a flexbox column", () => {
|
||||
const r = restructureDiagram("", [
|
||||
{ op: "add_container", id: "page", label: "", dir: "row", gap: 20 },
|
||||
{
|
||||
op: "add_container",
|
||||
id: "left",
|
||||
parent: "page",
|
||||
label: "",
|
||||
dir: "col",
|
||||
gap: 12,
|
||||
},
|
||||
{
|
||||
op: "add_box",
|
||||
id: "intro",
|
||||
parent: "left",
|
||||
label: "How requests flow:",
|
||||
},
|
||||
{ op: "add_graph", id: "flow", parent: "left", ...FLOW },
|
||||
{ op: "add_box", id: "right", parent: "page", label: "Notes" },
|
||||
])
|
||||
expect(r.errors).toEqual([])
|
||||
const xml = r.xml as string
|
||||
// The decision's two branches share a layer — arrow-driven, not declaration order.
|
||||
const c = rectOf(xml, "c")
|
||||
const d = rectOf(xml, "d")
|
||||
expect(c).toBeTruthy()
|
||||
expect(d).toBeTruthy()
|
||||
// And the graph sits inside the column: the outline nests flow under left.
|
||||
expect(r.outline).toMatch(/left:[\s\S]*flow:/)
|
||||
})
|
||||
|
||||
it("two graphs on one page do not collide on synthetic layer ids", () => {
|
||||
const r = restructureDiagram("", [
|
||||
{ op: "add_graph", id: "g1", ...FLOW },
|
||||
{
|
||||
op: "add_graph",
|
||||
id: "g2",
|
||||
nodes: [
|
||||
{ id: "x", label: "start" },
|
||||
{ id: "y", label: "end" },
|
||||
],
|
||||
edges: [{ source: "x", target: "y" }],
|
||||
},
|
||||
])
|
||||
expect(r.errors).toEqual([])
|
||||
expect(r.xml).toContain('id="g1__layer')
|
||||
expect(r.xml).toContain('id="g2"')
|
||||
})
|
||||
|
||||
it("dir=row transposes the flow", () => {
|
||||
const make = (dir: "col" | "row") =>
|
||||
restructureDiagram("", [{ op: "add_graph", id: "g", dir, ...FLOW }])
|
||||
.xml as string
|
||||
const down = rectOf(make("col"), "g")
|
||||
const right = rectOf(make("row"), "g")
|
||||
// Four layers tall vs four layers wide.
|
||||
expect(down.h).toBeGreaterThan(down.w * 0.8)
|
||||
expect(right.w).toBeGreaterThan(right.h)
|
||||
})
|
||||
|
||||
it("reports unknown edge endpoints as an error", () => {
|
||||
const r = restructureDiagram("", [
|
||||
{
|
||||
op: "add_graph",
|
||||
id: "g",
|
||||
nodes: [{ id: "a", label: "a" }],
|
||||
edges: [{ source: "a", target: "ghost" }],
|
||||
},
|
||||
])
|
||||
expect(r.errors.join(" ")).toContain("ghost")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user