@hiabhee/archdraw-mcp-server 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -0
- package/dist/cli/project-link.d.ts +18 -0
- package/dist/cli/project-link.d.ts.map +1 -0
- package/dist/cli/project-link.js +129 -0
- package/dist/cli/project-link.js.map +1 -0
- package/dist/index.js +16 -812
- package/dist/index.js.map +1 -1
- package/dist/lib/checkpoints.d.ts +4 -0
- package/dist/lib/checkpoints.d.ts.map +1 -1
- package/dist/lib/checkpoints.js +14 -1
- package/dist/lib/checkpoints.js.map +1 -1
- package/dist/lib/diagram-state.d.ts +1 -0
- package/dist/lib/diagram-state.d.ts.map +1 -1
- package/dist/lib/diagram-state.js +5 -2
- package/dist/lib/diagram-state.js.map +1 -1
- package/dist/lib/http.d.ts +12 -0
- package/dist/lib/http.d.ts.map +1 -1
- package/dist/lib/http.js +11 -1
- package/dist/lib/http.js.map +1 -1
- package/dist/lib/project-binding.d.ts +29 -0
- package/dist/lib/project-binding.d.ts.map +1 -0
- package/dist/lib/project-binding.js +57 -0
- package/dist/lib/project-binding.js.map +1 -0
- package/dist/server.d.ts +32 -34
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +739 -141
- package/dist/server.js.map +1 -1
- package/dist/tools/apply-template.js +2 -2
- package/dist/tools/apply-template.js.map +1 -1
- package/dist/tools/export-diagram.d.ts +1 -1
- package/dist/tools/export-diagram.d.ts.map +1 -1
- package/dist/tools/export-diagram.js +6 -6
- package/dist/tools/export-diagram.js.map +1 -1
- package/dist/tools/fix-layout.js +2 -2
- package/dist/tools/fix-layout.js.map +1 -1
- package/dist/tools/generate-diagram.js +2 -2
- package/dist/tools/generate-diagram.js.map +1 -1
- package/dist/tools/update-diagram.js +2 -2
- package/dist/tools/update-diagram.js.map +1 -1
- package/package.json +12 -3
- package/dist/lib/__tests__/constants.test.d.ts +0 -2
- package/dist/lib/__tests__/constants.test.d.ts.map +0 -1
- package/dist/lib/__tests__/constants.test.js +0 -47
- package/dist/lib/__tests__/constants.test.js.map +0 -1
- package/dist/lib/__tests__/diagram-state.test.d.ts +0 -2
- package/dist/lib/__tests__/diagram-state.test.d.ts.map +0 -1
- package/dist/lib/__tests__/diagram-state.test.js +0 -82
- package/dist/lib/__tests__/diagram-state.test.js.map +0 -1
- package/dist/lib/__tests__/elk-runner.test.d.ts +0 -2
- package/dist/lib/__tests__/elk-runner.test.d.ts.map +0 -1
- package/dist/lib/__tests__/elk-runner.test.js +0 -87
- package/dist/lib/__tests__/elk-runner.test.js.map +0 -1
- package/dist/lib/elk-runner.d.ts +0 -29
- package/dist/lib/elk-runner.d.ts.map +0 -1
- package/dist/lib/elk-runner.js +0 -532
- package/dist/lib/elk-runner.js.map +0 -1
- package/dist/lib/prompt-builder.d.ts +0 -14
- package/dist/lib/prompt-builder.d.ts.map +0 -1
- package/dist/lib/prompt-builder.js +0 -133
- package/dist/lib/prompt-builder.js.map +0 -1
- package/dist/tools/__tests__/export-diagram.test.d.ts +0 -2
- package/dist/tools/__tests__/export-diagram.test.d.ts.map +0 -1
- package/dist/tools/__tests__/export-diagram.test.js +0 -40
- package/dist/tools/__tests__/export-diagram.test.js.map +0 -1
- package/dist/tools/__tests__/preview-links.test.d.ts +0 -2
- package/dist/tools/__tests__/preview-links.test.d.ts.map +0 -1
- package/dist/tools/__tests__/preview-links.test.js +0 -74
- package/dist/tools/__tests__/preview-links.test.js.map +0 -1
- package/dist/tools/__tests__/update-diagram.test.d.ts +0 -2
- package/dist/tools/__tests__/update-diagram.test.d.ts.map +0 -1
- package/dist/tools/__tests__/update-diagram.test.js +0 -98
- package/dist/tools/__tests__/update-diagram.test.js.map +0 -1
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { generateELKOptions, runFallbackLayout } from '../elk-runner.js';
|
|
3
|
-
function makeNode(id, overrides = {}) {
|
|
4
|
-
return {
|
|
5
|
-
id,
|
|
6
|
-
type: 'systemNode',
|
|
7
|
-
label: id,
|
|
8
|
-
layer: 'compute',
|
|
9
|
-
width: 200,
|
|
10
|
-
height: 70,
|
|
11
|
-
icon: 'box',
|
|
12
|
-
metadata: {},
|
|
13
|
-
...overrides,
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
function makeEdge(id, source, target) {
|
|
17
|
-
return {
|
|
18
|
-
id,
|
|
19
|
-
source,
|
|
20
|
-
target,
|
|
21
|
-
communicationType: 'sync',
|
|
22
|
-
pathType: 'smooth',
|
|
23
|
-
label: '',
|
|
24
|
-
labelPosition: 'center',
|
|
25
|
-
animated: false,
|
|
26
|
-
style: { stroke: '#94a3b8', strokeDasharray: '', strokeWidth: 2 },
|
|
27
|
-
markerEnd: 'arrowclosed',
|
|
28
|
-
markerStart: 'none',
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
describe('generateELKOptions', () => {
|
|
32
|
-
it('honors the requested direction', () => {
|
|
33
|
-
expect(generateELKOptions('DOWN')['elk.direction']).toBe('DOWN');
|
|
34
|
-
expect(generateELKOptions('UP')['elk.direction']).toBe('UP');
|
|
35
|
-
expect(generateELKOptions('LEFT')['elk.direction']).toBe('LEFT');
|
|
36
|
-
expect(generateELKOptions('RIGHT')['elk.direction']).toBe('RIGHT');
|
|
37
|
-
});
|
|
38
|
-
it('defaults to RIGHT', () => {
|
|
39
|
-
expect(generateELKOptions()['elk.direction']).toBe('RIGHT');
|
|
40
|
-
});
|
|
41
|
-
it('scales spacing by density', () => {
|
|
42
|
-
const low = generateELKOptions('RIGHT', 'low');
|
|
43
|
-
const high = generateELKOptions('RIGHT', 'high');
|
|
44
|
-
expect(Number(high['elk.spacing.nodeNode'])).toBeGreaterThan(Number(low['elk.spacing.nodeNode']));
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
describe('runFallbackLayout', () => {
|
|
48
|
-
it('emits parentNode at the top level for grouped children', () => {
|
|
49
|
-
const group = makeNode('backend_group', {
|
|
50
|
-
isGroup: true,
|
|
51
|
-
layer: 'compute',
|
|
52
|
-
width: 500,
|
|
53
|
-
height: 280,
|
|
54
|
-
});
|
|
55
|
-
const child = makeNode('order_svc', {
|
|
56
|
-
parentId: 'backend_group',
|
|
57
|
-
layer: 'compute',
|
|
58
|
-
});
|
|
59
|
-
const edge = makeEdge('e1', 'client', 'order_svc');
|
|
60
|
-
const result = runFallbackLayout([group, child, makeNode('client', { layer: 'client' })], [edge]);
|
|
61
|
-
const outputChild = result.nodes.find(n => n.id === 'order_svc');
|
|
62
|
-
expect(outputChild).toBeDefined();
|
|
63
|
-
expect(outputChild?.parentNode).toBe('backend_group');
|
|
64
|
-
expect(outputChild?.data.parentId).toBe('backend_group');
|
|
65
|
-
const outputGroup = result.nodes.find(n => n.id === 'backend_group');
|
|
66
|
-
expect(outputGroup?.type).toBe('groupNode');
|
|
67
|
-
expect(outputGroup?.data.isGroup).toBe(true);
|
|
68
|
-
});
|
|
69
|
-
it('produces positioned, non-overlapping nodes', () => {
|
|
70
|
-
const nodes = [
|
|
71
|
-
makeNode('web', { layer: 'client' }),
|
|
72
|
-
makeNode('gw', { layer: 'edge' }),
|
|
73
|
-
makeNode('db', { layer: 'data' }),
|
|
74
|
-
];
|
|
75
|
-
const edges = [makeEdge('e1', 'web', 'gw'), makeEdge('e2', 'gw', 'db')];
|
|
76
|
-
const result = runFallbackLayout(nodes, edges);
|
|
77
|
-
expect(result.nodes).toHaveLength(3);
|
|
78
|
-
for (const node of result.nodes) {
|
|
79
|
-
expect(typeof node.position.x).toBe('number');
|
|
80
|
-
expect(typeof node.position.y).toBe('number');
|
|
81
|
-
expect(node.data.tierColor).toMatch(/^#[0-9a-f]{6}$/i);
|
|
82
|
-
}
|
|
83
|
-
const positions = result.nodes.map(n => `${n.position.x},${n.position.y}`);
|
|
84
|
-
expect(new Set(positions).size).toBe(3);
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
//# sourceMappingURL=elk-runner.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"elk-runner.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/elk-runner.test.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/elk-runner.d.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { ArchitectureNode, ArchitectureEdge, ReactFlowNode, ReactFlowEdge } from '../types/index.js';
|
|
2
|
-
import type { ElkNode as LocalElkNode, ElkEdge as LocalElkEdge } from '../types/index.js';
|
|
3
|
-
import { type LayoutDirection } from './constants.js';
|
|
4
|
-
export interface LayoutOptions {
|
|
5
|
-
direction?: LayoutDirection;
|
|
6
|
-
spacingMultiplier?: number;
|
|
7
|
-
}
|
|
8
|
-
export interface LayoutResult {
|
|
9
|
-
nodes: ReactFlowNode[];
|
|
10
|
-
edges: ReactFlowEdge[];
|
|
11
|
-
elkPositions: Array<{
|
|
12
|
-
id: string;
|
|
13
|
-
x: number;
|
|
14
|
-
y: number;
|
|
15
|
-
width: number;
|
|
16
|
-
height: number;
|
|
17
|
-
}>;
|
|
18
|
-
}
|
|
19
|
-
export declare function generateELKOptions(direction?: LayoutDirection, density?: 'low' | 'medium' | 'high'): Record<string, string>;
|
|
20
|
-
export declare function computeDensity(nodes: ArchitectureNode[]): 'low' | 'medium' | 'high';
|
|
21
|
-
export declare function nodesToElkFormat(nodes: ArchitectureNode[]): LocalElkNode[];
|
|
22
|
-
export declare function edgesToElkFormat(edges: ArchitectureEdge[]): LocalElkEdge[];
|
|
23
|
-
export declare function runELKLayout(nodes: ArchitectureNode[], edges: ArchitectureEdge[], options?: LayoutOptions): Promise<LayoutResult>;
|
|
24
|
-
export declare function runFallbackLayout(nodes: ArchitectureNode[], edges: ArchitectureEdge[], options?: LayoutOptions): LayoutResult;
|
|
25
|
-
export declare function validateLayout(nodes: ReactFlowNode[], edges: ReactFlowEdge[]): {
|
|
26
|
-
valid: boolean;
|
|
27
|
-
errors: string[];
|
|
28
|
-
};
|
|
29
|
-
//# sourceMappingURL=elk-runner.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"elk-runner.d.ts","sourceRoot":"","sources":["../../src/lib/elk-runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,aAAa,EAAY,MAAM,mBAAmB,CAAC;AACpH,OAAO,KAAK,EAAE,OAAO,IAAI,YAAY,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAE1F,OAAO,EAOL,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,YAAY,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC1F;AAsLD,wBAAgB,kBAAkB,CAAC,SAAS,GAAE,eAAyB,EAAE,OAAO,GAAE,KAAK,GAAG,QAAQ,GAAG,MAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgC9I;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,gBAAgB,EAAE,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CASnF;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,EAAE,GAAG,YAAY,EAAE,CAW1E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,EAAE,GAAG,YAAY,EAAE,CAM1E;AAED,wBAAsB,YAAY,CAChC,KAAK,EAAE,gBAAgB,EAAE,EACzB,KAAK,EAAE,gBAAgB,EAAE,EACzB,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC,CA2IvB;AAyFD,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,gBAAgB,EAAE,EACzB,KAAK,EAAE,gBAAgB,EAAE,EACzB,OAAO,GAAE,aAAkB,GAC1B,YAAY,CA2Hd;AAED,wBAAgB,cAAc,CAC5B,KAAK,EAAE,aAAa,EAAE,EACtB,KAAK,EAAE,aAAa,EAAE,GACrB;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAwCtC"}
|
package/dist/lib/elk-runner.js
DELETED
|
@@ -1,532 +0,0 @@
|
|
|
1
|
-
import { COMM_COLORS, TIER_COLORS, TIER_ORDER, TIER_X_POSITIONS_LR, DEFAULT_NODE_WIDTH, DEFAULT_NODE_HEIGHT, } from './constants.js';
|
|
2
|
-
const MIN_VERTICAL_GAP = 80;
|
|
3
|
-
const COLLISION_BUFFER = 20;
|
|
4
|
-
const MIN_CANVAS_HEIGHT = 1200;
|
|
5
|
-
function normalizeTier(layer) {
|
|
6
|
-
if (!layer)
|
|
7
|
-
return 'compute';
|
|
8
|
-
const clean = layer.toLowerCase().trim().replace(/[\s-]/g, '');
|
|
9
|
-
if (clean === 'presentation' || clean === 'client' || clean === 'frontend')
|
|
10
|
-
return 'client';
|
|
11
|
-
if (clean === 'edge' || clean === 'infrastructure' || clean === 'gateway')
|
|
12
|
-
return 'edge';
|
|
13
|
-
if (clean === 'compute' || clean === 'application' || clean === 'compute/application')
|
|
14
|
-
return 'compute';
|
|
15
|
-
if (clean === 'async' || clean === 'queue')
|
|
16
|
-
return 'async';
|
|
17
|
-
if (clean === 'data' || clean === 'cache' || clean === 'storage')
|
|
18
|
-
return 'data';
|
|
19
|
-
if (clean === 'observe' || clean === 'observability' || clean === 'monitoring')
|
|
20
|
-
return 'observe';
|
|
21
|
-
if (clean === 'thirdparty' || clean === 'external')
|
|
22
|
-
return 'external';
|
|
23
|
-
// Substring matches as fallback
|
|
24
|
-
if (clean.includes('client') || clean.includes('present') || clean.includes('frontend'))
|
|
25
|
-
return 'client';
|
|
26
|
-
if (clean.includes('edge') || clean.includes('infra') || clean.includes('gate'))
|
|
27
|
-
return 'edge';
|
|
28
|
-
if (clean.includes('compute') || (clean.includes('app') && !clean.includes('frontend')))
|
|
29
|
-
return 'compute';
|
|
30
|
-
if (clean.includes('async') || clean.includes('queue') || clean.includes('bus') || clean.includes('stream'))
|
|
31
|
-
return 'async';
|
|
32
|
-
if (clean.includes('data') || clean.includes('db') || clean.includes('cache') || clean.includes('store') || clean.includes('sql') || clean.includes('mongo'))
|
|
33
|
-
return 'data';
|
|
34
|
-
if (clean.includes('observe') || clean.includes('monitor') || clean.includes('log') || clean.includes('trace') || clean.includes('alert'))
|
|
35
|
-
return 'observe';
|
|
36
|
-
if (clean.includes('third') || clean.includes('ext') || clean.includes('api') || clean.includes('vendor'))
|
|
37
|
-
return 'external';
|
|
38
|
-
return 'compute';
|
|
39
|
-
}
|
|
40
|
-
function getNodesByTier(nodes) {
|
|
41
|
-
const nodesByTier = new Map();
|
|
42
|
-
for (const tier of TIER_ORDER) {
|
|
43
|
-
nodesByTier.set(tier, []);
|
|
44
|
-
}
|
|
45
|
-
for (const node of nodes) {
|
|
46
|
-
if (node.isGroup)
|
|
47
|
-
continue;
|
|
48
|
-
const tier = normalizeTier(node.tier || node.layer);
|
|
49
|
-
const tierNodes = nodesByTier.get(tier) || [];
|
|
50
|
-
tierNodes.push(node);
|
|
51
|
-
nodesByTier.set(tier, tierNodes);
|
|
52
|
-
}
|
|
53
|
-
return nodesByTier;
|
|
54
|
-
}
|
|
55
|
-
function calculateCanvasHeight(nodesByTier) {
|
|
56
|
-
let maxNodesInLayer = 0;
|
|
57
|
-
for (const [, tierNodes] of nodesByTier) {
|
|
58
|
-
maxNodesInLayer = Math.max(maxNodesInLayer, tierNodes.length);
|
|
59
|
-
}
|
|
60
|
-
const requiredHeight = maxNodesInLayer * (DEFAULT_NODE_HEIGHT + MIN_VERTICAL_GAP) + 200;
|
|
61
|
-
return Math.max(requiredHeight, MIN_CANVAS_HEIGHT);
|
|
62
|
-
}
|
|
63
|
-
function checkCollision(newNode, placedNodes, buffer = COLLISION_BUFFER) {
|
|
64
|
-
for (const placed of placedNodes) {
|
|
65
|
-
const expanded = {
|
|
66
|
-
x: placed.x - buffer,
|
|
67
|
-
y: placed.y - buffer,
|
|
68
|
-
width: placed.width + buffer * 2,
|
|
69
|
-
height: placed.height + buffer * 2,
|
|
70
|
-
};
|
|
71
|
-
const overlaps = !(newNode.x + newNode.width <= expanded.x ||
|
|
72
|
-
newNode.x >= expanded.x + expanded.width ||
|
|
73
|
-
newNode.y + newNode.height <= expanded.y ||
|
|
74
|
-
newNode.y >= expanded.y + expanded.height);
|
|
75
|
-
if (overlaps)
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
function findNonCollidingY(x, startY, width, height, placedNodes, canvasHeight) {
|
|
81
|
-
let y = startY;
|
|
82
|
-
const maxAttempts = 100;
|
|
83
|
-
let attempts = 0;
|
|
84
|
-
while (checkCollision({ x, y, width, height }, placedNodes) && attempts < maxAttempts) {
|
|
85
|
-
y += MIN_VERTICAL_GAP / 2;
|
|
86
|
-
if (y + height > canvasHeight - 50) {
|
|
87
|
-
y = 50;
|
|
88
|
-
}
|
|
89
|
-
attempts++;
|
|
90
|
-
}
|
|
91
|
-
return Math.max(50, Math.min(y, canvasHeight - height - 50));
|
|
92
|
-
}
|
|
93
|
-
function distributeNodesVertically(nodes, startX, canvasHeight, placedNodes, tier) {
|
|
94
|
-
if (nodes.length === 0)
|
|
95
|
-
return [];
|
|
96
|
-
const nodeCount = nodes.length;
|
|
97
|
-
const gap = tier === 'compute' && nodeCount > 4 ? 100 : MIN_VERTICAL_GAP;
|
|
98
|
-
const totalHeight = nodeCount * DEFAULT_NODE_HEIGHT + (nodeCount - 1) * gap;
|
|
99
|
-
const startY = Math.max(50, (canvasHeight - totalHeight) / 2);
|
|
100
|
-
const placed = [];
|
|
101
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
102
|
-
const node = nodes[i];
|
|
103
|
-
const width = node.width || DEFAULT_NODE_WIDTH;
|
|
104
|
-
const height = node.height || DEFAULT_NODE_HEIGHT;
|
|
105
|
-
let y = startY + i * (height + gap);
|
|
106
|
-
y = findNonCollidingY(startX, y, width, height, [...placedNodes, ...placed], canvasHeight);
|
|
107
|
-
placed.push({
|
|
108
|
-
id: node.id,
|
|
109
|
-
x: startX,
|
|
110
|
-
y,
|
|
111
|
-
width,
|
|
112
|
-
height,
|
|
113
|
-
tier,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
return placed;
|
|
117
|
-
}
|
|
118
|
-
function createReactFlowEdge(edge, _sourceNode, _targetNode) {
|
|
119
|
-
const commType = edge.communicationType || 'sync';
|
|
120
|
-
const commStyle = COMM_COLORS[commType] || COMM_COLORS.sync;
|
|
121
|
-
const pathType = edge.pathType || 'smooth';
|
|
122
|
-
return {
|
|
123
|
-
id: edge.id,
|
|
124
|
-
source: edge.source,
|
|
125
|
-
target: edge.target,
|
|
126
|
-
type: 'simpleFloating',
|
|
127
|
-
animated: commStyle.animated,
|
|
128
|
-
label: edge.label || '',
|
|
129
|
-
labelShowBg: true,
|
|
130
|
-
labelBgPadding: [8, 4],
|
|
131
|
-
labelBgBorderRadius: 4,
|
|
132
|
-
labelBgStyle: { fill: '#1e1e2e', fillOpacity: 0.9 },
|
|
133
|
-
labelStyle: { fontSize: 10, fontWeight: 600, fill: '#e2e8f0' },
|
|
134
|
-
style: {
|
|
135
|
-
stroke: commStyle.color,
|
|
136
|
-
strokeWidth: 2,
|
|
137
|
-
strokeDasharray: commStyle.dash,
|
|
138
|
-
},
|
|
139
|
-
markerEnd: { type: 'arrowclosed', color: commStyle.color }, // Will be overridden or ignored by frontend components
|
|
140
|
-
data: {
|
|
141
|
-
communicationType: commType,
|
|
142
|
-
pathType,
|
|
143
|
-
edgeType: commType,
|
|
144
|
-
label: edge.label || '',
|
|
145
|
-
},
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
export function generateELKOptions(direction = 'RIGHT', density = 'medium') {
|
|
149
|
-
const spacingMultiplier = density === 'high' ? 1.5 : density === 'medium' ? 1.2 : 1.0;
|
|
150
|
-
const options = {
|
|
151
|
-
'elk.algorithm': 'layered',
|
|
152
|
-
'elk.direction': direction,
|
|
153
|
-
'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
|
|
154
|
-
'elk.edgeRouting': 'ORTHOGONAL',
|
|
155
|
-
'elk.portConstraints': 'FIXED_SIDE',
|
|
156
|
-
'elk.spacing.nodeNode': String(Math.round(60 * spacingMultiplier)),
|
|
157
|
-
'elk.spacing.edgeEdge': String(Math.round(40 * spacingMultiplier)),
|
|
158
|
-
'elk.spacing.edgeNode': String(Math.round(60 * spacingMultiplier)),
|
|
159
|
-
'elk.spacing.labelNode': '50',
|
|
160
|
-
'elk.layered.spacing.nodeNodeBetweenLayers': String(Math.round(120 * spacingMultiplier)),
|
|
161
|
-
'elk.layered.spacing.edgeNodeBetweenLayers': String(Math.round(100 * spacingMultiplier)),
|
|
162
|
-
'elk.layered.considerModelOrder.strategy': 'NODES_AND_EDGES',
|
|
163
|
-
'elk.layered.nodePlacement.strategy': 'BRANDES_KOEPF',
|
|
164
|
-
'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP',
|
|
165
|
-
'elk.layered.crossingMinimization.forceNodeModelOrder': 'false',
|
|
166
|
-
'elk.layered.separatingEdges.strategy': 'CENTERING',
|
|
167
|
-
'elk.layered.unnecessaryBendpoints': 'false',
|
|
168
|
-
'elk.layered.edgeRouting.selfLoopDistribution': 'EVEN',
|
|
169
|
-
'elk.layered.mergeEdges': 'true',
|
|
170
|
-
'elk.edgeLabels.inline': 'false',
|
|
171
|
-
'elk.edgeLabels.placement': 'CENTER',
|
|
172
|
-
'elk.padding': '[top=60, left=60, bottom=60, right=60]',
|
|
173
|
-
'elk.layered.layering.strategy': 'LONGEST_PATH',
|
|
174
|
-
'elk.layered.initialization.strategy': 'MULTI_LEVEL',
|
|
175
|
-
'elk.aspectRatio': '2.0',
|
|
176
|
-
};
|
|
177
|
-
return options;
|
|
178
|
-
}
|
|
179
|
-
export function computeDensity(nodes) {
|
|
180
|
-
const maxNodesPerTier = Math.max(...TIER_ORDER.map(tier => {
|
|
181
|
-
return nodes.filter(n => normalizeTier(n.tier || n.layer) === tier && !n.isGroup).length;
|
|
182
|
-
}), 0);
|
|
183
|
-
return maxNodesPerTier > 6 ? 'high' : maxNodesPerTier > 3 ? 'medium' : 'low';
|
|
184
|
-
}
|
|
185
|
-
export function nodesToElkFormat(nodes) {
|
|
186
|
-
return nodes.map(node => ({
|
|
187
|
-
id: node.id,
|
|
188
|
-
width: node.width || DEFAULT_NODE_WIDTH,
|
|
189
|
-
height: node.height || DEFAULT_NODE_HEIGHT,
|
|
190
|
-
layoutOptions: {
|
|
191
|
-
'elk.nodeLabels.placement': 'INSIDE',
|
|
192
|
-
'elk.portConstraints': 'FIXED_SIDE',
|
|
193
|
-
'elk.portAlignment.default': 'CENTER',
|
|
194
|
-
},
|
|
195
|
-
}));
|
|
196
|
-
}
|
|
197
|
-
export function edgesToElkFormat(edges) {
|
|
198
|
-
return edges.map(edge => ({
|
|
199
|
-
id: edge.id,
|
|
200
|
-
sources: [edge.source],
|
|
201
|
-
targets: [edge.target],
|
|
202
|
-
}));
|
|
203
|
-
}
|
|
204
|
-
export async function runELKLayout(nodes, edges, options = {}) {
|
|
205
|
-
const direction = options.direction || 'RIGHT';
|
|
206
|
-
const density = computeDensity(nodes);
|
|
207
|
-
const elkOptions = generateELKOptions(direction, density);
|
|
208
|
-
const elkNodes = nodesToElkFormat(nodes);
|
|
209
|
-
const elkEdges = edgesToElkFormat(edges);
|
|
210
|
-
try {
|
|
211
|
-
const elkModule = await import('elkjs/lib/elk.bundled.js');
|
|
212
|
-
const elk = new elkModule.default();
|
|
213
|
-
const layout = await elk.layout({
|
|
214
|
-
id: 'root',
|
|
215
|
-
layoutOptions: elkOptions,
|
|
216
|
-
children: elkNodes,
|
|
217
|
-
edges: elkEdges,
|
|
218
|
-
});
|
|
219
|
-
const nodeMap = new Map();
|
|
220
|
-
if (layout.children) {
|
|
221
|
-
for (const node of layout.children) {
|
|
222
|
-
if (node.x !== undefined && node.y !== undefined) {
|
|
223
|
-
const originalNode = nodes.find(n => n.id === node.id);
|
|
224
|
-
const tier = originalNode ? normalizeTier(originalNode.tier || originalNode.layer) : 'compute';
|
|
225
|
-
nodeMap.set(node.id, {
|
|
226
|
-
x: node.x,
|
|
227
|
-
y: node.y,
|
|
228
|
-
width: node.width || DEFAULT_NODE_WIDTH,
|
|
229
|
-
height: node.height || DEFAULT_NODE_HEIGHT,
|
|
230
|
-
tier,
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
const placedNodes = [];
|
|
236
|
-
for (const [id, pos] of nodeMap) {
|
|
237
|
-
placedNodes.push({
|
|
238
|
-
id,
|
|
239
|
-
x: pos.x,
|
|
240
|
-
y: pos.y,
|
|
241
|
-
width: pos.width,
|
|
242
|
-
height: pos.height,
|
|
243
|
-
tier: pos.tier,
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
const reactFlowNodes = nodes.map(node => {
|
|
247
|
-
const pos = nodeMap.get(node.id);
|
|
248
|
-
const width = node.width || DEFAULT_NODE_WIDTH;
|
|
249
|
-
const height = node.height || DEFAULT_NODE_HEIGHT;
|
|
250
|
-
const tier = pos?.tier || normalizeTier(node.tier || node.layer);
|
|
251
|
-
return {
|
|
252
|
-
id: node.id,
|
|
253
|
-
type: node.isGroup ? 'groupNode' : 'systemNode',
|
|
254
|
-
position: {
|
|
255
|
-
x: pos?.x ?? TIER_X_POSITIONS_LR[tier],
|
|
256
|
-
y: pos?.y ?? 200,
|
|
257
|
-
},
|
|
258
|
-
data: {
|
|
259
|
-
label: node.label,
|
|
260
|
-
icon: node.icon || 'box',
|
|
261
|
-
layer: node.layer,
|
|
262
|
-
tier,
|
|
263
|
-
tierColor: node.tierColor || TIER_COLORS[tier],
|
|
264
|
-
subtitle: node.subtitle,
|
|
265
|
-
serviceType: node.serviceType,
|
|
266
|
-
isGroup: node.isGroup,
|
|
267
|
-
parentId: node.parentId,
|
|
268
|
-
groupLabel: node.groupLabel,
|
|
269
|
-
groupColor: node.groupColor,
|
|
270
|
-
},
|
|
271
|
-
width: pos?.width ?? width,
|
|
272
|
-
height: pos?.height ?? height,
|
|
273
|
-
zIndex: node.isGroup ? 0 : 1,
|
|
274
|
-
...(node.parentId ? { parentNode: node.parentId } : {}),
|
|
275
|
-
};
|
|
276
|
-
});
|
|
277
|
-
const nodePositionMap = new Map(placedNodes.map(p => [p.id, p]));
|
|
278
|
-
const reactFlowEdges = edges.map(edge => {
|
|
279
|
-
const sourceNode = nodePositionMap.get(edge.source);
|
|
280
|
-
const targetNode = nodePositionMap.get(edge.target);
|
|
281
|
-
if (sourceNode && targetNode) {
|
|
282
|
-
return createReactFlowEdge(edge, sourceNode, targetNode);
|
|
283
|
-
}
|
|
284
|
-
const commType = edge.communicationType || 'sync';
|
|
285
|
-
const commStyle = COMM_COLORS[commType] || COMM_COLORS.sync;
|
|
286
|
-
const pathType = edge.pathType || 'smooth';
|
|
287
|
-
return {
|
|
288
|
-
id: edge.id,
|
|
289
|
-
source: edge.source,
|
|
290
|
-
target: edge.target,
|
|
291
|
-
type: 'simpleFloating',
|
|
292
|
-
animated: commStyle.animated,
|
|
293
|
-
label: edge.label || '',
|
|
294
|
-
labelShowBg: true,
|
|
295
|
-
labelBgPadding: [8, 4],
|
|
296
|
-
labelBgBorderRadius: 4,
|
|
297
|
-
labelBgStyle: { fill: '#1e1e2e', fillOpacity: 0.9 },
|
|
298
|
-
labelStyle: { fontSize: 10, fontWeight: 600, fill: '#e2e8f0' },
|
|
299
|
-
style: {
|
|
300
|
-
stroke: commStyle.color,
|
|
301
|
-
strokeWidth: 2,
|
|
302
|
-
strokeDasharray: commStyle.dash,
|
|
303
|
-
},
|
|
304
|
-
markerEnd: { type: 'arrowclosed', color: commStyle.color },
|
|
305
|
-
data: {
|
|
306
|
-
communicationType: commType,
|
|
307
|
-
pathType,
|
|
308
|
-
edgeType: commType,
|
|
309
|
-
label: edge.label || '',
|
|
310
|
-
},
|
|
311
|
-
};
|
|
312
|
-
});
|
|
313
|
-
const elkPositions = Array.from(nodeMap.entries()).map(([id, pos]) => ({
|
|
314
|
-
id,
|
|
315
|
-
x: pos.x,
|
|
316
|
-
y: pos.y,
|
|
317
|
-
width: pos.width,
|
|
318
|
-
height: pos.height,
|
|
319
|
-
}));
|
|
320
|
-
return {
|
|
321
|
-
nodes: reactFlowNodes,
|
|
322
|
-
edges: reactFlowEdges,
|
|
323
|
-
elkPositions,
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
catch (error) {
|
|
327
|
-
console.error('[ELK Runner] Layout computation failed:', error);
|
|
328
|
-
return runFallbackLayout(nodes, edges, options);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
function tierAnchorForDirection(direction) {
|
|
332
|
-
const lr = TIER_X_POSITIONS_LR;
|
|
333
|
-
if (direction === 'LEFT') {
|
|
334
|
-
const max = Math.max(...TIER_ORDER.map(t => lr[t]));
|
|
335
|
-
const reversed = {};
|
|
336
|
-
for (const tier of TIER_ORDER)
|
|
337
|
-
reversed[tier] = max - lr[tier];
|
|
338
|
-
return reversed;
|
|
339
|
-
}
|
|
340
|
-
if (direction === 'DOWN') {
|
|
341
|
-
// Row layout — the anchor becomes the top Y coordinate of each tier.
|
|
342
|
-
return { ...lr };
|
|
343
|
-
}
|
|
344
|
-
if (direction === 'UP') {
|
|
345
|
-
const max = Math.max(...TIER_ORDER.map(t => lr[t]));
|
|
346
|
-
const reversed = {};
|
|
347
|
-
for (const tier of TIER_ORDER)
|
|
348
|
-
reversed[tier] = max - lr[tier];
|
|
349
|
-
return reversed;
|
|
350
|
-
}
|
|
351
|
-
return { ...lr };
|
|
352
|
-
}
|
|
353
|
-
function calculateCanvasWidth(nodesByTier) {
|
|
354
|
-
let maxNodesInLayer = 0;
|
|
355
|
-
for (const [, tierNodes] of nodesByTier) {
|
|
356
|
-
maxNodesInLayer = Math.max(maxNodesInLayer, tierNodes.length);
|
|
357
|
-
}
|
|
358
|
-
return maxNodesInLayer * (DEFAULT_NODE_WIDTH + MIN_VERTICAL_GAP) + 200;
|
|
359
|
-
}
|
|
360
|
-
function findNonCollidingX(startX, y, width, height, placedNodes, canvasWidth) {
|
|
361
|
-
let x = startX;
|
|
362
|
-
const maxAttempts = 100;
|
|
363
|
-
let attempts = 0;
|
|
364
|
-
while (checkCollision({ x, y, width, height }, placedNodes) && attempts < maxAttempts) {
|
|
365
|
-
x += MIN_VERTICAL_GAP / 2;
|
|
366
|
-
if (x + width > canvasWidth - 50) {
|
|
367
|
-
x = 50;
|
|
368
|
-
}
|
|
369
|
-
attempts++;
|
|
370
|
-
}
|
|
371
|
-
return Math.max(50, Math.min(x, canvasWidth - width - 50));
|
|
372
|
-
}
|
|
373
|
-
function distributeNodesHorizontally(nodes, startY, canvasWidth, placedNodes, tier) {
|
|
374
|
-
if (nodes.length === 0)
|
|
375
|
-
return [];
|
|
376
|
-
const nodeCount = nodes.length;
|
|
377
|
-
const totalWidth = nodeCount * DEFAULT_NODE_WIDTH + (nodeCount - 1) * MIN_VERTICAL_GAP;
|
|
378
|
-
const startX = Math.max(50, (canvasWidth - totalWidth) / 2);
|
|
379
|
-
const placed = [];
|
|
380
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
381
|
-
const node = nodes[i];
|
|
382
|
-
const width = node.width || DEFAULT_NODE_WIDTH;
|
|
383
|
-
const height = node.height || DEFAULT_NODE_HEIGHT;
|
|
384
|
-
let x = startX + i * (width + MIN_VERTICAL_GAP);
|
|
385
|
-
x = findNonCollidingX(x, startY, width, height, [...placedNodes, ...placed], canvasWidth);
|
|
386
|
-
placed.push({
|
|
387
|
-
id: node.id,
|
|
388
|
-
x,
|
|
389
|
-
y: startY,
|
|
390
|
-
width,
|
|
391
|
-
height,
|
|
392
|
-
tier,
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
return placed;
|
|
396
|
-
}
|
|
397
|
-
export function runFallbackLayout(nodes, edges, options = {}) {
|
|
398
|
-
const direction = options.direction || 'RIGHT';
|
|
399
|
-
const isRowLayout = direction === 'DOWN' || direction === 'UP';
|
|
400
|
-
const nodesByTier = getNodesByTier(nodes);
|
|
401
|
-
const tierAnchor = tierAnchorForDirection(direction);
|
|
402
|
-
const canvasHeight = isRowLayout
|
|
403
|
-
? TIER_ORDER.length * (DEFAULT_NODE_HEIGHT + MIN_VERTICAL_GAP) + 200
|
|
404
|
-
: calculateCanvasHeight(nodesByTier);
|
|
405
|
-
const canvasWidth = isRowLayout ? calculateCanvasWidth(nodesByTier) : 0;
|
|
406
|
-
const placedNodes = [];
|
|
407
|
-
for (const tier of TIER_ORDER) {
|
|
408
|
-
const tierNodes = nodesByTier.get(tier) || [];
|
|
409
|
-
const anchor = tierAnchor[tier];
|
|
410
|
-
if (isRowLayout) {
|
|
411
|
-
const tierPlaced = distributeNodesHorizontally(tierNodes, anchor, canvasWidth, placedNodes, tier);
|
|
412
|
-
placedNodes.push(...tierPlaced);
|
|
413
|
-
}
|
|
414
|
-
else {
|
|
415
|
-
const tierPlaced = distributeNodesVertically(tierNodes, anchor, canvasHeight, placedNodes, tier);
|
|
416
|
-
placedNodes.push(...tierPlaced);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
const nodePositionMap = new Map(placedNodes.map(p => [p.id, p]));
|
|
420
|
-
const reactFlowNodes = nodes.map(node => {
|
|
421
|
-
const placed = nodePositionMap.get(node.id);
|
|
422
|
-
const tier = placed?.tier || normalizeTier(node.tier || node.layer);
|
|
423
|
-
const width = node.width || DEFAULT_NODE_WIDTH;
|
|
424
|
-
const height = node.height || DEFAULT_NODE_HEIGHT;
|
|
425
|
-
return {
|
|
426
|
-
id: node.id,
|
|
427
|
-
type: node.isGroup ? 'groupNode' : 'systemNode',
|
|
428
|
-
position: {
|
|
429
|
-
x: placed?.x ?? tierAnchorForDirection(direction)[tier],
|
|
430
|
-
y: placed?.y ?? 200,
|
|
431
|
-
},
|
|
432
|
-
data: {
|
|
433
|
-
label: node.label,
|
|
434
|
-
icon: node.icon || 'box',
|
|
435
|
-
layer: node.layer,
|
|
436
|
-
tier,
|
|
437
|
-
tierColor: node.tierColor || TIER_COLORS[tier],
|
|
438
|
-
subtitle: node.subtitle,
|
|
439
|
-
serviceType: node.serviceType,
|
|
440
|
-
isGroup: node.isGroup,
|
|
441
|
-
parentId: node.parentId,
|
|
442
|
-
groupLabel: node.groupLabel,
|
|
443
|
-
groupColor: node.groupColor,
|
|
444
|
-
},
|
|
445
|
-
width: placed?.width ?? width,
|
|
446
|
-
height: placed?.height ?? height,
|
|
447
|
-
zIndex: node.isGroup ? 0 : 1,
|
|
448
|
-
...(node.parentId ? { parentNode: node.parentId } : {}),
|
|
449
|
-
};
|
|
450
|
-
});
|
|
451
|
-
const reactFlowEdges = edges.map(edge => {
|
|
452
|
-
const sourceNode = nodePositionMap.get(edge.source);
|
|
453
|
-
const targetNode = nodePositionMap.get(edge.target);
|
|
454
|
-
if (sourceNode && targetNode) {
|
|
455
|
-
return createReactFlowEdge(edge, sourceNode, targetNode);
|
|
456
|
-
}
|
|
457
|
-
const commType = (edge.communicationType || 'sync');
|
|
458
|
-
const commStyle = COMM_COLORS[commType] || COMM_COLORS.sync;
|
|
459
|
-
const pathType = edge.pathType || 'smooth';
|
|
460
|
-
return {
|
|
461
|
-
id: edge.id,
|
|
462
|
-
source: edge.source,
|
|
463
|
-
target: edge.target,
|
|
464
|
-
type: 'simpleFloating',
|
|
465
|
-
animated: commStyle.animated,
|
|
466
|
-
label: edge.label || '',
|
|
467
|
-
labelShowBg: true,
|
|
468
|
-
labelBgPadding: [8, 4],
|
|
469
|
-
labelBgBorderRadius: 4,
|
|
470
|
-
labelBgStyle: { fill: '#1e1e2e', fillOpacity: 0.9 },
|
|
471
|
-
labelStyle: { fontSize: 10, fontWeight: 600, fill: '#e2e8f0' },
|
|
472
|
-
style: {
|
|
473
|
-
stroke: commStyle.color,
|
|
474
|
-
strokeWidth: 2,
|
|
475
|
-
strokeDasharray: commStyle.dash,
|
|
476
|
-
},
|
|
477
|
-
markerEnd: { type: 'arrowclosed', color: commStyle.color },
|
|
478
|
-
data: {
|
|
479
|
-
communicationType: commType,
|
|
480
|
-
pathType,
|
|
481
|
-
edgeType: commType,
|
|
482
|
-
label: edge.label || '',
|
|
483
|
-
},
|
|
484
|
-
};
|
|
485
|
-
});
|
|
486
|
-
const elkPositions = placedNodes.map(p => ({
|
|
487
|
-
id: p.id,
|
|
488
|
-
x: p.x,
|
|
489
|
-
y: p.y,
|
|
490
|
-
width: p.width,
|
|
491
|
-
height: p.height,
|
|
492
|
-
}));
|
|
493
|
-
return {
|
|
494
|
-
nodes: reactFlowNodes,
|
|
495
|
-
edges: reactFlowEdges,
|
|
496
|
-
elkPositions,
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
export function validateLayout(nodes, edges) {
|
|
500
|
-
const errors = [];
|
|
501
|
-
const nodeMap = new Map(nodes.map(n => [n.id, n]));
|
|
502
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
503
|
-
for (let j = i + 1; j < nodes.length; j++) {
|
|
504
|
-
const a = nodes[i];
|
|
505
|
-
const b = nodes[j];
|
|
506
|
-
const aRight = a.position.x + (a.width || DEFAULT_NODE_WIDTH);
|
|
507
|
-
const aBottom = a.position.y + (a.height || DEFAULT_NODE_HEIGHT);
|
|
508
|
-
const bRight = b.position.x + (b.width || DEFAULT_NODE_WIDTH);
|
|
509
|
-
const bBottom = b.position.y + (b.height || DEFAULT_NODE_HEIGHT);
|
|
510
|
-
const overlaps = !(aRight <= b.position.x + COLLISION_BUFFER ||
|
|
511
|
-
a.position.x >= bRight - COLLISION_BUFFER ||
|
|
512
|
-
aBottom <= b.position.y + COLLISION_BUFFER ||
|
|
513
|
-
a.position.y >= bBottom - COLLISION_BUFFER);
|
|
514
|
-
if (overlaps) {
|
|
515
|
-
errors.push(`Collision detected: ${a.data.label} overlaps with ${b.data.label}`);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
for (const edge of edges) {
|
|
520
|
-
const source = nodeMap.get(edge.source);
|
|
521
|
-
const target = nodeMap.get(edge.target);
|
|
522
|
-
if (!source)
|
|
523
|
-
errors.push(`Edge ${edge.id}: source node not found`);
|
|
524
|
-
if (!target)
|
|
525
|
-
errors.push(`Edge ${edge.id}: target node not found`);
|
|
526
|
-
}
|
|
527
|
-
return {
|
|
528
|
-
valid: errors.length === 0,
|
|
529
|
-
errors,
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
//# sourceMappingURL=elk-runner.js.map
|