@shumoku/core 0.1.0 → 0.2.0
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 +56 -0
- package/dist/constants.d.ts +23 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +25 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/layout/hierarchical.d.ts +12 -39
- package/dist/layout/hierarchical.d.ts.map +1 -1
- package/dist/layout/hierarchical.js +697 -1015
- package/dist/layout/hierarchical.js.map +1 -1
- package/dist/models/types.d.ts +30 -0
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js.map +1 -1
- package/dist/renderer/svg.d.ts +31 -5
- package/dist/renderer/svg.d.ts.map +1 -1
- package/dist/renderer/svg.js +312 -85
- package/dist/renderer/svg.js.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +35 -0
- package/src/index.ts +3 -0
- package/src/layout/hierarchical.ts +805 -1127
- package/src/models/types.ts +37 -0
- package/src/renderer/svg.ts +368 -88
- package/dist/renderer/components/index.d.ts +0 -8
- package/dist/renderer/components/index.d.ts.map +0 -1
- package/dist/renderer/components/index.js +0 -8
- package/dist/renderer/components/index.js.map +0 -1
- package/dist/renderer/components/link-renderer.d.ts +0 -11
- package/dist/renderer/components/link-renderer.d.ts.map +0 -1
- package/dist/renderer/components/link-renderer.js +0 -340
- package/dist/renderer/components/link-renderer.js.map +0 -1
- package/dist/renderer/components/node-renderer.d.ts +0 -14
- package/dist/renderer/components/node-renderer.d.ts.map +0 -1
- package/dist/renderer/components/node-renderer.js +0 -242
- package/dist/renderer/components/node-renderer.js.map +0 -1
- package/dist/renderer/components/port-renderer.d.ts +0 -8
- package/dist/renderer/components/port-renderer.d.ts.map +0 -1
- package/dist/renderer/components/port-renderer.js +0 -85
- package/dist/renderer/components/port-renderer.js.map +0 -1
- package/dist/renderer/components/subgraph-renderer.d.ts +0 -13
- package/dist/renderer/components/subgraph-renderer.d.ts.map +0 -1
- package/dist/renderer/components/subgraph-renderer.js +0 -85
- package/dist/renderer/components/subgraph-renderer.js.map +0 -1
- package/dist/renderer/icon-registry/index.d.ts +0 -6
- package/dist/renderer/icon-registry/index.d.ts.map +0 -1
- package/dist/renderer/icon-registry/index.js +0 -5
- package/dist/renderer/icon-registry/index.js.map +0 -1
- package/dist/renderer/icon-registry/registry.d.ts +0 -25
- package/dist/renderer/icon-registry/registry.d.ts.map +0 -1
- package/dist/renderer/icon-registry/registry.js +0 -85
- package/dist/renderer/icon-registry/registry.js.map +0 -1
- package/dist/renderer/icon-registry/types.d.ts +0 -44
- package/dist/renderer/icon-registry/types.d.ts.map +0 -1
- package/dist/renderer/icon-registry/types.js +0 -5
- package/dist/renderer/icon-registry/types.js.map +0 -1
- package/dist/renderer/render-model/builder.d.ts +0 -43
- package/dist/renderer/render-model/builder.d.ts.map +0 -1
- package/dist/renderer/render-model/builder.js +0 -646
- package/dist/renderer/render-model/builder.js.map +0 -1
- package/dist/renderer/render-model/index.d.ts +0 -6
- package/dist/renderer/render-model/index.d.ts.map +0 -1
- package/dist/renderer/render-model/index.js +0 -5
- package/dist/renderer/render-model/index.js.map +0 -1
- package/dist/renderer/render-model/types.d.ts +0 -216
- package/dist/renderer/render-model/types.d.ts.map +0 -1
- package/dist/renderer/render-model/types.js +0 -6
- package/dist/renderer/render-model/types.js.map +0 -1
- package/dist/renderer/renderer-types.d.ts +0 -55
- package/dist/renderer/renderer-types.d.ts.map +0 -1
- package/dist/renderer/renderer-types.js +0 -5
- package/dist/renderer/renderer-types.js.map +0 -1
- package/dist/renderer/svg-builder.d.ts +0 -152
- package/dist/renderer/svg-builder.d.ts.map +0 -1
- package/dist/renderer/svg-builder.js +0 -176
- package/dist/renderer/svg-builder.js.map +0 -1
- package/dist/renderer/svg-dom/builders/defs.d.ts +0 -10
- package/dist/renderer/svg-dom/builders/defs.d.ts.map +0 -1
- package/dist/renderer/svg-dom/builders/defs.js +0 -82
- package/dist/renderer/svg-dom/builders/defs.js.map +0 -1
- package/dist/renderer/svg-dom/builders/index.d.ts +0 -9
- package/dist/renderer/svg-dom/builders/index.d.ts.map +0 -1
- package/dist/renderer/svg-dom/builders/index.js +0 -9
- package/dist/renderer/svg-dom/builders/index.js.map +0 -1
- package/dist/renderer/svg-dom/builders/link.d.ts +0 -18
- package/dist/renderer/svg-dom/builders/link.d.ts.map +0 -1
- package/dist/renderer/svg-dom/builders/link.js +0 -188
- package/dist/renderer/svg-dom/builders/link.js.map +0 -1
- package/dist/renderer/svg-dom/builders/node.d.ts +0 -15
- package/dist/renderer/svg-dom/builders/node.d.ts.map +0 -1
- package/dist/renderer/svg-dom/builders/node.js +0 -262
- package/dist/renderer/svg-dom/builders/node.js.map +0 -1
- package/dist/renderer/svg-dom/builders/subgraph.d.ts +0 -14
- package/dist/renderer/svg-dom/builders/subgraph.d.ts.map +0 -1
- package/dist/renderer/svg-dom/builders/subgraph.js +0 -63
- package/dist/renderer/svg-dom/builders/subgraph.js.map +0 -1
- package/dist/renderer/svg-dom/builders/utils.d.ts +0 -40
- package/dist/renderer/svg-dom/builders/utils.d.ts.map +0 -1
- package/dist/renderer/svg-dom/builders/utils.js +0 -79
- package/dist/renderer/svg-dom/builders/utils.js.map +0 -1
- package/dist/renderer/svg-dom/index.d.ts +0 -9
- package/dist/renderer/svg-dom/index.d.ts.map +0 -1
- package/dist/renderer/svg-dom/index.js +0 -7
- package/dist/renderer/svg-dom/index.js.map +0 -1
- package/dist/renderer/svg-dom/interaction.d.ts +0 -69
- package/dist/renderer/svg-dom/interaction.d.ts.map +0 -1
- package/dist/renderer/svg-dom/interaction.js +0 -296
- package/dist/renderer/svg-dom/interaction.js.map +0 -1
- package/dist/renderer/svg-dom/renderer.d.ts +0 -47
- package/dist/renderer/svg-dom/renderer.d.ts.map +0 -1
- package/dist/renderer/svg-dom/renderer.js +0 -188
- package/dist/renderer/svg-dom/renderer.js.map +0 -1
- package/dist/renderer/svg-string/builders/defs.d.ts +0 -10
- package/dist/renderer/svg-string/builders/defs.d.ts.map +0 -1
- package/dist/renderer/svg-string/builders/defs.js +0 -43
- package/dist/renderer/svg-string/builders/defs.js.map +0 -1
- package/dist/renderer/svg-string/builders/link.d.ts +0 -10
- package/dist/renderer/svg-string/builders/link.d.ts.map +0 -1
- package/dist/renderer/svg-string/builders/link.js +0 -149
- package/dist/renderer/svg-string/builders/link.js.map +0 -1
- package/dist/renderer/svg-string/builders/node.d.ts +0 -10
- package/dist/renderer/svg-string/builders/node.d.ts.map +0 -1
- package/dist/renderer/svg-string/builders/node.js +0 -134
- package/dist/renderer/svg-string/builders/node.js.map +0 -1
- package/dist/renderer/svg-string/builders/subgraph.d.ts +0 -10
- package/dist/renderer/svg-string/builders/subgraph.d.ts.map +0 -1
- package/dist/renderer/svg-string/builders/subgraph.js +0 -59
- package/dist/renderer/svg-string/builders/subgraph.js.map +0 -1
- package/dist/renderer/svg-string/index.d.ts +0 -5
- package/dist/renderer/svg-string/index.d.ts.map +0 -1
- package/dist/renderer/svg-string/index.js +0 -5
- package/dist/renderer/svg-string/index.js.map +0 -1
- package/dist/renderer/svg-string/renderer.d.ts +0 -17
- package/dist/renderer/svg-string/renderer.d.ts.map +0 -1
- package/dist/renderer/svg-string/renderer.js +0 -53
- package/dist/renderer/svg-string/renderer.js.map +0 -1
- package/dist/renderer/text-measurer/browser-measurer.d.ts +0 -25
- package/dist/renderer/text-measurer/browser-measurer.d.ts.map +0 -1
- package/dist/renderer/text-measurer/browser-measurer.js +0 -85
- package/dist/renderer/text-measurer/browser-measurer.js.map +0 -1
- package/dist/renderer/text-measurer/fallback-measurer.d.ts +0 -22
- package/dist/renderer/text-measurer/fallback-measurer.d.ts.map +0 -1
- package/dist/renderer/text-measurer/fallback-measurer.js +0 -113
- package/dist/renderer/text-measurer/fallback-measurer.js.map +0 -1
- package/dist/renderer/text-measurer/index.d.ts +0 -13
- package/dist/renderer/text-measurer/index.d.ts.map +0 -1
- package/dist/renderer/text-measurer/index.js +0 -35
- package/dist/renderer/text-measurer/index.js.map +0 -1
- package/dist/renderer/text-measurer/types.d.ts +0 -30
- package/dist/renderer/text-measurer/types.d.ts.map +0 -1
- package/dist/renderer/text-measurer/types.js +0 -5
- package/dist/renderer/text-measurer/types.js.map +0 -1
- package/dist/renderer/theme.d.ts +0 -29
- package/dist/renderer/theme.d.ts.map +0 -1
- package/dist/renderer/theme.js +0 -80
- package/dist/renderer/theme.js.map +0 -1
package/dist/renderer/svg.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Renders NetworkGraph to SVG
|
|
4
4
|
*/
|
|
5
5
|
import { getDeviceIcon, getVendorIconEntry } from '../icons/index.js';
|
|
6
|
+
import { DEFAULT_ICON_SIZE, ICON_LABEL_GAP, LABEL_LINE_HEIGHT, MAX_ICON_WIDTH_RATIO, } from '../constants.js';
|
|
6
7
|
const LIGHT_THEME = {
|
|
7
8
|
backgroundColor: '#ffffff',
|
|
8
9
|
defaultNodeFill: '#e2e8f0',
|
|
@@ -69,30 +70,191 @@ export class SVGRenderer {
|
|
|
69
70
|
const theme = graph.settings?.theme;
|
|
70
71
|
this.themeColors = this.getThemeColors(theme);
|
|
71
72
|
this.iconTheme = this.getIconTheme(theme);
|
|
73
|
+
// Calculate legend dimensions if enabled
|
|
74
|
+
const legendSettings = this.getLegendSettings(graph.settings?.legend);
|
|
75
|
+
let legendWidth = 0;
|
|
76
|
+
let legendHeight = 0;
|
|
77
|
+
if (legendSettings.enabled) {
|
|
78
|
+
const legendDims = this.calculateLegendDimensions(graph, legendSettings);
|
|
79
|
+
legendWidth = legendDims.width;
|
|
80
|
+
legendHeight = legendDims.height;
|
|
81
|
+
}
|
|
82
|
+
// Expand bounds to include legend with padding
|
|
83
|
+
const legendPadding = 20;
|
|
84
|
+
const expandedBounds = {
|
|
85
|
+
x: bounds.x,
|
|
86
|
+
y: bounds.y,
|
|
87
|
+
width: bounds.width + (legendSettings.enabled && legendWidth > 0 ? legendPadding : 0),
|
|
88
|
+
height: bounds.height + (legendSettings.enabled && legendHeight > 0 ? legendPadding : 0),
|
|
89
|
+
};
|
|
72
90
|
const parts = [];
|
|
73
|
-
// SVG header using
|
|
74
|
-
const viewBox = `${
|
|
75
|
-
parts.push(this.renderHeader(
|
|
91
|
+
// SVG header using expanded bounds
|
|
92
|
+
const viewBox = `${expandedBounds.x} ${expandedBounds.y} ${expandedBounds.width} ${expandedBounds.height}`;
|
|
93
|
+
parts.push(this.renderHeader(expandedBounds.width, expandedBounds.height, viewBox));
|
|
76
94
|
// Defs (markers, gradients)
|
|
77
95
|
parts.push(this.renderDefs());
|
|
78
96
|
// Styles
|
|
79
97
|
parts.push(this.renderStyles());
|
|
80
|
-
// Subgraphs (background
|
|
98
|
+
// Layer 1: Subgraphs (background)
|
|
81
99
|
layout.subgraphs.forEach((sg) => {
|
|
82
100
|
parts.push(this.renderSubgraph(sg));
|
|
83
101
|
});
|
|
84
|
-
//
|
|
102
|
+
// Layer 2: Node backgrounds (shapes)
|
|
103
|
+
layout.nodes.forEach((node) => {
|
|
104
|
+
parts.push(this.renderNodeBackground(node));
|
|
105
|
+
});
|
|
106
|
+
// Layer 3: Links (on top of node backgrounds)
|
|
85
107
|
layout.links.forEach((link) => {
|
|
86
108
|
parts.push(this.renderLink(link, layout.nodes));
|
|
87
109
|
});
|
|
88
|
-
//
|
|
110
|
+
// Layer 4: Node foregrounds (content + ports, on top of links)
|
|
89
111
|
layout.nodes.forEach((node) => {
|
|
90
|
-
parts.push(this.
|
|
112
|
+
parts.push(this.renderNodeForeground(node));
|
|
91
113
|
});
|
|
114
|
+
// Legend (if enabled) - use already calculated legendSettings
|
|
115
|
+
if (legendSettings.enabled && legendWidth > 0) {
|
|
116
|
+
parts.push(this.renderLegend(graph, layout, legendSettings));
|
|
117
|
+
}
|
|
92
118
|
// Close SVG
|
|
93
119
|
parts.push('</svg>');
|
|
94
120
|
return parts.join('\n');
|
|
95
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Calculate legend dimensions without rendering
|
|
124
|
+
*/
|
|
125
|
+
calculateLegendDimensions(graph, settings) {
|
|
126
|
+
const lineHeight = 20;
|
|
127
|
+
const padding = 12;
|
|
128
|
+
const iconWidth = 30;
|
|
129
|
+
const maxLabelWidth = 100;
|
|
130
|
+
// Count items
|
|
131
|
+
let itemCount = 0;
|
|
132
|
+
if (settings.showBandwidth) {
|
|
133
|
+
const usedBandwidths = new Set();
|
|
134
|
+
graph.links.forEach(link => {
|
|
135
|
+
if (link.bandwidth)
|
|
136
|
+
usedBandwidths.add(link.bandwidth);
|
|
137
|
+
});
|
|
138
|
+
itemCount += usedBandwidths.size;
|
|
139
|
+
}
|
|
140
|
+
if (itemCount === 0) {
|
|
141
|
+
return { width: 0, height: 0 };
|
|
142
|
+
}
|
|
143
|
+
const width = iconWidth + maxLabelWidth + padding * 2;
|
|
144
|
+
const height = itemCount * lineHeight + padding * 2 + 20; // +20 for title
|
|
145
|
+
return { width, height };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Parse legend settings from various input formats
|
|
149
|
+
*/
|
|
150
|
+
getLegendSettings(legend) {
|
|
151
|
+
if (legend === true) {
|
|
152
|
+
return {
|
|
153
|
+
enabled: true,
|
|
154
|
+
position: 'top-right',
|
|
155
|
+
showDeviceTypes: true,
|
|
156
|
+
showBandwidth: true,
|
|
157
|
+
showCableTypes: true,
|
|
158
|
+
showVlans: false,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (legend && typeof legend === 'object') {
|
|
162
|
+
return {
|
|
163
|
+
enabled: legend.enabled !== false,
|
|
164
|
+
position: legend.position ?? 'top-right',
|
|
165
|
+
showDeviceTypes: legend.showDeviceTypes ?? true,
|
|
166
|
+
showBandwidth: legend.showBandwidth ?? true,
|
|
167
|
+
showCableTypes: legend.showCableTypes ?? true,
|
|
168
|
+
showVlans: legend.showVlans ?? false,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return { enabled: false, position: 'top-right' };
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Render legend showing visual elements used in the diagram
|
|
175
|
+
*/
|
|
176
|
+
renderLegend(graph, layout, settings) {
|
|
177
|
+
const items = [];
|
|
178
|
+
const lineHeight = 20;
|
|
179
|
+
const padding = 12;
|
|
180
|
+
const iconWidth = 30;
|
|
181
|
+
const maxLabelWidth = 100;
|
|
182
|
+
// Collect used bandwidths
|
|
183
|
+
const usedBandwidths = new Set();
|
|
184
|
+
graph.links.forEach(link => {
|
|
185
|
+
if (link.bandwidth)
|
|
186
|
+
usedBandwidths.add(link.bandwidth);
|
|
187
|
+
});
|
|
188
|
+
// Collect used device types
|
|
189
|
+
const usedDeviceTypes = new Set();
|
|
190
|
+
graph.nodes.forEach(node => {
|
|
191
|
+
if (node.type)
|
|
192
|
+
usedDeviceTypes.add(node.type);
|
|
193
|
+
});
|
|
194
|
+
// Build legend items
|
|
195
|
+
if (settings.showBandwidth && usedBandwidths.size > 0) {
|
|
196
|
+
const sortedBandwidths = ['1G', '10G', '25G', '40G', '100G'].filter(b => usedBandwidths.has(b));
|
|
197
|
+
for (const bw of sortedBandwidths) {
|
|
198
|
+
const config = this.getBandwidthConfig(bw);
|
|
199
|
+
items.push({
|
|
200
|
+
icon: this.renderBandwidthLegendIcon(config.lineCount),
|
|
201
|
+
label: bw,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (items.length === 0)
|
|
206
|
+
return '';
|
|
207
|
+
// Calculate legend dimensions
|
|
208
|
+
const legendWidth = iconWidth + maxLabelWidth + padding * 2;
|
|
209
|
+
const legendHeight = items.length * lineHeight + padding * 2 + 20; // +20 for title
|
|
210
|
+
// Position based on settings
|
|
211
|
+
const { bounds } = layout;
|
|
212
|
+
let legendX = bounds.x + bounds.width - legendWidth - 10;
|
|
213
|
+
let legendY = bounds.y + bounds.height - legendHeight - 10;
|
|
214
|
+
switch (settings.position) {
|
|
215
|
+
case 'top-left':
|
|
216
|
+
legendX = bounds.x + 10;
|
|
217
|
+
legendY = bounds.y + 10;
|
|
218
|
+
break;
|
|
219
|
+
case 'top-right':
|
|
220
|
+
legendX = bounds.x + bounds.width - legendWidth - 10;
|
|
221
|
+
legendY = bounds.y + 10;
|
|
222
|
+
break;
|
|
223
|
+
case 'bottom-left':
|
|
224
|
+
legendX = bounds.x + 10;
|
|
225
|
+
legendY = bounds.y + bounds.height - legendHeight - 10;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
// Render legend box
|
|
229
|
+
let svg = `<g class="legend" transform="translate(${legendX}, ${legendY})">
|
|
230
|
+
<rect x="0" y="0" width="${legendWidth}" height="${legendHeight}" rx="4"
|
|
231
|
+
fill="${this.themeColors.backgroundColor}" stroke="${this.themeColors.subgraphStroke}" stroke-width="1" opacity="0.95" />
|
|
232
|
+
<text x="${padding}" y="${padding + 12}" class="subgraph-label" font-size="11">Legend</text>`;
|
|
233
|
+
// Render items
|
|
234
|
+
items.forEach((item, index) => {
|
|
235
|
+
const y = padding + 28 + index * lineHeight;
|
|
236
|
+
svg += `\n <g transform="translate(${padding}, ${y})">`;
|
|
237
|
+
svg += `\n ${item.icon}`;
|
|
238
|
+
svg += `\n <text x="${iconWidth + 4}" y="4" class="node-label" font-size="10">${this.escapeXml(item.label)}</text>`;
|
|
239
|
+
svg += `\n </g>`;
|
|
240
|
+
});
|
|
241
|
+
svg += '\n</g>';
|
|
242
|
+
return svg;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Render bandwidth indicator for legend
|
|
246
|
+
*/
|
|
247
|
+
renderBandwidthLegendIcon(lineCount) {
|
|
248
|
+
const lineSpacing = 3;
|
|
249
|
+
const lineWidth = 24;
|
|
250
|
+
const strokeWidth = 2;
|
|
251
|
+
const offsets = this.calculateLineOffsets(lineCount, lineSpacing);
|
|
252
|
+
const lines = offsets.map(offset => {
|
|
253
|
+
const y = offset;
|
|
254
|
+
return `<line x1="0" y1="${y}" x2="${lineWidth}" y2="${y}" stroke="${this.themeColors.defaultLinkStroke}" stroke-width="${strokeWidth}" />`;
|
|
255
|
+
});
|
|
256
|
+
return `<g transform="translate(0, 0)">${lines.join('')}</g>`;
|
|
257
|
+
}
|
|
96
258
|
renderHeader(width, height, viewBox) {
|
|
97
259
|
return `<svg xmlns="http://www.w3.org/2000/svg"
|
|
98
260
|
viewBox="${viewBox}"
|
|
@@ -195,8 +357,9 @@ export class SVGRenderer {
|
|
|
195
357
|
<text x="${labelX}" y="${labelY}" class="subgraph-label" text-anchor="${textAnchor}">${this.escapeXml(subgraph.label)}</text>
|
|
196
358
|
</g>`;
|
|
197
359
|
}
|
|
198
|
-
|
|
199
|
-
|
|
360
|
+
/** Render node background (shape only) */
|
|
361
|
+
renderNodeBackground(layoutNode) {
|
|
362
|
+
const { id, position, size, node } = layoutNode;
|
|
200
363
|
const x = position.x;
|
|
201
364
|
const y = position.y;
|
|
202
365
|
const w = size.width;
|
|
@@ -207,13 +370,18 @@ export class SVGRenderer {
|
|
|
207
370
|
const strokeWidth = style.strokeWidth || 1;
|
|
208
371
|
const strokeDasharray = style.strokeDasharray || '';
|
|
209
372
|
const shape = this.renderNodeShape(node.shape, x, y, w, h, fill, stroke, strokeWidth, strokeDasharray);
|
|
210
|
-
|
|
211
|
-
|
|
373
|
+
return `<g class="node-bg" data-id="${id}">${shape}</g>`;
|
|
374
|
+
}
|
|
375
|
+
/** Render node foreground (content + ports) */
|
|
376
|
+
renderNodeForeground(layoutNode) {
|
|
377
|
+
const { id, position, size, node, ports } = layoutNode;
|
|
378
|
+
const x = position.x;
|
|
379
|
+
const y = position.y;
|
|
380
|
+
const w = size.width;
|
|
381
|
+
const content = this.renderNodeContent(node, x, y, w);
|
|
212
382
|
const portsRendered = this.renderPorts(x, y, ports);
|
|
213
|
-
return `<g class="node" data-id="${id}"
|
|
214
|
-
${
|
|
215
|
-
${iconResult.svg}
|
|
216
|
-
${label}
|
|
383
|
+
return `<g class="node-fg" data-id="${id}">
|
|
384
|
+
${content}
|
|
217
385
|
${portsRendered}
|
|
218
386
|
</g>`;
|
|
219
387
|
}
|
|
@@ -320,9 +488,8 @@ export class SVGRenderer {
|
|
|
320
488
|
* Calculate icon dimensions for a node
|
|
321
489
|
*/
|
|
322
490
|
calculateIconInfo(node, w) {
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
const maxIconWidth = w - iconPadding;
|
|
491
|
+
// Cap icon width at MAX_ICON_WIDTH_RATIO of node width to leave room for ports
|
|
492
|
+
const maxIconWidth = Math.round(w * MAX_ICON_WIDTH_RATIO);
|
|
326
493
|
// Try vendor-specific icon first (service for cloud, model for hardware)
|
|
327
494
|
const iconKey = node.service || node.model;
|
|
328
495
|
if (node.vendor && iconKey) {
|
|
@@ -337,8 +504,8 @@ export class SVGRenderer {
|
|
|
337
504
|
const vbWidth = parseInt(viewBoxMatch[1]);
|
|
338
505
|
const vbHeight = parseInt(viewBoxMatch[2]);
|
|
339
506
|
const aspectRatio = vbWidth / vbHeight;
|
|
340
|
-
let iconWidth = Math.round(
|
|
341
|
-
let iconHeight =
|
|
507
|
+
let iconWidth = Math.round(DEFAULT_ICON_SIZE * aspectRatio);
|
|
508
|
+
let iconHeight = DEFAULT_ICON_SIZE;
|
|
342
509
|
if (iconWidth > maxIconWidth) {
|
|
343
510
|
iconWidth = maxIconWidth;
|
|
344
511
|
iconHeight = Math.round(maxIconWidth / aspectRatio);
|
|
@@ -357,8 +524,8 @@ export class SVGRenderer {
|
|
|
357
524
|
const vbWidth = parseInt(vbMatch[3]);
|
|
358
525
|
const vbHeight = parseInt(vbMatch[4]);
|
|
359
526
|
const aspectRatio = vbWidth / vbHeight;
|
|
360
|
-
let iconWidth = Math.abs(aspectRatio - 1) < 0.01 ?
|
|
361
|
-
let iconHeight =
|
|
527
|
+
let iconWidth = Math.abs(aspectRatio - 1) < 0.01 ? DEFAULT_ICON_SIZE : Math.round(DEFAULT_ICON_SIZE * aspectRatio);
|
|
528
|
+
let iconHeight = DEFAULT_ICON_SIZE;
|
|
362
529
|
if (iconWidth > maxIconWidth) {
|
|
363
530
|
iconWidth = maxIconWidth;
|
|
364
531
|
iconHeight = Math.round(maxIconWidth / aspectRatio);
|
|
@@ -371,9 +538,9 @@ export class SVGRenderer {
|
|
|
371
538
|
}
|
|
372
539
|
// Fallback: use viewBox directly
|
|
373
540
|
return {
|
|
374
|
-
width:
|
|
375
|
-
height:
|
|
376
|
-
svg: `<svg width="${
|
|
541
|
+
width: DEFAULT_ICON_SIZE,
|
|
542
|
+
height: DEFAULT_ICON_SIZE,
|
|
543
|
+
svg: `<svg width="${DEFAULT_ICON_SIZE}" height="${DEFAULT_ICON_SIZE}" viewBox="${viewBox}">${vendorIcon}</svg>`,
|
|
377
544
|
};
|
|
378
545
|
}
|
|
379
546
|
}
|
|
@@ -382,36 +549,41 @@ export class SVGRenderer {
|
|
|
382
549
|
if (!iconPath)
|
|
383
550
|
return null;
|
|
384
551
|
return {
|
|
385
|
-
width:
|
|
386
|
-
height:
|
|
387
|
-
svg: `<svg width="${
|
|
552
|
+
width: DEFAULT_ICON_SIZE,
|
|
553
|
+
height: DEFAULT_ICON_SIZE,
|
|
554
|
+
svg: `<svg width="${DEFAULT_ICON_SIZE}" height="${DEFAULT_ICON_SIZE}" viewBox="0 0 24 24" fill="currentColor">${iconPath}</svg>`,
|
|
388
555
|
};
|
|
389
556
|
}
|
|
390
|
-
|
|
557
|
+
/**
|
|
558
|
+
* Render node content (icon + label) with dynamic vertical centering
|
|
559
|
+
*/
|
|
560
|
+
renderNodeContent(node, x, y, w) {
|
|
391
561
|
const iconInfo = this.calculateIconInfo(node, w);
|
|
392
|
-
if (!iconInfo)
|
|
393
|
-
return { svg: '', height: 0 };
|
|
394
|
-
const iconY = y - h / 2 + 12; // Position near top of node
|
|
395
|
-
const svg = `<g class="node-icon" transform="translate(${x - iconInfo.width / 2}, ${iconY})">
|
|
396
|
-
${iconInfo.svg}
|
|
397
|
-
</g>`;
|
|
398
|
-
return { svg, height: iconInfo.height };
|
|
399
|
-
}
|
|
400
|
-
renderNodeLabel(node, x, y, _h, iconHeight) {
|
|
401
562
|
const labels = Array.isArray(node.label) ? node.label : [node.label];
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
563
|
+
const labelHeight = labels.length * LABEL_LINE_HEIGHT;
|
|
564
|
+
// Calculate total content height
|
|
565
|
+
const iconHeight = iconInfo?.height || 0;
|
|
566
|
+
const gap = iconHeight > 0 ? ICON_LABEL_GAP : 0;
|
|
567
|
+
const totalContentHeight = iconHeight + gap + labelHeight;
|
|
568
|
+
// Center the content block vertically in the node
|
|
569
|
+
const contentTop = y - totalContentHeight / 2;
|
|
570
|
+
const parts = [];
|
|
571
|
+
// Render icon at top of content block
|
|
572
|
+
if (iconInfo) {
|
|
573
|
+
const iconY = contentTop;
|
|
574
|
+
parts.push(`<g class="node-icon" transform="translate(${x - iconInfo.width / 2}, ${iconY})">
|
|
575
|
+
${iconInfo.svg}
|
|
576
|
+
</g>`);
|
|
577
|
+
}
|
|
578
|
+
// Render labels below icon
|
|
579
|
+
const labelStartY = contentTop + iconHeight + gap + LABEL_LINE_HEIGHT * 0.7; // 0.7 for text baseline adjustment
|
|
580
|
+
labels.forEach((line, i) => {
|
|
409
581
|
const isBold = line.includes('<b>') || line.includes('<strong>');
|
|
410
582
|
const cleanLine = line.replace(/<\/?b>|<\/?strong>|<br\s*\/?>/gi, '');
|
|
411
583
|
const className = isBold ? 'node-label node-label-bold' : 'node-label';
|
|
412
|
-
|
|
584
|
+
parts.push(`<text x="${x}" y="${labelStartY + i * LABEL_LINE_HEIGHT}" class="${className}" text-anchor="middle">${this.escapeXml(cleanLine)}</text>`);
|
|
413
585
|
});
|
|
414
|
-
return
|
|
586
|
+
return parts.join('\n ');
|
|
415
587
|
}
|
|
416
588
|
renderLink(layoutLink, nodes) {
|
|
417
589
|
const { id, points, link, fromEndpoint, toEndpoint } = layoutLink;
|
|
@@ -488,9 +660,6 @@ export class SVGRenderer {
|
|
|
488
660
|
// Normalize direction
|
|
489
661
|
const nx = len > 0 ? dx / len : 0;
|
|
490
662
|
const ny = len > 0 ? dy / len : 1;
|
|
491
|
-
// Perpendicular direction (90 degrees rotated)
|
|
492
|
-
const perpX = -ny;
|
|
493
|
-
const perpY = nx;
|
|
494
663
|
const isVertical = Math.abs(dy) > Math.abs(dx);
|
|
495
664
|
// Hash port name as fallback
|
|
496
665
|
const portHash = this.hashString(portName);
|
|
@@ -518,21 +687,27 @@ export class SVGRenderer {
|
|
|
518
687
|
// Position: offset along line direction + fixed horizontal offset for vertical links
|
|
519
688
|
let x;
|
|
520
689
|
let y;
|
|
690
|
+
let anchor;
|
|
521
691
|
if (isVertical) {
|
|
522
692
|
// For vertical links, use fixed horizontal offset (simpler and consistent)
|
|
523
693
|
x = endpoint.x + perpDist * sideMultiplier;
|
|
524
694
|
y = endpoint.y + ny * offsetDist;
|
|
695
|
+
// Text anchor based on final position relative to endpoint
|
|
696
|
+
anchor = 'middle';
|
|
697
|
+
const labelDx = x - endpoint.x;
|
|
698
|
+
if (Math.abs(labelDx) > 8) {
|
|
699
|
+
anchor = labelDx > 0 ? 'start' : 'end';
|
|
700
|
+
}
|
|
525
701
|
}
|
|
526
702
|
else {
|
|
527
|
-
// For horizontal links,
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
anchor = labelDx > 0 ? 'start' : 'end';
|
|
703
|
+
// For horizontal links, position label near the port (not toward center)
|
|
704
|
+
// Keep x near the endpoint, offset y below the line
|
|
705
|
+
x = endpoint.x;
|
|
706
|
+
y = endpoint.y + perpDist; // Always below the line
|
|
707
|
+
// Text anchor: extend toward the center of the link
|
|
708
|
+
// Start endpoint extends right (start), end endpoint extends left (end)
|
|
709
|
+
// Check direction: if nextPoint is to the left, we're on the right side
|
|
710
|
+
anchor = nx < 0 ? 'end' : 'start';
|
|
536
711
|
}
|
|
537
712
|
return { x, y, anchor };
|
|
538
713
|
}
|
|
@@ -570,7 +745,7 @@ export class SVGRenderer {
|
|
|
570
745
|
switch (type) {
|
|
571
746
|
case 'thick': return 3;
|
|
572
747
|
case 'double': return 2;
|
|
573
|
-
default: return
|
|
748
|
+
default: return 2;
|
|
574
749
|
}
|
|
575
750
|
}
|
|
576
751
|
/**
|
|
@@ -582,7 +757,7 @@ export class SVGRenderer {
|
|
|
582
757
|
* 100G → 5 lines
|
|
583
758
|
*/
|
|
584
759
|
getBandwidthConfig(bandwidth) {
|
|
585
|
-
const strokeWidth =
|
|
760
|
+
const strokeWidth = 2;
|
|
586
761
|
switch (bandwidth) {
|
|
587
762
|
case '1G':
|
|
588
763
|
return { lineCount: 1, strokeWidth };
|
|
@@ -632,21 +807,44 @@ ${result}`;
|
|
|
632
807
|
return paths.join('\n');
|
|
633
808
|
}
|
|
634
809
|
/**
|
|
635
|
-
* Generate SVG path string from points
|
|
810
|
+
* Generate SVG path string from points with rounded corners
|
|
636
811
|
*/
|
|
637
|
-
generatePath(points) {
|
|
638
|
-
if (points.length
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
}
|
|
642
|
-
else if (points.length === 2) {
|
|
643
|
-
// Straight line
|
|
812
|
+
generatePath(points, cornerRadius = 8) {
|
|
813
|
+
if (points.length < 2)
|
|
814
|
+
return '';
|
|
815
|
+
if (points.length === 2) {
|
|
644
816
|
return `M ${points[0].x} ${points[0].y} L ${points[1].x} ${points[1].y}`;
|
|
645
817
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
818
|
+
const parts = [`M ${points[0].x} ${points[0].y}`];
|
|
819
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
820
|
+
const prev = points[i - 1];
|
|
821
|
+
const curr = points[i];
|
|
822
|
+
const next = points[i + 1];
|
|
823
|
+
// Calculate distances to prev and next points
|
|
824
|
+
const distPrev = Math.hypot(curr.x - prev.x, curr.y - prev.y);
|
|
825
|
+
const distNext = Math.hypot(next.x - curr.x, next.y - curr.y);
|
|
826
|
+
// Limit radius to half the shortest segment
|
|
827
|
+
const maxRadius = Math.min(distPrev, distNext) / 2;
|
|
828
|
+
const radius = Math.min(cornerRadius, maxRadius);
|
|
829
|
+
if (radius < 1) {
|
|
830
|
+
// Too small for rounding, just use straight line
|
|
831
|
+
parts.push(`L ${curr.x} ${curr.y}`);
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
// Calculate direction vectors
|
|
835
|
+
const dirPrev = { x: (curr.x - prev.x) / distPrev, y: (curr.y - prev.y) / distPrev };
|
|
836
|
+
const dirNext = { x: (next.x - curr.x) / distNext, y: (next.y - curr.y) / distNext };
|
|
837
|
+
// Points where curve starts and ends
|
|
838
|
+
const startCurve = { x: curr.x - dirPrev.x * radius, y: curr.y - dirPrev.y * radius };
|
|
839
|
+
const endCurve = { x: curr.x + dirNext.x * radius, y: curr.y + dirNext.y * radius };
|
|
840
|
+
// Line to start of curve, then quadratic bezier through corner
|
|
841
|
+
parts.push(`L ${startCurve.x} ${startCurve.y}`);
|
|
842
|
+
parts.push(`Q ${curr.x} ${curr.y} ${endCurve.x} ${endCurve.y}`);
|
|
649
843
|
}
|
|
844
|
+
// Line to final point
|
|
845
|
+
const last = points[points.length - 1];
|
|
846
|
+
parts.push(`L ${last.x} ${last.y}`);
|
|
847
|
+
return parts.join(' ');
|
|
650
848
|
}
|
|
651
849
|
/**
|
|
652
850
|
* Calculate offsets for parallel lines (centered around 0)
|
|
@@ -661,25 +859,54 @@ ${result}`;
|
|
|
661
859
|
return offsets;
|
|
662
860
|
}
|
|
663
861
|
/**
|
|
664
|
-
* Offset points perpendicular to
|
|
862
|
+
* Offset points perpendicular to line direction, handling each segment properly
|
|
863
|
+
* For orthogonal paths, this maintains parallel lines through bends
|
|
665
864
|
*/
|
|
666
865
|
offsetPoints(points, offset) {
|
|
667
866
|
if (points.length < 2)
|
|
668
867
|
return points;
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
868
|
+
const result = [];
|
|
869
|
+
for (let i = 0; i < points.length; i++) {
|
|
870
|
+
const p = points[i];
|
|
871
|
+
if (i === 0) {
|
|
872
|
+
// First point: use direction to next point
|
|
873
|
+
const next = points[i + 1];
|
|
874
|
+
const perp = this.getPerpendicular(p, next);
|
|
875
|
+
result.push({ x: p.x + perp.x * offset, y: p.y + perp.y * offset });
|
|
876
|
+
}
|
|
877
|
+
else if (i === points.length - 1) {
|
|
878
|
+
// Last point: use direction from previous point
|
|
879
|
+
const prev = points[i - 1];
|
|
880
|
+
const perp = this.getPerpendicular(prev, p);
|
|
881
|
+
result.push({ x: p.x + perp.x * offset, y: p.y + perp.y * offset });
|
|
882
|
+
}
|
|
883
|
+
else {
|
|
884
|
+
// Middle point (bend): offset based on incoming segment direction
|
|
885
|
+
const prev = points[i - 1];
|
|
886
|
+
const perp = this.getPerpendicular(prev, p);
|
|
887
|
+
result.push({ x: p.x + perp.x * offset, y: p.y + perp.y * offset });
|
|
888
|
+
// Also add a point for the outgoing segment if direction changes
|
|
889
|
+
const next = points[i + 1];
|
|
890
|
+
const perpNext = this.getPerpendicular(p, next);
|
|
891
|
+
// Check if direction changed (bend point)
|
|
892
|
+
if (Math.abs(perp.x - perpNext.x) > 0.01 || Math.abs(perp.y - perpNext.y) > 0.01) {
|
|
893
|
+
result.push({ x: p.x + perpNext.x * offset, y: p.y + perpNext.y * offset });
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return result;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Get perpendicular unit vector for a line segment
|
|
901
|
+
*/
|
|
902
|
+
getPerpendicular(from, to) {
|
|
903
|
+
const dx = to.x - from.x;
|
|
904
|
+
const dy = to.y - from.y;
|
|
672
905
|
const len = Math.sqrt(dx * dx + dy * dy);
|
|
673
906
|
if (len === 0)
|
|
674
|
-
return
|
|
675
|
-
// Perpendicular unit vector
|
|
676
|
-
|
|
677
|
-
const perpY = dx / len;
|
|
678
|
-
// Offset all points
|
|
679
|
-
return points.map(p => ({
|
|
680
|
-
x: p.x + perpX * offset,
|
|
681
|
-
y: p.y + perpY * offset,
|
|
682
|
-
}));
|
|
907
|
+
return { x: 0, y: 0 };
|
|
908
|
+
// Perpendicular unit vector (rotate 90 degrees)
|
|
909
|
+
return { x: -dy / len, y: dx / len };
|
|
683
910
|
}
|
|
684
911
|
/**
|
|
685
912
|
* Get default link type based on redundancy
|