@shumoku/core 0.1.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.
Files changed (220) hide show
  1. package/dist/icons/build-icons.d.ts +6 -0
  2. package/dist/icons/build-icons.d.ts.map +1 -0
  3. package/dist/icons/build-icons.js +163 -0
  4. package/dist/icons/build-icons.js.map +1 -0
  5. package/dist/icons/generated-icons.d.ts +32 -0
  6. package/dist/icons/generated-icons.d.ts.map +1 -0
  7. package/dist/icons/generated-icons.js +88 -0
  8. package/dist/icons/generated-icons.js.map +1 -0
  9. package/dist/icons/index.d.ts +2 -0
  10. package/dist/icons/index.d.ts.map +1 -0
  11. package/dist/icons/index.js +2 -0
  12. package/dist/icons/index.js.map +1 -0
  13. package/dist/index.d.ts +10 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +16 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/layout/hierarchical.d.ts +73 -0
  18. package/dist/layout/hierarchical.d.ts.map +1 -0
  19. package/dist/layout/hierarchical.js +1320 -0
  20. package/dist/layout/hierarchical.js.map +1 -0
  21. package/dist/layout/index.d.ts +6 -0
  22. package/dist/layout/index.d.ts.map +1 -0
  23. package/dist/layout/index.js +5 -0
  24. package/dist/layout/index.js.map +1 -0
  25. package/dist/models/index.d.ts +5 -0
  26. package/dist/models/index.d.ts.map +1 -0
  27. package/dist/models/index.js +5 -0
  28. package/dist/models/index.js.map +1 -0
  29. package/dist/models/types.d.ts +381 -0
  30. package/dist/models/types.d.ts.map +1 -0
  31. package/dist/models/types.js +61 -0
  32. package/dist/models/types.js.map +1 -0
  33. package/dist/renderer/components/index.d.ts +8 -0
  34. package/dist/renderer/components/index.d.ts.map +1 -0
  35. package/dist/renderer/components/index.js +8 -0
  36. package/dist/renderer/components/index.js.map +1 -0
  37. package/dist/renderer/components/link-renderer.d.ts +11 -0
  38. package/dist/renderer/components/link-renderer.d.ts.map +1 -0
  39. package/dist/renderer/components/link-renderer.js +340 -0
  40. package/dist/renderer/components/link-renderer.js.map +1 -0
  41. package/dist/renderer/components/node-renderer.d.ts +14 -0
  42. package/dist/renderer/components/node-renderer.d.ts.map +1 -0
  43. package/dist/renderer/components/node-renderer.js +242 -0
  44. package/dist/renderer/components/node-renderer.js.map +1 -0
  45. package/dist/renderer/components/port-renderer.d.ts +8 -0
  46. package/dist/renderer/components/port-renderer.d.ts.map +1 -0
  47. package/dist/renderer/components/port-renderer.js +85 -0
  48. package/dist/renderer/components/port-renderer.js.map +1 -0
  49. package/dist/renderer/components/subgraph-renderer.d.ts +13 -0
  50. package/dist/renderer/components/subgraph-renderer.d.ts.map +1 -0
  51. package/dist/renderer/components/subgraph-renderer.js +85 -0
  52. package/dist/renderer/components/subgraph-renderer.js.map +1 -0
  53. package/dist/renderer/icon-registry/index.d.ts +6 -0
  54. package/dist/renderer/icon-registry/index.d.ts.map +1 -0
  55. package/dist/renderer/icon-registry/index.js +5 -0
  56. package/dist/renderer/icon-registry/index.js.map +1 -0
  57. package/dist/renderer/icon-registry/registry.d.ts +25 -0
  58. package/dist/renderer/icon-registry/registry.d.ts.map +1 -0
  59. package/dist/renderer/icon-registry/registry.js +85 -0
  60. package/dist/renderer/icon-registry/registry.js.map +1 -0
  61. package/dist/renderer/icon-registry/types.d.ts +44 -0
  62. package/dist/renderer/icon-registry/types.d.ts.map +1 -0
  63. package/dist/renderer/icon-registry/types.js +5 -0
  64. package/dist/renderer/icon-registry/types.js.map +1 -0
  65. package/dist/renderer/index.d.ts +6 -0
  66. package/dist/renderer/index.d.ts.map +1 -0
  67. package/dist/renderer/index.js +5 -0
  68. package/dist/renderer/index.js.map +1 -0
  69. package/dist/renderer/render-model/builder.d.ts +43 -0
  70. package/dist/renderer/render-model/builder.d.ts.map +1 -0
  71. package/dist/renderer/render-model/builder.js +646 -0
  72. package/dist/renderer/render-model/builder.js.map +1 -0
  73. package/dist/renderer/render-model/index.d.ts +6 -0
  74. package/dist/renderer/render-model/index.d.ts.map +1 -0
  75. package/dist/renderer/render-model/index.js +5 -0
  76. package/dist/renderer/render-model/index.js.map +1 -0
  77. package/dist/renderer/render-model/types.d.ts +216 -0
  78. package/dist/renderer/render-model/types.d.ts.map +1 -0
  79. package/dist/renderer/render-model/types.js +6 -0
  80. package/dist/renderer/render-model/types.js.map +1 -0
  81. package/dist/renderer/renderer-types.d.ts +55 -0
  82. package/dist/renderer/renderer-types.d.ts.map +1 -0
  83. package/dist/renderer/renderer-types.js +5 -0
  84. package/dist/renderer/renderer-types.js.map +1 -0
  85. package/dist/renderer/svg-builder.d.ts +152 -0
  86. package/dist/renderer/svg-builder.d.ts.map +1 -0
  87. package/dist/renderer/svg-builder.js +176 -0
  88. package/dist/renderer/svg-builder.js.map +1 -0
  89. package/dist/renderer/svg-dom/builders/defs.d.ts +10 -0
  90. package/dist/renderer/svg-dom/builders/defs.d.ts.map +1 -0
  91. package/dist/renderer/svg-dom/builders/defs.js +82 -0
  92. package/dist/renderer/svg-dom/builders/defs.js.map +1 -0
  93. package/dist/renderer/svg-dom/builders/index.d.ts +9 -0
  94. package/dist/renderer/svg-dom/builders/index.d.ts.map +1 -0
  95. package/dist/renderer/svg-dom/builders/index.js +9 -0
  96. package/dist/renderer/svg-dom/builders/index.js.map +1 -0
  97. package/dist/renderer/svg-dom/builders/link.d.ts +18 -0
  98. package/dist/renderer/svg-dom/builders/link.d.ts.map +1 -0
  99. package/dist/renderer/svg-dom/builders/link.js +188 -0
  100. package/dist/renderer/svg-dom/builders/link.js.map +1 -0
  101. package/dist/renderer/svg-dom/builders/node.d.ts +15 -0
  102. package/dist/renderer/svg-dom/builders/node.d.ts.map +1 -0
  103. package/dist/renderer/svg-dom/builders/node.js +262 -0
  104. package/dist/renderer/svg-dom/builders/node.js.map +1 -0
  105. package/dist/renderer/svg-dom/builders/subgraph.d.ts +14 -0
  106. package/dist/renderer/svg-dom/builders/subgraph.d.ts.map +1 -0
  107. package/dist/renderer/svg-dom/builders/subgraph.js +63 -0
  108. package/dist/renderer/svg-dom/builders/subgraph.js.map +1 -0
  109. package/dist/renderer/svg-dom/builders/utils.d.ts +40 -0
  110. package/dist/renderer/svg-dom/builders/utils.d.ts.map +1 -0
  111. package/dist/renderer/svg-dom/builders/utils.js +79 -0
  112. package/dist/renderer/svg-dom/builders/utils.js.map +1 -0
  113. package/dist/renderer/svg-dom/index.d.ts +9 -0
  114. package/dist/renderer/svg-dom/index.d.ts.map +1 -0
  115. package/dist/renderer/svg-dom/index.js +7 -0
  116. package/dist/renderer/svg-dom/index.js.map +1 -0
  117. package/dist/renderer/svg-dom/interaction.d.ts +69 -0
  118. package/dist/renderer/svg-dom/interaction.d.ts.map +1 -0
  119. package/dist/renderer/svg-dom/interaction.js +296 -0
  120. package/dist/renderer/svg-dom/interaction.js.map +1 -0
  121. package/dist/renderer/svg-dom/renderer.d.ts +47 -0
  122. package/dist/renderer/svg-dom/renderer.d.ts.map +1 -0
  123. package/dist/renderer/svg-dom/renderer.js +188 -0
  124. package/dist/renderer/svg-dom/renderer.js.map +1 -0
  125. package/dist/renderer/svg-string/builders/defs.d.ts +10 -0
  126. package/dist/renderer/svg-string/builders/defs.d.ts.map +1 -0
  127. package/dist/renderer/svg-string/builders/defs.js +43 -0
  128. package/dist/renderer/svg-string/builders/defs.js.map +1 -0
  129. package/dist/renderer/svg-string/builders/link.d.ts +10 -0
  130. package/dist/renderer/svg-string/builders/link.d.ts.map +1 -0
  131. package/dist/renderer/svg-string/builders/link.js +149 -0
  132. package/dist/renderer/svg-string/builders/link.js.map +1 -0
  133. package/dist/renderer/svg-string/builders/node.d.ts +10 -0
  134. package/dist/renderer/svg-string/builders/node.d.ts.map +1 -0
  135. package/dist/renderer/svg-string/builders/node.js +134 -0
  136. package/dist/renderer/svg-string/builders/node.js.map +1 -0
  137. package/dist/renderer/svg-string/builders/subgraph.d.ts +10 -0
  138. package/dist/renderer/svg-string/builders/subgraph.d.ts.map +1 -0
  139. package/dist/renderer/svg-string/builders/subgraph.js +59 -0
  140. package/dist/renderer/svg-string/builders/subgraph.js.map +1 -0
  141. package/dist/renderer/svg-string/index.d.ts +5 -0
  142. package/dist/renderer/svg-string/index.d.ts.map +1 -0
  143. package/dist/renderer/svg-string/index.js +5 -0
  144. package/dist/renderer/svg-string/index.js.map +1 -0
  145. package/dist/renderer/svg-string/renderer.d.ts +17 -0
  146. package/dist/renderer/svg-string/renderer.d.ts.map +1 -0
  147. package/dist/renderer/svg-string/renderer.js +53 -0
  148. package/dist/renderer/svg-string/renderer.js.map +1 -0
  149. package/dist/renderer/svg.d.ts +105 -0
  150. package/dist/renderer/svg.d.ts.map +1 -0
  151. package/dist/renderer/svg.js +804 -0
  152. package/dist/renderer/svg.js.map +1 -0
  153. package/dist/renderer/text-measurer/browser-measurer.d.ts +25 -0
  154. package/dist/renderer/text-measurer/browser-measurer.d.ts.map +1 -0
  155. package/dist/renderer/text-measurer/browser-measurer.js +85 -0
  156. package/dist/renderer/text-measurer/browser-measurer.js.map +1 -0
  157. package/dist/renderer/text-measurer/fallback-measurer.d.ts +22 -0
  158. package/dist/renderer/text-measurer/fallback-measurer.d.ts.map +1 -0
  159. package/dist/renderer/text-measurer/fallback-measurer.js +113 -0
  160. package/dist/renderer/text-measurer/fallback-measurer.js.map +1 -0
  161. package/dist/renderer/text-measurer/index.d.ts +13 -0
  162. package/dist/renderer/text-measurer/index.d.ts.map +1 -0
  163. package/dist/renderer/text-measurer/index.js +35 -0
  164. package/dist/renderer/text-measurer/index.js.map +1 -0
  165. package/dist/renderer/text-measurer/types.d.ts +30 -0
  166. package/dist/renderer/text-measurer/types.d.ts.map +1 -0
  167. package/dist/renderer/text-measurer/types.js +5 -0
  168. package/dist/renderer/text-measurer/types.js.map +1 -0
  169. package/dist/renderer/theme.d.ts +29 -0
  170. package/dist/renderer/theme.d.ts.map +1 -0
  171. package/dist/renderer/theme.js +80 -0
  172. package/dist/renderer/theme.js.map +1 -0
  173. package/dist/themes/dark.d.ts +6 -0
  174. package/dist/themes/dark.d.ts.map +1 -0
  175. package/dist/themes/dark.js +96 -0
  176. package/dist/themes/dark.js.map +1 -0
  177. package/dist/themes/index.d.ts +13 -0
  178. package/dist/themes/index.d.ts.map +1 -0
  179. package/dist/themes/index.js +15 -0
  180. package/dist/themes/index.js.map +1 -0
  181. package/dist/themes/modern.d.ts +6 -0
  182. package/dist/themes/modern.d.ts.map +1 -0
  183. package/dist/themes/modern.js +164 -0
  184. package/dist/themes/modern.js.map +1 -0
  185. package/dist/themes/types.d.ts +234 -0
  186. package/dist/themes/types.d.ts.map +1 -0
  187. package/dist/themes/types.js +5 -0
  188. package/dist/themes/types.js.map +1 -0
  189. package/dist/themes/utils.d.ts +21 -0
  190. package/dist/themes/utils.d.ts.map +1 -0
  191. package/dist/themes/utils.js +124 -0
  192. package/dist/themes/utils.js.map +1 -0
  193. package/package.json +92 -0
  194. package/src/icons/build-icons.ts +189 -0
  195. package/src/icons/default/access-point.svg +3 -0
  196. package/src/icons/default/cloud.svg +3 -0
  197. package/src/icons/default/database.svg +3 -0
  198. package/src/icons/default/firewall.svg +4 -0
  199. package/src/icons/default/generic.svg +3 -0
  200. package/src/icons/default/internet.svg +3 -0
  201. package/src/icons/default/l2-switch.svg +3 -0
  202. package/src/icons/default/l3-switch.svg +3 -0
  203. package/src/icons/default/load-balancer.svg +3 -0
  204. package/src/icons/default/router.svg +3 -0
  205. package/src/icons/default/server.svg +3 -0
  206. package/src/icons/default/vpn.svg +3 -0
  207. package/src/icons/generated-icons.ts +111 -0
  208. package/src/icons/index.ts +1 -0
  209. package/src/index.ts +21 -0
  210. package/src/layout/hierarchical.ts +1543 -0
  211. package/src/layout/index.ts +6 -0
  212. package/src/models/index.ts +5 -0
  213. package/src/models/types.ts +528 -0
  214. package/src/renderer/index.ts +6 -0
  215. package/src/renderer/svg.ts +997 -0
  216. package/src/themes/dark.ts +110 -0
  217. package/src/themes/index.ts +24 -0
  218. package/src/themes/modern.ts +186 -0
  219. package/src/themes/types.ts +262 -0
  220. package/src/themes/utils.ts +143 -0
@@ -0,0 +1,646 @@
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