project-graph-mcp 2.2.6 → 2.3.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 (150) hide show
  1. package/ARCHITECTURE.md +81 -0
  2. package/CHANGELOG.md +57 -0
  3. package/README.md +9 -4
  4. package/package.json +6 -13
  5. package/src/compact/expand.js +1 -1
  6. package/src/core/graph-builder.js +2 -2
  7. package/src/core/parser.js +2 -2
  8. package/src/network/server.js +1 -2
  9. package/vendor/symbiote-node/CHANGELOG.md +31 -0
  10. package/vendor/symbiote-node/LICENSE +21 -0
  11. package/vendor/symbiote-node/README.md +206 -0
  12. package/vendor/symbiote-node/canvas/AutoLayout.js +725 -0
  13. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.css.js +73 -0
  14. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.js +93 -0
  15. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.tpl.js +9 -0
  16. package/vendor/symbiote-node/canvas/CanvasConnectionRenderer.js +962 -0
  17. package/vendor/symbiote-node/canvas/ConnectionRenderer.js +1468 -0
  18. package/vendor/symbiote-node/canvas/FlowSimulator.js +323 -0
  19. package/vendor/symbiote-node/canvas/ForceLayout.js +189 -0
  20. package/vendor/symbiote-node/canvas/ForceWorker.js +1325 -0
  21. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.css.js +97 -0
  22. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.js +176 -0
  23. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.tpl.js +12 -0
  24. package/vendor/symbiote-node/canvas/LODManager.js +88 -0
  25. package/vendor/symbiote-node/canvas/Minimap/Minimap.css.js +71 -0
  26. package/vendor/symbiote-node/canvas/Minimap/Minimap.js +207 -0
  27. package/vendor/symbiote-node/canvas/Minimap/Minimap.tpl.js +9 -0
  28. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.css.js +261 -0
  29. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.js +1840 -0
  30. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.tpl.js +22 -0
  31. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.css.js +97 -0
  32. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.js +132 -0
  33. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.tpl.js +21 -0
  34. package/vendor/symbiote-node/canvas/NodeViewManager.js +584 -0
  35. package/vendor/symbiote-node/canvas/PinExpansion.js +131 -0
  36. package/vendor/symbiote-node/canvas/PseudoConnection.js +80 -0
  37. package/vendor/symbiote-node/canvas/SubgraphManager.js +201 -0
  38. package/vendor/symbiote-node/canvas/SubgraphRouter.js +443 -0
  39. package/vendor/symbiote-node/canvas/ViewportActions.js +446 -0
  40. package/vendor/symbiote-node/core/Connection.js +45 -0
  41. package/vendor/symbiote-node/core/Editor.js +451 -0
  42. package/vendor/symbiote-node/core/Frame.js +31 -0
  43. package/vendor/symbiote-node/core/GraphMermaid.js +348 -0
  44. package/vendor/symbiote-node/core/GraphText.js +210 -0
  45. package/vendor/symbiote-node/core/Node.js +143 -0
  46. package/vendor/symbiote-node/core/Portal.js +104 -0
  47. package/vendor/symbiote-node/core/Socket.js +185 -0
  48. package/vendor/symbiote-node/core/SubgraphNode.js +125 -0
  49. package/vendor/symbiote-node/index.js +103 -0
  50. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.css.js +361 -0
  51. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.js +332 -0
  52. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.tpl.js +96 -0
  53. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.css.js +104 -0
  54. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.js +133 -0
  55. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.tpl.js +33 -0
  56. package/vendor/symbiote-node/interactions/ConnectFlow.js +307 -0
  57. package/vendor/symbiote-node/interactions/Drag.js +102 -0
  58. package/vendor/symbiote-node/interactions/Selector.js +132 -0
  59. package/vendor/symbiote-node/interactions/SnapGrid.js +65 -0
  60. package/vendor/symbiote-node/interactions/Zoom.js +140 -0
  61. package/vendor/symbiote-node/layout/ActionZone/ActionZone.css.js +88 -0
  62. package/vendor/symbiote-node/layout/ActionZone/ActionZone.js +254 -0
  63. package/vendor/symbiote-node/layout/ActionZone/ActionZone.tpl.js +11 -0
  64. package/vendor/symbiote-node/layout/Layout/Layout.css.js +88 -0
  65. package/vendor/symbiote-node/layout/Layout/Layout.js +622 -0
  66. package/vendor/symbiote-node/layout/Layout/Layout.tpl.js +25 -0
  67. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.css.js +293 -0
  68. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.js +467 -0
  69. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.tpl.js +33 -0
  70. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.css.js +46 -0
  71. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.js +102 -0
  72. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.tpl.js +6 -0
  73. package/vendor/symbiote-node/layout/LayoutRouter/LayoutRouter.js +156 -0
  74. package/vendor/symbiote-node/layout/LayoutRouter/routerSync.js +250 -0
  75. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.css.js +379 -0
  76. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.js +263 -0
  77. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.tpl.js +20 -0
  78. package/vendor/symbiote-node/layout/LayoutSidebar/SidebarSection.js +183 -0
  79. package/vendor/symbiote-node/layout/LayoutTree.js +246 -0
  80. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.css.js +43 -0
  81. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.js +89 -0
  82. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.tpl.js +14 -0
  83. package/vendor/symbiote-node/layout/index.js +16 -0
  84. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.css.js +61 -0
  85. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.js +79 -0
  86. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.tpl.js +19 -0
  87. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.css.js +41 -0
  88. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.js +24 -0
  89. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.tpl.js +16 -0
  90. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.css.js +65 -0
  91. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.js +29 -0
  92. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.tpl.js +13 -0
  93. package/vendor/symbiote-node/node/GraphNode/GraphNode.css.js +683 -0
  94. package/vendor/symbiote-node/node/GraphNode/GraphNode.js +92 -0
  95. package/vendor/symbiote-node/node/GraphNode/GraphNode.tpl.js +17 -0
  96. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.js +25 -0
  97. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.tpl.js +7 -0
  98. package/vendor/symbiote-node/node/PortItem/PortItem.css.js +90 -0
  99. package/vendor/symbiote-node/node/PortItem/PortItem.js +87 -0
  100. package/vendor/symbiote-node/node/PortItem/PortItem.tpl.js +10 -0
  101. package/vendor/symbiote-node/package.json +59 -0
  102. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.css.js +143 -0
  103. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.js +131 -0
  104. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.tpl.js +16 -0
  105. package/vendor/symbiote-node/plugins/History.js +384 -0
  106. package/vendor/symbiote-node/plugins/Readonly.js +59 -0
  107. package/vendor/symbiote-node/shapes/CircleShape.js +80 -0
  108. package/vendor/symbiote-node/shapes/CommentShape.js +35 -0
  109. package/vendor/symbiote-node/shapes/DiamondShape.js +115 -0
  110. package/vendor/symbiote-node/shapes/NodeShape.js +80 -0
  111. package/vendor/symbiote-node/shapes/PillShape.js +91 -0
  112. package/vendor/symbiote-node/shapes/RectShape.js +72 -0
  113. package/vendor/symbiote-node/shapes/SVGShape.js +494 -0
  114. package/vendor/symbiote-node/shapes/index.js +53 -0
  115. package/vendor/symbiote-node/themes/Palette.js +32 -0
  116. package/vendor/symbiote-node/themes/Skin.js +113 -0
  117. package/vendor/symbiote-node/themes/Theme.js +84 -0
  118. package/vendor/symbiote-node/themes/carbon.js +137 -0
  119. package/vendor/symbiote-node/themes/dark.js +137 -0
  120. package/vendor/symbiote-node/themes/ebook.js +138 -0
  121. package/vendor/symbiote-node/themes/grey.js +137 -0
  122. package/vendor/symbiote-node/themes/light.js +137 -0
  123. package/vendor/symbiote-node/themes/neon.js +138 -0
  124. package/vendor/symbiote-node/themes/pcb.js +273 -0
  125. package/vendor/symbiote-node/themes/synthwave.js +137 -0
  126. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.css.js +86 -0
  127. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.js +128 -0
  128. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.tpl.js +29 -0
  129. package/web/app.js +6 -5
  130. package/web/components/canvas-graph.js +1666 -0
  131. package/web/components/event-feed/CodeWidget.js +32 -0
  132. package/web/components/event-feed/EventWidget.js +97 -0
  133. package/web/components/event-feed/ListWidget.js +57 -0
  134. package/web/components/event-feed/MiniGraphWidget.js +69 -0
  135. package/web/dashboard.js +1 -1
  136. package/web/index.html +4 -0
  137. package/web/panels/ActionBoard/ActionBoard.js +1 -1
  138. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -1
  139. package/web/panels/code-viewer.js +50 -15
  140. package/web/panels/dep-graph.js +2712 -7
  141. package/web/panels/file-tree.js +5 -2
  142. package/web/panels/live-monitor.js +75 -3
  143. package/web/style.css +33 -0
  144. package/docs/img/explorer-compact.jpg +0 -0
  145. package/docs/img/explorer-expanded.jpg +0 -0
  146. package/src/.contextignore +0 -22
  147. package/src/.project-graph-cache.json +0 -1
  148. package/src/compact/.project-graph-cache.json +0 -1
  149. package/web/.project-graph-cache.json +0 -1
  150. package/web/panels/SettingsPanel/.project-graph-cache.json +0 -1
@@ -0,0 +1,584 @@
1
+ /**
2
+ * NodeViewManager — creates/destroys graph-node elements with group drag
3
+ *
4
+ * Handles DOM creation, drag initialization with snap-to-grid,
5
+ * group drag for multi-selected nodes, and twitch click detection.
6
+ * Extracted from NodeCanvas to reduce complexity (was 27 cyclomatic).
7
+ *
8
+ * @module symbiote-node/canvas/NodeViewManager
9
+ */
10
+
11
+ import { Drag } from '../interactions/Drag.js';
12
+ import { Selector } from '../interactions/Selector.js';
13
+ import { animateOut } from '@symbiotejs/symbiote';
14
+ import { getShape } from '../shapes/index.js';
15
+
16
+ export class NodeViewManager {
17
+
18
+ /** @type {Map<string, HTMLElement>} */
19
+ #nodeViews;
20
+
21
+ /** @type {import('../core/Editor.js').NodeEditor} */
22
+ #editor;
23
+
24
+ /** @type {import('../interactions/Selector.js').Selector} */
25
+ #selector;
26
+
27
+ /** @type {import('../interactions/SnapGrid.js').SnapGrid} */
28
+ #snapGrid;
29
+
30
+ /** @type {function} */
31
+ #getZoom;
32
+
33
+ /** @type {function} */
34
+ #setNodePosition;
35
+
36
+ /** @type {function} */
37
+ #animateNodeToPosition;
38
+
39
+ /** @type {function} */
40
+ #onNodeClick;
41
+
42
+ /** @type {Object} */
43
+ #canvas;
44
+
45
+ /** @type {function|null} */
46
+ #onSvgShapeReady = null;
47
+
48
+ /** @type {boolean} */
49
+ #readonly = false;
50
+
51
+ /** @type {boolean} */
52
+ #snapEnabled = false;
53
+
54
+ /** @type {HTMLElement} */
55
+ #nodesLayer;
56
+
57
+ /** @type {number} Z-index counter: increments on each select/drag */
58
+ #zCounter = 1;
59
+
60
+ /**
61
+ * @param {object} config
62
+ * @param {Map<string, HTMLElement>} config.nodeViews - shared Map
63
+ * @param {import('../core/Editor.js').NodeEditor} config.editor
64
+ * @param {import('../interactions/Selector.js').Selector} config.selector
65
+ * @param {import('../interactions/SnapGrid.js').SnapGrid} config.snapGrid
66
+ * @param {function} config.getZoom
67
+ * @param {function} config.setNodePosition
68
+ * @param {function} config.animateNodeToPosition
69
+ * @param {function} config.onNodeClick
70
+ * @param {HTMLElement} config.nodesLayer
71
+ * @param {Object} config.canvas - NodeCanvas reference for socket registration
72
+ */
73
+ constructor({ nodeViews, editor, selector, snapGrid, getZoom, setNodePosition, animateNodeToPosition, onNodeClick, nodesLayer, canvas, onSvgShapeReady }) {
74
+ this.#nodeViews = nodeViews;
75
+ this.#editor = editor;
76
+ this.#selector = selector;
77
+ this.#snapGrid = snapGrid;
78
+ this.#getZoom = getZoom;
79
+ this.#setNodePosition = setNodePosition;
80
+ this.#animateNodeToPosition = animateNodeToPosition;
81
+ this.#onNodeClick = onNodeClick;
82
+ this.#nodesLayer = nodesLayer;
83
+ this.#canvas = canvas;
84
+ this.#onSvgShapeReady = onSvgShapeReady || null;
85
+ }
86
+
87
+ /** @param {boolean} readonly */
88
+ setReadonly(readonly) {
89
+ this.#readonly = readonly;
90
+ }
91
+
92
+ /** @param {boolean} enabled */
93
+ setSnapEnabled(enabled) {
94
+ this.#snapEnabled = enabled;
95
+ }
96
+
97
+ /**
98
+ * Create and append multiple node views in a single DOM batch.
99
+ * This prevents layout thrashing and O(N) mutation overhead during graph inflation.
100
+ * @param {import('../core/Node.js').Node[]} nodes
101
+ */
102
+ addViews(nodes) {
103
+ if (!nodes || nodes.length === 0) return;
104
+
105
+ const fragment = document.createDocumentFragment();
106
+
107
+ // 1. Create all elements and bind them (no DOM append yet)
108
+ for (const node of nodes) {
109
+ const el = this.#createNodeElement(node);
110
+ fragment.appendChild(el);
111
+ this.#nodeViews.set(node.id, el);
112
+ }
113
+
114
+ // 2. Single batch insert into live DOM
115
+ this.#nodesLayer.appendChild(fragment);
116
+
117
+ // 3. Post-processing (SVG injection, preview canvas) requires elements to be in DOM
118
+ for (const node of nodes) {
119
+ const el = this.#nodeViews.get(node.id);
120
+ if (el) this.#postProcessNodeView(node, el);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Create a graph-node element for a Node
126
+ * @param {import('../core/Node.js').Node} node
127
+ */
128
+ addView(node) {
129
+ const el = this.#createNodeElement(node);
130
+ this.#nodesLayer.appendChild(el);
131
+ this.#nodeViews.set(node.id, el);
132
+ this.#postProcessNodeView(node, el);
133
+ }
134
+
135
+ /**
136
+ * Creates the HTMLElement for a node with its drag behavior initialized.
137
+ * Does NOT append to the live DOM layer.
138
+ * @private
139
+ * @param {import('../core/Node.js').Node} node
140
+ * @returns {HTMLElement}
141
+ */
142
+ #createNodeElement(node) {
143
+ const el = document.createElement('graph-node');
144
+ el.style.position = 'absolute';
145
+ el.style.transform = 'translate(0px, 0px)';
146
+ el._position = { x: 0, y: 0 };
147
+ el._nodeData = node;
148
+ el.setAttribute('node-id', node.id);
149
+ el.setAttribute('node-label', node.label);
150
+ el.setAttribute('node-category', node.category);
151
+ el.setAttribute('node-shape', node.shape);
152
+ el.setAttribute('node-type', node.type || 'default');
153
+ el._canvas = this.#canvas;
154
+
155
+ const drag = new Drag();
156
+ let dragStart = null;
157
+
158
+ drag.initialize(
159
+ el,
160
+ {
161
+ getPosition: () => el._position,
162
+ getZoom: this.#getZoom,
163
+ },
164
+ {
165
+ shouldStart: (e) => {
166
+ // SVG shapes: only start drag if click is inside the SVG path
167
+ const svgPath = el.querySelector('svg > path');
168
+ if (!svgPath) return true; // not an SVG shape node
169
+ const svg = svgPath.ownerSVGElement;
170
+ const rect = svg.getBoundingClientRect();
171
+ const vb = svg.viewBox.baseVal;
172
+ // Convert page coords to SVG viewBox coords
173
+ const sx = (e.clientX - rect.left) / rect.width * vb.width + vb.x;
174
+ const sy = (e.clientY - rect.top) / rect.height * vb.height + vb.y;
175
+ const pt = new DOMPoint(sx, sy);
176
+ return svgPath.isPointInFill(pt);
177
+ },
178
+ onStart: (e) => {
179
+ dragStart = { x: e.pageX, y: e.pageY };
180
+ this.#autoSelectOnDragStart(node.id, e);
181
+ this.#captureDragStartPositions();
182
+ this.#bringToFront(node.id);
183
+ this.#applyLift(el);
184
+ this.#editor.emit('nodepicked', node);
185
+ },
186
+ onTranslate: (x, y) => {
187
+ this.#handleGroupTranslate(node.id, el, x, y);
188
+ },
189
+ onDrop: (e) => {
190
+ this.#handleDrop(node.id, el, e, dragStart);
191
+ dragStart = null;
192
+ },
193
+ }
194
+ );
195
+ el._drag = drag;
196
+
197
+ return el;
198
+ }
199
+
200
+ /**
201
+ * Applies SVG shaping or subgraph previews after the element is in the live DOM.
202
+ * @private
203
+ * @param {import('../core/Node.js').Node} node
204
+ * @param {HTMLElement} el
205
+ */
206
+ #postProcessNodeView(node, el) {
207
+ // Apply shape visuals: SVG background layer instead of clip-path
208
+ // Clip-path clips content (labels, ports). SVG bg preserves them.
209
+ const shape = getShape(node.shape);
210
+ if (shape && shape.pathData) {
211
+ // Set explicit element dimensions to match SVG viewBox aspect ratio
212
+ // This ensures correct proportions and reliable offsetWidth/Height
213
+ const vb = shape.viewBox.split(' ').map(Number);
214
+ const vbW = vb[2];
215
+ const vbH = vb[3];
216
+ const baseSize = 120; // base dimension
217
+ const aspect = vbW / vbH;
218
+ const nodeW = aspect >= 1 ? baseSize : Math.round(baseSize * aspect);
219
+ const nodeH = aspect >= 1 ? Math.round(baseSize / aspect) : baseSize;
220
+ el.style.width = nodeW + 'px';
221
+ el.style.height = nodeH + 'px';
222
+ el.style.minWidth = nodeW + 'px';
223
+ el.style.minHeight = nodeH + 'px';
224
+ }
225
+
226
+ requestAnimationFrame(() => {
227
+ if (shape && shape.pathData) {
228
+ const size = { width: el.offsetWidth, height: el.offsetHeight };
229
+
230
+ // 1. Inject SVG background — element is properly proportioned
231
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
232
+ svg.setAttribute('viewBox', shape.viewBox);
233
+ svg.setAttribute('preserveAspectRatio', 'none');
234
+ svg.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:0;overflow:visible;';
235
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
236
+ path.setAttribute('d', shape.pathData);
237
+ path.setAttribute('fill', `var(--sn-shape-${shape.name}-fill, var(--sn-shape-fill, var(--sn-node-bg, #16213e)))`);
238
+ path.setAttribute('stroke', `var(--sn-shape-${shape.name}-stroke, var(--sn-shape-stroke, var(--sn-node-border, #2a2a4a)))`);
239
+ path.setAttribute('stroke-width', 'var(--sn-shape-stroke-width, 0.4)');
240
+ path.setAttribute('stroke-linejoin', 'round');
241
+ svg.appendChild(path);
242
+ el.prepend(svg);
243
+ el.setAttribute('data-svg-shape', shape.name);
244
+
245
+ // Make node background transparent — SVG provides the shape
246
+ el.style.background = 'transparent';
247
+ el.style.border = 'none';
248
+ el.style.boxShadow = 'none';
249
+ el.style.borderRadius = '0';
250
+ el.style.overflow = 'visible';
251
+
252
+ // Elevate content above SVG layer
253
+ for (const child of el.children) {
254
+ if (child !== svg) child.style.position = 'relative';
255
+ }
256
+
257
+ // Watermark icon — large pale category icon centered inside shape
258
+ const iconEl = el.querySelector('.sn-node-icon');
259
+ if (iconEl) {
260
+ const watermark = document.createElement('span');
261
+ watermark.className = 'sn-shape-watermark material-symbols-outlined';
262
+ watermark.textContent = iconEl.textContent;
263
+ el.appendChild(watermark);
264
+ }
265
+
266
+ // Notify canvas to render free dots for this SVG node
267
+ if (this.#onSvgShapeReady) this.#onSvgShapeReady(node.id);
268
+
269
+
270
+ } else if (shape) {
271
+ // Standard shapes: apply border-radius
272
+ const size = { width: el.offsetWidth || 180, height: el.offsetHeight || 60 };
273
+ const radius = shape.getBorderRadius(size);
274
+ if (radius && radius !== 'var(--sn-node-radius, 10px)') {
275
+ el.style.borderRadius = radius;
276
+ }
277
+ }
278
+ });
279
+
280
+ // Subgraph preview canvas — inject DOM element synchronously so
281
+ // measureNodeSizes() includes the 80px canvas in offsetHeight.
282
+ // Only the drawing is deferred to rAF (needs inner editor data).
283
+ if (node._isSubgraph) {
284
+ const body = el.querySelector('.sn-node-body');
285
+ if (body) {
286
+ const canvas = document.createElement('canvas');
287
+ canvas.className = 'sn-subgraph-preview';
288
+ canvas.width = 200;
289
+ canvas.height = 80;
290
+ body.appendChild(canvas);
291
+ el._previewCanvas = canvas;
292
+ requestAnimationFrame(() => {
293
+ this.#initSubgraphPreview(el, node, canvas);
294
+ });
295
+ }
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Remove a graph-node element
301
+ * @param {import('../core/Node.js').Node} node
302
+ */
303
+ removeView(node) {
304
+ const el = this.#nodeViews.get(node.id);
305
+ if (!el) return;
306
+ if (el._previewRaf) clearTimeout(el._previewRaf);
307
+ el._previewRaf = null;
308
+ if (el._drag) el._drag.destroy();
309
+ animateOut(el);
310
+ this.#nodeViews.delete(node.id);
311
+ this.#selector.getSelectedNodes().delete(node.id);
312
+ }
313
+
314
+ /**
315
+ * Remove a node view instantly (no animation) for virtualization demote.
316
+ * Returns captured position/size for phantom conversion.
317
+ * @param {string} nodeId
318
+ * @returns {{ x: number, y: number, w: number, h: number } | null}
319
+ */
320
+ removeViewInstant(nodeId) {
321
+ const el = this.#nodeViews.get(nodeId);
322
+ if (!el) return null;
323
+ const pos = el._position || { x: 0, y: 0 };
324
+ const w = el._cachedW || el.offsetWidth || 180;
325
+ const h = el._cachedH || el.offsetHeight || 60;
326
+ if (el._previewRaf) clearTimeout(el._previewRaf);
327
+ if (el._drag) el._drag.destroy();
328
+ el.remove();
329
+ this.#nodeViews.delete(nodeId);
330
+ this.#selector.getSelectedNodes().delete(nodeId);
331
+ return { x: pos.x, y: pos.y, w, h };
332
+ }
333
+
334
+ // --- Private helpers ---
335
+
336
+
337
+
338
+
339
+ #autoSelectOnDragStart(nodeId, e) {
340
+ if (!this.#selector.isNodeSelected(nodeId)) {
341
+ const accumulate = e.ctrlKey || e.metaKey;
342
+ this.#selector.selectNode(nodeId, accumulate);
343
+ }
344
+ this.#bringToFront(nodeId);
345
+ }
346
+
347
+ /**
348
+ * Bring a node to front by setting highest z-index
349
+ * @param {string} nodeId
350
+ */
351
+ #bringToFront(nodeId) {
352
+ const el = this.#nodeViews.get(nodeId);
353
+ if (el) {
354
+ el.style.zIndex = ++this.#zCounter;
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Apply lift effect: scale up + shadow + parallax offset
360
+ * @param {HTMLElement} el
361
+ */
362
+ #applyLift(el) {
363
+ el.classList.add('sn-node-lifted');
364
+ }
365
+
366
+ /**
367
+ * Remove lift effect on drop
368
+ * @param {HTMLElement} el
369
+ */
370
+ #removeLift(el) {
371
+ el.classList.remove('sn-node-lifted');
372
+ }
373
+
374
+ #captureDragStartPositions() {
375
+ const selected = this.#selector.getSelectedNodes();
376
+ for (const id of selected) {
377
+ const nodeEl = this.#nodeViews.get(id);
378
+ if (nodeEl) nodeEl._dragStartPos = { ...nodeEl._position };
379
+ }
380
+ }
381
+
382
+ #handleGroupTranslate(nodeId, el, x, y) {
383
+ let finalX = x;
384
+ let finalY = y;
385
+
386
+ if (this.#snapEnabled && this.#snapGrid.isDynamic) {
387
+ const snapped = this.#snapGrid.snap(x, y);
388
+ finalX = snapped.x;
389
+ finalY = snapped.y;
390
+ }
391
+
392
+ const prev = el._dragStartPos || el._position;
393
+ const dx = finalX - prev.x;
394
+ const dy = finalY - prev.y;
395
+
396
+ const selected = this.#selector.getSelectedNodes();
397
+ if (selected.size > 1 && selected.has(nodeId)) {
398
+ for (const id of selected) {
399
+ const nodeEl = this.#nodeViews.get(id);
400
+ if (!nodeEl?._dragStartPos) continue;
401
+ let nx = nodeEl._dragStartPos.x + dx;
402
+ let ny = nodeEl._dragStartPos.y + dy;
403
+ if (this.#snapEnabled && this.#snapGrid.isDynamic) {
404
+ const snapped = this.#snapGrid.snap(nx, ny);
405
+ nx = snapped.x;
406
+ ny = snapped.y;
407
+ }
408
+ this.#setNodePosition(id, nx, ny);
409
+ }
410
+ } else {
411
+ this.#setNodePosition(nodeId, finalX, finalY);
412
+ }
413
+
414
+ this.#editor.emit('nodetranslated', { id: nodeId, position: { x: finalX, y: finalY } });
415
+ }
416
+
417
+ #handleDrop(nodeId, el, e, dragStart) {
418
+ // Static snap on drop
419
+ if (this.#snapEnabled && !this.#snapGrid.isDynamic) {
420
+ const selected = this.#selector.getSelectedNodes();
421
+ const targets = selected.size > 0 && selected.has(nodeId) ? selected : new Set([nodeId]);
422
+ for (const id of targets) {
423
+ const nodeEl = this.#nodeViews.get(id);
424
+ if (!nodeEl) continue;
425
+ const snapped = this.#snapGrid.snap(nodeEl._position.x, nodeEl._position.y);
426
+ this.#animateNodeToPosition(id, snapped.x, snapped.y);
427
+ }
428
+ }
429
+
430
+ // Clean up start positions
431
+ for (const [, nodeEl] of this.#nodeViews) {
432
+ delete nodeEl._dragStartPos;
433
+ }
434
+
435
+ // Remove lift effect
436
+ this.#removeLift(el);
437
+
438
+ // Click vs drag detection
439
+ if (dragStart && e && Selector.isTwitch(dragStart, { x: e.pageX, y: e.pageY })) {
440
+ this.#onNodeClick(nodeId, e);
441
+ }
442
+
443
+ this.#editor.emit('nodedragged', { id: nodeId });
444
+ }
445
+
446
+ /**
447
+ * Initialize subgraph preview canvas inside a graph-node
448
+ * @param {HTMLElement} el - graph-node element
449
+ * @param {import('../core/SubgraphNode.js').SubgraphNode} node
450
+ * @param {HTMLCanvasElement} canvas - pre-created canvas element (already in DOM)
451
+ */
452
+ #initSubgraphPreview(el, node, canvas) {
453
+ const ctx = canvas.getContext('2d');
454
+
455
+ const drawPreview = () => {
456
+ if (!el.isConnected) return;
457
+
458
+ const w = canvas.width;
459
+ const h = canvas.height;
460
+ ctx.clearRect(0, 0, w, h);
461
+
462
+ const innerEditor = node.innerEditor;
463
+ if (!innerEditor) return;
464
+
465
+ const nodes = innerEditor.getNodes();
466
+ if (nodes.length === 0) return;
467
+
468
+ // Get positions (from saved or auto-grid)
469
+ const positions = node.innerPositions;
470
+ const nodeRects = [];
471
+
472
+ for (const n of nodes) {
473
+ const pos = positions[n.id];
474
+ const x = pos ? pos.x : 0;
475
+ const y = pos ? pos.y : 0;
476
+ nodeRects.push({ x, y, w: 160, h: 60, id: n.id });
477
+ }
478
+
479
+ // Calculate bounds
480
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
481
+ for (const r of nodeRects) {
482
+ minX = Math.min(minX, r.x);
483
+ minY = Math.min(minY, r.y);
484
+ maxX = Math.max(maxX, r.x + r.w);
485
+ maxY = Math.max(maxY, r.y + r.h);
486
+ }
487
+
488
+ const pad = 30;
489
+ minX -= pad; minY -= pad;
490
+ maxX += pad; maxY += pad;
491
+
492
+ const graphW = maxX - minX;
493
+ const graphH = maxY - minY;
494
+ const scale = Math.min(w / graphW, h / graphH);
495
+ const offsetX = (w - graphW * scale) / 2;
496
+ const offsetY = (h - graphH * scale) / 2;
497
+
498
+ // Flow state map: nodeId -> 'processing' | 'completed'
499
+ const states = el._innerFlowStates || {};
500
+
501
+ // Draw connections as lines
502
+ const conns = innerEditor.getConnections();
503
+ for (const conn of conns) {
504
+ const src = nodeRects.find(r => r.id === conn.from);
505
+ const tgt = nodeRects.find(r => r.id === conn.to);
506
+ if (src && tgt) {
507
+ const sx = (src.x + src.w - minX) * scale + offsetX;
508
+ const sy = (src.y + src.h / 2 - minY) * scale + offsetY;
509
+ const tx = (tgt.x - minX) * scale + offsetX;
510
+ const ty = (tgt.y + tgt.h / 2 - minY) * scale + offsetY;
511
+
512
+ // Flowing connection: source completed
513
+ const srcState = states[conn.from];
514
+ if (srcState === 'completed') {
515
+ ctx.strokeStyle = 'rgba(92, 216, 122, 0.5)';
516
+ ctx.lineWidth = 2;
517
+ } else {
518
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)';
519
+ ctx.lineWidth = 1;
520
+ }
521
+
522
+ ctx.beginPath();
523
+ ctx.moveTo(sx, sy);
524
+ ctx.lineTo(tx, ty);
525
+ ctx.stroke();
526
+ }
527
+ }
528
+
529
+ // Draw node rectangles with flow state
530
+ for (const r of nodeRects) {
531
+ const rx = (r.x - minX) * scale + offsetX;
532
+ const ry = (r.y - minY) * scale + offsetY;
533
+ const rw = r.w * scale;
534
+ const rh = r.h * scale;
535
+ const state = states[r.id];
536
+ const radius = 4;
537
+
538
+ // Rounded rect helper
539
+ ctx.beginPath();
540
+ ctx.moveTo(rx + radius, ry);
541
+ ctx.lineTo(rx + rw - radius, ry);
542
+ ctx.quadraticCurveTo(rx + rw, ry, rx + rw, ry + radius);
543
+ ctx.lineTo(rx + rw, ry + rh - radius);
544
+ ctx.quadraticCurveTo(rx + rw, ry + rh, rx + rw - radius, ry + rh);
545
+ ctx.lineTo(rx + radius, ry + rh);
546
+ ctx.quadraticCurveTo(rx, ry + rh, rx, ry + rh - radius);
547
+ ctx.lineTo(rx, ry + radius);
548
+ ctx.quadraticCurveTo(rx, ry, rx + radius, ry);
549
+ ctx.closePath();
550
+
551
+ if (state === 'processing') {
552
+ ctx.fillStyle = 'rgba(74, 158, 255, 0.25)';
553
+ ctx.fill();
554
+ ctx.strokeStyle = 'rgba(74, 158, 255, 0.8)';
555
+ ctx.lineWidth = 1.5;
556
+ ctx.stroke();
557
+ // Glow effect
558
+ ctx.shadowColor = 'rgba(74, 158, 255, 0.6)';
559
+ ctx.shadowBlur = 8;
560
+ ctx.stroke();
561
+ ctx.shadowBlur = 0;
562
+ } else if (state === 'completed') {
563
+ ctx.fillStyle = 'rgba(92, 216, 122, 0.2)';
564
+ ctx.fill();
565
+ ctx.strokeStyle = 'rgba(92, 216, 122, 0.7)';
566
+ ctx.lineWidth = 1;
567
+ ctx.stroke();
568
+ } else {
569
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.08)';
570
+ ctx.fill();
571
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
572
+ ctx.lineWidth = 0.5;
573
+ ctx.stroke();
574
+ }
575
+ }
576
+ };
577
+
578
+ // Expose redraw for external triggering (FlowSimulator)
579
+ el._redrawPreview = drawPreview;
580
+
581
+ // Draw once. Re-draw on demand via el._redrawPreview().
582
+ drawPreview();
583
+ }
584
+ }
@@ -0,0 +1,131 @@
1
+ export class PinExpansion {
2
+ /** @type {import('./NodeCanvas/NodeCanvas.js').NodeCanvas} */
3
+ #canvas;
4
+
5
+ /** @type {Map<string, Array<object>>} Cache of pins per nodeId */
6
+ #pinCache = new Map();
7
+
8
+ /** @type {Function} Callback when a pin is clicked */
9
+ #onPinClick;
10
+
11
+ /**
12
+ * @param {import('./NodeCanvas/NodeCanvas.js').NodeCanvas} canvas
13
+ * @param {object} config
14
+ * @param {Function} [config.onPinClick]
15
+ */
16
+ constructor(canvas, { onPinClick } = {}) {
17
+ this.#canvas = canvas;
18
+ this.#onPinClick = onPinClick || (() => {});
19
+ }
20
+
21
+ /**
22
+ * Add pins data for a specific node
23
+ * @param {string} nodeId
24
+ * @param {Array<object>} pins
25
+ */
26
+ setPins(nodeId, pins) {
27
+ if (pins && pins.length > 0) {
28
+ this.#pinCache.set(nodeId, pins);
29
+ } else {
30
+ this.#pinCache.delete(nodeId);
31
+ }
32
+ }
33
+
34
+ clearPins() {
35
+ this.#pinCache.clear();
36
+ }
37
+
38
+ /**
39
+ * Remove pins and overlay for a node
40
+ * @param {string} nodeId
41
+ */
42
+ removePins(nodeId) {
43
+ this.#pinCache.delete(nodeId);
44
+ const el = this.#canvas.getNodeView?.(nodeId);
45
+ if (!el) return;
46
+ const overlay = el.querySelector('.pcb-pin-overlay');
47
+ if (overlay) overlay.remove();
48
+ }
49
+
50
+ /**
51
+ * Apply LOD state to render or hide pins
52
+ * @param {'expanded'|'collapsed'} lod
53
+ */
54
+ applyLOD(lod) {
55
+ if (!this.#canvas) return;
56
+
57
+ for (const [nodeId, pins] of this.#pinCache) {
58
+ const el = this.#canvas.getNodeView?.(nodeId);
59
+ if (!el) continue;
60
+
61
+ if (lod === 'expanded') {
62
+ this.#renderPinsForNode(el, pins);
63
+ } else {
64
+ const overlay = el.querySelector('.pcb-pin-overlay');
65
+ if (overlay) overlay.removeAttribute('data-visible');
66
+ }
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Render pin labels around a node element's border
72
+ * @param {HTMLElement} el
73
+ * @param {Array<object>} pins
74
+ */
75
+ #renderPinsForNode(el, pins) {
76
+ if (!pins || pins.length === 0) return;
77
+
78
+ // Create or reuse pin overlay
79
+ let overlay = el.querySelector('.pcb-pin-overlay');
80
+ if (!overlay) {
81
+ overlay = document.createElement('div');
82
+ overlay.className = 'pcb-pin-overlay';
83
+ el.appendChild(overlay);
84
+ }
85
+
86
+ // Prepare pins if they are empty
87
+ if (overlay.children.length === 0) {
88
+ const maxPins = Math.min(pins.length, 12);
89
+ const half = Math.ceil(maxPins / 2);
90
+ const nodeId = el.getAttribute('node-id');
91
+
92
+ const createPinEl = (pin, side, yPct) => {
93
+ const pinEl = document.createElement('span');
94
+ pinEl.className = 'pcb-pin';
95
+ pinEl.setAttribute('data-side', side);
96
+ if (pin.kind) pinEl.setAttribute('data-kind', pin.kind);
97
+
98
+ const suffix = pin.line ? ` :${pin.line}` : '';
99
+ const label = pin.label || pin.name || '';
100
+ pinEl.textContent = label + suffix;
101
+ pinEl.style.top = `${yPct}%`;
102
+
103
+ if (pin.interactable !== false) {
104
+ pinEl.style.cursor = 'pointer';
105
+ pinEl.title = pin.tooltip || (pin.line ? `${pin.file || ''}:${pin.line}` : (pin.file || ''));
106
+ pinEl.addEventListener('click', (e) => {
107
+ e.stopPropagation();
108
+ this.#onPinClick(pin, nodeId);
109
+ });
110
+ }
111
+
112
+ return pinEl;
113
+ };
114
+
115
+ // Right side: first half
116
+ for (let i = 0; i < half; i++) {
117
+ const yPct = ((i + 1) / (half + 1)) * 100;
118
+ overlay.appendChild(createPinEl(pins[i], 'right', yPct));
119
+ }
120
+
121
+ // Left side: remaining
122
+ for (let i = half; i < maxPins; i++) {
123
+ const yPct = ((i - half + 1) / (maxPins - half + 1)) * 100;
124
+ overlay.appendChild(createPinEl(pins[i], 'left', yPct));
125
+ }
126
+ }
127
+
128
+ // Animate in
129
+ requestAnimationFrame(() => overlay.setAttribute('data-visible', ''));
130
+ }
131
+ }