@shumoku/core 0.1.1 → 0.2.1

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.
Files changed (193) hide show
  1. package/dist/constants.d.ts +23 -0
  2. package/dist/constants.d.ts.map +1 -0
  3. package/dist/constants.js +25 -0
  4. package/dist/constants.js.map +1 -0
  5. package/dist/icons/build-icons.js +3 -3
  6. package/dist/icons/build-icons.js.map +1 -1
  7. package/dist/icons/generated-icons.js +10 -10
  8. package/dist/icons/generated-icons.js.map +1 -1
  9. package/dist/index.d.ts +3 -3
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +6 -6
  12. package/dist/index.js.map +1 -1
  13. package/dist/layout/hierarchical.d.ts +13 -40
  14. package/dist/layout/hierarchical.d.ts.map +1 -1
  15. package/dist/layout/hierarchical.js +726 -1028
  16. package/dist/layout/hierarchical.js.map +1 -1
  17. package/dist/layout/index.d.ts +1 -1
  18. package/dist/layout/index.d.ts.map +1 -1
  19. package/dist/layout/index.js.map +1 -1
  20. package/dist/models/types.d.ts +30 -0
  21. package/dist/models/types.d.ts.map +1 -1
  22. package/dist/models/types.js +13 -13
  23. package/dist/models/types.js.map +1 -1
  24. package/dist/themes/dark.d.ts.map +1 -1
  25. package/dist/themes/dark.js +1 -1
  26. package/dist/themes/dark.js.map +1 -1
  27. package/dist/themes/index.d.ts +3 -3
  28. package/dist/themes/index.d.ts.map +1 -1
  29. package/dist/themes/index.js +4 -4
  30. package/dist/themes/index.js.map +1 -1
  31. package/dist/themes/modern.d.ts.map +1 -1
  32. package/dist/themes/modern.js.map +1 -1
  33. package/dist/themes/types.d.ts.map +1 -1
  34. package/dist/themes/utils.d.ts +1 -1
  35. package/dist/themes/utils.d.ts.map +1 -1
  36. package/dist/themes/utils.js +5 -4
  37. package/dist/themes/utils.js.map +1 -1
  38. package/package.json +88 -92
  39. package/src/constants.ts +35 -0
  40. package/src/icons/build-icons.ts +12 -6
  41. package/src/icons/generated-icons.ts +12 -12
  42. package/src/index.test.ts +66 -0
  43. package/src/index.ts +6 -10
  44. package/src/layout/hierarchical.ts +1251 -1543
  45. package/src/layout/index.ts +1 -1
  46. package/src/models/types.ts +84 -37
  47. package/src/themes/dark.ts +15 -15
  48. package/src/themes/index.ts +7 -7
  49. package/src/themes/modern.ts +22 -22
  50. package/src/themes/types.ts +26 -26
  51. package/src/themes/utils.ts +25 -24
  52. package/dist/renderer/components/index.d.ts +0 -8
  53. package/dist/renderer/components/index.d.ts.map +0 -1
  54. package/dist/renderer/components/index.js +0 -8
  55. package/dist/renderer/components/index.js.map +0 -1
  56. package/dist/renderer/components/link-renderer.d.ts +0 -11
  57. package/dist/renderer/components/link-renderer.d.ts.map +0 -1
  58. package/dist/renderer/components/link-renderer.js +0 -340
  59. package/dist/renderer/components/link-renderer.js.map +0 -1
  60. package/dist/renderer/components/node-renderer.d.ts +0 -14
  61. package/dist/renderer/components/node-renderer.d.ts.map +0 -1
  62. package/dist/renderer/components/node-renderer.js +0 -242
  63. package/dist/renderer/components/node-renderer.js.map +0 -1
  64. package/dist/renderer/components/port-renderer.d.ts +0 -8
  65. package/dist/renderer/components/port-renderer.d.ts.map +0 -1
  66. package/dist/renderer/components/port-renderer.js +0 -85
  67. package/dist/renderer/components/port-renderer.js.map +0 -1
  68. package/dist/renderer/components/subgraph-renderer.d.ts +0 -13
  69. package/dist/renderer/components/subgraph-renderer.d.ts.map +0 -1
  70. package/dist/renderer/components/subgraph-renderer.js +0 -85
  71. package/dist/renderer/components/subgraph-renderer.js.map +0 -1
  72. package/dist/renderer/icon-registry/index.d.ts +0 -6
  73. package/dist/renderer/icon-registry/index.d.ts.map +0 -1
  74. package/dist/renderer/icon-registry/index.js +0 -5
  75. package/dist/renderer/icon-registry/index.js.map +0 -1
  76. package/dist/renderer/icon-registry/registry.d.ts +0 -25
  77. package/dist/renderer/icon-registry/registry.d.ts.map +0 -1
  78. package/dist/renderer/icon-registry/registry.js +0 -85
  79. package/dist/renderer/icon-registry/registry.js.map +0 -1
  80. package/dist/renderer/icon-registry/types.d.ts +0 -44
  81. package/dist/renderer/icon-registry/types.d.ts.map +0 -1
  82. package/dist/renderer/icon-registry/types.js +0 -5
  83. package/dist/renderer/icon-registry/types.js.map +0 -1
  84. package/dist/renderer/index.d.ts +0 -6
  85. package/dist/renderer/index.d.ts.map +0 -1
  86. package/dist/renderer/index.js +0 -5
  87. package/dist/renderer/index.js.map +0 -1
  88. package/dist/renderer/render-model/builder.d.ts +0 -43
  89. package/dist/renderer/render-model/builder.d.ts.map +0 -1
  90. package/dist/renderer/render-model/builder.js +0 -646
  91. package/dist/renderer/render-model/builder.js.map +0 -1
  92. package/dist/renderer/render-model/index.d.ts +0 -6
  93. package/dist/renderer/render-model/index.d.ts.map +0 -1
  94. package/dist/renderer/render-model/index.js +0 -5
  95. package/dist/renderer/render-model/index.js.map +0 -1
  96. package/dist/renderer/render-model/types.d.ts +0 -216
  97. package/dist/renderer/render-model/types.d.ts.map +0 -1
  98. package/dist/renderer/render-model/types.js +0 -6
  99. package/dist/renderer/render-model/types.js.map +0 -1
  100. package/dist/renderer/renderer-types.d.ts +0 -55
  101. package/dist/renderer/renderer-types.d.ts.map +0 -1
  102. package/dist/renderer/renderer-types.js +0 -5
  103. package/dist/renderer/renderer-types.js.map +0 -1
  104. package/dist/renderer/svg-builder.d.ts +0 -152
  105. package/dist/renderer/svg-builder.d.ts.map +0 -1
  106. package/dist/renderer/svg-builder.js +0 -176
  107. package/dist/renderer/svg-builder.js.map +0 -1
  108. package/dist/renderer/svg-dom/builders/defs.d.ts +0 -10
  109. package/dist/renderer/svg-dom/builders/defs.d.ts.map +0 -1
  110. package/dist/renderer/svg-dom/builders/defs.js +0 -82
  111. package/dist/renderer/svg-dom/builders/defs.js.map +0 -1
  112. package/dist/renderer/svg-dom/builders/index.d.ts +0 -9
  113. package/dist/renderer/svg-dom/builders/index.d.ts.map +0 -1
  114. package/dist/renderer/svg-dom/builders/index.js +0 -9
  115. package/dist/renderer/svg-dom/builders/index.js.map +0 -1
  116. package/dist/renderer/svg-dom/builders/link.d.ts +0 -18
  117. package/dist/renderer/svg-dom/builders/link.d.ts.map +0 -1
  118. package/dist/renderer/svg-dom/builders/link.js +0 -188
  119. package/dist/renderer/svg-dom/builders/link.js.map +0 -1
  120. package/dist/renderer/svg-dom/builders/node.d.ts +0 -15
  121. package/dist/renderer/svg-dom/builders/node.d.ts.map +0 -1
  122. package/dist/renderer/svg-dom/builders/node.js +0 -262
  123. package/dist/renderer/svg-dom/builders/node.js.map +0 -1
  124. package/dist/renderer/svg-dom/builders/subgraph.d.ts +0 -14
  125. package/dist/renderer/svg-dom/builders/subgraph.d.ts.map +0 -1
  126. package/dist/renderer/svg-dom/builders/subgraph.js +0 -63
  127. package/dist/renderer/svg-dom/builders/subgraph.js.map +0 -1
  128. package/dist/renderer/svg-dom/builders/utils.d.ts +0 -40
  129. package/dist/renderer/svg-dom/builders/utils.d.ts.map +0 -1
  130. package/dist/renderer/svg-dom/builders/utils.js +0 -79
  131. package/dist/renderer/svg-dom/builders/utils.js.map +0 -1
  132. package/dist/renderer/svg-dom/index.d.ts +0 -9
  133. package/dist/renderer/svg-dom/index.d.ts.map +0 -1
  134. package/dist/renderer/svg-dom/index.js +0 -7
  135. package/dist/renderer/svg-dom/index.js.map +0 -1
  136. package/dist/renderer/svg-dom/interaction.d.ts +0 -69
  137. package/dist/renderer/svg-dom/interaction.d.ts.map +0 -1
  138. package/dist/renderer/svg-dom/interaction.js +0 -296
  139. package/dist/renderer/svg-dom/interaction.js.map +0 -1
  140. package/dist/renderer/svg-dom/renderer.d.ts +0 -47
  141. package/dist/renderer/svg-dom/renderer.d.ts.map +0 -1
  142. package/dist/renderer/svg-dom/renderer.js +0 -188
  143. package/dist/renderer/svg-dom/renderer.js.map +0 -1
  144. package/dist/renderer/svg-string/builders/defs.d.ts +0 -10
  145. package/dist/renderer/svg-string/builders/defs.d.ts.map +0 -1
  146. package/dist/renderer/svg-string/builders/defs.js +0 -43
  147. package/dist/renderer/svg-string/builders/defs.js.map +0 -1
  148. package/dist/renderer/svg-string/builders/link.d.ts +0 -10
  149. package/dist/renderer/svg-string/builders/link.d.ts.map +0 -1
  150. package/dist/renderer/svg-string/builders/link.js +0 -149
  151. package/dist/renderer/svg-string/builders/link.js.map +0 -1
  152. package/dist/renderer/svg-string/builders/node.d.ts +0 -10
  153. package/dist/renderer/svg-string/builders/node.d.ts.map +0 -1
  154. package/dist/renderer/svg-string/builders/node.js +0 -134
  155. package/dist/renderer/svg-string/builders/node.js.map +0 -1
  156. package/dist/renderer/svg-string/builders/subgraph.d.ts +0 -10
  157. package/dist/renderer/svg-string/builders/subgraph.d.ts.map +0 -1
  158. package/dist/renderer/svg-string/builders/subgraph.js +0 -59
  159. package/dist/renderer/svg-string/builders/subgraph.js.map +0 -1
  160. package/dist/renderer/svg-string/index.d.ts +0 -5
  161. package/dist/renderer/svg-string/index.d.ts.map +0 -1
  162. package/dist/renderer/svg-string/index.js +0 -5
  163. package/dist/renderer/svg-string/index.js.map +0 -1
  164. package/dist/renderer/svg-string/renderer.d.ts +0 -17
  165. package/dist/renderer/svg-string/renderer.d.ts.map +0 -1
  166. package/dist/renderer/svg-string/renderer.js +0 -53
  167. package/dist/renderer/svg-string/renderer.js.map +0 -1
  168. package/dist/renderer/svg.d.ts +0 -105
  169. package/dist/renderer/svg.d.ts.map +0 -1
  170. package/dist/renderer/svg.js +0 -804
  171. package/dist/renderer/svg.js.map +0 -1
  172. package/dist/renderer/text-measurer/browser-measurer.d.ts +0 -25
  173. package/dist/renderer/text-measurer/browser-measurer.d.ts.map +0 -1
  174. package/dist/renderer/text-measurer/browser-measurer.js +0 -85
  175. package/dist/renderer/text-measurer/browser-measurer.js.map +0 -1
  176. package/dist/renderer/text-measurer/fallback-measurer.d.ts +0 -22
  177. package/dist/renderer/text-measurer/fallback-measurer.d.ts.map +0 -1
  178. package/dist/renderer/text-measurer/fallback-measurer.js +0 -113
  179. package/dist/renderer/text-measurer/fallback-measurer.js.map +0 -1
  180. package/dist/renderer/text-measurer/index.d.ts +0 -13
  181. package/dist/renderer/text-measurer/index.d.ts.map +0 -1
  182. package/dist/renderer/text-measurer/index.js +0 -35
  183. package/dist/renderer/text-measurer/index.js.map +0 -1
  184. package/dist/renderer/text-measurer/types.d.ts +0 -30
  185. package/dist/renderer/text-measurer/types.d.ts.map +0 -1
  186. package/dist/renderer/text-measurer/types.js +0 -5
  187. package/dist/renderer/text-measurer/types.js.map +0 -1
  188. package/dist/renderer/theme.d.ts +0 -29
  189. package/dist/renderer/theme.d.ts.map +0 -1
  190. package/dist/renderer/theme.js +0 -80
  191. package/dist/renderer/theme.js.map +0 -1
  192. package/src/renderer/index.ts +0 -6
  193. package/src/renderer/svg.ts +0 -997
@@ -1,646 +0,0 @@
1
- /**
2
- * RenderModel Builder
3
- * Converts LayoutResult to RenderModel
4
- */
5
- import { getThemeColors, getVlanColor } from '../theme.js';
6
- import { getDeviceIcon, getVendorIconEntry } from '../../icons/index.js';
7
- // ============================================
8
- // Constants
9
- // ============================================
10
- const DEFAULT_FONT_FAMILY = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
11
- const DEFAULT_FONT_SIZE = 12;
12
- const PORT_LABEL_FONT_SIZE = 9;
13
- const SUBGRAPH_LABEL_FONT_SIZE = 14;
14
- const LINK_LABEL_FONT_SIZE = 11;
15
- const ENDPOINT_LABEL_FONT_SIZE = 9;
16
- // ============================================
17
- // Builder Class
18
- // ============================================
19
- export class RenderModelBuilder {
20
- textMeasurer;
21
- iconRegistry;
22
- options;
23
- theme;
24
- version = 0;
25
- constructor(textMeasurer, iconRegistry, options = {}) {
26
- this.textMeasurer = textMeasurer;
27
- this.iconRegistry = iconRegistry;
28
- this.options = {
29
- fontFamily: options.fontFamily ?? DEFAULT_FONT_FAMILY,
30
- theme: options.theme ?? 'light',
31
- showPortLabels: options.showPortLabels ?? true,
32
- showEndpointLabels: options.showEndpointLabels ?? true,
33
- };
34
- this.theme = getThemeColors(this.options.theme);
35
- }
36
- /**
37
- * Build RenderModel from NetworkGraph and LayoutResult
38
- */
39
- build(_graph, layout) {
40
- const startTime = performance.now();
41
- // Clear icon registry for fresh build
42
- this.iconRegistry.clear();
43
- // Build nodes
44
- const nodes = new Map();
45
- for (const [id, layoutNode] of layout.nodes) {
46
- nodes.set(id, this.buildNode(layoutNode));
47
- }
48
- // Build links
49
- const links = new Map();
50
- for (const [id, layoutLink] of layout.links) {
51
- links.set(id, this.buildLink(layoutLink, layout.nodes));
52
- }
53
- // Build subgraphs
54
- const subgraphs = new Map();
55
- for (const [id, layoutSubgraph] of layout.subgraphs) {
56
- subgraphs.set(id, this.buildSubgraph(layoutSubgraph));
57
- }
58
- // Build defs
59
- const defs = this.buildDefs();
60
- // Build styles
61
- const styles = this.buildStyles();
62
- const renderTime = performance.now() - startTime;
63
- return {
64
- version: ++this.version,
65
- bounds: layout.bounds,
66
- viewBox: `${layout.bounds.x} ${layout.bounds.y} ${layout.bounds.width} ${layout.bounds.height}`,
67
- backgroundColor: this.theme.backgroundColor,
68
- defs,
69
- styles,
70
- subgraphs,
71
- links,
72
- nodes,
73
- metadata: {
74
- algorithm: layout.metadata?.algorithm ?? 'unknown',
75
- renderTime,
76
- nodeCount: nodes.size,
77
- linkCount: links.size,
78
- subgraphCount: subgraphs.size,
79
- },
80
- };
81
- }
82
- // ============================================
83
- // Node Building
84
- // ============================================
85
- buildNode(layoutNode) {
86
- const { id, position, size, node, ports } = layoutNode;
87
- const style = this.resolveNodeStyle(node);
88
- // Build labels
89
- const labels = this.buildNodeLabels(node, position, size);
90
- // Register icon
91
- const iconInfo = this.registerNodeIcon(node);
92
- // Build ports
93
- const renderPorts = ports ? this.buildPorts(id, position, ports) : [];
94
- return {
95
- id,
96
- position,
97
- size,
98
- shape: node.shape,
99
- style,
100
- labels,
101
- iconRef: iconInfo?.iconRef,
102
- iconPosition: iconInfo?.iconPosition
103
- ? { x: position.x + iconInfo.iconPosition.x, y: position.y + iconInfo.iconPosition.y }
104
- : undefined,
105
- iconSize: iconInfo?.iconSize,
106
- ports: renderPorts,
107
- classes: ['node'],
108
- dataAttributes: { 'data-id': id },
109
- zIndex: 2,
110
- dirty: false,
111
- source: node,
112
- };
113
- }
114
- resolveNodeStyle(node) {
115
- const style = node.style || {};
116
- return {
117
- fill: style.fill || this.theme.defaultNodeFill,
118
- stroke: style.stroke || this.theme.defaultNodeStroke,
119
- strokeWidth: style.strokeWidth || 1,
120
- strokeDasharray: style.strokeDasharray,
121
- opacity: style.opacity,
122
- filter: 'url(#shadow)',
123
- };
124
- }
125
- buildNodeLabels(node, position, _size) {
126
- const labels = Array.isArray(node.label) ? node.label : [node.label];
127
- const lineHeight = 16;
128
- const totalHeight = labels.length * lineHeight;
129
- // Check if node has icon
130
- const hasIcon = !!(node.vendor && (node.service || node.model)) || !!node.type;
131
- const iconOffset = hasIcon ? 28 : 0;
132
- const startY = position.y - totalHeight / 2 + lineHeight / 2 + 4 + iconOffset;
133
- return labels.map((labelText, i) => {
134
- const isBold = labelText.includes('<b>') || labelText.includes('<strong>');
135
- const cleanText = labelText.replace(/<\/?b>|<\/?strong>|<br\s*\/?>/gi, '');
136
- const textStyle = {
137
- fontFamily: this.options.fontFamily,
138
- fontSize: DEFAULT_FONT_SIZE,
139
- fontWeight: isBold ? 'bold' : 'normal',
140
- fill: this.theme.labelColor,
141
- textAnchor: 'middle',
142
- dominantBaseline: 'auto',
143
- };
144
- const metrics = this.textMeasurer.measure(cleanText, {
145
- fontFamily: textStyle.fontFamily,
146
- fontSize: textStyle.fontSize,
147
- fontWeight: textStyle.fontWeight,
148
- });
149
- return {
150
- id: `${node.id}-label-${i}`,
151
- text: cleanText,
152
- position: { x: position.x, y: startY + i * lineHeight },
153
- textStyle,
154
- measuredWidth: metrics.width,
155
- measuredHeight: metrics.height,
156
- };
157
- });
158
- }
159
- registerNodeIcon(node) {
160
- const iconKey = node.service || node.model;
161
- const iconSize = 40;
162
- const iconY = node.shape === 'circle' ? 0 : -20; // Offset from center
163
- // Try vendor-specific icon first
164
- if (node.vendor && iconKey) {
165
- const iconEntry = getVendorIconEntry(node.vendor, iconKey, node.resource);
166
- if (iconEntry) {
167
- const iconTheme = this.options.theme === 'dark' ? 'dark' : 'light';
168
- const content = iconEntry[iconTheme] || iconEntry.default;
169
- const viewBox = iconEntry.viewBox || '0 0 48 48';
170
- const symbolId = this.iconRegistry.register(`${node.vendor}-${iconKey}`, {
171
- content,
172
- viewBox,
173
- });
174
- return {
175
- iconRef: symbolId,
176
- iconPosition: { x: -iconSize / 2, y: iconY },
177
- iconSize: { width: iconSize, height: iconSize },
178
- };
179
- }
180
- }
181
- // Fall back to device type icon
182
- if (node.type) {
183
- const iconPath = getDeviceIcon(node.type);
184
- if (iconPath) {
185
- const symbolId = this.iconRegistry.register(`device-${node.type}`, {
186
- content: iconPath,
187
- viewBox: '0 0 24 24',
188
- width: 24,
189
- height: 24,
190
- });
191
- return {
192
- iconRef: symbolId,
193
- iconPosition: { x: -iconSize / 2, y: iconY },
194
- iconSize: { width: iconSize, height: iconSize },
195
- };
196
- }
197
- }
198
- return null;
199
- }
200
- // ============================================
201
- // Port Building
202
- // ============================================
203
- buildPorts(nodeId, nodePosition, ports) {
204
- const result = [];
205
- for (const [portId, port] of ports) {
206
- const absolutePosition = {
207
- x: nodePosition.x + port.position.x,
208
- y: nodePosition.y + port.position.y,
209
- };
210
- const labelPosition = this.calculatePortLabelPosition(absolutePosition, port.side);
211
- const labelMetrics = this.textMeasurer.measure(port.label, {
212
- fontFamily: this.options.fontFamily,
213
- fontSize: PORT_LABEL_FONT_SIZE,
214
- });
215
- const textAnchor = port.side === 'left' ? 'end' : port.side === 'right' ? 'start' : 'middle';
216
- const label = {
217
- id: `${portId}-label`,
218
- text: port.label,
219
- position: labelPosition,
220
- textStyle: {
221
- fontFamily: this.options.fontFamily,
222
- fontSize: PORT_LABEL_FONT_SIZE,
223
- fontWeight: 'normal',
224
- fill: this.theme.portLabelColor,
225
- textAnchor,
226
- dominantBaseline: 'auto',
227
- },
228
- measuredWidth: labelMetrics.width,
229
- measuredHeight: labelMetrics.height,
230
- background: {
231
- bounds: this.calculateLabelBackground(labelPosition, labelMetrics, textAnchor),
232
- style: { fill: this.theme.portLabelBg },
233
- cornerRadius: 2,
234
- },
235
- };
236
- result.push({
237
- id: portId,
238
- nodeId,
239
- relativePosition: port.position,
240
- absolutePosition,
241
- size: port.size,
242
- side: port.side,
243
- style: {
244
- fill: this.theme.portFill,
245
- stroke: this.theme.portStroke,
246
- strokeWidth: 1,
247
- },
248
- label,
249
- });
250
- }
251
- return result;
252
- }
253
- calculatePortLabelPosition(portPosition, side) {
254
- const offset = 12;
255
- switch (side) {
256
- case 'top':
257
- return { x: portPosition.x, y: portPosition.y - offset };
258
- case 'bottom':
259
- return { x: portPosition.x, y: portPosition.y + offset + 4 };
260
- case 'left':
261
- return { x: portPosition.x - offset, y: portPosition.y };
262
- case 'right':
263
- return { x: portPosition.x + offset, y: portPosition.y };
264
- }
265
- }
266
- calculateLabelBackground(position, metrics, textAnchor) {
267
- const padding = 2;
268
- const width = metrics.width + padding * 2;
269
- const height = metrics.height + padding * 2;
270
- let x = position.x - padding;
271
- if (textAnchor === 'middle') {
272
- x = position.x - width / 2;
273
- }
274
- else if (textAnchor === 'end') {
275
- x = position.x - width + padding;
276
- }
277
- return {
278
- x,
279
- y: position.y - metrics.height + padding,
280
- width,
281
- height,
282
- };
283
- }
284
- // ============================================
285
- // Link Building
286
- // ============================================
287
- buildLink(layoutLink, nodes) {
288
- const { id, from, to, fromEndpoint, toEndpoint, points, link } = layoutLink;
289
- const style = this.resolveLinkStyle(link);
290
- const pathData = this.generatePathData(points);
291
- const lineCount = this.getBandwidthLineCount(link.bandwidth);
292
- // Build center labels
293
- const midPoint = this.getMidPoint(points);
294
- const labels = this.buildLinkLabels(link, midPoint);
295
- // Build endpoint labels
296
- const fromNode = nodes.get(from);
297
- const toNode = nodes.get(to);
298
- const fromLabels = this.buildEndpointLabels(fromEndpoint, points, 'start', fromNode);
299
- const toLabels = this.buildEndpointLabels(toEndpoint, points, 'end', toNode);
300
- // Determine marker
301
- const arrow = link.arrow ?? 'none';
302
- const markerEnd = arrow === 'forward' || arrow === 'both' ? 'url(#arrow)' : undefined;
303
- const markerStart = arrow === 'back' || arrow === 'both' ? 'url(#arrow)' : undefined;
304
- return {
305
- id,
306
- fromNodeId: from,
307
- toNodeId: to,
308
- points,
309
- pathData,
310
- style,
311
- markerStart,
312
- markerEnd,
313
- lineCount,
314
- labels,
315
- fromLabels,
316
- toLabels,
317
- classes: ['link-group'],
318
- dataAttributes: { 'data-id': id },
319
- zIndex: 1,
320
- dirty: false,
321
- source: link,
322
- };
323
- }
324
- resolveLinkStyle(link) {
325
- const style = link.style || {};
326
- const stroke = style.stroke || getVlanColor(link.vlan) || this.theme.defaultLinkStroke;
327
- const strokeWidth = style.strokeWidth || this.getLinkStrokeWidth(link.type);
328
- const strokeDasharray = this.getLinkDasharray(link.type);
329
- return {
330
- fill: 'none',
331
- stroke,
332
- strokeWidth,
333
- strokeDasharray: strokeDasharray || undefined,
334
- };
335
- }
336
- getLinkStrokeWidth(type) {
337
- switch (type) {
338
- case 'thick': return 3;
339
- case 'double': return 2;
340
- default: return 1.5;
341
- }
342
- }
343
- getLinkDasharray(type) {
344
- switch (type) {
345
- case 'dashed': return '5 3';
346
- case 'invisible': return '0';
347
- default: return '';
348
- }
349
- }
350
- getBandwidthLineCount(bandwidth) {
351
- switch (bandwidth) {
352
- case '1G': return 1;
353
- case '10G': return 2;
354
- case '25G': return 3;
355
- case '40G': return 4;
356
- case '100G': return 5;
357
- default: return 1;
358
- }
359
- }
360
- generatePathData(points) {
361
- if (points.length === 4) {
362
- // Bezier curve
363
- 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}`;
364
- }
365
- else if (points.length === 2) {
366
- // Straight line
367
- return `M ${points[0].x} ${points[0].y} L ${points[1].x} ${points[1].y}`;
368
- }
369
- else {
370
- // Polyline
371
- return points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ');
372
- }
373
- }
374
- getMidPoint(points) {
375
- if (points.length === 4) {
376
- // Bezier midpoint at t=0.5
377
- const t = 0.5;
378
- const mt = 1 - t;
379
- return {
380
- x: mt * mt * mt * points[0].x + 3 * mt * mt * t * points[1].x + 3 * mt * t * t * points[2].x + t * t * t * points[3].x,
381
- y: mt * mt * mt * points[0].y + 3 * mt * mt * t * points[1].y + 3 * mt * t * t * points[2].y + t * t * t * points[3].y,
382
- };
383
- }
384
- else if (points.length >= 2) {
385
- const midIndex = Math.floor(points.length / 2);
386
- if (midIndex > 0) {
387
- return {
388
- x: (points[midIndex - 1].x + points[midIndex].x) / 2,
389
- y: (points[midIndex - 1].y + points[midIndex].y) / 2,
390
- };
391
- }
392
- }
393
- return points[0] || { x: 0, y: 0 };
394
- }
395
- buildLinkLabels(link, midPoint) {
396
- const labels = [];
397
- let yOffset = -8;
398
- // Link label
399
- if (link.label) {
400
- const labelText = Array.isArray(link.label) ? link.label.join(' / ') : link.label;
401
- const metrics = this.textMeasurer.measure(labelText, {
402
- fontFamily: this.options.fontFamily,
403
- fontSize: LINK_LABEL_FONT_SIZE,
404
- });
405
- labels.push({
406
- id: `${link.id || 'link'}-label`,
407
- text: labelText,
408
- position: { x: midPoint.x, y: midPoint.y + yOffset },
409
- textStyle: {
410
- fontFamily: this.options.fontFamily,
411
- fontSize: LINK_LABEL_FONT_SIZE,
412
- fontWeight: 'normal',
413
- fill: this.theme.labelSecondaryColor,
414
- textAnchor: 'middle',
415
- dominantBaseline: 'auto',
416
- },
417
- measuredWidth: metrics.width,
418
- measuredHeight: metrics.height,
419
- });
420
- yOffset += 12;
421
- }
422
- // VLAN label
423
- if (link.vlan && link.vlan.length > 0) {
424
- const vlanText = link.vlan.length === 1
425
- ? `VLAN ${link.vlan[0]}`
426
- : `VLAN ${link.vlan.join(', ')}`;
427
- const metrics = this.textMeasurer.measure(vlanText, {
428
- fontFamily: this.options.fontFamily,
429
- fontSize: LINK_LABEL_FONT_SIZE,
430
- });
431
- labels.push({
432
- id: `${link.id || 'link'}-vlan`,
433
- text: vlanText,
434
- position: { x: midPoint.x, y: midPoint.y + yOffset },
435
- textStyle: {
436
- fontFamily: this.options.fontFamily,
437
- fontSize: LINK_LABEL_FONT_SIZE,
438
- fontWeight: 'normal',
439
- fill: this.theme.labelSecondaryColor,
440
- textAnchor: 'middle',
441
- dominantBaseline: 'auto',
442
- },
443
- measuredWidth: metrics.width,
444
- measuredHeight: metrics.height,
445
- });
446
- }
447
- return labels;
448
- }
449
- buildEndpointLabels(endpoint, points, which, node) {
450
- if (!this.options.showEndpointLabels || !endpoint.ip) {
451
- return [];
452
- }
453
- const endpointIdx = which === 'start' ? 0 : points.length - 1;
454
- const endpointPoint = points[endpointIdx];
455
- const nextIdx = which === 'start' ? 1 : points.length - 2;
456
- const nextPoint = points[nextIdx];
457
- // Calculate label position (offset from endpoint)
458
- const dx = nextPoint.x - endpointPoint.x;
459
- const dy = nextPoint.y - endpointPoint.y;
460
- const len = Math.sqrt(dx * dx + dy * dy);
461
- const nx = len > 0 ? dx / len : 0;
462
- const ny = len > 0 ? dy / len : 1;
463
- const offsetDist = 30;
464
- const perpDist = 20;
465
- const perpX = -ny;
466
- const perpY = nx;
467
- const isVertical = Math.abs(dy) > Math.abs(dx);
468
- const sideMultiplier = isVertical
469
- ? (node ? (endpointPoint.x - node.position.x > 0 ? 1 : -1) : 1)
470
- : (which === 'start' ? -1 : 1);
471
- const labelX = isVertical
472
- ? endpointPoint.x + perpDist * sideMultiplier
473
- : endpointPoint.x + nx * offsetDist + perpX * perpDist * sideMultiplier;
474
- const labelY = isVertical
475
- ? endpointPoint.y + ny * offsetDist
476
- : endpointPoint.y + ny * offsetDist + perpY * perpDist * sideMultiplier;
477
- const textAnchor = Math.abs(labelX - endpointPoint.x) > 8
478
- ? (labelX > endpointPoint.x ? 'start' : 'end')
479
- : 'middle';
480
- const metrics = this.textMeasurer.measure(endpoint.ip, {
481
- fontFamily: this.options.fontFamily,
482
- fontSize: ENDPOINT_LABEL_FONT_SIZE,
483
- });
484
- return [{
485
- id: `${endpoint.node}-${endpoint.port || 'endpoint'}-ip`,
486
- text: endpoint.ip,
487
- position: { x: labelX, y: labelY },
488
- textStyle: {
489
- fontFamily: this.options.fontFamily,
490
- fontSize: ENDPOINT_LABEL_FONT_SIZE,
491
- fontWeight: 'normal',
492
- fill: this.theme.labelColor,
493
- textAnchor,
494
- dominantBaseline: 'auto',
495
- },
496
- measuredWidth: metrics.width,
497
- measuredHeight: metrics.height,
498
- background: {
499
- bounds: this.calculateLabelBackground({ x: labelX, y: labelY }, metrics, textAnchor),
500
- style: {
501
- fill: this.theme.endpointLabelBg,
502
- stroke: this.theme.endpointLabelStroke,
503
- strokeWidth: 0.5,
504
- },
505
- cornerRadius: 2,
506
- },
507
- }];
508
- }
509
- // ============================================
510
- // Subgraph Building
511
- // ============================================
512
- buildSubgraph(layoutSubgraph) {
513
- const { id, bounds, subgraph } = layoutSubgraph;
514
- const style = this.resolveSubgraphStyle(subgraph);
515
- // Build label
516
- const labelPos = { x: bounds.x + 10, y: bounds.y + 20 };
517
- const labelMetrics = this.textMeasurer.measure(subgraph.label, {
518
- fontFamily: this.options.fontFamily,
519
- fontSize: SUBGRAPH_LABEL_FONT_SIZE,
520
- fontWeight: 'bold',
521
- });
522
- // Register icon if present
523
- let iconRef;
524
- let iconPosition;
525
- let iconSize;
526
- const iconKey = subgraph.service || subgraph.model;
527
- if (subgraph.vendor && iconKey) {
528
- const iconEntry = getVendorIconEntry(subgraph.vendor, iconKey, subgraph.resource);
529
- if (iconEntry) {
530
- const iconTheme = this.options.theme === 'dark' ? 'dark' : 'light';
531
- const content = iconEntry[iconTheme] || iconEntry.default;
532
- const viewBox = iconEntry.viewBox || '0 0 48 48';
533
- iconRef = this.iconRegistry.register(`${subgraph.vendor}-${iconKey}`, {
534
- content,
535
- viewBox,
536
- });
537
- iconPosition = { x: bounds.x + 8, y: bounds.y + 8 };
538
- iconSize = { width: 24, height: 24 };
539
- // Adjust label position for icon
540
- labelPos.x = bounds.x + 24 + 16;
541
- }
542
- }
543
- const label = {
544
- id: `${id}-label`,
545
- text: subgraph.label,
546
- position: labelPos,
547
- textStyle: {
548
- fontFamily: this.options.fontFamily,
549
- fontSize: SUBGRAPH_LABEL_FONT_SIZE,
550
- fontWeight: 'bold',
551
- fill: this.theme.subgraphLabelColor,
552
- textAnchor: 'start',
553
- dominantBaseline: 'auto',
554
- },
555
- measuredWidth: labelMetrics.width,
556
- measuredHeight: labelMetrics.height,
557
- };
558
- return {
559
- id,
560
- bounds,
561
- style,
562
- cornerRadius: 8,
563
- label,
564
- iconRef,
565
- iconPosition,
566
- iconSize,
567
- classes: ['subgraph'],
568
- dataAttributes: { 'data-id': id },
569
- zIndex: 0,
570
- dirty: false,
571
- source: subgraph,
572
- };
573
- }
574
- resolveSubgraphStyle(subgraph) {
575
- const style = subgraph.style || {};
576
- return {
577
- fill: style.fill || this.theme.subgraphFill,
578
- stroke: style.stroke || this.theme.subgraphStroke,
579
- strokeWidth: style.strokeWidth || 1,
580
- strokeDasharray: style.strokeDasharray,
581
- };
582
- }
583
- // ============================================
584
- // Defs Building
585
- // ============================================
586
- buildDefs() {
587
- // Get icons from registry
588
- const icons = this.iconRegistry.getIcons();
589
- // Build markers
590
- const markers = new Map();
591
- markers.set('arrow', {
592
- id: 'arrow',
593
- width: 10,
594
- height: 7,
595
- refX: 9,
596
- refY: 3.5,
597
- orient: 'auto',
598
- content: '<polygon points="0 0, 10 3.5, 0 7" />',
599
- fill: this.theme.defaultLinkStroke,
600
- });
601
- markers.set('arrow-red', {
602
- id: 'arrow-red',
603
- width: 10,
604
- height: 7,
605
- refX: 9,
606
- refY: 3.5,
607
- orient: 'auto',
608
- content: '<polygon points="0 0, 10 3.5, 0 7" />',
609
- fill: '#dc2626',
610
- });
611
- // Build filters
612
- const filters = new Map();
613
- filters.set('shadow', {
614
- id: 'shadow',
615
- x: '-20%',
616
- y: '-20%',
617
- width: '140%',
618
- height: '140%',
619
- content: '<feDropShadow dx="2" dy="2" stdDeviation="3" flood-opacity="0.15" />',
620
- });
621
- return { icons, markers, filters };
622
- }
623
- // ============================================
624
- // Styles Building
625
- // ============================================
626
- buildStyles() {
627
- return `
628
- .node { cursor: pointer; }
629
- .node:hover rect, .node:hover circle, .node:hover polygon { filter: brightness(0.95); }
630
- .node-label { font-family: ${this.options.fontFamily}; font-size: 12px; fill: ${this.theme.labelColor}; }
631
- .node-label-bold { font-weight: bold; }
632
- .node-icon { color: ${this.theme.labelSecondaryColor}; }
633
- .subgraph-icon { opacity: 0.9; }
634
- .subgraph-label { font-family: ${this.options.fontFamily}; font-size: 14px; font-weight: 600; fill: ${this.theme.subgraphLabelColor}; }
635
- .link-label { font-family: ${this.options.fontFamily}; font-size: 11px; fill: ${this.theme.labelSecondaryColor}; }
636
- .endpoint-label { font-family: ${this.options.fontFamily}; font-size: 9px; fill: ${this.theme.labelColor}; }
637
- `;
638
- }
639
- }
640
- // ============================================
641
- // Factory Function
642
- // ============================================
643
- export function createRenderModelBuilder(textMeasurer, iconRegistry, options) {
644
- return new RenderModelBuilder(textMeasurer, iconRegistry, options);
645
- }
646
- //# sourceMappingURL=builder.js.map