@synergenius/flow-weaver 0.9.4 → 0.10.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/dist/cli/flow-weaver.mjs +402 -100
- package/dist/diagram/geometry.js +5 -5
- package/dist/diagram/html-viewer.d.ts +2 -1
- package/dist/diagram/html-viewer.js +401 -99
- package/dist/diagram/renderer.js +8 -0
- package/package.json +1 -1
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTML Viewer — wraps an SVG diagram in a self-contained interactive HTML page.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* The SVG element itself is the canvas: zoom and pan are driven by viewBox
|
|
5
|
+
* manipulation, so nodes can be dragged anywhere without hitting a boundary.
|
|
5
6
|
* No external dependencies — works standalone or inside an iframe.
|
|
6
7
|
*/
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Extract inner SVG content and viewBox from the rendered SVG string.
|
|
10
|
+
* Strips backgrounds, dot grid, and watermark (the viewer provides its own).
|
|
11
|
+
*/
|
|
12
|
+
function prepareSvgContent(svg) {
|
|
13
|
+
const vbMatch = svg.match(/viewBox="([^"]+)"/);
|
|
14
|
+
const viewBox = vbMatch ? vbMatch[1] : '0 0 800 600';
|
|
15
|
+
// Strip the outer <svg> wrapper — we inline content into the canvas SVG
|
|
16
|
+
let inner = svg.replace(/<svg[^>]*>\n?/, '').replace(/<\/svg>\s*$/, '');
|
|
17
|
+
// Remove dot-grid pattern and its fill rect (viewer uses its own)
|
|
18
|
+
inner = inner.replace(/<pattern\s+id="dot-grid"[^>]*>[\s\S]*?<\/pattern>/g, '');
|
|
19
|
+
inner = inner.replace(/<rect[^>]*fill="url\(#dot-grid\)"[^>]*\/>/g, '');
|
|
20
|
+
// Remove solid background rect (first rect after </defs>)
|
|
21
|
+
inner = inner.replace(/(<\/defs>\n)<rect[^>]*\/>\n/, '$1');
|
|
22
|
+
// Remove watermark (HTML viewer has its own branding badge)
|
|
23
|
+
inner = inner.replace(/<g opacity="0\.5">[\s\S]*?Flow Weaver<\/text>\s*<\/g>/, '');
|
|
24
|
+
return { inner, viewBox };
|
|
14
25
|
}
|
|
15
26
|
export function wrapSVGInHTML(svgContent, options = {}) {
|
|
16
27
|
const title = options.title ?? 'Workflow Diagram';
|
|
17
28
|
const theme = options.theme ?? 'dark';
|
|
18
|
-
const
|
|
29
|
+
const { inner, viewBox } = prepareSvgContent(svgContent);
|
|
19
30
|
const isDark = theme === 'dark';
|
|
20
31
|
const bg = isDark ? '#202139' : '#f6f7ff';
|
|
21
32
|
const dotColor = isDark ? 'rgba(142, 158, 255, 0.6)' : 'rgba(84, 104, 255, 0.6)';
|
|
@@ -25,6 +36,7 @@ export function wrapSVGInHTML(svgContent, options = {}) {
|
|
|
25
36
|
const textMed = isDark ? '#babac0' : '#606060';
|
|
26
37
|
const textLow = isDark ? '#767682' : '#999999';
|
|
27
38
|
const surfaceHigh = isDark ? '#313143' : '#f0f0f5';
|
|
39
|
+
const brandAccent = isDark ? '#8e9eff' : '#5468ff';
|
|
28
40
|
return `<!DOCTYPE html>
|
|
29
41
|
<html lang="en">
|
|
30
42
|
<head>
|
|
@@ -36,25 +48,16 @@ export function wrapSVGInHTML(svgContent, options = {}) {
|
|
|
36
48
|
|
|
37
49
|
body {
|
|
38
50
|
width: 100vw; height: 100vh; overflow: hidden;
|
|
39
|
-
background: ${bg};
|
|
40
51
|
font-family: Montserrat, 'Segoe UI', Roboto, sans-serif;
|
|
41
52
|
color: ${textHigh};
|
|
42
53
|
}
|
|
43
54
|
|
|
44
|
-
#
|
|
45
|
-
width: 100%; height: 100%;
|
|
46
|
-
|
|
55
|
+
#canvas {
|
|
56
|
+
display: block; width: 100%; height: 100%;
|
|
57
|
+
cursor: grab;
|
|
47
58
|
touch-action: none; user-select: none;
|
|
48
|
-
background-image: radial-gradient(circle, ${dotColor} 7.5%, transparent 7.5%);
|
|
49
|
-
background-size: 20px 20px;
|
|
50
|
-
}
|
|
51
|
-
#viewport.dragging { cursor: grabbing; }
|
|
52
|
-
|
|
53
|
-
#content {
|
|
54
|
-
transform-origin: 0 0;
|
|
55
|
-
will-change: transform;
|
|
56
59
|
}
|
|
57
|
-
#
|
|
60
|
+
#canvas.dragging { cursor: grabbing; }
|
|
58
61
|
|
|
59
62
|
/* Port labels: hidden by default, shown on node hover */
|
|
60
63
|
.nodes > g .port-label,
|
|
@@ -63,16 +66,36 @@ body {
|
|
|
63
66
|
opacity: 0; pointer-events: none;
|
|
64
67
|
transition: opacity 0.15s ease-in-out;
|
|
65
68
|
}
|
|
66
|
-
/* Show port labels for hovered node */
|
|
67
|
-
.nodes > g:hover ~ .show-port-labels .port-label,
|
|
68
|
-
.nodes > g:hover ~ .show-port-labels .port-type-label { opacity: 1; }
|
|
69
69
|
|
|
70
70
|
/* Connection hover & dimming (attribute selector covers both main and scope connections) */
|
|
71
71
|
path[data-source] { transition: opacity 0.2s ease, stroke-width 0.15s ease; }
|
|
72
72
|
path[data-source]:hover { stroke-width: 4; cursor: pointer; }
|
|
73
|
-
body.node-active path[data-source].dimmed
|
|
73
|
+
body.node-active path[data-source].dimmed,
|
|
74
|
+
body.port-active path[data-source].dimmed { opacity: 0.1; }
|
|
75
|
+
body.port-hovered path[data-source].dimmed { opacity: 0.25; }
|
|
76
|
+
|
|
77
|
+
/* Port circles are interactive */
|
|
78
|
+
circle[data-port-id] { cursor: pointer; }
|
|
79
|
+
circle[data-port-id]:hover { stroke-width: 3; filter: brightness(1.3); }
|
|
80
|
+
|
|
81
|
+
/* Port-click highlighting */
|
|
82
|
+
path[data-source].highlighted { opacity: 1; }
|
|
83
|
+
circle[data-port-id].port-selected { filter: drop-shadow(0 0 6px currentColor); stroke-width: 4; }
|
|
84
|
+
|
|
85
|
+
/* Node selection glow */
|
|
86
|
+
@keyframes select-pop {
|
|
87
|
+
0% { opacity: 0; stroke-width: 0; }
|
|
88
|
+
50% { opacity: 0.6; stroke-width: 12; }
|
|
89
|
+
100% { opacity: 0.35; stroke-width: 8; }
|
|
90
|
+
}
|
|
91
|
+
.node-glow { fill: none; pointer-events: none; animation: select-pop 0.3s ease-out forwards; }
|
|
92
|
+
|
|
93
|
+
/* Port hover path highlight */
|
|
94
|
+
path[data-source].port-hover { opacity: 1; }
|
|
74
95
|
|
|
75
|
-
/* Node hover glow */
|
|
96
|
+
/* Node hover glow + draggable cursor */
|
|
97
|
+
.nodes g[data-node-id] { cursor: grab; }
|
|
98
|
+
.nodes g[data-node-id]:active { cursor: grabbing; }
|
|
76
99
|
.nodes g[data-node-id]:hover > rect:first-of-type { filter: brightness(1.08); }
|
|
77
100
|
|
|
78
101
|
/* Zoom controls */
|
|
@@ -98,7 +121,7 @@ body.node-active path[data-source].dimmed { opacity: 0.15; }
|
|
|
98
121
|
|
|
99
122
|
/* Info panel */
|
|
100
123
|
#info-panel {
|
|
101
|
-
position: fixed; bottom:
|
|
124
|
+
position: fixed; bottom: 52px; left: 16px;
|
|
102
125
|
max-width: 320px; min-width: 200px;
|
|
103
126
|
background: ${surfaceMain}; border: 1px solid ${borderSubtle};
|
|
104
127
|
border-radius: 8px; padding: 12px 16px;
|
|
@@ -118,6 +141,19 @@ body.node-active path[data-source].dimmed { opacity: 0.15; }
|
|
|
118
141
|
#info-panel .port-list li { padding: 1px 0; }
|
|
119
142
|
#info-panel .port-list li::before { content: '\\2022'; margin-right: 6px; color: ${textLow}; }
|
|
120
143
|
|
|
144
|
+
/* Branding badge */
|
|
145
|
+
#branding {
|
|
146
|
+
position: fixed; bottom: 16px; left: 16px;
|
|
147
|
+
display: flex; align-items: center; gap: 6px;
|
|
148
|
+
background: ${surfaceMain}; border: 1px solid ${borderSubtle};
|
|
149
|
+
border-radius: 8px; padding: 6px 12px;
|
|
150
|
+
font-size: 12px; font-weight: 600; color: ${textMed};
|
|
151
|
+
text-decoration: none; z-index: 9;
|
|
152
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
153
|
+
transition: color 0.15s, border-color 0.15s;
|
|
154
|
+
}
|
|
155
|
+
#branding:hover { color: ${textHigh}; border-color: ${textLow}; }
|
|
156
|
+
|
|
121
157
|
/* Scroll hint */
|
|
122
158
|
#scroll-hint {
|
|
123
159
|
position: fixed; top: 50%; left: 50%;
|
|
@@ -137,9 +173,16 @@ body.node-active path[data-source].dimmed { opacity: 0.15; }
|
|
|
137
173
|
</style>
|
|
138
174
|
</head>
|
|
139
175
|
<body>
|
|
140
|
-
<
|
|
141
|
-
<
|
|
142
|
-
|
|
176
|
+
<svg id="canvas" xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}">
|
|
177
|
+
<defs>
|
|
178
|
+
<pattern id="viewer-dots" width="20" height="20" patternUnits="userSpaceOnUse">
|
|
179
|
+
<circle cx="10" cy="10" r="1.5" fill="${dotColor}" opacity="0.6"/>
|
|
180
|
+
</pattern>
|
|
181
|
+
</defs>
|
|
182
|
+
<rect x="-100000" y="-100000" width="200000" height="200000" fill="${bg}"/>
|
|
183
|
+
<rect x="-100000" y="-100000" width="200000" height="200000" fill="url(#viewer-dots)"/>
|
|
184
|
+
<g id="diagram">${inner}</g>
|
|
185
|
+
</svg>
|
|
143
186
|
<div id="controls">
|
|
144
187
|
<button class="ctrl-btn" id="btn-in" title="Zoom in" aria-label="Zoom in">+</button>
|
|
145
188
|
<span id="zoom-label">100%</span>
|
|
@@ -155,23 +198,33 @@ body.node-active path[data-source].dimmed { opacity: 0.15; }
|
|
|
155
198
|
<h3 id="info-title"></h3>
|
|
156
199
|
<div id="info-body"></div>
|
|
157
200
|
</div>
|
|
201
|
+
<a id="branding" href="https://flowweaver.ai" target="_blank" rel="noopener">
|
|
202
|
+
<svg width="16" height="16" viewBox="0 0 256 256" fill="none"><path d="M80 128C134 128 122 49 176 49" stroke="${brandAccent}" stroke-width="14" stroke-linecap="round"/><path d="M80 128C134 128 122 207 176 207" stroke="${brandAccent}" stroke-width="14" stroke-linecap="round"/><rect x="28" y="102" width="52" height="52" rx="10" stroke="${brandAccent}" stroke-width="14"/><rect x="176" y="23" width="52" height="52" rx="10" stroke="${brandAccent}" stroke-width="14"/><rect x="176" y="181" width="52" height="52" rx="10" stroke="${brandAccent}" stroke-width="14"/></svg>
|
|
203
|
+
<span>Flow Weaver</span>
|
|
204
|
+
</a>
|
|
158
205
|
<div id="scroll-hint">Use <kbd id="mod-key">Ctrl</kbd> + scroll to zoom</div>
|
|
159
206
|
<script>
|
|
160
207
|
(function() {
|
|
161
208
|
'use strict';
|
|
162
209
|
|
|
163
|
-
var MIN_ZOOM = 0.25, MAX_ZOOM = 3
|
|
164
|
-
var
|
|
165
|
-
var content = document.getElementById('
|
|
210
|
+
var MIN_ZOOM = 0.25, MAX_ZOOM = 3;
|
|
211
|
+
var canvas = document.getElementById('canvas');
|
|
212
|
+
var content = document.getElementById('diagram');
|
|
166
213
|
var zoomLabel = document.getElementById('zoom-label');
|
|
167
214
|
var infoPanel = document.getElementById('info-panel');
|
|
168
215
|
var infoTitle = document.getElementById('info-title');
|
|
169
216
|
var infoBody = document.getElementById('info-body');
|
|
170
217
|
var scrollHint = document.getElementById('scroll-hint');
|
|
171
218
|
|
|
172
|
-
|
|
173
|
-
var
|
|
219
|
+
// Parse the original viewBox (diagram bounding box)
|
|
220
|
+
var vbParts = '${viewBox}'.split(/\\s+/).map(Number);
|
|
221
|
+
var origX = vbParts[0], origY = vbParts[1], origW = vbParts[2], origH = vbParts[3];
|
|
222
|
+
var vbX = origX, vbY = origY, vbW = origW, vbH = origH;
|
|
223
|
+
var baseW = origW; // reference width for 100% zoom
|
|
224
|
+
|
|
225
|
+
var pointerDown = false, didDrag = false, dragLast = { x: 0, y: 0 };
|
|
174
226
|
var selectedNodeId = null;
|
|
227
|
+
var selectedPortId = null;
|
|
175
228
|
var hintTimer = null;
|
|
176
229
|
|
|
177
230
|
// Detect Mac for modifier key
|
|
@@ -180,29 +233,34 @@ body.node-active path[data-source].dimmed { opacity: 0.15; }
|
|
|
180
233
|
|
|
181
234
|
function clamp(v, min, max) { return Math.min(max, Math.max(min, v)); }
|
|
182
235
|
|
|
183
|
-
function
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
viewport.style.backgroundPosition = tx + 'px ' + ty + 'px';
|
|
187
|
-
zoomLabel.textContent = Math.round(scale * 100) + '%';
|
|
236
|
+
function applyViewBox() {
|
|
237
|
+
canvas.setAttribute('viewBox', vbX + ' ' + vbY + ' ' + vbW + ' ' + vbH);
|
|
238
|
+
zoomLabel.textContent = Math.round(baseW / vbW * 100) + '%';
|
|
188
239
|
}
|
|
189
240
|
|
|
190
241
|
function fitToView() {
|
|
191
|
-
var
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
var
|
|
195
|
-
var
|
|
196
|
-
var
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
242
|
+
var pad = 60;
|
|
243
|
+
var cw = canvas.clientWidth, ch = canvas.clientHeight;
|
|
244
|
+
if (!cw || !ch) return;
|
|
245
|
+
var dw = origW + pad * 2, dh = origH + pad * 2;
|
|
246
|
+
var vpRatio = cw / ch;
|
|
247
|
+
var dRatio = dw / dh;
|
|
248
|
+
if (vpRatio > dRatio) {
|
|
249
|
+
vbH = dh; vbW = dh * vpRatio;
|
|
250
|
+
} else {
|
|
251
|
+
vbW = dw; vbH = dw / vpRatio;
|
|
252
|
+
}
|
|
253
|
+
vbX = origX - pad - (vbW - dw) / 2;
|
|
254
|
+
vbY = origY - pad - (vbH - dh) / 2;
|
|
255
|
+
baseW = vbW;
|
|
256
|
+
applyViewBox();
|
|
202
257
|
}
|
|
203
258
|
|
|
259
|
+
// Convert pixel delta to SVG coordinate delta
|
|
260
|
+
function pxToSvg() { return vbW / canvas.clientWidth; }
|
|
261
|
+
|
|
204
262
|
// ---- Zoom (Ctrl/Cmd + scroll) ----
|
|
205
|
-
|
|
263
|
+
canvas.addEventListener('wheel', function(e) {
|
|
206
264
|
if (!e.ctrlKey && !e.metaKey) {
|
|
207
265
|
scrollHint.classList.add('visible');
|
|
208
266
|
clearTimeout(hintTimer);
|
|
@@ -210,48 +268,100 @@ body.node-active path[data-source].dimmed { opacity: 0.15; }
|
|
|
210
268
|
return;
|
|
211
269
|
}
|
|
212
270
|
e.preventDefault();
|
|
213
|
-
var rect =
|
|
214
|
-
var
|
|
215
|
-
var
|
|
271
|
+
var rect = canvas.getBoundingClientRect();
|
|
272
|
+
var mx = (e.clientX - rect.left) / rect.width;
|
|
273
|
+
var my = (e.clientY - rect.top) / rect.height;
|
|
274
|
+
var pivotX = vbX + mx * vbW;
|
|
275
|
+
var pivotY = vbY + my * vbH;
|
|
216
276
|
var delta = clamp(e.deltaY, -10, 10);
|
|
217
|
-
var
|
|
218
|
-
var
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
277
|
+
var factor = 1 + delta * 0.005;
|
|
278
|
+
var newW = clamp(vbW * factor, baseW / MAX_ZOOM, baseW / MIN_ZOOM);
|
|
279
|
+
var ratio = vbH / vbW;
|
|
280
|
+
var newH = newW * ratio;
|
|
281
|
+
vbX = pivotX - mx * newW;
|
|
282
|
+
vbY = pivotY - my * newH;
|
|
283
|
+
vbW = newW; vbH = newH;
|
|
284
|
+
applyViewBox();
|
|
223
285
|
}, { passive: false });
|
|
224
286
|
|
|
225
|
-
// ---- Pan (drag) ----
|
|
226
|
-
|
|
287
|
+
// ---- Pan (drag) + Node drag ----
|
|
288
|
+
var draggedNodeId = null, dragNodeStart = null, didDragNode = false;
|
|
289
|
+
|
|
290
|
+
canvas.addEventListener('pointerdown', function(e) {
|
|
227
291
|
if (e.button !== 0) return;
|
|
228
|
-
|
|
292
|
+
// Check if clicking on a node body (walk up to detect data-node-id)
|
|
293
|
+
var t = e.target;
|
|
294
|
+
while (t && t !== canvas) {
|
|
295
|
+
if (t.hasAttribute && t.hasAttribute('data-node-id')) {
|
|
296
|
+
// Don't start node drag if clicking on a port circle
|
|
297
|
+
if (e.target.hasAttribute && e.target.hasAttribute('data-port-id')) break;
|
|
298
|
+
draggedNodeId = t.getAttribute('data-node-id');
|
|
299
|
+
dragNodeStart = { x: e.clientX, y: e.clientY };
|
|
300
|
+
didDragNode = false;
|
|
301
|
+
canvas.setPointerCapture(e.pointerId);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
t = t.parentElement;
|
|
305
|
+
}
|
|
306
|
+
// Canvas pan
|
|
307
|
+
pointerDown = true;
|
|
308
|
+
didDrag = false;
|
|
229
309
|
dragLast = { x: e.clientX, y: e.clientY };
|
|
230
|
-
|
|
231
|
-
viewport.classList.add('dragging');
|
|
310
|
+
canvas.setPointerCapture(e.pointerId);
|
|
232
311
|
});
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
312
|
+
|
|
313
|
+
canvas.addEventListener('pointermove', function(e) {
|
|
314
|
+
var ratio = pxToSvg();
|
|
315
|
+
|
|
316
|
+
// Node drag
|
|
317
|
+
if (draggedNodeId) {
|
|
318
|
+
var dx = (e.clientX - dragNodeStart.x) * ratio;
|
|
319
|
+
var dy = (e.clientY - dragNodeStart.y) * ratio;
|
|
320
|
+
if (!didDragNode && (Math.abs(dx) > 3 || Math.abs(dy) > 3)) {
|
|
321
|
+
didDragNode = true;
|
|
322
|
+
canvas.classList.add('dragging');
|
|
323
|
+
}
|
|
324
|
+
if (didDragNode) {
|
|
325
|
+
moveNode(draggedNodeId, dx, dy);
|
|
326
|
+
}
|
|
327
|
+
dragNodeStart = { x: e.clientX, y: e.clientY };
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Canvas pan — shift the viewBox origin
|
|
332
|
+
if (!pointerDown) return;
|
|
333
|
+
var dxPx = e.clientX - dragLast.x, dyPx = e.clientY - dragLast.y;
|
|
334
|
+
if (!didDrag && (Math.abs(dxPx) > 3 || Math.abs(dyPx) > 3)) {
|
|
335
|
+
didDrag = true;
|
|
336
|
+
canvas.classList.add('dragging');
|
|
337
|
+
}
|
|
338
|
+
if (didDrag) {
|
|
339
|
+
vbX -= dxPx * ratio;
|
|
340
|
+
vbY -= dyPx * ratio;
|
|
341
|
+
applyViewBox();
|
|
342
|
+
}
|
|
237
343
|
dragLast = { x: e.clientX, y: e.clientY };
|
|
238
|
-
applyTransform();
|
|
239
344
|
});
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
345
|
+
|
|
346
|
+
function endDrag() {
|
|
347
|
+
pointerDown = false;
|
|
348
|
+
draggedNodeId = null;
|
|
349
|
+
canvas.classList.remove('dragging');
|
|
350
|
+
}
|
|
351
|
+
canvas.addEventListener('pointerup', endDrag);
|
|
352
|
+
canvas.addEventListener('pointercancel', endDrag);
|
|
243
353
|
|
|
244
354
|
// ---- Zoom buttons ----
|
|
245
355
|
function zoomBy(dir) {
|
|
246
|
-
var
|
|
247
|
-
var
|
|
248
|
-
var
|
|
249
|
-
var
|
|
250
|
-
var
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
356
|
+
var cx = vbX + vbW / 2, cy = vbY + vbH / 2;
|
|
357
|
+
var factor = dir > 0 ? 0.85 : 1.18;
|
|
358
|
+
var newW = clamp(vbW * factor, baseW / MAX_ZOOM, baseW / MIN_ZOOM);
|
|
359
|
+
var ratio = vbH / vbW;
|
|
360
|
+
var newH = newW * ratio;
|
|
361
|
+
vbX = cx - newW / 2;
|
|
362
|
+
vbY = cy - newH / 2;
|
|
363
|
+
vbW = newW; vbH = newH;
|
|
364
|
+
applyViewBox();
|
|
255
365
|
}
|
|
256
366
|
document.getElementById('btn-in').addEventListener('click', function() { zoomBy(1); });
|
|
257
367
|
document.getElementById('btn-out').addEventListener('click', function() { zoomBy(-1); });
|
|
@@ -263,7 +373,7 @@ body.node-active path[data-source].dimmed { opacity: 0.15; }
|
|
|
263
373
|
if (e.key === '+' || e.key === '=') zoomBy(1);
|
|
264
374
|
else if (e.key === '-') zoomBy(-1);
|
|
265
375
|
else if (e.key === '0') fitToView();
|
|
266
|
-
else if (e.key === 'Escape') deselectNode();
|
|
376
|
+
else if (e.key === 'Escape') { deselectPort(); deselectNode(); }
|
|
267
377
|
});
|
|
268
378
|
|
|
269
379
|
// ---- Port label visibility ----
|
|
@@ -272,7 +382,7 @@ body.node-active path[data-source].dimmed { opacity: 0.15; }
|
|
|
272
382
|
labelMap[lbl.getAttribute('data-port-label')] = lbl;
|
|
273
383
|
});
|
|
274
384
|
|
|
275
|
-
// Build adjacency: portId
|
|
385
|
+
// Build adjacency: portId -> array of connected portIds
|
|
276
386
|
var portConnections = {};
|
|
277
387
|
content.querySelectorAll('path[data-source]').forEach(function(p) {
|
|
278
388
|
var src = p.getAttribute('data-source');
|
|
@@ -284,6 +394,125 @@ body.node-active path[data-source].dimmed { opacity: 0.15; }
|
|
|
284
394
|
portConnections[tgt].push(src);
|
|
285
395
|
});
|
|
286
396
|
|
|
397
|
+
// ---- Connection path computation (from geometry.ts) ----
|
|
398
|
+
function quadCurveControl(ax, ay, bx, by, ux, uy) {
|
|
399
|
+
var dn = Math.abs(ay - by);
|
|
400
|
+
return [bx + (ux * dn) / Math.abs(uy), ay];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function computeConnectionPath(sx, sy, tx, ty) {
|
|
404
|
+
var e = 0.0001;
|
|
405
|
+
var ax = sx + e, ay = sy + e, hx = tx - e, hy = ty - e;
|
|
406
|
+
var ramp = Math.min(20, (hx - ax) / 10);
|
|
407
|
+
var bx = ax + ramp, by = ay + e, gx = hx - ramp, gy = hy - e;
|
|
408
|
+
var curveSizeX = Math.min(60, Math.abs(ax - hx) / 4);
|
|
409
|
+
var curveSizeY = Math.min(60, Math.abs(ay - hy) / 4);
|
|
410
|
+
var curveMag = Math.sqrt(curveSizeX * curveSizeX + curveSizeY * curveSizeY);
|
|
411
|
+
var bgX = gx - bx, bgY = gy - by;
|
|
412
|
+
var bgLen = Math.sqrt(bgX * bgX + bgY * bgY);
|
|
413
|
+
var bgUx = bgX / bgLen, bgUy = bgY / bgLen;
|
|
414
|
+
var dx = bx + bgUx * curveMag, dy = by + (bgUy * curveMag) / 2;
|
|
415
|
+
var ex = gx - bgUx * curveMag, ey = gy - (bgUy * curveMag) / 2;
|
|
416
|
+
var deX = ex - dx, deY = ey - dy;
|
|
417
|
+
var deLen = Math.sqrt(deX * deX + deY * deY);
|
|
418
|
+
var deUx = deX / deLen, deUy = deY / deLen;
|
|
419
|
+
var c = quadCurveControl(bx, by, dx, dy, -deUx, -deUy);
|
|
420
|
+
var f = quadCurveControl(gx, gy, ex, ey, deUx, deUy);
|
|
421
|
+
return 'M ' + c[0] + ',' + c[1] + ' M ' + ax + ',' + ay +
|
|
422
|
+
' L ' + bx + ',' + by + ' Q ' + c[0] + ',' + c[1] + ' ' + dx + ',' + dy +
|
|
423
|
+
' L ' + ex + ',' + ey + ' Q ' + f[0] + ',' + f[1] + ' ' + gx + ',' + gy +
|
|
424
|
+
' L ' + hx + ',' + hy;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ---- Port position + connection path indexes ----
|
|
428
|
+
var portPositions = {};
|
|
429
|
+
content.querySelectorAll('[data-port-id]').forEach(function(el) {
|
|
430
|
+
var id = el.getAttribute('data-port-id');
|
|
431
|
+
portPositions[id] = { cx: parseFloat(el.getAttribute('cx')), cy: parseFloat(el.getAttribute('cy')) };
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
var nodeOffsets = {};
|
|
435
|
+
var connIndex = [];
|
|
436
|
+
content.querySelectorAll('path[data-source]').forEach(function(p) {
|
|
437
|
+
var src = p.getAttribute('data-source'), tgt = p.getAttribute('data-target');
|
|
438
|
+
connIndex.push({ el: p, src: src, tgt: tgt, srcNode: src.split('.')[0], tgtNode: tgt.split('.')[0] });
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// ---- Node drag: moveNode ----
|
|
442
|
+
function moveNode(nodeId, dx, dy) {
|
|
443
|
+
if (!nodeOffsets[nodeId]) nodeOffsets[nodeId] = { dx: 0, dy: 0 };
|
|
444
|
+
var off = nodeOffsets[nodeId];
|
|
445
|
+
off.dx += dx; off.dy += dy;
|
|
446
|
+
var tr = 'translate(' + off.dx + ',' + off.dy + ')';
|
|
447
|
+
|
|
448
|
+
// Move node group
|
|
449
|
+
var nodeG = content.querySelector('.nodes [data-node-id="' + CSS.escape(nodeId) + '"]');
|
|
450
|
+
if (nodeG) nodeG.setAttribute('transform', tr);
|
|
451
|
+
|
|
452
|
+
// Move label
|
|
453
|
+
var labelG = content.querySelector('[data-label-for="' + CSS.escape(nodeId) + '"]');
|
|
454
|
+
if (labelG) labelG.setAttribute('transform', tr);
|
|
455
|
+
|
|
456
|
+
// Move port labels
|
|
457
|
+
allLabelIds.forEach(function(id) {
|
|
458
|
+
if (id.indexOf(nodeId + '.') === 0) {
|
|
459
|
+
var el = labelMap[id];
|
|
460
|
+
if (el) el.setAttribute('transform', tr);
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Update port positions
|
|
465
|
+
for (var pid in portPositions) {
|
|
466
|
+
if (pid.indexOf(nodeId + '.') === 0) {
|
|
467
|
+
portPositions[pid].cx += dx;
|
|
468
|
+
portPositions[pid].cy += dy;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Move child nodes inside scoped parents
|
|
473
|
+
if (nodeG) {
|
|
474
|
+
var children = nodeG.querySelectorAll(':scope > g[data-node-id]');
|
|
475
|
+
children.forEach(function(childG) {
|
|
476
|
+
var childId = childG.getAttribute('data-node-id');
|
|
477
|
+
if (!nodeOffsets[childId]) nodeOffsets[childId] = { dx: 0, dy: 0 };
|
|
478
|
+
nodeOffsets[childId].dx += dx;
|
|
479
|
+
nodeOffsets[childId].dy += dy;
|
|
480
|
+
for (var pid in portPositions) {
|
|
481
|
+
if (pid.indexOf(childId + '.') === 0) {
|
|
482
|
+
portPositions[pid].cx += dx;
|
|
483
|
+
portPositions[pid].cy += dy;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
var childLabel = content.querySelector('[data-label-for="' + CSS.escape(childId) + '"]');
|
|
487
|
+
if (childLabel) childLabel.setAttribute('transform', 'translate(' + nodeOffsets[childId].dx + ',' + nodeOffsets[childId].dy + ')');
|
|
488
|
+
allLabelIds.forEach(function(id) {
|
|
489
|
+
if (id.indexOf(childId + '.') === 0) {
|
|
490
|
+
var el = labelMap[id];
|
|
491
|
+
if (el) el.setAttribute('transform', 'translate(' + nodeOffsets[childId].dx + ',' + nodeOffsets[childId].dy + ')');
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Recalculate affected connection paths
|
|
498
|
+
connIndex.forEach(function(c) {
|
|
499
|
+
if (c.srcNode === nodeId || c.tgtNode === nodeId) {
|
|
500
|
+
var sp = portPositions[c.src], tp = portPositions[c.tgt];
|
|
501
|
+
if (sp && tp) c.el.setAttribute('d', computeConnectionPath(sp.cx, sp.cy, tp.cx, tp.cy));
|
|
502
|
+
}
|
|
503
|
+
if (nodeG) {
|
|
504
|
+
var children = nodeG.querySelectorAll(':scope > g[data-node-id]');
|
|
505
|
+
children.forEach(function(childG) {
|
|
506
|
+
var childId = childG.getAttribute('data-node-id');
|
|
507
|
+
if (c.srcNode === childId || c.tgtNode === childId) {
|
|
508
|
+
var sp = portPositions[c.src], tp = portPositions[c.tgt];
|
|
509
|
+
if (sp && tp) c.el.setAttribute('d', computeConnectionPath(sp.cx, sp.cy, tp.cx, tp.cy));
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
287
516
|
var allLabelIds = Object.keys(labelMap);
|
|
288
517
|
var hoveredPort = null;
|
|
289
518
|
|
|
@@ -308,7 +537,7 @@ body.node-active path[data-source].dimmed { opacity: 0.15; }
|
|
|
308
537
|
var parentNodeG = nodeG.parentElement ? nodeG.parentElement.closest('g[data-node-id]') : null;
|
|
309
538
|
var parentId = parentNodeG ? parentNodeG.getAttribute('data-node-id') : null;
|
|
310
539
|
nodeG.addEventListener('mouseenter', function() {
|
|
311
|
-
if (hoveredPort) return;
|
|
540
|
+
if (hoveredPort) return;
|
|
312
541
|
if (parentId) hideLabelsFor(parentId);
|
|
313
542
|
showLabelsFor(nodeId);
|
|
314
543
|
});
|
|
@@ -327,24 +556,93 @@ body.node-active path[data-source].dimmed { opacity: 0.15; }
|
|
|
327
556
|
|
|
328
557
|
portEl.addEventListener('mouseenter', function() {
|
|
329
558
|
hoveredPort = portId;
|
|
330
|
-
// Hide all labels for this node first, then show only the relevant ones
|
|
331
559
|
hideLabelsFor(nodeId);
|
|
332
560
|
peers.forEach(showLabel);
|
|
561
|
+
document.body.classList.add('port-hovered');
|
|
562
|
+
content.querySelectorAll('path[data-source]').forEach(function(p) {
|
|
563
|
+
if (p.getAttribute('data-source') === portId || p.getAttribute('data-target') === portId) {
|
|
564
|
+
p.classList.remove('dimmed');
|
|
565
|
+
} else {
|
|
566
|
+
p.classList.add('dimmed');
|
|
567
|
+
}
|
|
568
|
+
});
|
|
333
569
|
});
|
|
334
570
|
portEl.addEventListener('mouseleave', function() {
|
|
335
571
|
hoveredPort = null;
|
|
336
572
|
peers.forEach(hideLabel);
|
|
337
|
-
// Restore all labels for the node since we're still inside it
|
|
338
573
|
showLabelsFor(nodeId);
|
|
574
|
+
document.body.classList.remove('port-hovered');
|
|
575
|
+
content.querySelectorAll('path[data-source].dimmed').forEach(function(p) {
|
|
576
|
+
p.classList.remove('dimmed');
|
|
577
|
+
});
|
|
339
578
|
});
|
|
340
579
|
});
|
|
341
580
|
|
|
581
|
+
// ---- Node glow helpers ----
|
|
582
|
+
function removeNodeGlow() {
|
|
583
|
+
var glow = content.querySelector('.node-glow');
|
|
584
|
+
if (glow) glow.remove();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function addNodeGlow(nodeG) {
|
|
588
|
+
removeNodeGlow();
|
|
589
|
+
var rect = nodeG.querySelector('rect');
|
|
590
|
+
if (!rect) return;
|
|
591
|
+
var ns = 'http://www.w3.org/2000/svg';
|
|
592
|
+
var glow = document.createElementNS(ns, 'rect');
|
|
593
|
+
glow.setAttribute('x', rect.getAttribute('x'));
|
|
594
|
+
glow.setAttribute('y', rect.getAttribute('y'));
|
|
595
|
+
glow.setAttribute('width', rect.getAttribute('width'));
|
|
596
|
+
glow.setAttribute('height', rect.getAttribute('height'));
|
|
597
|
+
glow.setAttribute('rx', rect.getAttribute('rx') || '0');
|
|
598
|
+
glow.setAttribute('stroke', rect.getAttribute('stroke') || '#5468ff');
|
|
599
|
+
glow.setAttribute('class', 'node-glow');
|
|
600
|
+
nodeG.insertBefore(glow, rect);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ---- Port selection ----
|
|
604
|
+
function deselectPort() {
|
|
605
|
+
if (!selectedPortId) return;
|
|
606
|
+
selectedPortId = null;
|
|
607
|
+
document.body.classList.remove('port-active');
|
|
608
|
+
content.querySelectorAll('circle.port-selected').forEach(function(c) {
|
|
609
|
+
c.classList.remove('port-selected');
|
|
610
|
+
});
|
|
611
|
+
content.querySelectorAll('path[data-source].dimmed, path[data-source].highlighted').forEach(function(p) {
|
|
612
|
+
p.classList.remove('dimmed');
|
|
613
|
+
p.classList.remove('highlighted');
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function selectPort(portId) {
|
|
618
|
+
if (selectedPortId === portId) { deselectPort(); return; }
|
|
619
|
+
if (selectedNodeId) deselectNode();
|
|
620
|
+
deselectPort();
|
|
621
|
+
selectedPortId = portId;
|
|
622
|
+
document.body.classList.add('port-active');
|
|
623
|
+
|
|
624
|
+
var portEl = content.querySelector('[data-port-id="' + CSS.escape(portId) + '"]');
|
|
625
|
+
if (portEl) portEl.classList.add('port-selected');
|
|
626
|
+
|
|
627
|
+
content.querySelectorAll('path[data-source]').forEach(function(p) {
|
|
628
|
+
if (p.getAttribute('data-source') === portId || p.getAttribute('data-target') === portId) {
|
|
629
|
+
p.classList.add('highlighted');
|
|
630
|
+
} else {
|
|
631
|
+
p.classList.add('dimmed');
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
var peers = (portConnections[portId] || []).concat(portId);
|
|
636
|
+
peers.forEach(showLabel);
|
|
637
|
+
}
|
|
638
|
+
|
|
342
639
|
// ---- Click to inspect node ----
|
|
343
640
|
function deselectNode() {
|
|
344
641
|
selectedNodeId = null;
|
|
345
642
|
document.body.classList.remove('node-active');
|
|
346
643
|
infoPanel.classList.remove('visible');
|
|
347
|
-
|
|
644
|
+
removeNodeGlow();
|
|
645
|
+
content.querySelectorAll('path[data-source].dimmed').forEach(function(p) {
|
|
348
646
|
p.classList.remove('dimmed');
|
|
349
647
|
});
|
|
350
648
|
}
|
|
@@ -354,8 +652,9 @@ body.node-active path[data-source].dimmed { opacity: 0.15; }
|
|
|
354
652
|
selectedNodeId = nodeId;
|
|
355
653
|
document.body.classList.add('node-active');
|
|
356
654
|
|
|
357
|
-
// Gather info
|
|
358
655
|
var nodeG = content.querySelector('[data-node-id="' + CSS.escape(nodeId) + '"]');
|
|
656
|
+
addNodeGlow(nodeG);
|
|
657
|
+
|
|
359
658
|
var labelG = content.querySelector('[data-label-for="' + CSS.escape(nodeId) + '"]');
|
|
360
659
|
var labelText = labelG ? (labelG.querySelector('.node-label') || {}).textContent || nodeId : nodeId;
|
|
361
660
|
|
|
@@ -372,7 +671,6 @@ body.node-active path[data-source].dimmed { opacity: 0.15; }
|
|
|
372
671
|
|
|
373
672
|
// Connected paths
|
|
374
673
|
var allPaths = content.querySelectorAll('path[data-source]');
|
|
375
|
-
var connectedPaths = [];
|
|
376
674
|
var connectedNodes = new Set();
|
|
377
675
|
allPaths.forEach(function(p) {
|
|
378
676
|
var src = p.getAttribute('data-source') || '';
|
|
@@ -380,7 +678,6 @@ body.node-active path[data-source].dimmed { opacity: 0.15; }
|
|
|
380
678
|
var srcNode = src.split('.')[0];
|
|
381
679
|
var tgtNode = tgt.split('.')[0];
|
|
382
680
|
if (srcNode === nodeId || tgtNode === nodeId) {
|
|
383
|
-
connectedPaths.push(p);
|
|
384
681
|
if (srcNode !== nodeId) connectedNodes.add(srcNode);
|
|
385
682
|
if (tgtNode !== nodeId) connectedNodes.add(tgtNode);
|
|
386
683
|
p.classList.remove('dimmed');
|
|
@@ -415,20 +712,25 @@ body.node-active path[data-source].dimmed { opacity: 0.15; }
|
|
|
415
712
|
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
416
713
|
}
|
|
417
714
|
|
|
418
|
-
// Delegate click
|
|
419
|
-
|
|
420
|
-
if (
|
|
715
|
+
// Delegate click: port click > node click > background
|
|
716
|
+
canvas.addEventListener('click', function(e) {
|
|
717
|
+
if (didDrag || didDragNode) { didDragNode = false; return; }
|
|
421
718
|
var target = e.target;
|
|
422
|
-
|
|
423
|
-
|
|
719
|
+
while (target && target !== canvas) {
|
|
720
|
+
if (target.hasAttribute && target.hasAttribute('data-port-id')) {
|
|
721
|
+
e.stopPropagation();
|
|
722
|
+
selectPort(target.getAttribute('data-port-id'));
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
424
725
|
if (target.hasAttribute && target.hasAttribute('data-node-id')) {
|
|
425
726
|
e.stopPropagation();
|
|
727
|
+
deselectPort();
|
|
426
728
|
selectNode(target.getAttribute('data-node-id'));
|
|
427
729
|
return;
|
|
428
730
|
}
|
|
429
731
|
target = target.parentElement;
|
|
430
732
|
}
|
|
431
|
-
|
|
733
|
+
deselectPort();
|
|
432
734
|
deselectNode();
|
|
433
735
|
});
|
|
434
736
|
|