@shumoku/renderer 0.2.3 → 0.2.5
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/html/index.d.ts +25 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +478 -2
- package/dist/html/index.js.map +1 -1
- package/dist/html/navigation.d.ts +54 -0
- package/dist/html/navigation.d.ts.map +1 -0
- package/dist/html/navigation.js +210 -0
- package/dist/html/navigation.js.map +1 -0
- package/dist/html/runtime.d.ts.map +1 -1
- package/dist/html/runtime.js +37 -0
- package/dist/html/runtime.js.map +1 -1
- package/dist/html/tooltip.d.ts.map +1 -1
- package/dist/html/tooltip.js +30 -2
- package/dist/html/tooltip.js.map +1 -1
- package/dist/iife-string.d.ts +2 -0
- package/dist/iife-string.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/shumoku-interactive.iife.js +10 -8
- package/dist/svg.d.ts +27 -0
- package/dist/svg.d.ts.map +1 -1
- package/dist/svg.js +202 -101
- package/dist/svg.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -3
- package/src/build-iife-string.ts +26 -19
- package/src/html/index.ts +550 -4
- package/src/html/navigation.ts +256 -0
- package/src/html/runtime.ts +42 -0
- package/src/html/tooltip.ts +28 -2
- package/src/index.ts +25 -22
- package/src/svg.ts +1640 -1502
- package/src/types.ts +127 -125
package/dist/svg.d.ts
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { LayoutResult, NetworkGraph } from '@shumoku/core';
|
|
6
6
|
import type { DataAttributeOptions, RenderMode } from './types.js';
|
|
7
|
+
/** Embedded content for a subgraph */
|
|
8
|
+
export interface EmbeddedSubgraphContent {
|
|
9
|
+
/** SVG content to embed (inner content, without outer <svg> tag) */
|
|
10
|
+
svgContent: string;
|
|
11
|
+
/** ViewBox of the embedded content */
|
|
12
|
+
viewBox: string;
|
|
13
|
+
}
|
|
7
14
|
export interface SVGRendererOptions {
|
|
8
15
|
/** Font family */
|
|
9
16
|
fontFamily?: string;
|
|
@@ -20,6 +27,16 @@ export interface SVGRendererOptions {
|
|
|
20
27
|
* Only used when renderMode is 'interactive'
|
|
21
28
|
*/
|
|
22
29
|
dataAttributes?: DataAttributeOptions;
|
|
30
|
+
/**
|
|
31
|
+
* Unique sheet ID for generating unique filter/marker IDs
|
|
32
|
+
* Required when multiple SVGs are embedded in the same HTML page
|
|
33
|
+
*/
|
|
34
|
+
sheetId?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Embedded content for subgraphs (subgraphId -> content)
|
|
37
|
+
* Used to embed child SVG content into parent subgraph boxes
|
|
38
|
+
*/
|
|
39
|
+
embeddedContent?: Map<string, EmbeddedSubgraphContent>;
|
|
23
40
|
}
|
|
24
41
|
export declare class SVGRenderer {
|
|
25
42
|
private options;
|
|
@@ -30,6 +47,14 @@ export declare class SVGRenderer {
|
|
|
30
47
|
private get isInteractive();
|
|
31
48
|
/** Get data attribute options with defaults */
|
|
32
49
|
private get dataAttrs();
|
|
50
|
+
/** Get unique ID suffix for this sheet */
|
|
51
|
+
private get idSuffix();
|
|
52
|
+
/** Get shadow filter ID */
|
|
53
|
+
private get shadowId();
|
|
54
|
+
/** Get arrow marker ID */
|
|
55
|
+
private get arrowId();
|
|
56
|
+
/** Get red arrow marker ID */
|
|
57
|
+
private get arrowRedId();
|
|
33
58
|
/**
|
|
34
59
|
* Get theme colors based on theme type
|
|
35
60
|
*/
|
|
@@ -80,6 +105,8 @@ export declare class SVGRenderer {
|
|
|
80
105
|
* Render node content (icon + label) with dynamic vertical centering
|
|
81
106
|
*/
|
|
82
107
|
private renderNodeContent;
|
|
108
|
+
/** Render content for export connector nodes */
|
|
109
|
+
private renderExportConnectorContent;
|
|
83
110
|
private renderLink;
|
|
84
111
|
/** Build data attributes for a link (interactive mode only) */
|
|
85
112
|
private buildLinkDataAttributes;
|
package/dist/svg.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"svg.d.ts","sourceRoot":"","sources":["../src/svg.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAKV,YAAY,EAKZ,YAAY,EAIb,MAAM,eAAe,CAAA;AAUtB,OAAO,KAAK,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAgElE,MAAM,WAAW,kBAAkB;IACjC,kBAAkB;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gEAAgE;IAChE,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB;;;;OAIG;IACH,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB;;;OAGG;IACH,cAAc,CAAC,EAAE,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"svg.d.ts","sourceRoot":"","sources":["../src/svg.ts"],"names":[],"mappings":"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"}
|
package/dist/svg.js
CHANGED
|
@@ -42,6 +42,8 @@ const DEFAULT_OPTIONS = {
|
|
|
42
42
|
interactive: true,
|
|
43
43
|
renderMode: 'static',
|
|
44
44
|
dataAttributes: { device: true, link: true, metadata: true },
|
|
45
|
+
sheetId: '',
|
|
46
|
+
embeddedContent: new Map(),
|
|
45
47
|
};
|
|
46
48
|
// ============================================
|
|
47
49
|
// SVG Renderer
|
|
@@ -65,6 +67,22 @@ export class SVGRenderer {
|
|
|
65
67
|
metadata: this.options.dataAttributes?.metadata ?? true,
|
|
66
68
|
};
|
|
67
69
|
}
|
|
70
|
+
/** Get unique ID suffix for this sheet */
|
|
71
|
+
get idSuffix() {
|
|
72
|
+
return this.options.sheetId ? `-${this.options.sheetId}` : '';
|
|
73
|
+
}
|
|
74
|
+
/** Get shadow filter ID */
|
|
75
|
+
get shadowId() {
|
|
76
|
+
return `shadow${this.idSuffix}`;
|
|
77
|
+
}
|
|
78
|
+
/** Get arrow marker ID */
|
|
79
|
+
get arrowId() {
|
|
80
|
+
return `arrow${this.idSuffix}`;
|
|
81
|
+
}
|
|
82
|
+
/** Get red arrow marker ID */
|
|
83
|
+
get arrowRedId() {
|
|
84
|
+
return `arrow-red${this.idSuffix}`;
|
|
85
|
+
}
|
|
68
86
|
/**
|
|
69
87
|
* Get theme colors based on theme type
|
|
70
88
|
*/
|
|
@@ -242,9 +260,9 @@ export class SVGRenderer {
|
|
|
242
260
|
break;
|
|
243
261
|
}
|
|
244
262
|
// Render legend box
|
|
245
|
-
let svg = `<g class="legend" transform="translate(${legendX}, ${legendY})">
|
|
246
|
-
<rect x="0" y="0" width="${legendWidth}" height="${legendHeight}" rx="4"
|
|
247
|
-
fill="${this.themeColors.backgroundColor}" stroke="${this.themeColors.subgraphStroke}" stroke-width="1" opacity="0.95" />
|
|
263
|
+
let svg = `<g class="legend" transform="translate(${legendX}, ${legendY})">
|
|
264
|
+
<rect x="0" y="0" width="${legendWidth}" height="${legendHeight}" rx="4"
|
|
265
|
+
fill="${this.themeColors.backgroundColor}" stroke="${this.themeColors.subgraphStroke}" stroke-width="1" opacity="0.95" />
|
|
248
266
|
<text x="${padding}" y="${padding + 12}" class="subgraph-label" font-size="11">Legend</text>`;
|
|
249
267
|
// Render items
|
|
250
268
|
for (const [index, item] of items.entries()) {
|
|
@@ -272,52 +290,52 @@ export class SVGRenderer {
|
|
|
272
290
|
return `<g transform="translate(0, 0)">${lines.join('')}</g>`;
|
|
273
291
|
}
|
|
274
292
|
renderHeader(width, height, viewBox) {
|
|
275
|
-
return `<svg xmlns="http://www.w3.org/2000/svg"
|
|
276
|
-
viewBox="${viewBox}"
|
|
277
|
-
width="${width}"
|
|
278
|
-
height="${height}"
|
|
293
|
+
return `<svg xmlns="http://www.w3.org/2000/svg"
|
|
294
|
+
viewBox="${viewBox}"
|
|
295
|
+
width="${width}"
|
|
296
|
+
height="${height}"
|
|
279
297
|
style="background: ${this.themeColors.backgroundColor}">`;
|
|
280
298
|
}
|
|
281
299
|
renderDefs() {
|
|
282
|
-
return `<defs>
|
|
283
|
-
<!-- Arrow marker -->
|
|
284
|
-
<marker id="
|
|
285
|
-
<polygon points="0 0, 10 3.5, 0 7" fill="${this.themeColors.defaultLinkStroke}" />
|
|
286
|
-
</marker>
|
|
287
|
-
<marker id="
|
|
288
|
-
<polygon points="0 0, 10 3.5, 0 7" fill="#dc2626" />
|
|
289
|
-
</marker>
|
|
290
|
-
|
|
291
|
-
<!-- Filters -->
|
|
292
|
-
<filter id="
|
|
293
|
-
<feDropShadow dx="2" dy="2" stdDeviation="3" flood-opacity="0.15"/>
|
|
294
|
-
</filter>
|
|
300
|
+
return `<defs>
|
|
301
|
+
<!-- Arrow marker -->
|
|
302
|
+
<marker id="${this.arrowId}" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
|
303
|
+
<polygon points="0 0, 10 3.5, 0 7" fill="${this.themeColors.defaultLinkStroke}" />
|
|
304
|
+
</marker>
|
|
305
|
+
<marker id="${this.arrowRedId}" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
|
306
|
+
<polygon points="0 0, 10 3.5, 0 7" fill="#dc2626" />
|
|
307
|
+
</marker>
|
|
308
|
+
|
|
309
|
+
<!-- Filters -->
|
|
310
|
+
<filter id="${this.shadowId}" x="-20%" y="-20%" width="140%" height="140%">
|
|
311
|
+
<feDropShadow dx="2" dy="2" stdDeviation="3" flood-opacity="0.15"/>
|
|
312
|
+
</filter>
|
|
295
313
|
</defs>`;
|
|
296
314
|
}
|
|
297
315
|
renderStyles() {
|
|
298
316
|
// CSS variables for interactive runtime theming
|
|
299
317
|
const cssVars = this.isInteractive
|
|
300
|
-
? `
|
|
301
|
-
:root {
|
|
302
|
-
--shumoku-bg: ${this.themeColors.backgroundColor};
|
|
303
|
-
--shumoku-surface: ${this.themeColors.subgraphFill};
|
|
304
|
-
--shumoku-text: ${this.themeColors.labelColor};
|
|
305
|
-
--shumoku-text-secondary: ${this.themeColors.labelSecondaryColor};
|
|
306
|
-
--shumoku-border: ${this.themeColors.subgraphStroke};
|
|
307
|
-
--shumoku-node-fill: ${this.themeColors.defaultNodeFill};
|
|
308
|
-
--shumoku-node-stroke: ${this.themeColors.defaultNodeStroke};
|
|
309
|
-
--shumoku-link-stroke: ${this.themeColors.defaultLinkStroke};
|
|
310
|
-
--shumoku-font: ${this.options.fontFamily};
|
|
318
|
+
? `
|
|
319
|
+
:root {
|
|
320
|
+
--shumoku-bg: ${this.themeColors.backgroundColor};
|
|
321
|
+
--shumoku-surface: ${this.themeColors.subgraphFill};
|
|
322
|
+
--shumoku-text: ${this.themeColors.labelColor};
|
|
323
|
+
--shumoku-text-secondary: ${this.themeColors.labelSecondaryColor};
|
|
324
|
+
--shumoku-border: ${this.themeColors.subgraphStroke};
|
|
325
|
+
--shumoku-node-fill: ${this.themeColors.defaultNodeFill};
|
|
326
|
+
--shumoku-node-stroke: ${this.themeColors.defaultNodeStroke};
|
|
327
|
+
--shumoku-link-stroke: ${this.themeColors.defaultLinkStroke};
|
|
328
|
+
--shumoku-font: ${this.options.fontFamily};
|
|
311
329
|
}`
|
|
312
330
|
: '';
|
|
313
|
-
return `<style>${cssVars}
|
|
314
|
-
.node-label { font-family: ${this.options.fontFamily}; font-size: 12px; fill: ${this.themeColors.labelColor}; }
|
|
315
|
-
.node-label-bold { font-weight: bold; }
|
|
316
|
-
.node-icon { color: ${this.themeColors.labelSecondaryColor}; }
|
|
317
|
-
.subgraph-icon { opacity: 0.9; }
|
|
318
|
-
.subgraph-label { font-family: ${this.options.fontFamily}; font-size: 14px; font-weight: 600; fill: ${this.themeColors.subgraphLabelColor}; }
|
|
319
|
-
.link-label { font-family: ${this.options.fontFamily}; font-size: 11px; fill: ${this.themeColors.labelSecondaryColor}; }
|
|
320
|
-
.endpoint-label { font-family: ${this.options.fontFamily}; font-size: 9px; fill: ${this.themeColors.labelColor}; }
|
|
331
|
+
return `<style>${cssVars}
|
|
332
|
+
.node-label { font-family: ${this.options.fontFamily}; font-size: 12px; fill: ${this.themeColors.labelColor}; }
|
|
333
|
+
.node-label-bold { font-weight: bold; }
|
|
334
|
+
.node-icon { color: ${this.themeColors.labelSecondaryColor}; }
|
|
335
|
+
.subgraph-icon { opacity: 0.9; }
|
|
336
|
+
.subgraph-label { font-family: ${this.options.fontFamily}; font-size: 14px; font-weight: 600; fill: ${this.themeColors.subgraphLabelColor}; }
|
|
337
|
+
.link-label { font-family: ${this.options.fontFamily}; font-size: 11px; fill: ${this.themeColors.labelSecondaryColor}; }
|
|
338
|
+
.endpoint-label { font-family: ${this.options.fontFamily}; font-size: 9px; fill: ${this.themeColors.labelColor}; }
|
|
321
339
|
</style>`;
|
|
322
340
|
}
|
|
323
341
|
renderSubgraph(sg) {
|
|
@@ -360,30 +378,92 @@ export class SVGRenderer {
|
|
|
360
378
|
const vbHeight = Number.parseInt(viewBoxMatch[2], 10);
|
|
361
379
|
const aspectRatio = vbWidth / vbHeight;
|
|
362
380
|
const iconWidth = Math.round(iconSize * aspectRatio);
|
|
363
|
-
iconSvg = `<g class="subgraph-icon" transform="translate(${iconX}, ${iconY})">
|
|
364
|
-
<svg width="${iconWidth}" height="${iconSize}" viewBox="0 0 ${vbWidth} ${vbHeight}">
|
|
365
|
-
${iconContent.replace(/<svg[^>]*>/, '').replace(/<\/svg>$/, '')}
|
|
366
|
-
</svg>
|
|
381
|
+
iconSvg = `<g class="subgraph-icon" transform="translate(${iconX}, ${iconY})">
|
|
382
|
+
<svg width="${iconWidth}" height="${iconSize}" viewBox="0 0 ${vbWidth} ${vbHeight}">
|
|
383
|
+
${iconContent.replace(/<svg[^>]*>/, '').replace(/<\/svg>$/, '')}
|
|
384
|
+
</svg>
|
|
367
385
|
</g>`;
|
|
368
386
|
}
|
|
369
387
|
}
|
|
370
388
|
else {
|
|
371
389
|
// Use viewBox from entry
|
|
372
|
-
iconSvg = `<g class="subgraph-icon" transform="translate(${iconX}, ${iconY})">
|
|
373
|
-
<svg width="${iconSize}" height="${iconSize}" viewBox="${viewBox}">
|
|
374
|
-
${iconContent}
|
|
375
|
-
</svg>
|
|
390
|
+
iconSvg = `<g class="subgraph-icon" transform="translate(${iconX}, ${iconY})">
|
|
391
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="${viewBox}">
|
|
392
|
+
${iconContent}
|
|
393
|
+
</svg>
|
|
376
394
|
</g>`;
|
|
377
395
|
}
|
|
378
396
|
}
|
|
379
397
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
398
|
+
// Hierarchical navigation attributes
|
|
399
|
+
const hasSheet = subgraph.file || (subgraph.pins && subgraph.pins.length > 0);
|
|
400
|
+
const sheetAttrs = hasSheet ? ` data-has-sheet="true" data-sheet-id="${sg.id}"` : '';
|
|
401
|
+
// Check for embedded content
|
|
402
|
+
const embeddedContent = this.options.embeddedContent?.get(sg.id);
|
|
403
|
+
let embeddedSvg = '';
|
|
404
|
+
if (embeddedContent) {
|
|
405
|
+
// Calculate content area (below label, with padding)
|
|
406
|
+
const labelHeight = 30; // Space for label
|
|
407
|
+
const padding = 10;
|
|
408
|
+
const contentX = bounds.x + padding;
|
|
409
|
+
const contentY = bounds.y + labelHeight;
|
|
410
|
+
const contentWidth = bounds.width - padding * 2;
|
|
411
|
+
const contentHeight = bounds.height - labelHeight - padding;
|
|
412
|
+
// Embed child SVG with viewBox for automatic scaling
|
|
413
|
+
embeddedSvg = `
|
|
414
|
+
<svg x="${contentX}" y="${contentY}" width="${contentWidth}" height="${contentHeight}"
|
|
415
|
+
viewBox="${embeddedContent.viewBox}" preserveAspectRatio="xMidYMid meet">
|
|
416
|
+
${embeddedContent.svgContent}
|
|
417
|
+
</svg>`;
|
|
418
|
+
}
|
|
419
|
+
// Render boundary ports for hierarchical connections
|
|
420
|
+
let portsSvg = '';
|
|
421
|
+
if (sg.ports && sg.ports.size > 0) {
|
|
422
|
+
const portParts = [];
|
|
423
|
+
const centerX = bounds.x + bounds.width / 2;
|
|
424
|
+
const centerY = bounds.y + bounds.height / 2;
|
|
425
|
+
for (const port of sg.ports.values()) {
|
|
426
|
+
const px = centerX + port.position.x;
|
|
427
|
+
const py = centerY + port.position.y;
|
|
428
|
+
const pw = port.size.width;
|
|
429
|
+
const ph = port.size.height;
|
|
430
|
+
// Port circle/diamond on boundary
|
|
431
|
+
portParts.push(`<circle class="subgraph-port" cx="${px}" cy="${py}" r="${Math.max(pw, ph) / 2 + 2}"
|
|
432
|
+
fill="#3b82f6" stroke="#1d4ed8" stroke-width="2" />`);
|
|
433
|
+
// Port label
|
|
434
|
+
let labelX = px;
|
|
435
|
+
let labelY = py;
|
|
436
|
+
let anchor = 'middle';
|
|
437
|
+
const labelOffset = 16;
|
|
438
|
+
switch (port.side) {
|
|
439
|
+
case 'top':
|
|
440
|
+
labelY = py - labelOffset;
|
|
441
|
+
break;
|
|
442
|
+
case 'bottom':
|
|
443
|
+
labelY = py + labelOffset + 4;
|
|
444
|
+
break;
|
|
445
|
+
case 'left':
|
|
446
|
+
labelX = px - labelOffset;
|
|
447
|
+
anchor = 'end';
|
|
448
|
+
break;
|
|
449
|
+
case 'right':
|
|
450
|
+
labelX = px + labelOffset;
|
|
451
|
+
anchor = 'start';
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
portParts.push(`<text x="${labelX}" y="${labelY}" class="port-label" text-anchor="${anchor}"
|
|
455
|
+
fill="#3b82f6" font-size="10" font-weight="500">${this.escapeXml(port.label)}</text>`);
|
|
456
|
+
}
|
|
457
|
+
portsSvg = portParts.join('\n ');
|
|
458
|
+
}
|
|
459
|
+
return `<g class="subgraph" data-id="${sg.id}"${sheetAttrs}>
|
|
460
|
+
<rect x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}"
|
|
461
|
+
rx="${rx}" ry="${rx}"
|
|
462
|
+
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}"
|
|
463
|
+
${strokeDasharray ? `stroke-dasharray="${strokeDasharray}"` : ''} />
|
|
464
|
+
${iconSvg}
|
|
465
|
+
<text x="${labelX}" y="${labelY}" class="subgraph-label" text-anchor="${textAnchor}">${this.escapeXml(subgraph.label)}</text>${embeddedSvg}
|
|
466
|
+
${portsSvg}
|
|
387
467
|
</g>`;
|
|
388
468
|
}
|
|
389
469
|
/** Render node background (shape only) */
|
|
@@ -393,9 +473,9 @@ export class SVGRenderer {
|
|
|
393
473
|
const dataAttrs = this.buildNodeDataAttributes(node);
|
|
394
474
|
const bg = this.renderNodeBackground(layoutNode);
|
|
395
475
|
const fg = this.renderNodeForeground(layoutNode);
|
|
396
|
-
return `<g class="node" data-id="${id}"${dataAttrs}>
|
|
397
|
-
${bg}
|
|
398
|
-
${fg}
|
|
476
|
+
return `<g class="node" data-id="${id}"${dataAttrs}>
|
|
477
|
+
${bg}
|
|
478
|
+
${fg}
|
|
399
479
|
</g>`;
|
|
400
480
|
}
|
|
401
481
|
renderNodeBackground(layoutNode) {
|
|
@@ -405,8 +485,15 @@ ${fg}
|
|
|
405
485
|
const w = size.width;
|
|
406
486
|
const h = size.height;
|
|
407
487
|
const style = node.style || {};
|
|
408
|
-
|
|
409
|
-
const
|
|
488
|
+
// Check if this is an export connector node (for hierarchical diagrams)
|
|
489
|
+
const isExport = node.metadata?._isExport === true;
|
|
490
|
+
// Special styling for export connector nodes - use subgraph colors
|
|
491
|
+
let fill = style.fill || this.themeColors.defaultNodeFill;
|
|
492
|
+
let stroke = style.stroke || this.themeColors.defaultNodeStroke;
|
|
493
|
+
if (isExport) {
|
|
494
|
+
fill = style.fill || this.themeColors.subgraphFill;
|
|
495
|
+
stroke = style.stroke || this.themeColors.defaultNodeStroke;
|
|
496
|
+
}
|
|
410
497
|
const strokeWidth = style.strokeWidth || 1;
|
|
411
498
|
const strokeDasharray = style.strokeDasharray || '';
|
|
412
499
|
const shape = this.renderNodeShape(node.shape, x, y, w, h, fill, stroke, strokeWidth, strokeDasharray);
|
|
@@ -454,8 +541,8 @@ ${fg}
|
|
|
454
541
|
const content = this.renderNodeContent(node, x, y, w);
|
|
455
542
|
// Include data attributes for interactive mode (same as node-bg)
|
|
456
543
|
const dataAttrs = this.buildNodeDataAttributes(node);
|
|
457
|
-
return `<g class="node-fg" data-id="${id}"${dataAttrs}>
|
|
458
|
-
${content}
|
|
544
|
+
return `<g class="node-fg" data-id="${id}"${dataAttrs}>
|
|
545
|
+
${content}
|
|
459
546
|
</g>`;
|
|
460
547
|
}
|
|
461
548
|
/**
|
|
@@ -474,8 +561,8 @@ ${fg}
|
|
|
474
561
|
const portDeviceAttr = this.isInteractive ? ` data-port-device="${nodeId}"` : '';
|
|
475
562
|
const parts = [];
|
|
476
563
|
// Port box
|
|
477
|
-
parts.push(`<rect class="port-box"
|
|
478
|
-
x="${px - pw / 2}" y="${py - ph / 2}" width="${pw}" height="${ph}"
|
|
564
|
+
parts.push(`<rect class="port-box"
|
|
565
|
+
x="${px - pw / 2}" y="${py - ph / 2}" width="${pw}" height="${ph}"
|
|
479
566
|
fill="${this.themeColors.portFill}" stroke="${this.themeColors.portStroke}" stroke-width="1" rx="2" />`);
|
|
480
567
|
// Port label - position based on side
|
|
481
568
|
let labelX = px;
|
|
@@ -515,8 +602,8 @@ ${fg}
|
|
|
515
602
|
parts.push(`<rect class="port-label-bg" x="${bgX}" y="${bgY}" width="${labelWidth}" height="${labelHeight}" rx="2" fill="${this.themeColors.portLabelBg}" />`);
|
|
516
603
|
parts.push(`<text class="port-label" x="${labelX}" y="${labelY}" text-anchor="${textAnchor}" font-size="9" fill="${this.themeColors.portLabelColor}">${labelText}</text>`);
|
|
517
604
|
// Wrap in a group with data attributes
|
|
518
|
-
groups.push(`<g class="port" data-port="${port.id}"${portDeviceAttr}>
|
|
519
|
-
${parts.join('\n ')}
|
|
605
|
+
groups.push(`<g class="port" data-port="${port.id}"${portDeviceAttr}>
|
|
606
|
+
${parts.join('\n ')}
|
|
520
607
|
</g>`);
|
|
521
608
|
}
|
|
522
609
|
return groups.join('\n');
|
|
@@ -527,45 +614,45 @@ ${fg}
|
|
|
527
614
|
const halfH = h / 2;
|
|
528
615
|
switch (shape) {
|
|
529
616
|
case 'rect':
|
|
530
|
-
return `<rect x="${x - halfW}" y="${y - halfH}" width="${w}" height="${h}"
|
|
531
|
-
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${dashAttr} filter="url(
|
|
617
|
+
return `<rect x="${x - halfW}" y="${y - halfH}" width="${w}" height="${h}"
|
|
618
|
+
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${dashAttr} filter="url(#${this.shadowId})" />`;
|
|
532
619
|
case 'rounded':
|
|
533
|
-
return `<rect x="${x - halfW}" y="${y - halfH}" width="${w}" height="${h}" rx="8" ry="8"
|
|
534
|
-
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${dashAttr} filter="url(
|
|
620
|
+
return `<rect x="${x - halfW}" y="${y - halfH}" width="${w}" height="${h}" rx="8" ry="8"
|
|
621
|
+
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${dashAttr} filter="url(#${this.shadowId})" />`;
|
|
535
622
|
case 'circle': {
|
|
536
623
|
const r = Math.min(halfW, halfH);
|
|
537
|
-
return `<circle cx="${x}" cy="${y}" r="${r}"
|
|
538
|
-
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${dashAttr} filter="url(
|
|
624
|
+
return `<circle cx="${x}" cy="${y}" r="${r}"
|
|
625
|
+
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${dashAttr} filter="url(#${this.shadowId})" />`;
|
|
539
626
|
}
|
|
540
627
|
case 'diamond':
|
|
541
|
-
return `<polygon points="${x},${y - halfH} ${x + halfW},${y} ${x},${y + halfH} ${x - halfW},${y}"
|
|
542
|
-
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${dashAttr} filter="url(
|
|
628
|
+
return `<polygon points="${x},${y - halfH} ${x + halfW},${y} ${x},${y + halfH} ${x - halfW},${y}"
|
|
629
|
+
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${dashAttr} filter="url(#${this.shadowId})" />`;
|
|
543
630
|
case 'hexagon': {
|
|
544
631
|
const hx = halfW * 0.866;
|
|
545
|
-
return `<polygon points="${x - halfW},${y} ${x - hx},${y - halfH} ${x + hx},${y - halfH} ${x + halfW},${y} ${x + hx},${y + halfH} ${x - hx},${y + halfH}"
|
|
546
|
-
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${dashAttr} filter="url(
|
|
632
|
+
return `<polygon points="${x - halfW},${y} ${x - hx},${y - halfH} ${x + hx},${y - halfH} ${x + halfW},${y} ${x + hx},${y + halfH} ${x - hx},${y + halfH}"
|
|
633
|
+
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${dashAttr} filter="url(#${this.shadowId})" />`;
|
|
547
634
|
}
|
|
548
635
|
case 'cylinder': {
|
|
549
636
|
const ellipseH = h * 0.15;
|
|
550
|
-
return `<g>
|
|
551
|
-
<ellipse cx="${x}" cy="${y + halfH - ellipseH}" rx="${halfW}" ry="${ellipseH}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${dashAttr} />
|
|
552
|
-
<rect x="${x - halfW}" y="${y - halfH + ellipseH}" width="${w}" height="${h - ellipseH * 2}" fill="${fill}" stroke="none" />
|
|
553
|
-
<line x1="${x - halfW}" y1="${y - halfH + ellipseH}" x2="${x - halfW}" y2="${y + halfH - ellipseH}" stroke="${stroke}" stroke-width="${strokeWidth}" />
|
|
554
|
-
<line x1="${x + halfW}" y1="${y - halfH + ellipseH}" x2="${x + halfW}" y2="${y + halfH - ellipseH}" stroke="${stroke}" stroke-width="${strokeWidth}" />
|
|
555
|
-
<ellipse cx="${x}" cy="${y - halfH + ellipseH}" rx="${halfW}" ry="${ellipseH}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${dashAttr} filter="url(
|
|
637
|
+
return `<g>
|
|
638
|
+
<ellipse cx="${x}" cy="${y + halfH - ellipseH}" rx="${halfW}" ry="${ellipseH}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${dashAttr} />
|
|
639
|
+
<rect x="${x - halfW}" y="${y - halfH + ellipseH}" width="${w}" height="${h - ellipseH * 2}" fill="${fill}" stroke="none" />
|
|
640
|
+
<line x1="${x - halfW}" y1="${y - halfH + ellipseH}" x2="${x - halfW}" y2="${y + halfH - ellipseH}" stroke="${stroke}" stroke-width="${strokeWidth}" />
|
|
641
|
+
<line x1="${x + halfW}" y1="${y - halfH + ellipseH}" x2="${x + halfW}" y2="${y + halfH - ellipseH}" stroke="${stroke}" stroke-width="${strokeWidth}" />
|
|
642
|
+
<ellipse cx="${x}" cy="${y - halfH + ellipseH}" rx="${halfW}" ry="${ellipseH}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${dashAttr} filter="url(#${this.shadowId})" />
|
|
556
643
|
</g>`;
|
|
557
644
|
}
|
|
558
645
|
case 'stadium':
|
|
559
|
-
return `<rect x="${x - halfW}" y="${y - halfH}" width="${w}" height="${h}" rx="${halfH}" ry="${halfH}"
|
|
560
|
-
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${dashAttr} filter="url(
|
|
646
|
+
return `<rect x="${x - halfW}" y="${y - halfH}" width="${w}" height="${h}" rx="${halfH}" ry="${halfH}"
|
|
647
|
+
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${dashAttr} filter="url(#${this.shadowId})" />`;
|
|
561
648
|
case 'trapezoid': {
|
|
562
649
|
const indent = w * 0.15;
|
|
563
|
-
return `<polygon points="${x - halfW + indent},${y - halfH} ${x + halfW - indent},${y - halfH} ${x + halfW},${y + halfH} ${x - halfW},${y + halfH}"
|
|
564
|
-
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${dashAttr} filter="url(
|
|
650
|
+
return `<polygon points="${x - halfW + indent},${y - halfH} ${x + halfW - indent},${y - halfH} ${x + halfW},${y + halfH} ${x - halfW},${y + halfH}"
|
|
651
|
+
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${dashAttr} filter="url(#${this.shadowId})" />`;
|
|
565
652
|
}
|
|
566
653
|
default:
|
|
567
|
-
return `<rect x="${x - halfW}" y="${y - halfH}" width="${w}" height="${h}" rx="4" ry="4"
|
|
568
|
-
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${dashAttr} filter="url(
|
|
654
|
+
return `<rect x="${x - halfW}" y="${y - halfH}" width="${w}" height="${h}" rx="4" ry="4"
|
|
655
|
+
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" ${dashAttr} filter="url(#${this.shadowId})" />`;
|
|
569
656
|
}
|
|
570
657
|
}
|
|
571
658
|
/**
|
|
@@ -644,6 +731,12 @@ ${fg}
|
|
|
644
731
|
* Render node content (icon + label) with dynamic vertical centering
|
|
645
732
|
*/
|
|
646
733
|
renderNodeContent(node, x, y, w) {
|
|
734
|
+
// Check if this is an export connector node
|
|
735
|
+
const isExport = node.metadata?._isExport === true;
|
|
736
|
+
// For export connectors, render with arrow icon
|
|
737
|
+
if (isExport) {
|
|
738
|
+
return this.renderExportConnectorContent(node, x, y);
|
|
739
|
+
}
|
|
647
740
|
const iconInfo = this.calculateIconInfo(node, w);
|
|
648
741
|
const labels = Array.isArray(node.label) ? node.label : [node.label];
|
|
649
742
|
const labelHeight = labels.length * LABEL_LINE_HEIGHT;
|
|
@@ -657,8 +750,8 @@ ${fg}
|
|
|
657
750
|
// Render icon at top of content block
|
|
658
751
|
if (iconInfo) {
|
|
659
752
|
const iconY = contentTop;
|
|
660
|
-
parts.push(`<g class="node-icon" transform="translate(${x - iconInfo.width / 2}, ${iconY})">
|
|
661
|
-
${iconInfo.svg}
|
|
753
|
+
parts.push(`<g class="node-icon" transform="translate(${x - iconInfo.width / 2}, ${iconY})">
|
|
754
|
+
${iconInfo.svg}
|
|
662
755
|
</g>`);
|
|
663
756
|
}
|
|
664
757
|
// Render labels below icon
|
|
@@ -671,6 +764,13 @@ ${fg}
|
|
|
671
764
|
}
|
|
672
765
|
return parts.join('\n ');
|
|
673
766
|
}
|
|
767
|
+
/** Render content for export connector nodes */
|
|
768
|
+
renderExportConnectorContent(node, x, y) {
|
|
769
|
+
const labels = Array.isArray(node.label) ? node.label : [node.label];
|
|
770
|
+
const labelText = labels[0] || '';
|
|
771
|
+
// Just render the label (subgraph name), no arrows
|
|
772
|
+
return `<text x="${x}" y="${y + 4}" class="node-label" text-anchor="middle">${this.escapeXml(labelText)}</text>`;
|
|
773
|
+
}
|
|
674
774
|
renderLink(layoutLink, nodes) {
|
|
675
775
|
const { id, points, link, fromEndpoint, toEndpoint } = layoutLink;
|
|
676
776
|
const label = link.label;
|
|
@@ -679,7 +779,7 @@ ${fg}
|
|
|
679
779
|
const arrow = link.arrow ?? this.getDefaultArrowType(link.redundancy);
|
|
680
780
|
const stroke = link.style?.stroke || this.getVlanStroke(link.vlan) || this.themeColors.defaultLinkStroke;
|
|
681
781
|
const dasharray = link.style?.strokeDasharray || this.getLinkDasharray(type);
|
|
682
|
-
const markerEnd = arrow !== 'none' ?
|
|
782
|
+
const markerEnd = arrow !== 'none' ? `url(#${this.arrowId})` : '';
|
|
683
783
|
// Get bandwidth rendering config
|
|
684
784
|
const bandwidthConfig = this.getBandwidthConfig(link.bandwidth);
|
|
685
785
|
const strokeWidth = link.style?.strokeWidth || bandwidthConfig.strokeWidth || this.getLinkStrokeWidth(type);
|
|
@@ -751,6 +851,7 @@ ${fg}
|
|
|
751
851
|
vlan: link.vlan,
|
|
752
852
|
redundancy: link.redundancy,
|
|
753
853
|
label: link.label,
|
|
854
|
+
metadata: link.metadata,
|
|
754
855
|
};
|
|
755
856
|
attrs.push(`data-link-json="${this.escapeXml(JSON.stringify(linkInfo))}"`);
|
|
756
857
|
}
|
|
@@ -909,14 +1010,14 @@ ${fg}
|
|
|
909
1010
|
const basePath = this.generatePath(points);
|
|
910
1011
|
if (lineCount === 1) {
|
|
911
1012
|
// Single line
|
|
912
|
-
let linePath = `<path class="link" data-id="${id}" d="${basePath}"
|
|
913
|
-
fill="none" stroke="${stroke}" stroke-width="${strokeWidth}"
|
|
914
|
-
${dasharray ? `stroke-dasharray="${dasharray}"` : ''}
|
|
1013
|
+
let linePath = `<path class="link" data-id="${id}" d="${basePath}"
|
|
1014
|
+
fill="none" stroke="${stroke}" stroke-width="${strokeWidth}"
|
|
1015
|
+
${dasharray ? `stroke-dasharray="${dasharray}"` : ''}
|
|
915
1016
|
${markerEnd ? `marker-end="${markerEnd}"` : ''} pointer-events="none" />`;
|
|
916
1017
|
// Double line effect for redundancy types
|
|
917
1018
|
if (type === 'double') {
|
|
918
|
-
linePath = `<path class="link-double-outer" d="${basePath}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth + 2}" pointer-events="none" />
|
|
919
|
-
<path class="link-double-inner" d="${basePath}" fill="none" stroke="white" stroke-width="${strokeWidth - 1}" pointer-events="none" />
|
|
1019
|
+
linePath = `<path class="link-double-outer" d="${basePath}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth + 2}" pointer-events="none" />
|
|
1020
|
+
<path class="link-double-inner" d="${basePath}" fill="none" stroke="white" stroke-width="${strokeWidth - 1}" pointer-events="none" />
|
|
920
1021
|
${linePath}`;
|
|
921
1022
|
}
|
|
922
1023
|
lines.push(linePath);
|
|
@@ -927,18 +1028,18 @@ ${linePath}`;
|
|
|
927
1028
|
for (const offset of offsets) {
|
|
928
1029
|
const offsetPoints = this.offsetPoints(points, offset);
|
|
929
1030
|
const path = this.generatePath(offsetPoints);
|
|
930
|
-
lines.push(`<path class="link" data-id="${id}" d="${path}"
|
|
931
|
-
fill="none" stroke="${stroke}" stroke-width="${strokeWidth}"
|
|
1031
|
+
lines.push(`<path class="link" data-id="${id}" d="${path}"
|
|
1032
|
+
fill="none" stroke="${stroke}" stroke-width="${strokeWidth}"
|
|
932
1033
|
${dasharray ? `stroke-dasharray="${dasharray}"` : ''} pointer-events="none" />`);
|
|
933
1034
|
}
|
|
934
1035
|
}
|
|
935
1036
|
// Calculate hit area width (same as actual lines, hidden underneath)
|
|
936
1037
|
const hitWidth = lineCount === 1 ? strokeWidth : (lineCount - 1) * lineSpacing + strokeWidth;
|
|
937
1038
|
// Wrap all lines in a group with transparent hit area on top
|
|
938
|
-
return `<g class="link-lines">
|
|
939
|
-
${lines.join('\n')}
|
|
940
|
-
<path class="link-hit-area" d="${basePath}"
|
|
941
|
-
fill="none" stroke="${stroke}" stroke-width="${hitWidth}" opacity="0" />
|
|
1039
|
+
return `<g class="link-lines">
|
|
1040
|
+
${lines.join('\n')}
|
|
1041
|
+
<path class="link-hit-area" d="${basePath}"
|
|
1042
|
+
fill="none" stroke="${stroke}" stroke-width="${hitWidth}" opacity="0" />
|
|
942
1043
|
</g>`;
|
|
943
1044
|
}
|
|
944
1045
|
/**
|