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.
- package/ARCHITECTURE.md +81 -0
- package/CHANGELOG.md +57 -0
- package/README.md +9 -4
- package/package.json +6 -13
- package/src/compact/expand.js +1 -1
- package/src/core/graph-builder.js +2 -2
- package/src/core/parser.js +2 -2
- package/src/network/server.js +1 -2
- package/vendor/symbiote-node/CHANGELOG.md +31 -0
- package/vendor/symbiote-node/LICENSE +21 -0
- package/vendor/symbiote-node/README.md +206 -0
- package/vendor/symbiote-node/canvas/AutoLayout.js +725 -0
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.css.js +73 -0
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.js +93 -0
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.tpl.js +9 -0
- package/vendor/symbiote-node/canvas/CanvasConnectionRenderer.js +962 -0
- package/vendor/symbiote-node/canvas/ConnectionRenderer.js +1468 -0
- package/vendor/symbiote-node/canvas/FlowSimulator.js +323 -0
- package/vendor/symbiote-node/canvas/ForceLayout.js +189 -0
- package/vendor/symbiote-node/canvas/ForceWorker.js +1325 -0
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.css.js +97 -0
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.js +176 -0
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.tpl.js +12 -0
- package/vendor/symbiote-node/canvas/LODManager.js +88 -0
- package/vendor/symbiote-node/canvas/Minimap/Minimap.css.js +71 -0
- package/vendor/symbiote-node/canvas/Minimap/Minimap.js +207 -0
- package/vendor/symbiote-node/canvas/Minimap/Minimap.tpl.js +9 -0
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.css.js +261 -0
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.js +1840 -0
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.tpl.js +22 -0
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.css.js +97 -0
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.js +132 -0
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.tpl.js +21 -0
- package/vendor/symbiote-node/canvas/NodeViewManager.js +584 -0
- package/vendor/symbiote-node/canvas/PinExpansion.js +131 -0
- package/vendor/symbiote-node/canvas/PseudoConnection.js +80 -0
- package/vendor/symbiote-node/canvas/SubgraphManager.js +201 -0
- package/vendor/symbiote-node/canvas/SubgraphRouter.js +443 -0
- package/vendor/symbiote-node/canvas/ViewportActions.js +446 -0
- package/vendor/symbiote-node/core/Connection.js +45 -0
- package/vendor/symbiote-node/core/Editor.js +451 -0
- package/vendor/symbiote-node/core/Frame.js +31 -0
- package/vendor/symbiote-node/core/GraphMermaid.js +348 -0
- package/vendor/symbiote-node/core/GraphText.js +210 -0
- package/vendor/symbiote-node/core/Node.js +143 -0
- package/vendor/symbiote-node/core/Portal.js +104 -0
- package/vendor/symbiote-node/core/Socket.js +185 -0
- package/vendor/symbiote-node/core/SubgraphNode.js +125 -0
- package/vendor/symbiote-node/index.js +103 -0
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.css.js +361 -0
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.js +332 -0
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.tpl.js +96 -0
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.css.js +104 -0
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.js +133 -0
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.tpl.js +33 -0
- package/vendor/symbiote-node/interactions/ConnectFlow.js +307 -0
- package/vendor/symbiote-node/interactions/Drag.js +102 -0
- package/vendor/symbiote-node/interactions/Selector.js +132 -0
- package/vendor/symbiote-node/interactions/SnapGrid.js +65 -0
- package/vendor/symbiote-node/interactions/Zoom.js +140 -0
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.css.js +88 -0
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.js +254 -0
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.tpl.js +11 -0
- package/vendor/symbiote-node/layout/Layout/Layout.css.js +88 -0
- package/vendor/symbiote-node/layout/Layout/Layout.js +622 -0
- package/vendor/symbiote-node/layout/Layout/Layout.tpl.js +25 -0
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.css.js +293 -0
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.js +467 -0
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.tpl.js +33 -0
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.css.js +46 -0
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.js +102 -0
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.tpl.js +6 -0
- package/vendor/symbiote-node/layout/LayoutRouter/LayoutRouter.js +156 -0
- package/vendor/symbiote-node/layout/LayoutRouter/routerSync.js +250 -0
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.css.js +379 -0
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.js +263 -0
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.tpl.js +20 -0
- package/vendor/symbiote-node/layout/LayoutSidebar/SidebarSection.js +183 -0
- package/vendor/symbiote-node/layout/LayoutTree.js +246 -0
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.css.js +43 -0
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.js +89 -0
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.tpl.js +14 -0
- package/vendor/symbiote-node/layout/index.js +16 -0
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.css.js +61 -0
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.js +79 -0
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.tpl.js +19 -0
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.css.js +41 -0
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.js +24 -0
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.tpl.js +16 -0
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.css.js +65 -0
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.js +29 -0
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.tpl.js +13 -0
- package/vendor/symbiote-node/node/GraphNode/GraphNode.css.js +683 -0
- package/vendor/symbiote-node/node/GraphNode/GraphNode.js +92 -0
- package/vendor/symbiote-node/node/GraphNode/GraphNode.tpl.js +17 -0
- package/vendor/symbiote-node/node/NodeSocket/NodeSocket.js +25 -0
- package/vendor/symbiote-node/node/NodeSocket/NodeSocket.tpl.js +7 -0
- package/vendor/symbiote-node/node/PortItem/PortItem.css.js +90 -0
- package/vendor/symbiote-node/node/PortItem/PortItem.js +87 -0
- package/vendor/symbiote-node/node/PortItem/PortItem.tpl.js +10 -0
- package/vendor/symbiote-node/package.json +59 -0
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.css.js +143 -0
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.js +131 -0
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.tpl.js +16 -0
- package/vendor/symbiote-node/plugins/History.js +384 -0
- package/vendor/symbiote-node/plugins/Readonly.js +59 -0
- package/vendor/symbiote-node/shapes/CircleShape.js +80 -0
- package/vendor/symbiote-node/shapes/CommentShape.js +35 -0
- package/vendor/symbiote-node/shapes/DiamondShape.js +115 -0
- package/vendor/symbiote-node/shapes/NodeShape.js +80 -0
- package/vendor/symbiote-node/shapes/PillShape.js +91 -0
- package/vendor/symbiote-node/shapes/RectShape.js +72 -0
- package/vendor/symbiote-node/shapes/SVGShape.js +494 -0
- package/vendor/symbiote-node/shapes/index.js +53 -0
- package/vendor/symbiote-node/themes/Palette.js +32 -0
- package/vendor/symbiote-node/themes/Skin.js +113 -0
- package/vendor/symbiote-node/themes/Theme.js +84 -0
- package/vendor/symbiote-node/themes/carbon.js +137 -0
- package/vendor/symbiote-node/themes/dark.js +137 -0
- package/vendor/symbiote-node/themes/ebook.js +138 -0
- package/vendor/symbiote-node/themes/grey.js +137 -0
- package/vendor/symbiote-node/themes/light.js +137 -0
- package/vendor/symbiote-node/themes/neon.js +138 -0
- package/vendor/symbiote-node/themes/pcb.js +273 -0
- package/vendor/symbiote-node/themes/synthwave.js +137 -0
- package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.css.js +86 -0
- package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.js +128 -0
- package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.tpl.js +29 -0
- package/web/app.js +6 -5
- package/web/components/canvas-graph.js +1666 -0
- package/web/components/event-feed/CodeWidget.js +32 -0
- package/web/components/event-feed/EventWidget.js +97 -0
- package/web/components/event-feed/ListWidget.js +57 -0
- package/web/components/event-feed/MiniGraphWidget.js +69 -0
- package/web/dashboard.js +1 -1
- package/web/index.html +4 -0
- package/web/panels/ActionBoard/ActionBoard.js +1 -1
- package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -1
- package/web/panels/code-viewer.js +50 -15
- package/web/panels/dep-graph.js +2712 -7
- package/web/panels/file-tree.js +5 -2
- package/web/panels/live-monitor.js +75 -3
- package/web/style.css +33 -0
- package/docs/img/explorer-compact.jpg +0 -0
- package/docs/img/explorer-expanded.jpg +0 -0
- package/src/.contextignore +0 -22
- package/src/.project-graph-cache.json +0 -1
- package/src/compact/.project-graph-cache.json +0 -1
- package/web/.project-graph-cache.json +0 -1
- 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
|
+
}
|