@shumoku/core 0.1.1 → 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/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/src/models/types.ts
CHANGED
|
@@ -378,6 +378,38 @@ export function paperSizeToPixels(
|
|
|
378
378
|
*/
|
|
379
379
|
export type ThemeType = 'light' | 'dark'
|
|
380
380
|
|
|
381
|
+
export interface LegendSettings {
|
|
382
|
+
/**
|
|
383
|
+
* Show legend in the diagram
|
|
384
|
+
*/
|
|
385
|
+
enabled?: boolean
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Legend position
|
|
389
|
+
*/
|
|
390
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Show device type icons
|
|
394
|
+
*/
|
|
395
|
+
showDeviceTypes?: boolean
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Show bandwidth indicators
|
|
399
|
+
*/
|
|
400
|
+
showBandwidth?: boolean
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Show cable/link types
|
|
404
|
+
*/
|
|
405
|
+
showCableTypes?: boolean
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Show VLAN colors
|
|
409
|
+
*/
|
|
410
|
+
showVlans?: boolean
|
|
411
|
+
}
|
|
412
|
+
|
|
381
413
|
export interface GraphSettings {
|
|
382
414
|
/**
|
|
383
415
|
* Default layout direction
|
|
@@ -408,6 +440,11 @@ export interface GraphSettings {
|
|
|
408
440
|
* Canvas/sheet size settings
|
|
409
441
|
*/
|
|
410
442
|
canvas?: CanvasSettings
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Legend configuration
|
|
446
|
+
*/
|
|
447
|
+
legend?: boolean | LegendSettings
|
|
411
448
|
}
|
|
412
449
|
|
|
413
450
|
export interface NetworkGraph {
|
package/src/renderer/svg.ts
CHANGED
|
@@ -14,8 +14,16 @@ import type {
|
|
|
14
14
|
NodeShape,
|
|
15
15
|
LinkType,
|
|
16
16
|
ThemeType,
|
|
17
|
+
LegendSettings,
|
|
18
|
+
LinkBandwidth,
|
|
17
19
|
} from '../models/index.js'
|
|
18
20
|
import { getDeviceIcon, getVendorIconEntry, type IconThemeVariant } from '../icons/index.js'
|
|
21
|
+
import {
|
|
22
|
+
DEFAULT_ICON_SIZE,
|
|
23
|
+
ICON_LABEL_GAP,
|
|
24
|
+
LABEL_LINE_HEIGHT,
|
|
25
|
+
MAX_ICON_WIDTH_RATIO,
|
|
26
|
+
} from '../constants.js'
|
|
19
27
|
|
|
20
28
|
// ============================================
|
|
21
29
|
// Theme Colors
|
|
@@ -126,11 +134,30 @@ export class SVGRenderer {
|
|
|
126
134
|
this.themeColors = this.getThemeColors(theme)
|
|
127
135
|
this.iconTheme = this.getIconTheme(theme)
|
|
128
136
|
|
|
137
|
+
// Calculate legend dimensions if enabled
|
|
138
|
+
const legendSettings = this.getLegendSettings(graph.settings?.legend)
|
|
139
|
+
let legendWidth = 0
|
|
140
|
+
let legendHeight = 0
|
|
141
|
+
if (legendSettings.enabled) {
|
|
142
|
+
const legendDims = this.calculateLegendDimensions(graph, legendSettings)
|
|
143
|
+
legendWidth = legendDims.width
|
|
144
|
+
legendHeight = legendDims.height
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Expand bounds to include legend with padding
|
|
148
|
+
const legendPadding = 20
|
|
149
|
+
const expandedBounds = {
|
|
150
|
+
x: bounds.x,
|
|
151
|
+
y: bounds.y,
|
|
152
|
+
width: bounds.width + (legendSettings.enabled && legendWidth > 0 ? legendPadding : 0),
|
|
153
|
+
height: bounds.height + (legendSettings.enabled && legendHeight > 0 ? legendPadding : 0),
|
|
154
|
+
}
|
|
155
|
+
|
|
129
156
|
const parts: string[] = []
|
|
130
157
|
|
|
131
|
-
// SVG header using
|
|
132
|
-
const viewBox = `${
|
|
133
|
-
parts.push(this.renderHeader(
|
|
158
|
+
// SVG header using expanded bounds
|
|
159
|
+
const viewBox = `${expandedBounds.x} ${expandedBounds.y} ${expandedBounds.width} ${expandedBounds.height}`
|
|
160
|
+
parts.push(this.renderHeader(expandedBounds.width, expandedBounds.height, viewBox))
|
|
134
161
|
|
|
135
162
|
// Defs (markers, gradients)
|
|
136
163
|
parts.push(this.renderDefs())
|
|
@@ -138,27 +165,195 @@ export class SVGRenderer {
|
|
|
138
165
|
// Styles
|
|
139
166
|
parts.push(this.renderStyles())
|
|
140
167
|
|
|
141
|
-
// Subgraphs (background
|
|
168
|
+
// Layer 1: Subgraphs (background)
|
|
142
169
|
layout.subgraphs.forEach((sg) => {
|
|
143
170
|
parts.push(this.renderSubgraph(sg))
|
|
144
171
|
})
|
|
145
172
|
|
|
146
|
-
//
|
|
173
|
+
// Layer 2: Node backgrounds (shapes)
|
|
174
|
+
layout.nodes.forEach((node) => {
|
|
175
|
+
parts.push(this.renderNodeBackground(node))
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// Layer 3: Links (on top of node backgrounds)
|
|
147
179
|
layout.links.forEach((link) => {
|
|
148
180
|
parts.push(this.renderLink(link, layout.nodes))
|
|
149
181
|
})
|
|
150
182
|
|
|
151
|
-
//
|
|
183
|
+
// Layer 4: Node foregrounds (content + ports, on top of links)
|
|
152
184
|
layout.nodes.forEach((node) => {
|
|
153
|
-
parts.push(this.
|
|
185
|
+
parts.push(this.renderNodeForeground(node))
|
|
154
186
|
})
|
|
155
187
|
|
|
188
|
+
// Legend (if enabled) - use already calculated legendSettings
|
|
189
|
+
if (legendSettings.enabled && legendWidth > 0) {
|
|
190
|
+
parts.push(this.renderLegend(graph, layout, legendSettings))
|
|
191
|
+
}
|
|
192
|
+
|
|
156
193
|
// Close SVG
|
|
157
194
|
parts.push('</svg>')
|
|
158
195
|
|
|
159
196
|
return parts.join('\n')
|
|
160
197
|
}
|
|
161
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Calculate legend dimensions without rendering
|
|
201
|
+
*/
|
|
202
|
+
private calculateLegendDimensions(graph: NetworkGraph, settings: LegendSettings): { width: number; height: number } {
|
|
203
|
+
const lineHeight = 20
|
|
204
|
+
const padding = 12
|
|
205
|
+
const iconWidth = 30
|
|
206
|
+
const maxLabelWidth = 100
|
|
207
|
+
|
|
208
|
+
// Count items
|
|
209
|
+
let itemCount = 0
|
|
210
|
+
|
|
211
|
+
if (settings.showBandwidth) {
|
|
212
|
+
const usedBandwidths = new Set<LinkBandwidth>()
|
|
213
|
+
graph.links.forEach(link => {
|
|
214
|
+
if (link.bandwidth) usedBandwidths.add(link.bandwidth)
|
|
215
|
+
})
|
|
216
|
+
itemCount += usedBandwidths.size
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (itemCount === 0) {
|
|
220
|
+
return { width: 0, height: 0 }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const width = iconWidth + maxLabelWidth + padding * 2
|
|
224
|
+
const height = itemCount * lineHeight + padding * 2 + 20 // +20 for title
|
|
225
|
+
|
|
226
|
+
return { width, height }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Parse legend settings from various input formats
|
|
231
|
+
*/
|
|
232
|
+
private getLegendSettings(legend?: boolean | LegendSettings): LegendSettings & { enabled: boolean } {
|
|
233
|
+
if (legend === true) {
|
|
234
|
+
return {
|
|
235
|
+
enabled: true,
|
|
236
|
+
position: 'top-right',
|
|
237
|
+
showDeviceTypes: true,
|
|
238
|
+
showBandwidth: true,
|
|
239
|
+
showCableTypes: true,
|
|
240
|
+
showVlans: false,
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (legend && typeof legend === 'object') {
|
|
245
|
+
return {
|
|
246
|
+
enabled: legend.enabled !== false,
|
|
247
|
+
position: legend.position ?? 'top-right',
|
|
248
|
+
showDeviceTypes: legend.showDeviceTypes ?? true,
|
|
249
|
+
showBandwidth: legend.showBandwidth ?? true,
|
|
250
|
+
showCableTypes: legend.showCableTypes ?? true,
|
|
251
|
+
showVlans: legend.showVlans ?? false,
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { enabled: false, position: 'top-right' }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Render legend showing visual elements used in the diagram
|
|
260
|
+
*/
|
|
261
|
+
private renderLegend(graph: NetworkGraph, layout: LayoutResult, settings: LegendSettings): string {
|
|
262
|
+
const items: { icon: string; label: string }[] = []
|
|
263
|
+
const lineHeight = 20
|
|
264
|
+
const padding = 12
|
|
265
|
+
const iconWidth = 30
|
|
266
|
+
const maxLabelWidth = 100
|
|
267
|
+
|
|
268
|
+
// Collect used bandwidths
|
|
269
|
+
const usedBandwidths = new Set<LinkBandwidth>()
|
|
270
|
+
graph.links.forEach(link => {
|
|
271
|
+
if (link.bandwidth) usedBandwidths.add(link.bandwidth)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
// Collect used device types
|
|
275
|
+
const usedDeviceTypes = new Set<string>()
|
|
276
|
+
graph.nodes.forEach(node => {
|
|
277
|
+
if (node.type) usedDeviceTypes.add(node.type)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
// Build legend items
|
|
281
|
+
if (settings.showBandwidth && usedBandwidths.size > 0) {
|
|
282
|
+
const sortedBandwidths: LinkBandwidth[] = ['1G', '10G', '25G', '40G', '100G'].filter(
|
|
283
|
+
b => usedBandwidths.has(b as LinkBandwidth)
|
|
284
|
+
) as LinkBandwidth[]
|
|
285
|
+
|
|
286
|
+
for (const bw of sortedBandwidths) {
|
|
287
|
+
const config = this.getBandwidthConfig(bw)
|
|
288
|
+
items.push({
|
|
289
|
+
icon: this.renderBandwidthLegendIcon(config.lineCount),
|
|
290
|
+
label: bw,
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (items.length === 0) return ''
|
|
296
|
+
|
|
297
|
+
// Calculate legend dimensions
|
|
298
|
+
const legendWidth = iconWidth + maxLabelWidth + padding * 2
|
|
299
|
+
const legendHeight = items.length * lineHeight + padding * 2 + 20 // +20 for title
|
|
300
|
+
|
|
301
|
+
// Position based on settings
|
|
302
|
+
const { bounds } = layout
|
|
303
|
+
let legendX = bounds.x + bounds.width - legendWidth - 10
|
|
304
|
+
let legendY = bounds.y + bounds.height - legendHeight - 10
|
|
305
|
+
|
|
306
|
+
switch (settings.position) {
|
|
307
|
+
case 'top-left':
|
|
308
|
+
legendX = bounds.x + 10
|
|
309
|
+
legendY = bounds.y + 10
|
|
310
|
+
break
|
|
311
|
+
case 'top-right':
|
|
312
|
+
legendX = bounds.x + bounds.width - legendWidth - 10
|
|
313
|
+
legendY = bounds.y + 10
|
|
314
|
+
break
|
|
315
|
+
case 'bottom-left':
|
|
316
|
+
legendX = bounds.x + 10
|
|
317
|
+
legendY = bounds.y + bounds.height - legendHeight - 10
|
|
318
|
+
break
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Render legend box
|
|
322
|
+
let svg = `<g class="legend" transform="translate(${legendX}, ${legendY})">
|
|
323
|
+
<rect x="0" y="0" width="${legendWidth}" height="${legendHeight}" rx="4"
|
|
324
|
+
fill="${this.themeColors.backgroundColor}" stroke="${this.themeColors.subgraphStroke}" stroke-width="1" opacity="0.95" />
|
|
325
|
+
<text x="${padding}" y="${padding + 12}" class="subgraph-label" font-size="11">Legend</text>`
|
|
326
|
+
|
|
327
|
+
// Render items
|
|
328
|
+
items.forEach((item, index) => {
|
|
329
|
+
const y = padding + 28 + index * lineHeight
|
|
330
|
+
svg += `\n <g transform="translate(${padding}, ${y})">`
|
|
331
|
+
svg += `\n ${item.icon}`
|
|
332
|
+
svg += `\n <text x="${iconWidth + 4}" y="4" class="node-label" font-size="10">${this.escapeXml(item.label)}</text>`
|
|
333
|
+
svg += `\n </g>`
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
svg += '\n</g>'
|
|
337
|
+
return svg
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Render bandwidth indicator for legend
|
|
342
|
+
*/
|
|
343
|
+
private renderBandwidthLegendIcon(lineCount: number): string {
|
|
344
|
+
const lineSpacing = 3
|
|
345
|
+
const lineWidth = 24
|
|
346
|
+
const strokeWidth = 2
|
|
347
|
+
const offsets = this.calculateLineOffsets(lineCount, lineSpacing)
|
|
348
|
+
|
|
349
|
+
const lines = offsets.map(offset => {
|
|
350
|
+
const y = offset
|
|
351
|
+
return `<line x1="0" y1="${y}" x2="${lineWidth}" y2="${y}" stroke="${this.themeColors.defaultLinkStroke}" stroke-width="${strokeWidth}" />`
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
return `<g transform="translate(0, 0)">${lines.join('')}</g>`
|
|
355
|
+
}
|
|
356
|
+
|
|
162
357
|
private renderHeader(width: number, height: number, viewBox: string): string {
|
|
163
358
|
return `<svg xmlns="http://www.w3.org/2000/svg"
|
|
164
359
|
viewBox="${viewBox}"
|
|
@@ -273,8 +468,9 @@ export class SVGRenderer {
|
|
|
273
468
|
</g>`
|
|
274
469
|
}
|
|
275
470
|
|
|
276
|
-
|
|
277
|
-
|
|
471
|
+
/** Render node background (shape only) */
|
|
472
|
+
private renderNodeBackground(layoutNode: LayoutNode): string {
|
|
473
|
+
const { id, position, size, node } = layoutNode
|
|
278
474
|
const x = position.x
|
|
279
475
|
const y = position.y
|
|
280
476
|
const w = size.width
|
|
@@ -287,18 +483,27 @@ export class SVGRenderer {
|
|
|
287
483
|
const strokeDasharray = style.strokeDasharray || ''
|
|
288
484
|
|
|
289
485
|
const shape = this.renderNodeShape(node.shape, x, y, w, h, fill, stroke, strokeWidth, strokeDasharray)
|
|
290
|
-
|
|
291
|
-
|
|
486
|
+
|
|
487
|
+
return `<g class="node-bg" data-id="${id}">${shape}</g>`
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Render node foreground (content + ports) */
|
|
491
|
+
private renderNodeForeground(layoutNode: LayoutNode): string {
|
|
492
|
+
const { id, position, size, node, ports } = layoutNode
|
|
493
|
+
const x = position.x
|
|
494
|
+
const y = position.y
|
|
495
|
+
const w = size.width
|
|
496
|
+
|
|
497
|
+
const content = this.renderNodeContent(node, x, y, w)
|
|
292
498
|
const portsRendered = this.renderPorts(x, y, ports)
|
|
293
499
|
|
|
294
|
-
return `<g class="node" data-id="${id}"
|
|
295
|
-
${
|
|
296
|
-
${iconResult.svg}
|
|
297
|
-
${label}
|
|
500
|
+
return `<g class="node-fg" data-id="${id}">
|
|
501
|
+
${content}
|
|
298
502
|
${portsRendered}
|
|
299
503
|
</g>`
|
|
300
504
|
}
|
|
301
505
|
|
|
506
|
+
|
|
302
507
|
/**
|
|
303
508
|
* Render ports on a node
|
|
304
509
|
*/
|
|
@@ -430,9 +635,8 @@ export class SVGRenderer {
|
|
|
430
635
|
* Calculate icon dimensions for a node
|
|
431
636
|
*/
|
|
432
637
|
private calculateIconInfo(node: Node, w: number): { width: number; height: number; svg: string } | null {
|
|
433
|
-
|
|
434
|
-
const
|
|
435
|
-
const maxIconWidth = w - iconPadding
|
|
638
|
+
// Cap icon width at MAX_ICON_WIDTH_RATIO of node width to leave room for ports
|
|
639
|
+
const maxIconWidth = Math.round(w * MAX_ICON_WIDTH_RATIO)
|
|
436
640
|
|
|
437
641
|
// Try vendor-specific icon first (service for cloud, model for hardware)
|
|
438
642
|
const iconKey = node.service || node.model
|
|
@@ -449,8 +653,8 @@ export class SVGRenderer {
|
|
|
449
653
|
const vbWidth = parseInt(viewBoxMatch[1])
|
|
450
654
|
const vbHeight = parseInt(viewBoxMatch[2])
|
|
451
655
|
const aspectRatio = vbWidth / vbHeight
|
|
452
|
-
let iconWidth = Math.round(
|
|
453
|
-
let iconHeight =
|
|
656
|
+
let iconWidth = Math.round(DEFAULT_ICON_SIZE * aspectRatio)
|
|
657
|
+
let iconHeight = DEFAULT_ICON_SIZE
|
|
454
658
|
if (iconWidth > maxIconWidth) {
|
|
455
659
|
iconWidth = maxIconWidth
|
|
456
660
|
iconHeight = Math.round(maxIconWidth / aspectRatio)
|
|
@@ -470,8 +674,8 @@ export class SVGRenderer {
|
|
|
470
674
|
const vbWidth = parseInt(vbMatch[3])
|
|
471
675
|
const vbHeight = parseInt(vbMatch[4])
|
|
472
676
|
const aspectRatio = vbWidth / vbHeight
|
|
473
|
-
let iconWidth = Math.abs(aspectRatio - 1) < 0.01 ?
|
|
474
|
-
let iconHeight =
|
|
677
|
+
let iconWidth = Math.abs(aspectRatio - 1) < 0.01 ? DEFAULT_ICON_SIZE : Math.round(DEFAULT_ICON_SIZE * aspectRatio)
|
|
678
|
+
let iconHeight = DEFAULT_ICON_SIZE
|
|
475
679
|
if (iconWidth > maxIconWidth) {
|
|
476
680
|
iconWidth = maxIconWidth
|
|
477
681
|
iconHeight = Math.round(maxIconWidth / aspectRatio)
|
|
@@ -485,9 +689,9 @@ export class SVGRenderer {
|
|
|
485
689
|
|
|
486
690
|
// Fallback: use viewBox directly
|
|
487
691
|
return {
|
|
488
|
-
width:
|
|
489
|
-
height:
|
|
490
|
-
svg: `<svg width="${
|
|
692
|
+
width: DEFAULT_ICON_SIZE,
|
|
693
|
+
height: DEFAULT_ICON_SIZE,
|
|
694
|
+
svg: `<svg width="${DEFAULT_ICON_SIZE}" height="${DEFAULT_ICON_SIZE}" viewBox="${viewBox}">${vendorIcon}</svg>`,
|
|
491
695
|
}
|
|
492
696
|
}
|
|
493
697
|
}
|
|
@@ -497,42 +701,48 @@ export class SVGRenderer {
|
|
|
497
701
|
if (!iconPath) return null
|
|
498
702
|
|
|
499
703
|
return {
|
|
500
|
-
width:
|
|
501
|
-
height:
|
|
502
|
-
svg: `<svg width="${
|
|
704
|
+
width: DEFAULT_ICON_SIZE,
|
|
705
|
+
height: DEFAULT_ICON_SIZE,
|
|
706
|
+
svg: `<svg width="${DEFAULT_ICON_SIZE}" height="${DEFAULT_ICON_SIZE}" viewBox="0 0 24 24" fill="currentColor">${iconPath}</svg>`,
|
|
503
707
|
}
|
|
504
708
|
}
|
|
505
709
|
|
|
506
|
-
|
|
710
|
+
/**
|
|
711
|
+
* Render node content (icon + label) with dynamic vertical centering
|
|
712
|
+
*/
|
|
713
|
+
private renderNodeContent(node: Node, x: number, y: number, w: number): string {
|
|
507
714
|
const iconInfo = this.calculateIconInfo(node, w)
|
|
508
|
-
|
|
715
|
+
const labels = Array.isArray(node.label) ? node.label : [node.label]
|
|
716
|
+
const labelHeight = labels.length * LABEL_LINE_HEIGHT
|
|
509
717
|
|
|
510
|
-
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
return { svg, height: iconInfo.height }
|
|
515
|
-
}
|
|
718
|
+
// Calculate total content height
|
|
719
|
+
const iconHeight = iconInfo?.height || 0
|
|
720
|
+
const gap = iconHeight > 0 ? ICON_LABEL_GAP : 0
|
|
721
|
+
const totalContentHeight = iconHeight + gap + labelHeight
|
|
516
722
|
|
|
517
|
-
|
|
518
|
-
const
|
|
519
|
-
const lineHeight = 16
|
|
520
|
-
const totalHeight = labels.length * lineHeight
|
|
723
|
+
// Center the content block vertically in the node
|
|
724
|
+
const contentTop = y - totalContentHeight / 2
|
|
521
725
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
726
|
+
const parts: string[] = []
|
|
727
|
+
|
|
728
|
+
// Render icon at top of content block
|
|
729
|
+
if (iconInfo) {
|
|
730
|
+
const iconY = contentTop
|
|
731
|
+
parts.push(`<g class="node-icon" transform="translate(${x - iconInfo.width / 2}, ${iconY})">
|
|
732
|
+
${iconInfo.svg}
|
|
733
|
+
</g>`)
|
|
734
|
+
}
|
|
525
735
|
|
|
526
|
-
|
|
527
|
-
|
|
736
|
+
// Render labels below icon
|
|
737
|
+
const labelStartY = contentTop + iconHeight + gap + LABEL_LINE_HEIGHT * 0.7 // 0.7 for text baseline adjustment
|
|
738
|
+
labels.forEach((line, i) => {
|
|
528
739
|
const isBold = line.includes('<b>') || line.includes('<strong>')
|
|
529
740
|
const cleanLine = line.replace(/<\/?b>|<\/?strong>|<br\s*\/?>/gi, '')
|
|
530
741
|
const className = isBold ? 'node-label node-label-bold' : 'node-label'
|
|
531
|
-
|
|
532
|
-
return `<text x="${x}" y="${startY + i * lineHeight}" class="${className}" text-anchor="middle">${this.escapeXml(cleanLine)}</text>`
|
|
742
|
+
parts.push(`<text x="${x}" y="${labelStartY + i * LABEL_LINE_HEIGHT}" class="${className}" text-anchor="middle">${this.escapeXml(cleanLine)}</text>`)
|
|
533
743
|
})
|
|
534
744
|
|
|
535
|
-
return
|
|
745
|
+
return parts.join('\n ')
|
|
536
746
|
}
|
|
537
747
|
|
|
538
748
|
private renderLink(layoutLink: LayoutLink, nodes: Map<string, LayoutNode>): string {
|
|
@@ -634,10 +844,6 @@ export class SVGRenderer {
|
|
|
634
844
|
const nx = len > 0 ? dx / len : 0
|
|
635
845
|
const ny = len > 0 ? dy / len : 1
|
|
636
846
|
|
|
637
|
-
// Perpendicular direction (90 degrees rotated)
|
|
638
|
-
const perpX = -ny
|
|
639
|
-
const perpY = nx
|
|
640
|
-
|
|
641
847
|
const isVertical = Math.abs(dy) > Math.abs(dx)
|
|
642
848
|
|
|
643
849
|
// Hash port name as fallback
|
|
@@ -670,21 +876,29 @@ export class SVGRenderer {
|
|
|
670
876
|
let x: number
|
|
671
877
|
let y: number
|
|
672
878
|
|
|
879
|
+
let anchor: string
|
|
880
|
+
|
|
673
881
|
if (isVertical) {
|
|
674
882
|
// For vertical links, use fixed horizontal offset (simpler and consistent)
|
|
675
883
|
x = endpoint.x + perpDist * sideMultiplier
|
|
676
884
|
y = endpoint.y + ny * offsetDist
|
|
677
|
-
} else {
|
|
678
|
-
// For horizontal links, use perpendicular calculation
|
|
679
|
-
x = endpoint.x + nx * offsetDist + perpX * perpDist * sideMultiplier
|
|
680
|
-
y = endpoint.y + ny * offsetDist + perpY * perpDist * sideMultiplier
|
|
681
|
-
}
|
|
682
885
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
886
|
+
// Text anchor based on final position relative to endpoint
|
|
887
|
+
anchor = 'middle'
|
|
888
|
+
const labelDx = x - endpoint.x
|
|
889
|
+
if (Math.abs(labelDx) > 8) {
|
|
890
|
+
anchor = labelDx > 0 ? 'start' : 'end'
|
|
891
|
+
}
|
|
892
|
+
} else {
|
|
893
|
+
// For horizontal links, position label near the port (not toward center)
|
|
894
|
+
// Keep x near the endpoint, offset y below the line
|
|
895
|
+
x = endpoint.x
|
|
896
|
+
y = endpoint.y + perpDist // Always below the line
|
|
897
|
+
|
|
898
|
+
// Text anchor: extend toward the center of the link
|
|
899
|
+
// Start endpoint extends right (start), end endpoint extends left (end)
|
|
900
|
+
// Check direction: if nextPoint is to the left, we're on the right side
|
|
901
|
+
anchor = nx < 0 ? 'end' : 'start'
|
|
688
902
|
}
|
|
689
903
|
|
|
690
904
|
return { x, y, anchor }
|
|
@@ -730,7 +944,7 @@ export class SVGRenderer {
|
|
|
730
944
|
switch (type) {
|
|
731
945
|
case 'thick': return 3
|
|
732
946
|
case 'double': return 2
|
|
733
|
-
default: return
|
|
947
|
+
default: return 2
|
|
734
948
|
}
|
|
735
949
|
}
|
|
736
950
|
|
|
@@ -743,7 +957,7 @@ export class SVGRenderer {
|
|
|
743
957
|
* 100G → 5 lines
|
|
744
958
|
*/
|
|
745
959
|
private getBandwidthConfig(bandwidth?: string): { lineCount: number; strokeWidth: number } {
|
|
746
|
-
const strokeWidth =
|
|
960
|
+
const strokeWidth = 2
|
|
747
961
|
switch (bandwidth) {
|
|
748
962
|
case '1G':
|
|
749
963
|
return { lineCount: 1, strokeWidth }
|
|
@@ -809,19 +1023,53 @@ ${result}`
|
|
|
809
1023
|
}
|
|
810
1024
|
|
|
811
1025
|
/**
|
|
812
|
-
* Generate SVG path string from points
|
|
1026
|
+
* Generate SVG path string from points with rounded corners
|
|
813
1027
|
*/
|
|
814
|
-
private generatePath(points: { x: number; y: number }[]): string {
|
|
815
|
-
if (points.length
|
|
816
|
-
|
|
817
|
-
return `M ${points[0].x} ${points[0].y} C ${points[1].x} ${points[1].y}, ${points[2].x} ${points[2].y}, ${points[3].x} ${points[3].y}`
|
|
818
|
-
} else if (points.length === 2) {
|
|
819
|
-
// Straight line
|
|
1028
|
+
private generatePath(points: { x: number; y: number }[], cornerRadius: number = 8): string {
|
|
1029
|
+
if (points.length < 2) return ''
|
|
1030
|
+
if (points.length === 2) {
|
|
820
1031
|
return `M ${points[0].x} ${points[0].y} L ${points[1].x} ${points[1].y}`
|
|
821
|
-
} else {
|
|
822
|
-
// Polyline
|
|
823
|
-
return `M ${points[0].x} ${points[0].y} ` + points.slice(1).map(p => `L ${p.x} ${p.y}`).join(' ')
|
|
824
1032
|
}
|
|
1033
|
+
|
|
1034
|
+
const parts: string[] = [`M ${points[0].x} ${points[0].y}`]
|
|
1035
|
+
|
|
1036
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
1037
|
+
const prev = points[i - 1]
|
|
1038
|
+
const curr = points[i]
|
|
1039
|
+
const next = points[i + 1]
|
|
1040
|
+
|
|
1041
|
+
// Calculate distances to prev and next points
|
|
1042
|
+
const distPrev = Math.hypot(curr.x - prev.x, curr.y - prev.y)
|
|
1043
|
+
const distNext = Math.hypot(next.x - curr.x, next.y - curr.y)
|
|
1044
|
+
|
|
1045
|
+
// Limit radius to half the shortest segment
|
|
1046
|
+
const maxRadius = Math.min(distPrev, distNext) / 2
|
|
1047
|
+
const radius = Math.min(cornerRadius, maxRadius)
|
|
1048
|
+
|
|
1049
|
+
if (radius < 1) {
|
|
1050
|
+
// Too small for rounding, just use straight line
|
|
1051
|
+
parts.push(`L ${curr.x} ${curr.y}`)
|
|
1052
|
+
continue
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Calculate direction vectors
|
|
1056
|
+
const dirPrev = { x: (curr.x - prev.x) / distPrev, y: (curr.y - prev.y) / distPrev }
|
|
1057
|
+
const dirNext = { x: (next.x - curr.x) / distNext, y: (next.y - curr.y) / distNext }
|
|
1058
|
+
|
|
1059
|
+
// Points where curve starts and ends
|
|
1060
|
+
const startCurve = { x: curr.x - dirPrev.x * radius, y: curr.y - dirPrev.y * radius }
|
|
1061
|
+
const endCurve = { x: curr.x + dirNext.x * radius, y: curr.y + dirNext.y * radius }
|
|
1062
|
+
|
|
1063
|
+
// Line to start of curve, then quadratic bezier through corner
|
|
1064
|
+
parts.push(`L ${startCurve.x} ${startCurve.y}`)
|
|
1065
|
+
parts.push(`Q ${curr.x} ${curr.y} ${endCurve.x} ${endCurve.y}`)
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Line to final point
|
|
1069
|
+
const last = points[points.length - 1]
|
|
1070
|
+
parts.push(`L ${last.x} ${last.y}`)
|
|
1071
|
+
|
|
1072
|
+
return parts.join(' ')
|
|
825
1073
|
}
|
|
826
1074
|
|
|
827
1075
|
/**
|
|
@@ -839,27 +1087,59 @@ ${result}`
|
|
|
839
1087
|
}
|
|
840
1088
|
|
|
841
1089
|
/**
|
|
842
|
-
* Offset points perpendicular to
|
|
1090
|
+
* Offset points perpendicular to line direction, handling each segment properly
|
|
1091
|
+
* For orthogonal paths, this maintains parallel lines through bends
|
|
843
1092
|
*/
|
|
844
1093
|
private offsetPoints(points: { x: number; y: number }[], offset: number): { x: number; y: number }[] {
|
|
845
1094
|
if (points.length < 2) return points
|
|
846
1095
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1096
|
+
const result: { x: number; y: number }[] = []
|
|
1097
|
+
|
|
1098
|
+
for (let i = 0; i < points.length; i++) {
|
|
1099
|
+
const p = points[i]
|
|
1100
|
+
|
|
1101
|
+
if (i === 0) {
|
|
1102
|
+
// First point: use direction to next point
|
|
1103
|
+
const next = points[i + 1]
|
|
1104
|
+
const perp = this.getPerpendicular(p, next)
|
|
1105
|
+
result.push({ x: p.x + perp.x * offset, y: p.y + perp.y * offset })
|
|
1106
|
+
} else if (i === points.length - 1) {
|
|
1107
|
+
// Last point: use direction from previous point
|
|
1108
|
+
const prev = points[i - 1]
|
|
1109
|
+
const perp = this.getPerpendicular(prev, p)
|
|
1110
|
+
result.push({ x: p.x + perp.x * offset, y: p.y + perp.y * offset })
|
|
1111
|
+
} else {
|
|
1112
|
+
// Middle point (bend): offset based on incoming segment direction
|
|
1113
|
+
const prev = points[i - 1]
|
|
1114
|
+
const perp = this.getPerpendicular(prev, p)
|
|
1115
|
+
result.push({ x: p.x + perp.x * offset, y: p.y + perp.y * offset })
|
|
1116
|
+
|
|
1117
|
+
// Also add a point for the outgoing segment if direction changes
|
|
1118
|
+
const next = points[i + 1]
|
|
1119
|
+
const perpNext = this.getPerpendicular(p, next)
|
|
1120
|
+
|
|
1121
|
+
// Check if direction changed (bend point)
|
|
1122
|
+
if (Math.abs(perp.x - perpNext.x) > 0.01 || Math.abs(perp.y - perpNext.y) > 0.01) {
|
|
1123
|
+
result.push({ x: p.x + perpNext.x * offset, y: p.y + perpNext.y * offset })
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
851
1127
|
|
|
852
|
-
|
|
1128
|
+
return result
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Get perpendicular unit vector for a line segment
|
|
1133
|
+
*/
|
|
1134
|
+
private getPerpendicular(from: { x: number; y: number }, to: { x: number; y: number }): { x: number; y: number } {
|
|
1135
|
+
const dx = to.x - from.x
|
|
1136
|
+
const dy = to.y - from.y
|
|
1137
|
+
const len = Math.sqrt(dx * dx + dy * dy)
|
|
853
1138
|
|
|
854
|
-
|
|
855
|
-
const perpX = -dy / len
|
|
856
|
-
const perpY = dx / len
|
|
1139
|
+
if (len === 0) return { x: 0, y: 0 }
|
|
857
1140
|
|
|
858
|
-
//
|
|
859
|
-
return
|
|
860
|
-
x: p.x + perpX * offset,
|
|
861
|
-
y: p.y + perpY * offset,
|
|
862
|
-
}))
|
|
1141
|
+
// Perpendicular unit vector (rotate 90 degrees)
|
|
1142
|
+
return { x: -dy / len, y: dx / len }
|
|
863
1143
|
}
|
|
864
1144
|
|
|
865
1145
|
/**
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Renderer Components
|
|
3
|
-
*/
|
|
4
|
-
export { renderNode, type NodeRendererContext } from './node-renderer.js';
|
|
5
|
-
export { renderLink, type LinkRendererContext } from './link-renderer.js';
|
|
6
|
-
export { renderPorts } from './port-renderer.js';
|
|
7
|
-
export { renderSubgraph, type SubgraphRendererContext } from './subgraph-renderer.js';
|
|
8
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/renderer/components/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AACzE,OAAO,EAAE,UAAU,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,KAAK,uBAAuB,EAAE,MAAM,wBAAwB,CAAA"}
|