fix(diagram): keep architecture auto layout grouped and compact

Lay out each architecture boundary independently, place ungrouped terminals in the group-level graph, stack groups when a left-to-right row would overflow, and route architecture edges orthogonally.
This commit is contained in:
tianma-if committed 2026-09-10 16:50:44 +08:00
1 parent 88f8f80b4c
commit 04439b5a1a
5 files changed
+315 -70

No files matched your search

@@ -484,6 +484,7 @@ const FLOW_QUICK_CREATE_WIDTH = 330;
const FLOW_QUICK_CREATE_HEIGHT = 132;
const createId = (prefix: string) => `${prefix}-${crypto.randomUUID()}`;
const isConnectableDiagram = (kind: DiagramDocument["kind"]) => kind !== "mind-map";
const usesOrthogonalDiagramEdges = (kind: DiagramDocument["kind"]) => kind === "flowchart" || kind === "architecture";
const architectureNodeLabel = (shape: DiagramNodeShape, t: (key: string) => string) => {
const labels: Partial<Record<DiagramNodeShape, string>> = {
client: t("diagram.newClient"),
@@ -1029,7 +1030,7 @@ const edgeMetadata = (
id: edge.id,
source: { cell: edge.source },
target: { cell: edge.target },
router: kind === "flowchart" ? FLOWCHART_EDGE_ROUTER : undefined,
router: usesOrthogonalDiagramEdges(kind) ? FLOWCHART_EDGE_ROUTER : undefined,
connector: kind === "mind-map"
? { name: MIND_MAP_CONNECTOR_NAME, args: { sourceWidth: mindEdge?.sourceWidth, targetWidth: mindEdge?.targetWidth, structure } }
: { name: "rounded", args: { radius: 10 } },
@@ -1579,7 +1580,7 @@ export const DiagramEditorPane = ({
allowMulti: false,
highlight: isConnectableDiagram(document.kind),
snap: { radius: 24 },
router: document.kind === "flowchart" ? FLOWCHART_EDGE_ROUTER : "normal",
router: usesOrthogonalDiagramEdges(document.kind) ? FLOWCHART_EDGE_ROUTER : "normal",
connector: document.kind === "mind-map" ? MIND_MAP_CONNECTOR_NAME : "rounded",
validateConnection: ({ sourceCell, targetCell, sourcePort, targetPort }) => {
if (!isConnectableDiagram(document.kind) || !sourceCell || !sourcePort) return false;
@@ -1647,7 +1648,7 @@ export const DiagramEditorPane = ({
}
}
graph.addEdges(document.edges.map((edge) => edgeMetadata(edge, document.kind, documentTheme, appearance, documentStructure)));
if (document.kind === "flowchart") applyFlowchartEdgePorts(graph);
if (usesOrthogonalDiagramEdges(document.kind)) applyFlowchartEdgePorts(graph);
applyGraphPalette(graph, documentTheme, document.kind, appearance, documentStructure);
graph.on("scale", () => setZoomPercent(Math.round(graph.scale().sx * 100)));
graph.cleanHistory();
@@ -1818,7 +1819,7 @@ export const DiagramEditorPane = ({
target: { cell: currentCell.id, ...(currentPort ? { port: currentPort } : {}) },
});
graph.stopBatch("connect");
if (document.kind === "flowchart") applyFlowchartEdgePorts(graph);
if (usesOrthogonalDiagramEdges(document.kind)) applyFlowchartEdgePorts(graph);
return;
}
if (!currentPoint || !containerRef.current) {
@@ -2393,7 +2394,7 @@ export const DiagramEditorPane = ({
node.resize(geometry.width, geometry.height);
}
}
if (document.kind === "flowchart") applyFlowchartEdgePorts(graph);
if (usesOrthogonalDiagramEdges(document.kind)) applyFlowchartEdgePorts(graph);
if (document.kind === "mind-map") applyMindMapHierarchy(graph, themeRef.current, appearanceRef.current, structureRef.current);
graph.stopBatch("layout");
ensureDiagramPaperContainsNodes(graph);
+74
View File
@@ -238,6 +238,80 @@ describe("diagram auto layout", () => {
expect(result.viewport.minScale).toBe(0.64);
});
test("keeps ungrouped architecture terminals next to the groups they connect to", () => {
const document = compileDiagramIr({
kind: "architecture",
nodes: [
{ id: "client", label: "Client", type: "client" },
{ id: "app", label: "App", type: "boundary" },
{ id: "web", label: "Web", type: "frontend", parentId: "app" },
{ id: "core", label: "Core", type: "boundary" },
{ id: "api", label: "API", type: "service", parentId: "core" },
{ id: "out", label: "CDN", type: "external" },
],
edges: [
{ source: "client", target: "web" },
{ source: "web", target: "api" },
{ source: "api", target: "out" },
],
});
const byId = Object.fromEntries(document.nodes.map((node) => [node.id, node]));
const center = (id) => ({ x: byId[id].x + byId[id].width / 2, y: byId[id].y + byId[id].height / 2 });
const distance = (leftId, rightId) => {
const left = center(leftId);
const right = center(rightId);
return Math.hypot(left.x - right.x, left.y - right.y);
};
const overlaps = (leftId, rightId, gap = 8) => {
const left = byId[leftId];
const right = byId[rightId];
return left.x < right.x + right.width + gap && left.x + left.width + gap > right.x
&& left.y < right.y + right.height + gap && left.y + left.height + gap > right.y;
};
expect(byId.client.x).toBeLessThan(byId.web.x);
expect(byId.web.x).toBeLessThan(byId.api.x);
expect(byId.api.x).toBeLessThan(byId.out.x);
expect(distance("client", "web")).toBeLessThan(420);
expect(distance("out", "api")).toBeLessThan(420);
expect(overlaps("client", "app")).toBeFalse();
expect(overlaps("out", "core")).toBeFalse();
});
test("lays out architecture group internals without letting cross-group back edges stretch the group", () => {
const document = compileDiagramIr({
kind: "architecture",
nodes: [
{ id: "ingress", label: "Ingress", type: "boundary" },
{ id: "a1", label: "Parse", type: "frontend", parentId: "ingress" },
{ id: "a2", label: "Guard", type: "security", parentId: "ingress" },
{ id: "runtime", label: "Runtime", type: "boundary" },
{ id: "b1", label: "Encode", type: "storage", parentId: "runtime" },
{ id: "b2", label: "Attend", type: "service", parentId: "runtime" },
{ id: "b3", label: "Project", type: "service", parentId: "runtime" },
{ id: "loop", label: "Cache", type: "database" },
],
edges: [
{ source: "a1", target: "a2" },
{ source: "a2", target: "b1" },
{ source: "b1", target: "b2" },
{ source: "b2", target: "b3" },
{ source: "b3", target: "loop" },
{ source: "loop", target: "b1" },
],
});
const byId = Object.fromEntries(document.nodes.map((node) => [node.id, node]));
const centerY = (id) => byId[id].y + byId[id].height / 2;
expect(byId.a1.x).toBeLessThan(byId.a2.x);
expect(Math.abs(centerY("a1") - centerY("a2"))).toBeLessThanOrEqual(24);
expect(byId.b1.x).toBeLessThan(byId.b2.x);
expect(byId.b2.x).toBeLessThan(byId.b3.x);
expect(Math.abs(centerY("b1") - centerY("b3"))).toBeLessThanOrEqual(24);
expect(byId.runtime.height).toBeLessThan(280);
const loopCenter = { x: byId.loop.x + byId.loop.width / 2, y: byId.loop.y + byId.loop.height / 2 };
const b3Center = { x: byId.b3.x + byId.b3.width / 2, y: byId.b3.y + byId.b3.height / 2 };
expect(Math.hypot(loopCenter.x - b3Center.x, loopCenter.y - b3Center.y)).toBeLessThan(420);
});
test("keeps flowchart viewports at reading size instead of shrinking the whole stack", () => {
expect(getDiagramLayoutViewport("flowchart")).toEqual({
anchor: "center",
+224 -57
View File
@@ -112,6 +112,11 @@ const FLOWCHART_DETACHED_ROW_WIDTH = 960;
const ARCHITECTURE_LAYOUT_ROW_WIDTH = 1480;
const ARCHITECTURE_GROUP_HORIZONTAL_GAP = 72;
const ARCHITECTURE_GROUP_VERTICAL_GAP = 88;
const ARCHITECTURE_GROUP_PAD_X = 36;
const ARCHITECTURE_GROUP_PAD_Y = 56;
const ARCHITECTURE_LAYOUT_SPACING = { rank: 96, node: 40 };
const ARCHITECTURE_META_SPACING = { rank: 88, node: 56 };
const ARCHITECTURE_ORIGIN = 32;
export const compactArchitectureNodeSize = (
shape: DiagramNodeShape,
@@ -441,25 +446,62 @@ const computeMindMapLayout = (
return layoutBranchMap(mindMapIsOneSided(document.structure), MIND_MAP_HORIZONTAL_GAP, "center");
};
const architectureTopLevelBoundaryId = (
nodeId: string,
nodeById: Map<string, DiagramDocument["nodes"][number]>,
) => {
let current = nodeById.get(nodeId);
let boundaryId: string | undefined;
const visited = new Set<string>();
while (current?.parentId && !visited.has(current.parentId)) {
visited.add(current.parentId);
const parent = nodeById.get(current.parentId);
if (!parent) break;
if (parent.shape === "boundary") boundaryId = parent.id;
current = parent;
}
return boundaryId;
};
const layoutDagreGraph = (
nodes: Array<{ id: string; width: number; height: number }>,
edges: Array<{ source: string; target: string }>,
direction: "left-to-right" | "top-to-bottom",
spacing: { rank: number; node: number } = { rank: 96, node: 40 },
margin = { x: 32, y: 32 },
): DiagramLayoutPositions => {
if (nodes.length === 0) return {};
const layoutGraph = new graphlib.Graph();
layoutGraph.setGraph({
rankdir: direction === "top-to-bottom" ? "TB" : "LR",
ranksep: spacing.rank,
nodesep: spacing.node,
marginx: margin.x,
marginy: margin.y,
});
layoutGraph.setDefaultEdgeLabel(() => ({}));
const layoutNodeIds = new Set(nodes.map((node) => node.id));
for (const node of nodes) layoutGraph.setNode(node.id, { width: node.width, height: node.height });
for (const edge of edges) {
if (edge.source === edge.target) continue;
if (layoutNodeIds.has(edge.source) && layoutNodeIds.has(edge.target)) layoutGraph.setEdge(edge.source, edge.target);
}
runDagreLayout(layoutGraph);
return Object.fromEntries(nodes.flatMap((node) => {
const position = layoutGraph.node(node.id) as { x: number; y: number } | undefined;
if (!position) return [];
return [[node.id, {
x: Math.round(position.x - node.width / 2),
y: Math.round(position.y - node.height / 2),
}]];
}));
};
const wrapArchitectureGroups = (
document: DiagramDocument,
positions: DiagramLayoutPositions,
) => {
const nodeById = new Map(document.nodes.map((node) => [node.id, node]));
const topLevelBoundaryId = (nodeId: string) => {
let current = nodeById.get(nodeId);
let boundaryId: string | undefined;
const visited = new Set<string>();
while (current?.parentId && !visited.has(current.parentId)) {
visited.add(current.parentId);
const parent = nodeById.get(current.parentId);
if (!parent) break;
if (parent.shape === "boundary") boundaryId = parent.id;
current = parent;
}
return boundaryId;
};
const topLevelBoundaries = document.nodes.filter((node) => (
node.shape === "boundary" && (!node.parentId || nodeById.get(node.parentId)?.shape !== "boundary")
));
@@ -467,7 +509,9 @@ const wrapArchitectureGroups = (
const groups = topLevelBoundaries.flatMap((boundary) => {
const members = document.nodes.filter((node) => (
node.shape !== "boundary" && topLevelBoundaryId(node.id) === boundary.id && positions[node.id]
node.shape !== "boundary"
&& architectureTopLevelBoundaryId(node.id, nodeById) === boundary.id
&& positions[node.id]
));
if (members.length === 0) return [];
const left = Math.min(...members.map((node) => positions[node.id].x));
@@ -476,30 +520,32 @@ const wrapArchitectureGroups = (
const bottom = Math.max(...members.map((node) => positions[node.id].y + node.height));
return [{
boundary,
members: document.nodes.filter((node) => node.id === boundary.id || topLevelBoundaryId(node.id) === boundary.id),
members: document.nodes.filter((node) => (
node.id === boundary.id || architectureTopLevelBoundaryId(node.id, nodeById) === boundary.id
)),
left,
top,
width: Math.max(260, right - left + 72),
height: Math.max(180, bottom - top + 92),
width: Math.max(260, right - left + ARCHITECTURE_GROUP_PAD_X * 2),
height: Math.max(180, bottom - top + ARCHITECTURE_GROUP_PAD_Y + ARCHITECTURE_GROUP_PAD_X),
}];
}).sort((left, right) => left.left - right.left || left.top - right.top || left.boundary.id.localeCompare(right.boundary.id));
if (groups.length < 2) return positions;
const contentLeft = Math.min(...groups.map((group) => group.left - 36));
const contentRight = Math.max(...groups.map((group) => group.left - 36 + group.width));
const contentLeft = Math.min(...groups.map((group) => group.left - ARCHITECTURE_GROUP_PAD_X));
const contentRight = Math.max(...groups.map((group) => group.left - ARCHITECTURE_GROUP_PAD_X + group.width));
if (contentRight - contentLeft <= ARCHITECTURE_LAYOUT_ROW_WIDTH) return positions;
let cursorX = 32;
let cursorY = 32;
let cursorX = ARCHITECTURE_ORIGIN;
let cursorY = ARCHITECTURE_ORIGIN;
let rowHeight = 0;
for (const group of groups) {
if (cursorX > 32 && cursorX + group.width > 32 + ARCHITECTURE_LAYOUT_ROW_WIDTH) {
cursorX = 32;
if (cursorX > ARCHITECTURE_ORIGIN && cursorX + group.width > ARCHITECTURE_ORIGIN + ARCHITECTURE_LAYOUT_ROW_WIDTH) {
cursorX = ARCHITECTURE_ORIGIN;
cursorY += rowHeight + ARCHITECTURE_GROUP_VERTICAL_GAP;
rowHeight = 0;
}
const deltaX = cursorX + 36 - group.left;
const deltaY = cursorY + 56 - group.top;
const deltaX = cursorX + ARCHITECTURE_GROUP_PAD_X - group.left;
const deltaY = cursorY + ARCHITECTURE_GROUP_PAD_Y - group.top;
for (const node of group.members) {
const position = positions[node.id] ?? { x: node.x, y: node.y };
positions[node.id] = { x: position.x + deltaX, y: position.y + deltaY };
@@ -510,41 +556,59 @@ const wrapArchitectureGroups = (
return positions;
};
const placeUnpositionedArchitectureNodes = (
document: DiagramDocument,
positions: DiagramLayoutPositions,
) => {
const nodeById = new Map(document.nodes.map((node) => [node.id, node]));
const detached = document.nodes.filter((node) => (
node.shape !== "boundary"
&& !positions[node.id]
&& !architectureTopLevelBoundaryId(node.id, nodeById)
)).sort((left, right) => left.y - right.y || left.x - right.x || left.id.localeCompare(right.id));
if (detached.length === 0) return positions;
const placed = document.nodes.filter((node) => node.shape !== "boundary" && positions[node.id]);
const contentLeft = placed.length > 0
? Math.min(...placed.map((node) => positions[node.id].x))
: ARCHITECTURE_ORIGIN;
const contentBottom = placed.length > 0
? Math.max(...placed.map((node) => positions[node.id].y + node.height))
: ARCHITECTURE_ORIGIN;
let cursorX = contentLeft;
let cursorY = contentBottom + FLOWCHART_DETACHED_GAP;
let rowHeight = 0;
for (const node of detached) {
if (cursorX > contentLeft && cursorX + node.width > contentLeft + FLOWCHART_DETACHED_ROW_WIDTH) {
cursorX = contentLeft;
cursorY += rowHeight + FLOWCHART_DETACHED_ROW_GAP;
rowHeight = 0;
}
positions[node.id] = { x: cursorX, y: cursorY };
cursorX += node.width + 40;
rowHeight = Math.max(rowHeight, node.height);
}
return positions;
};
const computeDagreLayout = (
document: DiagramDocument,
options: DiagramLayoutOptions,
excludeBoundaries: boolean,
spacing: { rank: number; node: number } = { rank: 96, node: 40 },
): DiagramLayoutPositions => {
const layoutGraph = new graphlib.Graph();
layoutGraph.setGraph({
rankdir: options.direction === "top-to-bottom" ? "TB" : "LR",
ranksep: spacing.rank,
nodesep: spacing.node,
marginx: 32,
marginy: 32,
});
layoutGraph.setDefaultEdgeLabel(() => ({}));
const layoutNodes = excludeBoundaries
? document.nodes.filter((node) => node.shape !== "boundary")
: document.nodes;
const layoutNodeIds = new Set(layoutNodes.map((node) => node.id));
for (const node of layoutNodes) layoutGraph.setNode(node.id, { width: node.width, height: node.height });
for (const edge of document.edges) {
if (layoutNodeIds.has(edge.source) && layoutNodeIds.has(edge.target)) layoutGraph.setEdge(edge.source, edge.target);
const positions = layoutDagreGraph(
layoutNodes,
document.edges,
options.direction === "top-to-bottom" ? "top-to-bottom" : "left-to-right",
spacing,
);
for (const node of document.nodes) {
if (node.shape === "boundary" && !positions[node.id]) positions[node.id] = { x: node.x, y: node.y };
}
runDagreLayout(layoutGraph);
const positions = Object.fromEntries(document.nodes.flatMap((node) => {
if (node.shape === "boundary") return [[node.id, { x: node.x, y: node.y }]];
const position = layoutGraph.node(node.id) as { x: number; y: number } | undefined;
if (!position) return [];
return [[node.id, {
x: Math.round(position.x - node.width / 2),
y: Math.round(position.y - node.height / 2),
}]];
}));
return positions;
};
@@ -651,12 +715,105 @@ const computeFlowchartLayout = (document: DiagramDocument, options: DiagramLayou
return placeDetachedFlowchartNodes(document, positions);
};
const computeArchitectureLayout = (document: DiagramDocument, options: DiagramLayoutOptions) => (
wrapArchitectureGroups(
document,
computeDagreLayout(document, { ...options, direction: options.direction ?? "left-to-right" }, true),
)
);
const computeArchitectureLayout = (document: DiagramDocument, options: DiagramLayoutOptions) => {
const direction = options.direction ?? "left-to-right";
const nodeById = new Map(document.nodes.map((node) => [node.id, node]));
const topLevelBoundaries = document.nodes.filter((node) => (
node.shape === "boundary" && (!node.parentId || nodeById.get(node.parentId)?.shape !== "boundary")
));
const groups = topLevelBoundaries.flatMap((boundary) => {
const members = document.nodes.filter((node) => (
node.shape !== "boundary" && architectureTopLevelBoundaryId(node.id, nodeById) === boundary.id
));
return members.length > 0 ? [{ boundary, members }] : [];
});
if (groups.length === 0) {
return computeDagreLayout(document, { ...options, direction }, true, ARCHITECTURE_LAYOUT_SPACING);
}
const innerByGroup = new Map<string, DiagramLayoutPositions>();
const groupMetaNodes = groups.map((group) => {
const memberIds = new Set(group.members.map((node) => node.id));
const intraEdges = document.edges.filter((edge) => memberIds.has(edge.source) && memberIds.has(edge.target));
const inner = layoutDagreGraph(group.members, intraEdges, direction, ARCHITECTURE_LAYOUT_SPACING, { x: 0, y: 0 });
const left = Math.min(...group.members.map((node) => inner[node.id].x));
const top = Math.min(...group.members.map((node) => inner[node.id].y));
const normalized = Object.fromEntries(group.members.map((node) => [node.id, {
x: inner[node.id].x - left,
y: inner[node.id].y - top,
}]));
innerByGroup.set(group.boundary.id, normalized);
const width = Math.max(...group.members.map((node) => normalized[node.id].x + node.width));
const height = Math.max(...group.members.map((node) => normalized[node.id].y + node.height));
return {
id: group.boundary.id,
width: Math.max(260, width + ARCHITECTURE_GROUP_PAD_X * 2),
height: Math.max(180, height + ARCHITECTURE_GROUP_PAD_Y + ARCHITECTURE_GROUP_PAD_X),
};
});
const connectedIds = new Set<string>();
for (const edge of document.edges) {
connectedIds.add(edge.source);
connectedIds.add(edge.target);
}
const connectedFree = document.nodes.filter((node) => (
node.shape !== "boundary"
&& !architectureTopLevelBoundaryId(node.id, nodeById)
&& connectedIds.has(node.id)
));
const metaNodes = [
...groupMetaNodes,
...connectedFree.map((node) => ({ id: node.id, width: node.width, height: node.height })),
];
const metaNodeIds = new Set(metaNodes.map((node) => node.id));
const metaId = (nodeId: string) => {
const groupId = architectureTopLevelBoundaryId(nodeId, nodeById);
if (groupId && metaNodeIds.has(groupId)) return groupId;
return metaNodeIds.has(nodeId) ? nodeId : undefined;
};
const seenMetaEdges = new Set<string>();
const metaEdges: Array<{ source: string; target: string }> = [];
for (const edge of document.edges) {
const sourceId = metaId(edge.source);
const targetId = metaId(edge.target);
if (!sourceId || !targetId || sourceId === targetId) continue;
const key = `${sourceId}\0${targetId}`;
if (seenMetaEdges.has(key)) continue;
seenMetaEdges.add(key);
metaEdges.push({ source: sourceId, target: targetId });
}
const metaMargin = { x: ARCHITECTURE_ORIGIN, y: ARCHITECTURE_ORIGIN };
const metaLr = layoutDagreGraph(metaNodes, metaEdges, direction, ARCHITECTURE_META_SPACING, metaMargin);
const lrWidth = Math.max(...metaNodes.map((node) => metaLr[node.id].x + node.width))
- Math.min(...metaNodes.map((node) => metaLr[node.id].x));
const metaPositions = groups.length > 1 && lrWidth > ARCHITECTURE_LAYOUT_ROW_WIDTH
? layoutDagreGraph(metaNodes, metaEdges, "top-to-bottom", ARCHITECTURE_META_SPACING, metaMargin)
: metaLr;
const positions: DiagramLayoutPositions = Object.fromEntries(
document.nodes.filter((node) => node.shape === "boundary").map((node) => [node.id, { x: node.x, y: node.y }]),
);
for (const group of groups) {
const inner = innerByGroup.get(group.boundary.id)!;
const origin = metaPositions[group.boundary.id];
for (const member of group.members) {
positions[member.id] = {
x: origin.x + ARCHITECTURE_GROUP_PAD_X + inner[member.id].x,
y: origin.y + ARCHITECTURE_GROUP_PAD_Y + inner[member.id].y,
};
}
}
for (const node of connectedFree) {
const position = metaPositions[node.id];
if (position) positions[node.id] = position;
}
wrapArchitectureGroups(document, positions);
placeUnpositionedArchitectureNodes(document, positions);
return positions;
};
const irNodeShape = (kind: DiagramKind, type: DiagramIrNodeType | undefined): DiagramNodeShape => {
if (kind === "mind-map") return "topic";
@@ -723,6 +880,16 @@ const finalizeArchitectureLayout = (document: DiagramDocument) => {
}
placed.push(boundary);
}
const minX = Math.min(...document.nodes.map((node) => node.x));
const minY = Math.min(...document.nodes.map((node) => node.y));
const deltaX = minX < ARCHITECTURE_ORIGIN ? ARCHITECTURE_ORIGIN - minX : 0;
const deltaY = minY < ARCHITECTURE_ORIGIN ? ARCHITECTURE_ORIGIN - minY : 0;
if (deltaX === 0 && deltaY === 0) return;
for (const node of document.nodes) {
node.x += deltaX;
node.y += deltaY;
}
};
export type DiagramLayoutStrategy = {
+8 -8
View File
@@ -165,24 +165,24 @@ export const diagramDocumentToX6Cells = (
?? mindEdge?.stroke
?? flowchartSurface?.edge
?? palette.flowEdge;
const flowchartPorts = document.kind === "flowchart" && sourceNode && targetNode
const orthogonalPorts = (document.kind === "flowchart" || document.kind === "architecture") && sourceNode && targetNode
? flowchartEdgePorts(sourceNode, targetNode)
: null;
const flowchartStraight = Boolean(flowchartPorts && sourceNode && targetNode && flowchartEdgeIsStraight(sourceNode, targetNode));
const orthogonalStraight = Boolean(orthogonalPorts && sourceNode && targetNode && flowchartEdgeIsStraight(sourceNode, targetNode));
return {
id: edge.id,
source: document.kind === "mind-map"
? { cell: edge.source, ...(sourceTerminal ?? { anchor: { name: sides.source } }) }
: flowchartPorts
? { cell: edge.source, port: flowchartPorts.source }
: orthogonalPorts
? { cell: edge.source, port: orthogonalPorts.source }
: { cell: edge.source },
target: document.kind === "mind-map"
? { cell: edge.target, ...(targetTerminal ?? { anchor: { name: sides.target } }) }
: flowchartPorts
? { cell: edge.target, port: flowchartPorts.target }
: orthogonalPorts
? { cell: edge.target, port: orthogonalPorts.target }
: { cell: edge.target },
router: document.kind === "flowchart"
? (flowchartStraight ? { name: "normal" } : FLOWCHART_EDGE_ROUTER)
router: document.kind === "flowchart" || document.kind === "architecture"
? (orthogonalStraight ? { name: "normal" } : FLOWCHART_EDGE_ROUTER)
: undefined,
connector: document.kind === "mind-map"
? {
+3
View File
@@ -184,6 +184,9 @@ describe("diagram document", () => {
expect(dark.nodes.find((node) => node.id === "api").attrs.body.fill).not.toBe(
dark.nodes.find((node) => node.id === "database").attrs.body.fill,
);
expect(projected.edges[0].source.port).toBeDefined();
expect(projected.edges[0].target.port).toBeDefined();
expect(["normal", "manhattan"]).toContain(projected.edges[0].router.name);
});
test("rejects malformed and dangling graph data", () => {