fix(diagram): route architecture edges around nodes
This commit is contained in:
1 parent
b64772a46b
commit
c72b5b277d
5 files changed
+83
-8
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user