fix(diagram): route architecture edges around nodes

This commit is contained in:
tianma-if committed 2026-10-04 23:23:45 +08:00
1 parent b64772a46b
commit c72b5b277d
5 files changed
+83 -8

No files matched your search

+28 -3
View File
@@ -116,6 +116,7 @@ import {
ARCHITECTURE_NODE_LINE_HEIGHT,
ARCHITECTURE_SHAPE_RESOURCE,
architectureEdgePorts,
architectureEdgeRouter,
architectureEdgeVisual,
architectureIconOffset,
architectureNodeVisual,
@@ -1348,7 +1349,9 @@ const edgeMetadata = (
id: edge.id,
source: { cell: edge.source },
target: { cell: edge.target },
router: usesOrthogonalDiagramEdges(kind) ? FLOWCHART_EDGE_ROUTER : undefined,
router: kind === "architecture"
? architectureEdgeRouter(edge.source, edge.target, [])
: 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 } },
@@ -1370,6 +1373,9 @@ const edgeMetadata = (
};
const applyOrthogonalEdgePorts = (graph: Graph, kind: DiagramDocument["kind"]) => {
const boundaryIds = kind === "architecture"
? graph.getNodes().filter((node) => node.getData<NodeData>()?.shape === "boundary").map((node) => node.id)
: [];
for (const edge of graph.getEdges()) {
const source = edge.getSourceNode();
const target = edge.getTargetNode();
@@ -1381,7 +1387,9 @@ const applyOrthogonalEdgePorts = (graph: Graph, kind: DiagramDocument["kind"]) =
: flowchartEdgePorts(sourceBox, targetBox);
edge.setSource({ cell: source.id, port: ports.source });
edge.setTarget({ cell: target.id, port: ports.target });
edge.setRouter(flowchartEdgeIsStraight(sourceBox, targetBox) ? { name: "normal" } : FLOWCHART_EDGE_ROUTER);
edge.setRouter(kind === "architecture"
? architectureEdgeRouter(source.id, target.id, boundaryIds)
: flowchartEdgeIsStraight(sourceBox, targetBox) ? { name: "normal" } : FLOWCHART_EDGE_ROUTER);
}
};
@@ -1949,7 +1957,9 @@ export const DiagramEditorPane = ({
allowMulti: false,
highlight: isConnectableDiagram(document.kind),
snap: { radius: 24 },
router: usesOrthogonalDiagramEdges(document.kind) ? FLOWCHART_EDGE_ROUTER : "normal",
router: document.kind === "architecture"
? architectureEdgeRouter("", "", [])
: 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;
@@ -2053,6 +2063,16 @@ export const DiagramEditorPane = ({
}, SCROLLER_AUTORESIZE_SETTLE_MS);
const updateHistory = () => setHistoryState({ undo: graph.canUndo(), redo: graph.canRedo() });
const refreshArchitecturePorts = () => {
if (document.kind !== "architecture") return;
const historyEnabled = graph.isHistoryEnabled();
if (historyEnabled) graph.disableHistory();
try {
applyOrthogonalEdgePorts(graph, document.kind);
} finally {
if (historyEnabled) graph.enableHistory();
}
};
const markDirty = () => {
if (viewOnlyRef.current) return;
if (!readOnly) {
@@ -2064,6 +2084,7 @@ export const DiagramEditorPane = ({
updateHistory();
};
const clearSelectionAfterHistory = () => {
refreshArchitecturePorts();
applyGraphPalette(graph, themeRef.current, document.kind, appearanceRef.current, structureRef.current);
graph.cleanSelection();
if (isConnectableDiagram(document.kind)) setOnlyFlowNodePortsActive(graph);
@@ -2100,6 +2121,10 @@ export const DiagramEditorPane = ({
});
graph.on("node:dblclick", ({ node }: { node: Node }) => beginNodeEdit(node));
graph.on("node:mouseup", () => {
if (document.kind === "architecture") {
refreshArchitecturePorts();
return;
}
if (document.kind !== "mind-map") return;
const historyEnabled = graph.isHistoryEnabled();
if (historyEnabled) graph.disableHistory();
@@ -7,6 +7,7 @@ import {
ARCHITECTURE_EDGE_LABEL_LINE_HEIGHT,
ARCHITECTURE_SURFACES,
architectureEdgePorts,
architectureEdgeRouter,
architectureEdgeVisual,
architectureIconElements,
architectureMermaidClassDefs,
@@ -42,6 +43,19 @@ describe("architecture semantic paint", () => {
expect(architectureEdgePorts(lowerLeft, upperRight)).toEqual({ source: "right", target: "left" });
});
test("exits leftward connections from the side facing their target", () => {
const source = { x: 520, y: 340, width: 156, height: 64 };
const target = { x: 240, y: 700, width: 156, height: 64 };
expect(architectureEdgePorts(source, target)).toEqual({ source: "left", target: "right" });
});
test("keeps architecture endpoints and group containers out of obstacle routing", () => {
const router = architectureEdgeRouter("from", "to", ["group", "group"]);
expect(router.name).toBe("manhattan");
expect(router.args.excludeNodes).toEqual(["from", "to", "group"]);
expect(router.args.padding).toBeGreaterThan(0);
});
test("keeps every component fill distinct in light and dark", () => {
for (const appearance of ["light", "dark"]) {
const fills = ARCHITECTURE_COMPONENT_SHAPES.map((shape) => resolveArchitectureSurface(appearance).nodes[shape].fill);
@@ -23,6 +23,22 @@ export type ArchitectureAppearance = "light" | "dark";
export type ArchitectureEdgePortName = "top" | "right" | "bottom" | "left";
export type ArchitectureEdgeBox = { x: number; y: number; width: number; height: number };
export const architectureEdgeRouter = (
sourceId: string,
targetId: string,
boundaryIds: string[],
) => ({
name: "manhattan" as const,
args: {
padding: 12,
step: 8,
maxLoopCount: 5000,
// Boundaries are visual containers; components and endpoints are the obstacles.
// Per-edge exclusions also keep X6's shared obstacle map specific to this edge.
excludeNodes: [...new Set([sourceId, targetId, ...boundaryIds])],
},
});
export const architectureEdgePorts = (
source: ArchitectureEdgeBox,
target: ArchitectureEdgeBox,
@@ -30,7 +46,7 @@ export const architectureEdgePorts = (
const sourceRight = source.x + source.width;
const targetRight = target.x + target.width;
if (target.x >= sourceRight) return { source: "right", target: "left" };
if (targetRight <= source.x) return { source: "top", target: "top" };
if (targetRight <= source.x) return { source: "left", target: "right" };
const sourceCenterY = source.y + source.height / 2;
const targetCenterY = target.y + target.height / 2;
+9 -4
View File
@@ -4,6 +4,7 @@ import {
ARCHITECTURE_EDGE_LABEL_LINE_HEIGHT,
ARCHITECTURE_LABEL_FONT,
architectureEdgePorts,
architectureEdgeRouter,
architectureEdgeVisual,
architectureNodeVisual,
resolveArchitectureSurface,
@@ -147,6 +148,7 @@ export const diagramDocumentToX6Cells = (
};
});
const projectedById = new Map(nodes.map((node) => [node.id, node]));
const architectureBoundaryIds = document.nodes.filter((node) => node.shape === "boundary").map((node) => node.id);
const edges = document.edges.map((edge) => {
const edgeKind = edge.kind ?? (document.kind === "architecture" ? "dependency" : undefined);
const sourceNode = projectedById.get(edge.source);
@@ -183,7 +185,8 @@ export const diagramDocumentToX6Cells = (
? flowchartEdgePorts(sourceNode, targetNode)
: null
: null;
const orthogonalStraight = Boolean(orthogonalPorts && sourceNode && targetNode && flowchartEdgeIsStraight(sourceNode, targetNode));
const orthogonalStraight = document.kind === "flowchart"
&& Boolean(orthogonalPorts && sourceNode && targetNode && flowchartEdgeIsStraight(sourceNode, targetNode));
return {
id: edge.id,
source: document.kind === "mind-map"
@@ -196,9 +199,11 @@ export const diagramDocumentToX6Cells = (
: orthogonalPorts
? { cell: edge.target, port: orthogonalPorts.target }
: { cell: edge.target },
router: document.kind === "flowchart" || document.kind === "architecture"
? (orthogonalStraight ? { name: "normal" } : FLOWCHART_EDGE_ROUTER)
: undefined,
router: document.kind === "architecture"
? architectureEdgeRouter(edge.source, edge.target, architectureBoundaryIds)
: document.kind === "flowchart"
? (orthogonalStraight ? { name: "normal" } : FLOWCHART_EDGE_ROUTER)
: undefined,
connector: document.kind === "mind-map"
? {
name: MIND_MAP_CONNECTOR_NAME,
+15
View File
@@ -194,6 +194,21 @@ describe("diagram document", () => {
expect(projectedEdge.target.port).toBe("left");
});
test("routes aligned architecture edges around intervening components", () => {
const document = createDefaultDiagramDocument("architecture");
document.nodes.find((node) => node.id === "client").x = 32;
document.nodes.find((node) => node.id === "client").y = 32;
document.nodes.find((node) => node.id === "api").x = 32;
document.nodes.find((node) => node.id === "api").y = 400;
document.nodes.find((node) => node.id === "database").x = 32;
document.nodes.find((node) => node.id === "database").y = 216;
const projected = diagramDocumentToX6Cells(document, "light");
const edge = projected.edges.find((item) => item.source.cell === "client" && item.target.cell === "api");
expect(edge.router.name).toBe("manhattan");
expect(edge.router.args.excludeNodes).toContain("system");
expect(edge.router.args.excludeNodes).not.toContain("database");
});
test("projects architecture edge labels below component typography", () => {
const document = createDefaultDiagramDocument("architecture");
const projectedLabel = diagramDocumentToX6Cells(document, "light").edges[0].labels[0].attrs.label;