living-documentation 7.35.0 → 7.37.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.
|
@@ -44,6 +44,12 @@ function setTool(tool, shape) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
window.addEventListener('diagram:setTool', (e) => {
|
|
48
|
+
const detail = e.detail || {};
|
|
49
|
+
if (!detail.tool) return;
|
|
50
|
+
setTool(detail.tool, detail.shape);
|
|
51
|
+
});
|
|
52
|
+
|
|
47
53
|
function selectAll() {
|
|
48
54
|
if (!st.network || !st.nodes || !st.edges) return;
|
|
49
55
|
const ids = st.nodes.getIds();
|
|
@@ -34,6 +34,8 @@ let _draggingAnchorIds = new Set();
|
|
|
34
34
|
let _rehookEdgeId = null;
|
|
35
35
|
let _rehookHoveredNodeId = null;
|
|
36
36
|
let _rehookHoveredPortKey = null;
|
|
37
|
+
let _pointerDownSelection = { nodeIds: [], edgeIds: [] };
|
|
38
|
+
let _edgeLabelPointerAbort = null;
|
|
37
39
|
|
|
38
40
|
// Returns true when an edge can enter rehook mode (at least one non-anchor endpoint).
|
|
39
41
|
// Works for both port edges and native vis-network edges.
|
|
@@ -126,7 +128,11 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
126
128
|
callback(data);
|
|
127
129
|
markDirty();
|
|
128
130
|
_addEdgeFromPort = null;
|
|
129
|
-
setTimeout(() => {
|
|
131
|
+
setTimeout(() => {
|
|
132
|
+
if (st.currentTool === 'addEdge') {
|
|
133
|
+
window.dispatchEvent(new CustomEvent('diagram:setTool', { detail: { tool: 'select' } }));
|
|
134
|
+
}
|
|
135
|
+
}, 0);
|
|
130
136
|
},
|
|
131
137
|
},
|
|
132
138
|
};
|
|
@@ -138,6 +144,13 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
138
144
|
// Must be registered BEFORE any other capture-phase mousedown listeners on
|
|
139
145
|
// the container so it can stopImmediatePropagation for locked targets.
|
|
140
146
|
installUnlockHold(container);
|
|
147
|
+
container.addEventListener('mousedown', (e) => {
|
|
148
|
+
if (e.button !== 0) return;
|
|
149
|
+
_pointerDownSelection = {
|
|
150
|
+
nodeIds: [...(st.selectedNodeIds || [])],
|
|
151
|
+
edgeIds: [...(st.selectedEdgeIds || [])],
|
|
152
|
+
};
|
|
153
|
+
}, { capture: true });
|
|
141
154
|
|
|
142
155
|
// ── Z-order patch ──────────────────────────────────────────────────────────
|
|
143
156
|
// vis.js renders in 3 passes (normal → selected → hovered), which breaks
|
|
@@ -189,7 +202,7 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
189
202
|
} else if (fromIsAnchor && !toIsAnchor) {
|
|
190
203
|
level = toLevel;
|
|
191
204
|
} else {
|
|
192
|
-
level = Math.
|
|
205
|
+
level = Math.max(fromLevel, toLevel);
|
|
193
206
|
}
|
|
194
207
|
if (!edgesByLevel.has(level)) edgesByLevel.set(level, []);
|
|
195
208
|
edgesByLevel.get(level).push(edge);
|
|
@@ -385,14 +398,13 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
385
398
|
ctx.restore();
|
|
386
399
|
});
|
|
387
400
|
|
|
388
|
-
|
|
389
|
-
container.addEventListener('mousemove', (e) => {
|
|
401
|
+
function edgeLabelHitAt(clientX, clientY) {
|
|
390
402
|
if (!st.network || !st.edgeLabelBBox) return;
|
|
391
|
-
const
|
|
403
|
+
const rect = container.getBoundingClientRect();
|
|
404
|
+
const cp = st.network.DOMtoCanvas({ x: clientX - rect.left, y: clientY - rect.top });
|
|
392
405
|
const hr = 8 / st.network.getScale();
|
|
393
406
|
|
|
394
407
|
// ── Resize handle detection ──────────────────────────────────────────────
|
|
395
|
-
let found = null;
|
|
396
408
|
for (const edgeId of (st.selectedEdgeIds || [])) {
|
|
397
409
|
const edge = st.edges && st.edges.get(edgeId);
|
|
398
410
|
if (!edge || !edge.label) continue;
|
|
@@ -405,18 +417,12 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
405
417
|
const ly = dx * Math.sin(r) + dy * Math.cos(r);
|
|
406
418
|
|
|
407
419
|
if (Math.hypot(lx - (-bbox.w / 2), ly) < hr || Math.hypot(lx - (bbox.w / 2), ly) < hr) {
|
|
408
|
-
|
|
409
|
-
break;
|
|
420
|
+
return { type: 'handle', edgeId, bboxCx: bbox.cx, bboxCy: bbox.cy, rotation: bbox.rotation || 0 };
|
|
410
421
|
}
|
|
411
422
|
}
|
|
412
|
-
if (Boolean(found) !== Boolean(_hoverHandle)) {
|
|
413
|
-
container.style.cursor = found ? 'ew-resize' : (_hoverLabelDrag ? 'grab' : '');
|
|
414
|
-
}
|
|
415
|
-
_hoverHandle = found;
|
|
416
423
|
|
|
417
|
-
// ── Label box
|
|
418
|
-
|
|
419
|
-
if (!found && st.selectedEdgeIds && st.selectedEdgeIds.length === 1) {
|
|
424
|
+
// ── Label box detection (drag) — only when no handle hovered ───────────
|
|
425
|
+
if (st.selectedEdgeIds && st.selectedEdgeIds.length === 1) {
|
|
420
426
|
const edgeId = st.selectedEdgeIds[0];
|
|
421
427
|
const edge = st.edges && st.edges.get(edgeId);
|
|
422
428
|
if (edge && edge.label) {
|
|
@@ -427,39 +433,104 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
427
433
|
const lx = dx * Math.cos(r) - dy * Math.sin(r);
|
|
428
434
|
const ly = dx * Math.sin(r) + dy * Math.cos(r);
|
|
429
435
|
if (Math.abs(lx) <= bbox.w / 2 && Math.abs(ly) <= bbox.h / 2) {
|
|
430
|
-
|
|
436
|
+
return { type: 'label', edgeId };
|
|
431
437
|
}
|
|
432
438
|
}
|
|
433
439
|
}
|
|
434
440
|
}
|
|
441
|
+
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Track hover during mousemove — computed before any mousedown fires.
|
|
446
|
+
container.addEventListener('mousemove', (e) => {
|
|
447
|
+
const hit = edgeLabelHitAt(e.clientX, e.clientY);
|
|
448
|
+
const found = hit && hit.type === 'handle'
|
|
449
|
+
? { edgeId: hit.edgeId, bboxCx: hit.bboxCx, bboxCy: hit.bboxCy, rotation: hit.rotation }
|
|
450
|
+
: null;
|
|
451
|
+
if (Boolean(found) !== Boolean(_hoverHandle)) {
|
|
452
|
+
container.style.cursor = found ? 'ew-resize' : (_hoverLabelDrag ? 'grab' : '');
|
|
453
|
+
}
|
|
454
|
+
_hoverHandle = found;
|
|
455
|
+
|
|
456
|
+
const labelFound = !found && hit && hit.type === 'label' ? { edgeId: hit.edgeId } : null;
|
|
435
457
|
if (Boolean(labelFound) !== Boolean(_hoverLabelDrag)) {
|
|
436
458
|
if (!found) container.style.cursor = labelFound ? 'grab' : '';
|
|
437
459
|
}
|
|
438
460
|
_hoverLabelDrag = labelFound;
|
|
439
461
|
});
|
|
440
462
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
if (e.
|
|
444
|
-
|
|
463
|
+
function startEdgeLabelPointerInteraction(e) {
|
|
464
|
+
if (e.button !== 0) return false;
|
|
465
|
+
if (!container.contains(e.target)) return false;
|
|
466
|
+
const hit = edgeLabelHitAt(e.clientX, e.clientY);
|
|
467
|
+
if (hit && hit.type === 'handle') {
|
|
468
|
+
e.preventDefault();
|
|
469
|
+
e.stopImmediatePropagation();
|
|
470
|
+
_hoverHandle = { edgeId: hit.edgeId, bboxCx: hit.bboxCx, bboxCy: hit.bboxCy, rotation: hit.rotation };
|
|
445
471
|
_lr = { ..._hoverHandle, dragging: false };
|
|
446
472
|
st.network.setOptions({ interaction: { dragView: false } });
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
473
|
+
return true;
|
|
474
|
+
} else if (hit && hit.type === 'label') {
|
|
475
|
+
const edge = st.edges && st.edges.get(hit.edgeId);
|
|
476
|
+
if (!edge) return false;
|
|
477
|
+
e.preventDefault();
|
|
478
|
+
e.stopImmediatePropagation();
|
|
479
|
+
_hoverLabelDrag = { edgeId: hit.edgeId };
|
|
450
480
|
_ld = {
|
|
451
|
-
edgeId:
|
|
481
|
+
edgeId: hit.edgeId,
|
|
452
482
|
startMouse: { x: e.clientX, y: e.clientY },
|
|
453
483
|
startOffsetX: edge.edgeLabelOffsetX || 0,
|
|
454
484
|
startOffsetY: edge.edgeLabelOffsetY || 0,
|
|
455
485
|
dragging: false,
|
|
456
486
|
};
|
|
457
487
|
st.network.setOptions({ interaction: { dragView: false } });
|
|
488
|
+
return true;
|
|
458
489
|
}
|
|
459
|
-
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (_edgeLabelPointerAbort) _edgeLabelPointerAbort.abort();
|
|
494
|
+
_edgeLabelPointerAbort = new AbortController();
|
|
495
|
+
|
|
496
|
+
// Mousedown: start resize (handles take priority) or label drag.
|
|
497
|
+
// The document-level capture listener runs before vis-network's internal
|
|
498
|
+
// pointer handlers, so a node underneath the edge label cannot start moving.
|
|
499
|
+
document.addEventListener('pointerdown', startEdgeLabelPointerInteraction, {
|
|
500
|
+
capture: true,
|
|
501
|
+
signal: _edgeLabelPointerAbort.signal,
|
|
502
|
+
});
|
|
503
|
+
document.addEventListener('mousedown', startEdgeLabelPointerInteraction, {
|
|
504
|
+
capture: true,
|
|
505
|
+
signal: _edgeLabelPointerAbort.signal,
|
|
506
|
+
});
|
|
507
|
+
container.addEventListener('pointerdown', (e) => {
|
|
508
|
+
startEdgeLabelPointerInteraction(e);
|
|
509
|
+
}, { capture: true, signal: _edgeLabelPointerAbort.signal });
|
|
510
|
+
container.addEventListener('mousedown', (e) => {
|
|
511
|
+
startEdgeLabelPointerInteraction(e);
|
|
512
|
+
}, { capture: true, signal: _edgeLabelPointerAbort.signal });
|
|
513
|
+
|
|
514
|
+
document.addEventListener('dblclick', (e) => {
|
|
515
|
+
if (!container.contains(e.target)) return;
|
|
516
|
+
const hit = edgeLabelHitAt(e.clientX, e.clientY);
|
|
517
|
+
if (!hit || hit.type !== 'label') return;
|
|
518
|
+
|
|
519
|
+
e.preventDefault();
|
|
520
|
+
e.stopImmediatePropagation();
|
|
521
|
+
_lr = null;
|
|
522
|
+
_ld = null;
|
|
523
|
+
st.network.setOptions({ interaction: { dragView: true } });
|
|
524
|
+
st.selectedNodeIds = [];
|
|
525
|
+
st.selectedEdgeIds = [hit.edgeId];
|
|
526
|
+
st.network.setSelection({ nodes: [], edges: [hit.edgeId] });
|
|
527
|
+
hideNodePanel();
|
|
528
|
+
showEdgePanel();
|
|
529
|
+
startEdgeLabelEdit();
|
|
530
|
+
}, { capture: true, signal: _edgeLabelPointerAbort.signal });
|
|
460
531
|
|
|
461
532
|
// Update width (resize) or offset (label drag) while dragging.
|
|
462
|
-
|
|
533
|
+
function onEdgeLabelPointerMove(e) {
|
|
463
534
|
if (!st.network) return;
|
|
464
535
|
if (_lr) {
|
|
465
536
|
if (!_lr.dragging) { _lr.dragging = true; pushSnapshot(); }
|
|
@@ -485,10 +556,12 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
485
556
|
});
|
|
486
557
|
st.network.redraw();
|
|
487
558
|
}
|
|
488
|
-
}
|
|
559
|
+
}
|
|
560
|
+
document.addEventListener('pointermove', onEdgeLabelPointerMove, { signal: _edgeLabelPointerAbort.signal });
|
|
561
|
+
document.addEventListener('mousemove', onEdgeLabelPointerMove, { signal: _edgeLabelPointerAbort.signal });
|
|
489
562
|
|
|
490
563
|
// Commit on mouseup.
|
|
491
|
-
|
|
564
|
+
function onEdgeLabelPointerUp() {
|
|
492
565
|
if (_lr) {
|
|
493
566
|
st.network.setOptions({ interaction: { dragView: true } });
|
|
494
567
|
if (_lr.dragging) markDirty();
|
|
@@ -500,7 +573,9 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
500
573
|
_ld = null;
|
|
501
574
|
}
|
|
502
575
|
container.style.cursor = _hoverHandle ? 'ew-resize' : (_hoverLabelDrag ? 'grab' : '');
|
|
503
|
-
}
|
|
576
|
+
}
|
|
577
|
+
document.addEventListener('pointerup', onEdgeLabelPointerUp, { signal: _edgeLabelPointerAbort.signal });
|
|
578
|
+
document.addEventListener('mouseup', onEdgeLabelPointerUp, { signal: _edgeLabelPointerAbort.signal });
|
|
504
579
|
}
|
|
505
580
|
|
|
506
581
|
// ── Free-arrow body drag ──────────────────────────────────────────────────────
|
|
@@ -1037,48 +1112,185 @@ function _distToSegment(px, py, ax, ay, bx, by) {
|
|
|
1037
1112
|
}
|
|
1038
1113
|
|
|
1039
1114
|
|
|
1040
|
-
|
|
1115
|
+
function nodeContainsCanvasPoint(id, canvasPos) {
|
|
1116
|
+
const n = st.nodes.get(id);
|
|
1117
|
+
const bn = st.network && st.network.body.nodes[id];
|
|
1118
|
+
if (!n || !bn || n.shapeType === 'anchor') return false;
|
|
1119
|
+
|
|
1120
|
+
const shapeType = n.shapeType || 'box';
|
|
1121
|
+
const defaults = SHAPE_DEFAULTS[shapeType] || [60, 28];
|
|
1122
|
+
const W = n.nodeWidth || defaults[0];
|
|
1123
|
+
const H = shapeType === 'circle' ? W : (n.nodeHeight || defaults[1]);
|
|
1124
|
+
|
|
1125
|
+
const dx = canvasPos.x - bn.x;
|
|
1126
|
+
const dy = canvasPos.y - bn.y;
|
|
1127
|
+
const rot = n.rotation || 0;
|
|
1128
|
+
const lx = rot ? dx * Math.cos(-rot) - dy * Math.sin(-rot) : dx;
|
|
1129
|
+
const ly = rot ? dx * Math.sin(-rot) + dy * Math.cos(-rot) : dy;
|
|
1130
|
+
|
|
1131
|
+
if (shapeType === 'circle' || shapeType === 'ellipse') {
|
|
1132
|
+
const rx = W / 2;
|
|
1133
|
+
const ry = H / 2;
|
|
1134
|
+
return rx > 0 && ry > 0 && ((lx * lx) / (rx * rx) + (ly * ly) / (ry * ry)) <= 1;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
return Math.abs(lx) <= W / 2 && Math.abs(ly) <= H / 2;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// Returns the topmost (highest z-order) node containing canvasPos.
|
|
1041
1141
|
// Ignores anchor nodes and respects st.canonicalOrder.
|
|
1042
1142
|
function topmostNodeAt(canvasPos) {
|
|
1043
|
-
let
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1143
|
+
for (let i = st.canonicalOrder.length - 1; i >= 0; i--) {
|
|
1144
|
+
const id = st.canonicalOrder[i];
|
|
1145
|
+
if (nodeContainsCanvasPoint(id, canvasPos)) return id;
|
|
1146
|
+
}
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function edgeDrawLevel(edgeData) {
|
|
1151
|
+
if (!edgeData) return -1;
|
|
1152
|
+
const fromLevel = st.canonicalOrder.indexOf(edgeData.from);
|
|
1153
|
+
const toLevel = st.canonicalOrder.indexOf(edgeData.to);
|
|
1154
|
+
if (fromLevel === -1 && toLevel === -1) return -1;
|
|
1155
|
+
if (fromLevel === -1) return toLevel;
|
|
1156
|
+
if (toLevel === -1) return fromLevel;
|
|
1157
|
+
|
|
1158
|
+
const fromNode = st.nodes.get(edgeData.from);
|
|
1159
|
+
const toNode = st.nodes.get(edgeData.to);
|
|
1160
|
+
const fromIsAnchor = fromNode && fromNode.shapeType === 'anchor';
|
|
1161
|
+
const toIsAnchor = toNode && toNode.shapeType === 'anchor';
|
|
1162
|
+
if (toIsAnchor && !fromIsAnchor) return fromLevel;
|
|
1163
|
+
if (fromIsAnchor && !toIsAnchor) return toLevel;
|
|
1164
|
+
return Math.max(fromLevel, toLevel);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function nearestPortEdgeAt(canvasPos, threshold = 8) {
|
|
1168
|
+
const portEdges = st.edges.get({ filter: (e) => e.fromPort || e.toPort });
|
|
1169
|
+
let nearest = null;
|
|
1170
|
+
let nearestDist = Infinity;
|
|
1171
|
+
for (const edge of portEdges) {
|
|
1172
|
+
const d = distanceToPortEdge(edge, canvasPos);
|
|
1173
|
+
if (d < nearestDist) {
|
|
1174
|
+
nearestDist = d;
|
|
1175
|
+
nearest = edge;
|
|
1057
1176
|
}
|
|
1058
1177
|
}
|
|
1059
|
-
return
|
|
1178
|
+
return nearest && nearestDist <= threshold ? nearest : null;
|
|
1060
1179
|
}
|
|
1061
1180
|
|
|
1062
|
-
function
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1181
|
+
function edgeLabelAtCanvasPoint(canvasPos) {
|
|
1182
|
+
if (!st.edgeLabelBBox || !st.edges) return null;
|
|
1183
|
+
|
|
1184
|
+
let bestEdgeId = null;
|
|
1185
|
+
let bestLevel = -1;
|
|
1186
|
+
for (const edge of st.edges.get()) {
|
|
1187
|
+
if (!edge || !edge.label) continue;
|
|
1188
|
+
const bbox = st.edgeLabelBBox[edge.id];
|
|
1189
|
+
if (!bbox) continue;
|
|
1190
|
+
|
|
1191
|
+
const r = -(bbox.rotation || 0);
|
|
1192
|
+
const dx = canvasPos.x - bbox.cx;
|
|
1193
|
+
const dy = canvasPos.y - bbox.cy;
|
|
1194
|
+
const lx = dx * Math.cos(r) - dy * Math.sin(r);
|
|
1195
|
+
const ly = dx * Math.sin(r) + dy * Math.cos(r);
|
|
1196
|
+
if (Math.abs(lx) > bbox.w / 2 || Math.abs(ly) > bbox.h / 2) continue;
|
|
1197
|
+
|
|
1198
|
+
const level = edgeDrawLevel(edge);
|
|
1199
|
+
if (level >= bestLevel) {
|
|
1200
|
+
bestLevel = level;
|
|
1201
|
+
bestEdgeId = edge.id;
|
|
1202
|
+
}
|
|
1073
1203
|
}
|
|
1204
|
+
return bestEdgeId;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function isEdgeInteractive(edge) {
|
|
1208
|
+
if (!edge) return false;
|
|
1209
|
+
const fromN = st.nodes.get(edge.from);
|
|
1210
|
+
const toN = st.nodes.get(edge.to);
|
|
1211
|
+
const isFreeArrow = fromN && fromN.shapeType === 'anchor' && toN && toN.shapeType === 'anchor';
|
|
1212
|
+
return isFreeArrow ? !(fromN.locked && toN.locked) : !edge.edgeLocked;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function selectableEdgesForNodes(nodeIds) {
|
|
1216
|
+
const selectedSet = new Set(nodeIds);
|
|
1217
|
+
return st.edges.get().filter((e) => {
|
|
1218
|
+
if (!selectedSet.has(e.from) || !selectedSet.has(e.to)) return false;
|
|
1219
|
+
return isEdgeInteractive(e);
|
|
1220
|
+
}).map((e) => e.id);
|
|
1221
|
+
}
|
|
1074
1222
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1223
|
+
function selectNodesFromClick(nodeId, srcEvent) {
|
|
1224
|
+
const additive = !!(srcEvent && (srcEvent.metaKey || srcEvent.ctrlKey));
|
|
1225
|
+
const clicked = expandSelectionToGroup([nodeId]).filter((id) => {
|
|
1226
|
+
const n = st.nodes.get(id);
|
|
1227
|
+
return n && !n.locked && n.shapeType !== 'anchor';
|
|
1228
|
+
});
|
|
1229
|
+
if (!clicked.length) return;
|
|
1230
|
+
|
|
1231
|
+
let nodeIds;
|
|
1232
|
+
if (additive) {
|
|
1233
|
+
const next = new Set(_pointerDownSelection.nodeIds || []);
|
|
1234
|
+
const allAlreadySelected = clicked.every((id) => next.has(id));
|
|
1235
|
+
clicked.forEach((id) => {
|
|
1236
|
+
if (allAlreadySelected) next.delete(id);
|
|
1237
|
+
else next.add(id);
|
|
1238
|
+
});
|
|
1239
|
+
nodeIds = Array.from(next).filter((id) => {
|
|
1240
|
+
const n = st.nodes.get(id);
|
|
1241
|
+
return n && !n.locked && n.shapeType !== 'anchor';
|
|
1242
|
+
});
|
|
1243
|
+
} else {
|
|
1244
|
+
nodeIds = clicked;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const edgeIds = selectableEdgesForNodes(nodeIds);
|
|
1248
|
+
_addingEdgesToSelection = true;
|
|
1249
|
+
st.network.setSelection({ nodes: nodeIds, edges: edgeIds });
|
|
1250
|
+
_addingEdgesToSelection = false;
|
|
1251
|
+
st.selectedNodeIds = nodeIds;
|
|
1252
|
+
st.selectedEdgeIds = edgeIds;
|
|
1253
|
+
_rehookEdgeId = _rehookHoveredNodeId = _rehookHoveredPortKey = null;
|
|
1254
|
+
|
|
1255
|
+
hideEdgePanel();
|
|
1256
|
+
if (nodeIds.length) showNodePanel();
|
|
1257
|
+
else {
|
|
1258
|
+
hideNodePanel();
|
|
1259
|
+
hideSelectionOverlay();
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function onDoubleClick(params) {
|
|
1264
|
+
const srcEvent = params.event && params.event.srcEvent;
|
|
1265
|
+
const clientPos = srcEvent ? { x: srcEvent.clientX, y: srcEvent.clientY } : null;
|
|
1266
|
+
const labelEdgeId = edgeLabelAtCanvasPoint(params.pointer.canvas);
|
|
1267
|
+
const nativeEdgeId = clientPos ? st.network.getEdgeAt(clientPos) : null;
|
|
1268
|
+
const portEdge = nearestPortEdgeAt(params.pointer.canvas);
|
|
1269
|
+
const edgeCandidates = [labelEdgeId, nativeEdgeId, portEdge && portEdge.id, ...params.edges]
|
|
1270
|
+
.filter((id, index, list) => id && list.indexOf(id) === index)
|
|
1271
|
+
.filter((id) => isEdgeInteractive(st.edges.get(id)));
|
|
1272
|
+
const edgeId = edgeCandidates.reduce((bestId, id) => {
|
|
1273
|
+
if (!bestId) return id;
|
|
1274
|
+
return edgeDrawLevel(st.edges.get(id)) >= edgeDrawLevel(st.edges.get(bestId)) ? id : bestId;
|
|
1275
|
+
}, null);
|
|
1276
|
+
|
|
1277
|
+
const topNodeId = topmostNodeAt(params.pointer.canvas);
|
|
1278
|
+
const topNode = topNodeId && st.nodes.get(topNodeId);
|
|
1279
|
+
const canEditTopNode = topNode && !topNode.locked && topNode.shapeType !== 'anchor';
|
|
1280
|
+
const topNodeWins = canEditTopNode && !labelEdgeId && (!edgeId || st.canonicalOrder.indexOf(topNodeId) >= edgeDrawLevel(st.edges.get(edgeId)));
|
|
1281
|
+
|
|
1282
|
+
if (topNodeWins) {
|
|
1283
|
+
st.selectedNodeIds = [topNodeId];
|
|
1284
|
+
st.selectedEdgeIds = [];
|
|
1285
|
+
st.network.setSelection({ nodes: st.selectedNodeIds, edges: [] });
|
|
1078
1286
|
showNodePanel();
|
|
1287
|
+
hideEdgePanel();
|
|
1079
1288
|
startLabelEdit();
|
|
1080
|
-
} else if (
|
|
1081
|
-
st.
|
|
1289
|
+
} else if (edgeId) {
|
|
1290
|
+
st.selectedNodeIds = [];
|
|
1291
|
+
st.selectedEdgeIds = [edgeId];
|
|
1292
|
+
st.network.setSelection({ nodes: [], edges: [edgeId] });
|
|
1293
|
+
hideNodePanel();
|
|
1082
1294
|
showEdgePanel();
|
|
1083
1295
|
startEdgeLabelEdit();
|
|
1084
1296
|
} else if (st.currentTool === 'addNode' && st.pendingShape === 'image') {
|
|
@@ -1229,8 +1441,8 @@ function onClickNode(params) {
|
|
|
1229
1441
|
update.color = { color: 'rgba(0,0,0,0)', highlight: 'rgba(0,0,0,0)', hover: 'rgba(0,0,0,0)' };
|
|
1230
1442
|
update.arrows = { to: { enabled: false }, from: { enabled: false } };
|
|
1231
1443
|
}
|
|
1232
|
-
st.edges.update(update);
|
|
1233
1444
|
pushSnapshot();
|
|
1445
|
+
st.edges.update(update);
|
|
1234
1446
|
markDirty();
|
|
1235
1447
|
const edgeId = edgeData.id;
|
|
1236
1448
|
_rehookHoveredNodeId = _rehookHoveredPortKey = null;
|
|
@@ -1244,14 +1456,34 @@ function onClickNode(params) {
|
|
|
1244
1456
|
}
|
|
1245
1457
|
}
|
|
1246
1458
|
|
|
1247
|
-
// When a node is reported, params.edges is
|
|
1459
|
+
// When a node is reported, params.edges is usually empty — vis-network short-circuits
|
|
1248
1460
|
// edge detection once a node is found. Fix: call getEdgeAt() directly with CLIENT
|
|
1249
1461
|
// coordinates. params.pointer.DOM is already offset-relative to the container, so
|
|
1250
1462
|
// passing it to getEdgeAt() causes a double-subtraction of the container rect and
|
|
1251
1463
|
// returns null. Passing clientX/Y lets vis-network do its own pixel-perfect detection.
|
|
1252
1464
|
if (params.nodes.length > 0) {
|
|
1253
1465
|
const clientPos = { x: params.event.srcEvent.clientX, y: params.event.srcEvent.clientY };
|
|
1254
|
-
const
|
|
1466
|
+
const nativeEdgeId = st.network.getEdgeAt(clientPos);
|
|
1467
|
+
const portEdge = nearestPortEdgeAt(params.pointer.canvas);
|
|
1468
|
+
const edgeId = nativeEdgeId || (portEdge && portEdge.id);
|
|
1469
|
+
|
|
1470
|
+
// First honour the app-managed z-order. vis-network may report an edge or a
|
|
1471
|
+
// lower node at the click point; compare draw levels so the visibly top
|
|
1472
|
+
// element gets the click.
|
|
1473
|
+
const top = topmostNodeAt(params.pointer.canvas);
|
|
1474
|
+
if (top) {
|
|
1475
|
+
const topNode = st.nodes.get(top);
|
|
1476
|
+
const topLevel = st.canonicalOrder.indexOf(top);
|
|
1477
|
+
const edgeLevel = edgeId ? edgeDrawLevel(st.edges.get(edgeId)) : -1;
|
|
1478
|
+
if (topNode && !topNode.locked && topNode.shapeType !== 'anchor') {
|
|
1479
|
+
if (!edgeId || topLevel >= edgeLevel) {
|
|
1480
|
+
setTimeout(() => {
|
|
1481
|
+
selectNodesFromClick(top, params.event.srcEvent);
|
|
1482
|
+
}, 0);
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1255
1487
|
if (edgeId) {
|
|
1256
1488
|
const edge = st.edges.get(edgeId);
|
|
1257
1489
|
const fromN = edge && st.nodes.get(edge.from);
|
|
@@ -1277,12 +1509,12 @@ function onClickNode(params) {
|
|
|
1277
1509
|
return;
|
|
1278
1510
|
}
|
|
1279
1511
|
// No edge at click position — apply z-order correction for the topmost node.
|
|
1280
|
-
const
|
|
1512
|
+
const fallbackTop = topmostNodeAt(params.pointer.canvas);
|
|
1281
1513
|
const clickable = params.nodes.filter(id => {
|
|
1282
1514
|
const n = st.nodes.get(id);
|
|
1283
1515
|
return n && !n.locked && n.shapeType !== 'anchor';
|
|
1284
1516
|
});
|
|
1285
|
-
if (!
|
|
1517
|
+
if (!fallbackTop || !clickable.includes(fallbackTop)) {
|
|
1286
1518
|
const fallbackEdgeId = params.edges.length > 0 ? params.edges[0] : null;
|
|
1287
1519
|
if (fallbackEdgeId) {
|
|
1288
1520
|
setTimeout(() => {
|
|
@@ -1305,19 +1537,12 @@ function onClickNode(params) {
|
|
|
1305
1537
|
// Also check the canvas for port-edge proximity when nothing was hit.
|
|
1306
1538
|
if (params.nodes.length === 0 && params.edges.length === 0) {
|
|
1307
1539
|
const cp = params.pointer.canvas;
|
|
1308
|
-
const THRESHOLD = 8;
|
|
1309
1540
|
|
|
1310
1541
|
// ── Port edge proximity check ──────────────────────────────────────────
|
|
1311
1542
|
// vis-network's hit detection uses the invisible centre-to-centre ghost,
|
|
1312
1543
|
// so port edges that diverge visually from that path are not selectable.
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
let nearest = null, nearestDist = Infinity;
|
|
1316
|
-
for (const edge of portEdges) {
|
|
1317
|
-
const d = distanceToPortEdge(edge, cp);
|
|
1318
|
-
if (d < nearestDist) { nearestDist = d; nearest = edge; }
|
|
1319
|
-
}
|
|
1320
|
-
if (nearest && nearestDist <= THRESHOLD) {
|
|
1544
|
+
const nearest = nearestPortEdgeAt(cp);
|
|
1545
|
+
if (nearest) {
|
|
1321
1546
|
st.network.selectEdges([nearest.id]);
|
|
1322
1547
|
st.selectedEdgeIds = [nearest.id];
|
|
1323
1548
|
st.selectedNodeIds = [];
|
|
@@ -21,6 +21,29 @@ function isEdgeLocked(edge) {
|
|
|
21
21
|
return isFreeArrow ? !!(fromN.locked && toN.locked) : !!edge.edgeLocked;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function hitInteractiveEdgeLabel(container, clientX, clientY) {
|
|
25
|
+
if (!st.network || !st.edgeLabelBBox || !st.selectedEdgeIds || st.selectedEdgeIds.length !== 1) return false;
|
|
26
|
+
const edgeId = st.selectedEdgeIds[0];
|
|
27
|
+
const edge = st.edges && st.edges.get(edgeId);
|
|
28
|
+
if (!edge || !edge.label) return false;
|
|
29
|
+
const bbox = st.edgeLabelBBox[edgeId];
|
|
30
|
+
if (!bbox) return false;
|
|
31
|
+
|
|
32
|
+
const rect = container.getBoundingClientRect();
|
|
33
|
+
const cp = st.network.DOMtoCanvas({ x: clientX - rect.left, y: clientY - rect.top });
|
|
34
|
+
const r = -(bbox.rotation || 0);
|
|
35
|
+
const dx = cp.x - bbox.cx;
|
|
36
|
+
const dy = cp.y - bbox.cy;
|
|
37
|
+
const lx = dx * Math.cos(r) - dy * Math.sin(r);
|
|
38
|
+
const ly = dx * Math.sin(r) + dy * Math.cos(r);
|
|
39
|
+
|
|
40
|
+
const handleRadius = 8 / st.network.getScale();
|
|
41
|
+
const onLeftHandle = Math.hypot(lx + bbox.w / 2, ly) <= handleRadius;
|
|
42
|
+
const onRightHandle = Math.hypot(lx - bbox.w / 2, ly) <= handleRadius;
|
|
43
|
+
const insideLabelBox = Math.abs(lx) <= bbox.w / 2 && Math.abs(ly) <= bbox.h / 2;
|
|
44
|
+
return onLeftHandle || onRightHandle || insideLabelBox;
|
|
45
|
+
}
|
|
46
|
+
|
|
24
47
|
// Returns a target descriptor ({type, id, ...}) if the DOM point lands on a
|
|
25
48
|
// locked node or locked edge, otherwise null.
|
|
26
49
|
function hitTestLocked(container, clientX, clientY) {
|
|
@@ -154,6 +177,7 @@ function onUp() {
|
|
|
154
177
|
export function installUnlockHold(container) {
|
|
155
178
|
container.addEventListener('mousedown', (e) => {
|
|
156
179
|
if (e.button !== 0) return;
|
|
180
|
+
if (hitInteractiveEdgeLabel(container, e.clientX, e.clientY)) return;
|
|
157
181
|
const target = hitTestLocked(container, e.clientX, e.clientY);
|
|
158
182
|
if (!target) return;
|
|
159
183
|
|
|
@@ -1902,8 +1902,8 @@
|
|
|
1902
1902
|
"groupId": null,
|
|
1903
1903
|
"nodeLink": null,
|
|
1904
1904
|
"locked": false,
|
|
1905
|
-
"x": -
|
|
1906
|
-
"y":
|
|
1905
|
+
"x": -164,
|
|
1906
|
+
"y": 84
|
|
1907
1907
|
},
|
|
1908
1908
|
{
|
|
1909
1909
|
"id": "n1",
|
|
@@ -1996,50 +1996,50 @@
|
|
|
1996
1996
|
"dashes": false,
|
|
1997
1997
|
"fontSize": null,
|
|
1998
1998
|
"labelRotation": 0,
|
|
1999
|
-
"edgeLabelOffsetX":
|
|
2000
|
-
"edgeLabelOffsetY": -
|
|
2001
|
-
"fromPort":
|
|
2002
|
-
"toPort":
|
|
1999
|
+
"edgeLabelOffsetX": 31,
|
|
2000
|
+
"edgeLabelOffsetY": -22,
|
|
2001
|
+
"fromPort": "E",
|
|
2002
|
+
"toPort": "W",
|
|
2003
2003
|
"edgeColor": null,
|
|
2004
2004
|
"edgeWidth": null,
|
|
2005
2005
|
"edgeLocked": false,
|
|
2006
2006
|
"edgeLabelWidth": 209.01385498046875
|
|
2007
2007
|
},
|
|
2008
2008
|
{
|
|
2009
|
-
"id": "
|
|
2010
|
-
"from": "
|
|
2011
|
-
"to": "
|
|
2012
|
-
"label": "
|
|
2013
|
-
"arrowDir": "
|
|
2009
|
+
"id": "e3",
|
|
2010
|
+
"from": "n1",
|
|
2011
|
+
"to": "n4",
|
|
2012
|
+
"label": "reads from / writes to",
|
|
2013
|
+
"arrowDir": "both",
|
|
2014
2014
|
"dashes": false,
|
|
2015
2015
|
"fontSize": null,
|
|
2016
2016
|
"labelRotation": 0,
|
|
2017
|
-
"edgeLabelOffsetX":
|
|
2018
|
-
"edgeLabelOffsetY":
|
|
2019
|
-
"fromPort":
|
|
2020
|
-
"toPort":
|
|
2017
|
+
"edgeLabelOffsetX": 0,
|
|
2018
|
+
"edgeLabelOffsetY": 0,
|
|
2019
|
+
"fromPort": "S",
|
|
2020
|
+
"toPort": "N",
|
|
2021
2021
|
"edgeColor": null,
|
|
2022
2022
|
"edgeWidth": null,
|
|
2023
2023
|
"edgeLocked": false,
|
|
2024
|
-
"edgeLabelWidth":
|
|
2024
|
+
"edgeLabelWidth": 126.26385498046875
|
|
2025
2025
|
},
|
|
2026
2026
|
{
|
|
2027
|
-
"id": "
|
|
2028
|
-
"from": "
|
|
2029
|
-
"to": "
|
|
2030
|
-
"label": "
|
|
2031
|
-
"arrowDir": "
|
|
2027
|
+
"id": "e1777978776487",
|
|
2028
|
+
"from": "n3",
|
|
2029
|
+
"to": "n1",
|
|
2030
|
+
"label": "browses and edits docs via",
|
|
2031
|
+
"arrowDir": "to",
|
|
2032
2032
|
"dashes": false,
|
|
2033
2033
|
"fontSize": null,
|
|
2034
2034
|
"labelRotation": 0,
|
|
2035
2035
|
"edgeLabelOffsetX": 0,
|
|
2036
2036
|
"edgeLabelOffsetY": 0,
|
|
2037
|
-
"fromPort":
|
|
2038
|
-
"toPort":
|
|
2037
|
+
"fromPort": "E",
|
|
2038
|
+
"toPort": "W",
|
|
2039
2039
|
"edgeColor": null,
|
|
2040
2040
|
"edgeWidth": null,
|
|
2041
2041
|
"edgeLocked": false,
|
|
2042
|
-
"edgeLabelWidth":
|
|
2042
|
+
"edgeLabelWidth": 178.28289794921875
|
|
2043
2043
|
}
|
|
2044
2044
|
],
|
|
2045
2045
|
"edgesStraight": false,
|