From ab23e48f4fd2c8084e39b4dcf5259f78e7cbf6c1 Mon Sep 17 00:00:00 2001 From: tianma-if Date: Mon, 5 Oct 2026 00:20:01 +0800 Subject: [PATCH] Tighten architecture nodes by label length --- apps/web/src/components/DiagramEditorPane.tsx | 10 +++++++- apps/web/src/lib/diagram-layout.test.mjs | 16 ++++++++++++ apps/web/src/lib/diagram-layout.ts | 1 + packages/shared/src/diagram-layout.ts | 25 ++++++++++++++++--- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/DiagramEditorPane.tsx b/apps/web/src/components/DiagramEditorPane.tsx index c88b3e30..b987a05f 100644 --- a/apps/web/src/components/DiagramEditorPane.tsx +++ b/apps/web/src/components/DiagramEditorPane.tsx @@ -174,6 +174,7 @@ import { api } from "@/lib/api"; import { EDITOR_LOCAL_SAVE_DELAY_MS, formatShortcutBinding, getNotebookMoveOptions, type ShortcutSettings } from "@/lib/app-helpers"; import { copyTextToClipboard } from "@/lib/clipboard"; import { + architectureNodeHeight, compactArchitectureNodeSize, compactFlowchartNodeSize, flowchartNodePresentation, @@ -1114,7 +1115,14 @@ const diagramNodePresentation = ( fontSize, 'font-size': fontSize, 'font-weight': kind === "architecture" ? ARCHITECTURE_NODE_FONT_WEIGHT : !node.parentId ? 650 : 500, lineHeight, }); - return { ...size, height: Math.max(size.height, text.split("\n").length * lineHeight + 16), text }; + const lineCount = text.split("\n").length; + return { + ...size, + height: kind === "architecture" + ? architectureNodeHeight(node.shape, lineCount) + : Math.max(size.height, lineCount * lineHeight + 16), + text, + }; }; const diagramNodeSize = (node: DiagramDocument["nodes"][number], kind: DiagramDocument["kind"], structure?: DiagramStructure) => { diff --git a/apps/web/src/lib/diagram-layout.test.mjs b/apps/web/src/lib/diagram-layout.test.mjs index 498f788e..c5b2c2f0 100644 --- a/apps/web/src/lib/diagram-layout.test.mjs +++ b/apps/web/src/lib/diagram-layout.test.mjs @@ -180,6 +180,22 @@ describe("diagram auto layout", () => { expect(compactArchitectureNodeSize("boundary", { width: 640, height: 360 })).toEqual({ width: 640, height: 360 }); }); + test("reserves height for wrapped architecture labels while shortening compact nodes", () => { + const document = compileDiagramIr({ + kind: "architecture", + nodes: [ + { id: "short", type: "service", label: "API" }, + { id: "medium", type: "service", label: "商品与库存查询服务" }, + { id: "long", type: "service", label: "商品与库存查询服务及活动规则配置中心" }, + ], + edges: [], + }); + const heights = Object.fromEntries(document.nodes.map((node) => [node.id, node.height])); + expect(heights.short).toBe(56); + expect(heights.medium).toBe(60); + expect(heights.long).toBeGreaterThan(heights.medium); + }); + test("turns a long ungrouped architecture pipeline downward on creation and auto layout", () => { const nodes = Array.from({ length: 17 }, (_, index) => ({ id: `service-${index}`, diff --git a/apps/web/src/lib/diagram-layout.ts b/apps/web/src/lib/diagram-layout.ts index 707eacfa..f00728f2 100644 --- a/apps/web/src/lib/diagram-layout.ts +++ b/apps/web/src/lib/diagram-layout.ts @@ -1,5 +1,6 @@ export { compileDiagramIr, + architectureNodeHeight, compactArchitectureNodeSize, compactFlowchartNodeSize, flowchartNodePresentation, diff --git a/packages/shared/src/diagram-layout.ts b/packages/shared/src/diagram-layout.ts index c8837153..8bd816ed 100644 --- a/packages/shared/src/diagram-layout.ts +++ b/packages/shared/src/diagram-layout.ts @@ -1,4 +1,4 @@ -import { compactFlowchartNodeSize, flowchartNodePresentation } from "./diagram-node-presentation"; +import { compactFlowchartNodeSize, flowchartNodePresentation, visualTextUnits } from "./diagram-node-presentation"; export { compactFlowchartNodeSize, flowchartNodePresentation } from "./diagram-node-presentation"; import { DIAGRAM_READABLE_MIN_SCALE, FLOWCHART_LAYOUT_SPACING } from "./diagram-flowchart-style"; export { @@ -131,8 +131,25 @@ export const compactArchitectureNodeSize = ( if (shape === "boundary") return authored ?? { width: 560, height: 320 }; if (shape === "database") return { width: 150, height: 72 }; if (shape === "queue") return { width: 156, height: 60 }; - if (shape === "security") return { width: 148, height: 68 }; - return { width: 156, height: 64 }; + if (shape === "security") return { width: 148, height: 60 }; + return { width: 156, height: 56 }; +}; + +export const architectureNodeHeight = (shape: DiagramNodeShape, lineCount: number) => { + const baseHeight = compactArchitectureNodeSize(shape).height; + return Math.max(baseHeight, lineCount <= 1 ? 56 : lineCount === 2 ? 60 : lineCount * 17 + 20); +}; + +const architectureNodeSizeForLabel = (shape: DiagramNodeShape, label: string) => { + const { width } = compactArchitectureNodeSize(shape); + if (shape === "boundary") return compactArchitectureNodeSize(shape); + // Reserve space during semantic layout before the browser measures the final wrapping. + const capacity = (width - 66) / 12; + const lineCount = label.split("\n").reduce( + (count, paragraph) => count + Math.max(1, Math.ceil(visualTextUnits(paragraph) / capacity)), + 0, + ); + return { width, height: architectureNodeHeight(shape, lineCount) }; }; const computeMindMapLayout = ( @@ -1115,7 +1132,7 @@ export const compileDiagramIr = (ir: DiagramIr): DiagramDocument => { const size = ir.kind === "mind-map" ? mindMapNodePresentation(node.label, mindMapNodeRole(ir.nodes, node.id), ir.structure) : ir.kind === "architecture" - ? compactArchitectureNodeSize(shape) + ? architectureNodeSizeForLabel(shape, node.label) : flowchartNodePresentation(shape, node.label); return { id: node.id,