fix(diagram): balance architecture group layouts

This commit is contained in:
tianma-if committed 2026-10-04 23:46:53 +08:00
1 parent 940c4d8cde
commit 43e5675d0a
2 files changed
+77 -2

No files matched your search

+29
View File
@@ -286,6 +286,35 @@ describe("diagram auto layout", () => {
expect(new Set(boundaries.map((node) => node.y)).size).toBeGreaterThan(1);
});
test("uses two rows of architecture groups when a single column wastes horizontal space", () => {
const nodes = [];
const edges = [];
let previousId;
for (let groupIndex = 0; groupIndex < 4; groupIndex += 1) {
const boundaryId = `group-${groupIndex}`;
nodes.push({ id: boundaryId, label: boundaryId, type: "boundary" });
for (let nodeIndex = 0; nodeIndex < 2; nodeIndex += 1) {
const id = `${boundaryId}-node-${nodeIndex}`;
nodes.push({ id, label: id, type: "service", parentId: boundaryId });
if (previousId) edges.push({ source: previousId, target: id });
previousId = id;
}
}
const document = compileDiagramIr({ kind: "architecture", nodes, edges });
const boundaries = document.nodes.filter((node) => node.shape === "boundary");
const width = Math.max(...boundaries.map((node) => node.x + node.width))
- Math.min(...boundaries.map((node) => node.x));
expect(width).toBeGreaterThan(900);
expect(width).toBeLessThanOrEqual(1480);
expect(new Set(boundaries.map((node) => node.y)).size).toBe(2);
const relayout = computeDiagramLayoutResult(document);
const relayoutWidth = Math.max(...boundaries.map((node) => relayout.nodes[node.id].x + relayout.nodes[node.id].width))
- Math.min(...boundaries.map((node) => relayout.nodes[node.id].x));
expect(relayoutWidth).toBeGreaterThan(900);
const vertical = computeDiagramLayoutResult(document, { direction: "top-to-bottom" });
expect(vertical.nodes["group-1"].y).toBeGreaterThan(vertical.nodes["group-0"].y);
});
test("returns a complete strategy result for every diagram kind", () => {
for (const kind of ["mind-map", "flowchart", "architecture"]) {
const document = createDefaultDiagramDocument(kind);
+48 -2
View File
@@ -115,6 +115,7 @@ const FLOWCHART_DETACHED_GAP = 56;
const FLOWCHART_DETACHED_ROW_GAP = 24;
const FLOWCHART_DETACHED_ROW_WIDTH = 960;
const ARCHITECTURE_LAYOUT_ROW_WIDTH = 1480;
const ARCHITECTURE_LAYOUT_TARGET_RATIO = 1.35;
const ARCHITECTURE_GROUP_HORIZONTAL_GAP = 72;
const ARCHITECTURE_GROUP_VERTICAL_GAP = 88;
const ARCHITECTURE_GROUP_PAD_X = 36;
@@ -519,6 +520,52 @@ const layoutArchitectureGraph = (
return width(vertical) < width(positions) ? vertical : positions;
};
const layoutArchitectureMetaGraph = (
nodes: Array<{ id: string; width: number; height: number }>,
edges: Array<{ source: string; target: string }>,
direction: "left-to-right" | "top-to-bottom",
margin: { x: number; y: number },
): DiagramLayoutPositions => {
const vertical = layoutDagreGraph(nodes, edges, "top-to-bottom", ARCHITECTURE_META_SPACING, margin);
if (direction === "top-to-bottom" || nodes.length === 0) return vertical;
const horizontal = layoutDagreGraph(nodes, edges, "left-to-right", ARCHITECTURE_META_SPACING, margin);
const size = (positions: DiagramLayoutPositions) => ({
width: Math.max(...nodes.map((node) => positions[node.id].x + node.width))
- Math.min(...nodes.map((node) => positions[node.id].x)),
height: Math.max(...nodes.map((node) => positions[node.id].y + node.height))
- Math.min(...nodes.map((node) => positions[node.id].y)),
});
if (size(horizontal).width <= ARCHITECTURE_LAYOUT_ROW_WIDTH) return horizontal;
if (nodes.length < 3) return vertical;
const wrapped: DiagramLayoutPositions = {};
let x = margin.x;
let y = margin.y;
let rowHeight = 0;
for (const node of [...nodes].sort((left, right) => (
horizontal[left.id].x - horizontal[right.id].x
|| horizontal[left.id].y - horizontal[right.id].y
|| left.id.localeCompare(right.id)
))) {
if (x > margin.x && x + node.width > margin.x + ARCHITECTURE_LAYOUT_ROW_WIDTH) {
x = margin.x;
y += rowHeight + ARCHITECTURE_GROUP_VERTICAL_GAP;
rowHeight = 0;
}
wrapped[node.id] = { x, y };
x += node.width + ARCHITECTURE_GROUP_HORIZONTAL_GAP;
rowHeight = Math.max(rowHeight, node.height);
}
const fitCost = (positions: DiagramLayoutPositions) => {
const bounds = size(positions);
const targetHeight = ARCHITECTURE_LAYOUT_ROW_WIDTH / ARCHITECTURE_LAYOUT_TARGET_RATIO;
const overflow = Math.max(bounds.width / ARCHITECTURE_LAYOUT_ROW_WIDTH, bounds.height / targetHeight);
const aspectError = Math.abs(Math.log((bounds.width / bounds.height) / ARCHITECTURE_LAYOUT_TARGET_RATIO));
return overflow + aspectError * 0.2;
};
return fitCost(wrapped) < fitCost(vertical) ? wrapped : vertical;
};
const wrapArchitectureGroups = (
document: DiagramDocument,
positions: DiagramLayoutPositions,
@@ -869,11 +916,10 @@ const computeArchitectureLayout = (document: DiagramDocument, options: DiagramLa
}
const metaMargin = { x: ARCHITECTURE_ORIGIN, y: ARCHITECTURE_ORIGIN };
const metaPositions = layoutArchitectureGraph(
const metaPositions = layoutArchitectureMetaGraph(
metaNodes,
metaEdges,
direction,
ARCHITECTURE_META_SPACING,
metaMargin,
);