living-documentation 7.43.0 → 7.45.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/src/frontend/diagram/custom-shapes.js +11 -3
- package/dist/src/frontend/diagram/main.js +3 -1
- package/dist/src/frontend/diagram/network.js +1000 -497
- package/dist/src/frontend/diagram/node-panel.js +47 -0
- package/dist/src/frontend/diagram/node-rendering.js +117 -23
- package/dist/src/frontend/diagram/persistence.js +1 -0
- package/dist/src/frontend/diagram/ports.js +7 -5
- package/dist/src/frontend/diagram/selection-overlay.js +64 -13
- package/dist/src/frontend/diagram.html +34 -0
- package/dist/src/frontend/i18n/en.json +14 -1
- package/dist/src/frontend/i18n/fr.json +14 -1
- package/dist/src/frontend/shape-editor.html +385 -165
- package/dist/src/routes/shape-libraries.d.ts +1 -1
- package/dist/src/routes/shape-libraries.d.ts.map +1 -1
- package/dist/src/routes/shape-libraries.js +46 -26
- package/dist/src/routes/shape-libraries.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,38 +2,62 @@
|
|
|
2
2
|
// Creates the vis.js Network, patches _drawNodes for canonical z-order,
|
|
3
3
|
// and wires all network-level events.
|
|
4
4
|
|
|
5
|
-
import { st, markDirty } from
|
|
6
|
-
import { pushSnapshot }
|
|
7
|
-
import { visNodeProps, SHAPE_DEFAULTS }
|
|
8
|
-
import { uploadImageFile } from
|
|
9
|
-
import { promptImageName } from
|
|
10
|
-
import { showToast }
|
|
11
|
-
import { t }
|
|
12
|
-
import { visEdgeProps }
|
|
13
|
-
import { showNodePanel, hideNodePanel } from
|
|
14
|
-
import { showEdgePanel, hideEdgePanel } from
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
|
|
5
|
+
import { st, markDirty } from "./state.js";
|
|
6
|
+
import { pushSnapshot } from "./history.js";
|
|
7
|
+
import { visNodeProps, SHAPE_DEFAULTS } from "./node-rendering.js";
|
|
8
|
+
import { uploadImageFile } from "./image-upload.js";
|
|
9
|
+
import { promptImageName } from "./image-name-modal.js";
|
|
10
|
+
import { showToast } from "./toast.js";
|
|
11
|
+
import { t } from "./t.js";
|
|
12
|
+
import { visEdgeProps } from "./edge-rendering.js";
|
|
13
|
+
import { showNodePanel, hideNodePanel } from "./node-panel.js";
|
|
14
|
+
import { showEdgePanel, hideEdgePanel } from "./edge-panel.js";
|
|
15
|
+
import {
|
|
16
|
+
startLabelEdit,
|
|
17
|
+
startEdgeLabelEdit,
|
|
18
|
+
commitLabelEdit,
|
|
19
|
+
hideLabelInput,
|
|
20
|
+
} from "./label-editor.js";
|
|
21
|
+
import {
|
|
22
|
+
updateSelectionOverlay,
|
|
23
|
+
hideSelectionOverlay,
|
|
24
|
+
} from "./selection-overlay.js";
|
|
25
|
+
import { drawGrid, onDragEnd, snapToGrid } from "./grid.js";
|
|
26
|
+
import {
|
|
27
|
+
onDragging,
|
|
28
|
+
drawAlignmentGuides,
|
|
29
|
+
clearAlignGuides,
|
|
30
|
+
} from "./alignment.js";
|
|
31
|
+
import { drawDebugOverlay } from "./debug.js";
|
|
32
|
+
import { updateZoomDisplay } from "./zoom.js";
|
|
33
|
+
import { expandSelectionToGroup, drawGroupOutlines } from "./groups.js";
|
|
34
|
+
import { navigateNodeLink, hideLinkPanel } from "./link-panel.js";
|
|
35
|
+
import {
|
|
36
|
+
getNearestPort,
|
|
37
|
+
getPortPosition,
|
|
38
|
+
drawPortDots,
|
|
39
|
+
drawPortEdge,
|
|
40
|
+
distanceToPortEdge,
|
|
41
|
+
wrapText,
|
|
42
|
+
} from "./ports.js";
|
|
43
|
+
import { getLastFreeArrowStyle } from "./edge-panel.js";
|
|
44
|
+
import { getLastNodeStyle } from "./node-panel.js";
|
|
45
|
+
import { installUnlockHold } from "./unlock-hold.js";
|
|
46
|
+
import {
|
|
47
|
+
CUSTOM_SHAPE_TYPE,
|
|
48
|
+
customShapeIdFromTool,
|
|
49
|
+
getCustomShapeDefaultSize,
|
|
50
|
+
getCustomShapeDefinition,
|
|
51
|
+
} from "./custom-shapes.js";
|
|
28
52
|
|
|
29
53
|
// Module-level port-hover state — shared between initNetwork event handlers and
|
|
30
54
|
// module-level helpers (_onAnchorSnapConnect).
|
|
31
55
|
let _hoveredPortNodeId = null;
|
|
32
|
-
let _hoveredPortKey
|
|
56
|
+
let _hoveredPortKey = null;
|
|
33
57
|
let _draggingAnchorIds = new Set();
|
|
34
58
|
// Rehook state: tracks which port is hovered while a port edge is selected.
|
|
35
|
-
let _rehookEdgeId
|
|
36
|
-
let _rehookHoveredNodeId
|
|
59
|
+
let _rehookEdgeId = null;
|
|
60
|
+
let _rehookHoveredNodeId = null;
|
|
37
61
|
let _rehookHoveredPortKey = null;
|
|
38
62
|
let _pointerDownSelection = { nodeIds: [], edgeIds: [] };
|
|
39
63
|
let _edgeLabelPointerAbort = null;
|
|
@@ -43,62 +67,116 @@ let _edgeLabelPointerAbort = null;
|
|
|
43
67
|
function isRehookable(edgeData) {
|
|
44
68
|
if (!edgeData) return false;
|
|
45
69
|
const fromNode = st.nodes.get(edgeData.from);
|
|
46
|
-
const toNode
|
|
47
|
-
return
|
|
70
|
+
const toNode = st.nodes.get(edgeData.to);
|
|
71
|
+
return (
|
|
72
|
+
!!(fromNode && fromNode.shapeType !== "anchor") ||
|
|
73
|
+
!!(toNode && toNode.shapeType !== "anchor")
|
|
74
|
+
);
|
|
48
75
|
}
|
|
49
76
|
|
|
50
77
|
export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
51
|
-
const container = document.getElementById(
|
|
78
|
+
const container = document.getElementById("vis-canvas");
|
|
52
79
|
|
|
53
|
-
const edgeSmooth = edgesStraight
|
|
80
|
+
const edgeSmooth = edgesStraight
|
|
81
|
+
? { enabled: false }
|
|
82
|
+
: { type: "continuous" };
|
|
54
83
|
|
|
55
84
|
st.nodes = new vis.DataSet(
|
|
56
85
|
savedNodes.map((n) => {
|
|
57
|
-
const shapeType = n.shapeType || n.renderAs ||
|
|
86
|
+
const shapeType = n.shapeType || n.renderAs || "box";
|
|
58
87
|
return {
|
|
59
88
|
...n,
|
|
60
89
|
shapeType,
|
|
61
|
-
...visNodeProps(
|
|
90
|
+
...visNodeProps(
|
|
91
|
+
shapeType,
|
|
92
|
+
n.colorKey || "c-gray",
|
|
93
|
+
n.nodeWidth,
|
|
94
|
+
n.nodeHeight,
|
|
95
|
+
n.fontSize,
|
|
96
|
+
n.textAlign,
|
|
97
|
+
n.textValign,
|
|
98
|
+
),
|
|
62
99
|
...(n.locked ? { fixed: { x: true, y: true }, draggable: false } : {}),
|
|
63
100
|
};
|
|
64
|
-
})
|
|
101
|
+
}),
|
|
65
102
|
);
|
|
66
103
|
st.edges = new vis.DataSet(
|
|
67
104
|
savedEdges.map((e) => {
|
|
68
105
|
const toNode = savedNodes.find((n) => n.id === e.to);
|
|
69
|
-
const isAnchor = toNode && toNode.shapeType ===
|
|
106
|
+
const isAnchor = toNode && toNode.shapeType === "anchor";
|
|
70
107
|
const edgeObj = {
|
|
71
108
|
...e,
|
|
72
|
-
...visEdgeProps(e.arrowDir ??
|
|
109
|
+
...visEdgeProps(e.arrowDir ?? "to", e.dashes ?? false),
|
|
73
110
|
smooth: isAnchor ? { enabled: false } : edgeSmooth,
|
|
74
|
-
...(e.edgeColor
|
|
111
|
+
...(e.edgeColor
|
|
112
|
+
? {
|
|
113
|
+
color: {
|
|
114
|
+
color: e.edgeColor,
|
|
115
|
+
highlight: "#f97316",
|
|
116
|
+
hover: "#f97316",
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
: {}),
|
|
75
120
|
...(e.edgeWidth ? { width: e.edgeWidth } : {}),
|
|
76
121
|
// Edge labels are always drawn by drawEdgeLabels() in afterDrawing
|
|
77
122
|
// (gives us positioning + rotation control). Hide vis-network's native
|
|
78
123
|
// label text entirely so it never appears alongside ours.
|
|
79
124
|
...(e.label
|
|
80
|
-
? {
|
|
125
|
+
? {
|
|
126
|
+
font: {
|
|
127
|
+
size: e.fontSize || 11,
|
|
128
|
+
align: "middle",
|
|
129
|
+
color: "rgba(0,0,0,0)",
|
|
130
|
+
},
|
|
131
|
+
}
|
|
81
132
|
: e.fontSize
|
|
82
|
-
? { font: { size: e.fontSize,
|
|
83
|
-
: {}
|
|
84
|
-
),
|
|
133
|
+
? { font: { size: e.fontSize, align: "middle", color: "#6b7280" } }
|
|
134
|
+
: {}),
|
|
85
135
|
};
|
|
86
136
|
// Port edges: hide vis-network's own rendering (line + arrowhead).
|
|
87
137
|
// drawPortEdge() handles all visual output; vis-network edge is a
|
|
88
138
|
// transparent ghost kept only for hit-detection (click selection).
|
|
89
139
|
if (e.fromPort || e.toPort) {
|
|
90
|
-
edgeObj.color
|
|
140
|
+
edgeObj.color = {
|
|
141
|
+
color: "rgba(0,0,0,0)",
|
|
142
|
+
highlight: "rgba(0,0,0,0)",
|
|
143
|
+
hover: "rgba(0,0,0,0)",
|
|
144
|
+
};
|
|
91
145
|
edgeObj.arrows = { to: { enabled: false }, from: { enabled: false } };
|
|
92
146
|
}
|
|
93
147
|
return edgeObj;
|
|
94
|
-
})
|
|
148
|
+
}),
|
|
95
149
|
);
|
|
96
150
|
|
|
97
151
|
const options = {
|
|
98
152
|
physics: { enabled: false },
|
|
99
|
-
interaction: {
|
|
100
|
-
|
|
101
|
-
|
|
153
|
+
interaction: {
|
|
154
|
+
hover: true,
|
|
155
|
+
navigationButtons: false,
|
|
156
|
+
keyboard: false,
|
|
157
|
+
multiselect: true,
|
|
158
|
+
},
|
|
159
|
+
nodes: {
|
|
160
|
+
font: { size: 13, face: "system-ui,-apple-system,sans-serif" },
|
|
161
|
+
borderWidth: 1.5,
|
|
162
|
+
borderWidthSelected: 2.5,
|
|
163
|
+
shadow: false,
|
|
164
|
+
widthConstraint: { minimum: 60 },
|
|
165
|
+
heightConstraint: { minimum: 28 },
|
|
166
|
+
},
|
|
167
|
+
edges: {
|
|
168
|
+
smooth: edgeSmooth,
|
|
169
|
+
color: { color: "#a8a29e", highlight: "#f97316", hover: "#f97316" },
|
|
170
|
+
width: 1.5,
|
|
171
|
+
selectionWidth: 2.5,
|
|
172
|
+
font: {
|
|
173
|
+
size: 11,
|
|
174
|
+
align: "middle",
|
|
175
|
+
color: "rgba(0,0,0,0)",
|
|
176
|
+
strokeColor: "rgba(0,0,0,0)",
|
|
177
|
+
background: "rgba(0,0,0,0)",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
102
180
|
manipulation: {
|
|
103
181
|
enabled: false,
|
|
104
182
|
addEdge(data, callback) {
|
|
@@ -107,34 +185,53 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
107
185
|
// contains it: that target is acting as a background surface, and the
|
|
108
186
|
// mouseup handler turns the gesture into a free-arrow endpoint instead.
|
|
109
187
|
const fromNode = st.nodes.get(data.from) || {};
|
|
110
|
-
const toNode
|
|
111
|
-
const targetContainsSource = lowerZTargetContainsSource(
|
|
112
|
-
|
|
188
|
+
const toNode = st.nodes.get(data.to) || {};
|
|
189
|
+
const targetContainsSource = lowerZTargetContainsSource(
|
|
190
|
+
data.from,
|
|
191
|
+
data.to,
|
|
192
|
+
);
|
|
193
|
+
if (
|
|
194
|
+
fromNode.locked ||
|
|
195
|
+
toNode.locked ||
|
|
196
|
+
fromNode.shapeType === "anchor" ||
|
|
197
|
+
toNode.shapeType === "anchor" ||
|
|
198
|
+
targetContainsSource
|
|
199
|
+
) {
|
|
113
200
|
_addEdgeFromPort = null;
|
|
114
|
-
setTimeout(() => {
|
|
201
|
+
setTimeout(() => {
|
|
202
|
+
if (st.currentTool === "addEdge") st.network.addEdgeMode();
|
|
203
|
+
}, 0);
|
|
115
204
|
return;
|
|
116
205
|
}
|
|
117
206
|
pushSnapshot();
|
|
118
|
-
data.id
|
|
119
|
-
data.arrowDir =
|
|
120
|
-
data.dashes
|
|
207
|
+
data.id = "e" + Date.now();
|
|
208
|
+
data.arrowDir = "to";
|
|
209
|
+
data.dashes = false;
|
|
121
210
|
// Attach captured port selections — skip for anchor nodes.
|
|
122
|
-
const fromIsAnchor = fromNode.shapeType ===
|
|
123
|
-
const toIsAnchor
|
|
211
|
+
const fromIsAnchor = fromNode.shapeType === "anchor";
|
|
212
|
+
const toIsAnchor = toNode.shapeType === "anchor";
|
|
124
213
|
if (!fromIsAnchor) data.fromPort = _addEdgeFromPort || null;
|
|
125
|
-
if (!toIsAnchor)
|
|
126
|
-
Object.assign(data, visEdgeProps(
|
|
214
|
+
if (!toIsAnchor) data.toPort = _hoveredPortKey || null;
|
|
215
|
+
Object.assign(data, visEdgeProps("to", false));
|
|
127
216
|
// Port edges: make vis-network's ghost transparent so only drawPortEdge is visible.
|
|
128
217
|
if (data.fromPort || data.toPort) {
|
|
129
|
-
data.color
|
|
218
|
+
data.color = {
|
|
219
|
+
color: "rgba(0,0,0,0)",
|
|
220
|
+
highlight: "rgba(0,0,0,0)",
|
|
221
|
+
hover: "rgba(0,0,0,0)",
|
|
222
|
+
};
|
|
130
223
|
data.arrows = { to: { enabled: false }, from: { enabled: false } };
|
|
131
224
|
}
|
|
132
225
|
callback(data);
|
|
133
226
|
markDirty();
|
|
134
227
|
_addEdgeFromPort = null;
|
|
135
228
|
setTimeout(() => {
|
|
136
|
-
if (st.currentTool ===
|
|
137
|
-
window.dispatchEvent(
|
|
229
|
+
if (st.currentTool === "addEdge") {
|
|
230
|
+
window.dispatchEvent(
|
|
231
|
+
new CustomEvent("diagram:setTool", {
|
|
232
|
+
detail: { tool: "select" },
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
138
235
|
}
|
|
139
236
|
}, 0);
|
|
140
237
|
},
|
|
@@ -142,19 +239,27 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
142
239
|
};
|
|
143
240
|
|
|
144
241
|
if (st.network) st.network.destroy();
|
|
145
|
-
st.network = new vis.Network(
|
|
242
|
+
st.network = new vis.Network(
|
|
243
|
+
container,
|
|
244
|
+
{ nodes: st.nodes, edges: st.edges },
|
|
245
|
+
options,
|
|
246
|
+
);
|
|
146
247
|
|
|
147
248
|
// ── Lock interception ───────────────────────────────────────────────────────
|
|
148
249
|
// Must be registered BEFORE any other capture-phase mousedown listeners on
|
|
149
250
|
// the container so it can stopImmediatePropagation for locked targets.
|
|
150
251
|
installUnlockHold(container);
|
|
151
|
-
container.addEventListener(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
252
|
+
container.addEventListener(
|
|
253
|
+
"mousedown",
|
|
254
|
+
(e) => {
|
|
255
|
+
if (e.button !== 0) return;
|
|
256
|
+
_pointerDownSelection = {
|
|
257
|
+
nodeIds: [...(st.selectedNodeIds || [])],
|
|
258
|
+
edgeIds: [...(st.selectedEdgeIds || [])],
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
{ capture: true },
|
|
262
|
+
);
|
|
158
263
|
|
|
159
264
|
// ── Z-order patch ──────────────────────────────────────────────────────────
|
|
160
265
|
// vis.js renders in 3 passes (normal → selected → hovered), which breaks
|
|
@@ -164,10 +269,18 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
164
269
|
st.network.renderer._drawNodes = function (ctx, alwaysShow = false) {
|
|
165
270
|
const bodyNodes = this.body.nodes;
|
|
166
271
|
const bodyEdges = this.body.edges;
|
|
167
|
-
const margin
|
|
168
|
-
const topLeft
|
|
169
|
-
const bottomRight = this.canvas.DOMtoCanvas({
|
|
170
|
-
|
|
272
|
+
const margin = 20;
|
|
273
|
+
const topLeft = this.canvas.DOMtoCanvas({ x: -margin, y: -margin });
|
|
274
|
+
const bottomRight = this.canvas.DOMtoCanvas({
|
|
275
|
+
x: this.canvas.frame.canvas.clientWidth + margin,
|
|
276
|
+
y: this.canvas.frame.canvas.clientHeight + margin,
|
|
277
|
+
});
|
|
278
|
+
const viewableArea = {
|
|
279
|
+
top: topLeft.y,
|
|
280
|
+
left: topLeft.x,
|
|
281
|
+
bottom: bottomRight.y,
|
|
282
|
+
right: bottomRight.x,
|
|
283
|
+
};
|
|
171
284
|
|
|
172
285
|
// Build a map: canonical index → list of edges whose topmost endpoint is at that index.
|
|
173
286
|
const orderMap = new Map();
|
|
@@ -189,11 +302,11 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
189
302
|
// centre origin — otherwise the ghost line is visible from the node's
|
|
190
303
|
// centre instead of appearing to start at its boundary/port.
|
|
191
304
|
const fromData = st.nodes.get(edge.fromId);
|
|
192
|
-
const toData
|
|
193
|
-
const fromIsAnchor = fromData && fromData.shapeType ===
|
|
194
|
-
const toIsAnchor
|
|
305
|
+
const toData = st.nodes.get(edge.toId);
|
|
306
|
+
const fromIsAnchor = fromData && fromData.shapeType === "anchor";
|
|
307
|
+
const toIsAnchor = toData && toData.shapeType === "anchor";
|
|
195
308
|
const fromLevel = orderMap.get(edge.fromId);
|
|
196
|
-
const toLevel
|
|
309
|
+
const toLevel = orderMap.get(edge.toId);
|
|
197
310
|
let level;
|
|
198
311
|
if (fromLevel === undefined && toLevel === undefined) {
|
|
199
312
|
level = 0;
|
|
@@ -218,22 +331,24 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
218
331
|
for (const [level, edges] of edgesByLevel) {
|
|
219
332
|
for (const edge of edges) {
|
|
220
333
|
const fromData = st.nodes.get(edge.fromId);
|
|
221
|
-
const toData
|
|
222
|
-
if (toData
|
|
223
|
-
|
|
334
|
+
const toData = st.nodes.get(edge.toId);
|
|
335
|
+
if (toData && toData.shapeType === "anchor")
|
|
336
|
+
anchorEdgeLevel.set(edge.toId, level);
|
|
337
|
+
if (fromData && fromData.shapeType === "anchor")
|
|
338
|
+
anchorEdgeLevel.set(edge.fromId, level);
|
|
224
339
|
}
|
|
225
340
|
}
|
|
226
341
|
|
|
227
342
|
// Anchors with no connected edge are drawn at level 0 (bottom).
|
|
228
343
|
for (const id of st.canonicalOrder) {
|
|
229
344
|
const n = st.nodes.get(id);
|
|
230
|
-
if (!n || n.shapeType !==
|
|
345
|
+
if (!n || n.shapeType !== "anchor") continue;
|
|
231
346
|
if (!anchorEdgeLevel.has(id)) anchorEdgeLevel.set(id, 0);
|
|
232
347
|
}
|
|
233
348
|
|
|
234
349
|
for (let i = 0; i < st.canonicalOrder.length; i++) {
|
|
235
350
|
const id = st.canonicalOrder[i];
|
|
236
|
-
const n
|
|
351
|
+
const n = st.nodes.get(id);
|
|
237
352
|
|
|
238
353
|
// Draw edges whose level is i, before drawing the node at level i.
|
|
239
354
|
const edges = edgesByLevel.get(i);
|
|
@@ -254,7 +369,10 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
254
369
|
if (level !== i) continue;
|
|
255
370
|
const anchorNode = bodyNodes[anchorId];
|
|
256
371
|
if (!anchorNode) continue;
|
|
257
|
-
if (
|
|
372
|
+
if (
|
|
373
|
+
alwaysShow === true ||
|
|
374
|
+
anchorNode.isBoundingBoxOverlappingWith(viewableArea) === true
|
|
375
|
+
) {
|
|
258
376
|
anchorNode.draw(ctx);
|
|
259
377
|
} else {
|
|
260
378
|
anchorNode.updateBoundingBox(ctx, anchorNode.selected);
|
|
@@ -263,7 +381,7 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
263
381
|
}
|
|
264
382
|
|
|
265
383
|
// Skip anchor nodes here — they were drawn right after their edge above.
|
|
266
|
-
if (n && n.shapeType ===
|
|
384
|
+
if (n && n.shapeType === "anchor") continue;
|
|
267
385
|
|
|
268
386
|
const node = bodyNodes[id];
|
|
269
387
|
if (!node) continue;
|
|
@@ -280,84 +398,117 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
280
398
|
// We neutralise _drawEdges (make it a no-op) and instead draw each edge
|
|
281
399
|
// inside _drawNodes, just before the node whose canonical index equals the
|
|
282
400
|
// max index of its two endpoints. This guarantees true z-order interleaving.
|
|
283
|
-
st.network.renderer._drawEdges = function () {
|
|
401
|
+
st.network.renderer._drawEdges = function () {
|
|
402
|
+
/* no-op — edges drawn in _drawNodes */
|
|
403
|
+
};
|
|
284
404
|
|
|
285
405
|
// Keep canonicalOrder in sync with DataSet add/remove events
|
|
286
|
-
st.nodes.on(
|
|
406
|
+
st.nodes.on("add", (_, { items }) => {
|
|
287
407
|
const existing = new Set(st.canonicalOrder);
|
|
288
|
-
items.forEach((id) => {
|
|
408
|
+
items.forEach((id) => {
|
|
409
|
+
if (!existing.has(id)) st.canonicalOrder.push(id);
|
|
410
|
+
});
|
|
289
411
|
});
|
|
290
|
-
st.nodes.on(
|
|
412
|
+
st.nodes.on("remove", (_, { items, oldData }) => {
|
|
291
413
|
const removed = new Set(items);
|
|
292
414
|
st.canonicalOrder = st.canonicalOrder.filter((id) => !removed.has(id));
|
|
293
415
|
// If an anchor was deleted directly, also remove its connected edges.
|
|
294
416
|
(oldData || []).forEach((n) => {
|
|
295
|
-
if (n.shapeType !==
|
|
296
|
-
const connected = st.edges.get({
|
|
417
|
+
if (n.shapeType !== "anchor") return;
|
|
418
|
+
const connected = st.edges.get({
|
|
419
|
+
filter: (e) => e.from === n.id || e.to === n.id,
|
|
420
|
+
});
|
|
297
421
|
if (connected.length) st.edges.remove(connected.map((e) => e.id));
|
|
298
422
|
});
|
|
299
423
|
});
|
|
300
424
|
|
|
301
425
|
// When an edge is removed, delete any anchor node that has no remaining edges.
|
|
302
|
-
st.edges.on(
|
|
426
|
+
st.edges.on("remove", (_, { oldData }) => {
|
|
303
427
|
const anchorsToCheck = new Set();
|
|
304
428
|
(oldData || []).forEach((edge) => {
|
|
305
|
-
const toData
|
|
429
|
+
const toData = st.nodes.get(edge.to);
|
|
306
430
|
const fromData = st.nodes.get(edge.from);
|
|
307
|
-
if (toData
|
|
308
|
-
if (fromData && fromData.shapeType ===
|
|
431
|
+
if (toData && toData.shapeType === "anchor") anchorsToCheck.add(edge.to);
|
|
432
|
+
if (fromData && fromData.shapeType === "anchor")
|
|
433
|
+
anchorsToCheck.add(edge.from);
|
|
309
434
|
});
|
|
310
435
|
anchorsToCheck.forEach((anchorId) => {
|
|
311
|
-
const remaining = st.edges.get({
|
|
436
|
+
const remaining = st.edges.get({
|
|
437
|
+
filter: (e) => e.from === anchorId || e.to === anchorId,
|
|
438
|
+
});
|
|
312
439
|
if (remaining.length === 0) st.nodes.remove(anchorId);
|
|
313
440
|
});
|
|
314
441
|
});
|
|
315
442
|
|
|
316
|
-
st.network.on(
|
|
317
|
-
st.network.on(
|
|
318
|
-
st.network.on(
|
|
319
|
-
st.network.on(
|
|
320
|
-
st.network.on(
|
|
321
|
-
st.network.on(
|
|
322
|
-
st.network.on(
|
|
323
|
-
st.network.on(
|
|
324
|
-
st.network.on(
|
|
325
|
-
st.network.on(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
443
|
+
st.network.on("click", onClickNode);
|
|
444
|
+
st.network.on("doubleClick", onDoubleClick);
|
|
445
|
+
st.network.on("dragStart", onDragStart);
|
|
446
|
+
st.network.on("selectNode", onSelectNode);
|
|
447
|
+
st.network.on("deselectNode", onDeselectAll);
|
|
448
|
+
st.network.on("selectEdge", onSelectEdge);
|
|
449
|
+
st.network.on("deselectEdge", onDeselectAll);
|
|
450
|
+
st.network.on("zoom", updateZoomDisplay);
|
|
451
|
+
st.network.on("dragging", onDragging);
|
|
452
|
+
st.network.on("dragEnd", (p) => {
|
|
453
|
+
_draggingAnchorIds.clear();
|
|
454
|
+
_hoveredPortNodeId = null;
|
|
455
|
+
_hoveredPortKey = null;
|
|
456
|
+
_onAnchorSnapConnect(p);
|
|
457
|
+
onDragEnd(p);
|
|
458
|
+
clearAlignGuides();
|
|
459
|
+
});
|
|
460
|
+
st.network.on("beforeDrawing", drawGrid);
|
|
461
|
+
st.network.on("afterDrawing", updateSelectionOverlay);
|
|
462
|
+
st.network.on("afterDrawing", drawAlignmentGuides);
|
|
463
|
+
st.network.on("afterDrawing", (ctx) => drawGroupOutlines(ctx));
|
|
464
|
+
st.network.on("afterDrawing", () => drawDebugOverlay());
|
|
465
|
+
st.network.on("afterDrawing", drawEdgeLabels);
|
|
466
|
+
st.network.on("afterDrawing", (ctx) => {
|
|
467
|
+
if (
|
|
468
|
+
_hoveredPortNodeId &&
|
|
469
|
+
(st.currentTool === "addEdge" || _draggingAnchorIds.size > 0)
|
|
470
|
+
) {
|
|
334
471
|
drawPortDots(ctx, _hoveredPortNodeId, _hoveredPortKey);
|
|
335
472
|
}
|
|
336
473
|
// Rehook: show port dots on both endpoints of the selected port edge so
|
|
337
474
|
// the user can click a different port to reconnect that end of the arrow.
|
|
338
|
-
if (
|
|
475
|
+
if (
|
|
476
|
+
_rehookEdgeId &&
|
|
477
|
+
st.currentTool !== "addEdge" &&
|
|
478
|
+
_draggingAnchorIds.size === 0
|
|
479
|
+
) {
|
|
339
480
|
const edgeData = st.edges.get(_rehookEdgeId);
|
|
340
481
|
if (isRehookable(edgeData)) {
|
|
341
482
|
const fromNode = st.nodes.get(edgeData.from);
|
|
342
|
-
if (fromNode && fromNode.shapeType !==
|
|
343
|
-
drawPortDots(
|
|
483
|
+
if (fromNode && fromNode.shapeType !== "anchor") {
|
|
484
|
+
drawPortDots(
|
|
485
|
+
ctx,
|
|
486
|
+
edgeData.from,
|
|
487
|
+
_rehookHoveredNodeId === edgeData.from
|
|
488
|
+
? _rehookHoveredPortKey
|
|
489
|
+
: null,
|
|
490
|
+
);
|
|
344
491
|
}
|
|
345
492
|
const toNode = st.nodes.get(edgeData.to);
|
|
346
|
-
if (toNode && toNode.shapeType !==
|
|
347
|
-
drawPortDots(
|
|
493
|
+
if (toNode && toNode.shapeType !== "anchor") {
|
|
494
|
+
drawPortDots(
|
|
495
|
+
ctx,
|
|
496
|
+
edgeData.to,
|
|
497
|
+
_rehookHoveredNodeId === edgeData.to ? _rehookHoveredPortKey : null,
|
|
498
|
+
);
|
|
348
499
|
}
|
|
349
500
|
}
|
|
350
501
|
}
|
|
351
502
|
});
|
|
352
503
|
// Two-click free-arrow: draw an orange dot at the pending first-click origin.
|
|
353
|
-
st.network.on(
|
|
354
|
-
if (st.currentTool !==
|
|
504
|
+
st.network.on("afterDrawing", (ctx) => {
|
|
505
|
+
if (st.currentTool !== "addEdge" || !st.freeArrowFirstPoint) return;
|
|
355
506
|
const { x, y } = st.freeArrowFirstPoint;
|
|
356
507
|
ctx.beginPath();
|
|
357
508
|
ctx.arc(x, y, 6, 0, Math.PI * 2);
|
|
358
|
-
ctx.fillStyle =
|
|
509
|
+
ctx.fillStyle = "#f97316";
|
|
359
510
|
ctx.fill();
|
|
360
|
-
ctx.strokeStyle =
|
|
511
|
+
ctx.strokeStyle = "#ffffff";
|
|
361
512
|
ctx.lineWidth = 2;
|
|
362
513
|
ctx.stroke();
|
|
363
514
|
});
|
|
@@ -373,13 +524,13 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
373
524
|
// hit-detection — this avoids a race with vis-network's capture-phase handlers
|
|
374
525
|
// which may run first and alter selection state before our mousedown fires.
|
|
375
526
|
{
|
|
376
|
-
let _lr
|
|
377
|
-
let _hoverHandle
|
|
378
|
-
let _ld
|
|
527
|
+
let _lr = null; // active resize: { edgeId, bboxCx, bboxCy, rotation }
|
|
528
|
+
let _hoverHandle = null; // { edgeId, bboxCx, bboxCy, rotation } | null
|
|
529
|
+
let _ld = null; // active label drag: { edgeId, startMouse, startOffsetX, startOffsetY, dragging }
|
|
379
530
|
let _hoverLabelDrag = null; // { edgeId } | null
|
|
380
531
|
|
|
381
532
|
// Draw handles for the selected edge label.
|
|
382
|
-
st.network.on(
|
|
533
|
+
st.network.on("afterDrawing", (ctx) => {
|
|
383
534
|
if (!st.selectedEdgeIds || st.selectedEdgeIds.length !== 1) return;
|
|
384
535
|
const edgeId = st.selectedEdgeIds[0];
|
|
385
536
|
const e = st.edges.get(edgeId);
|
|
@@ -393,9 +544,9 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
393
544
|
for (const hx of [-bbox.w / 2, bbox.w / 2]) {
|
|
394
545
|
ctx.beginPath();
|
|
395
546
|
ctx.arc(hx, 0, 5, 0, Math.PI * 2);
|
|
396
|
-
ctx.fillStyle
|
|
397
|
-
ctx.strokeStyle =
|
|
398
|
-
ctx.lineWidth
|
|
547
|
+
ctx.fillStyle = "#f97316";
|
|
548
|
+
ctx.strokeStyle = "#ffffff";
|
|
549
|
+
ctx.lineWidth = 1.5;
|
|
399
550
|
ctx.fill();
|
|
400
551
|
ctx.stroke();
|
|
401
552
|
}
|
|
@@ -405,23 +556,36 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
405
556
|
function edgeLabelHitAt(clientX, clientY) {
|
|
406
557
|
if (!st.network || !st.edgeLabelBBox) return;
|
|
407
558
|
const rect = container.getBoundingClientRect();
|
|
408
|
-
const cp = st.network.DOMtoCanvas({
|
|
559
|
+
const cp = st.network.DOMtoCanvas({
|
|
560
|
+
x: clientX - rect.left,
|
|
561
|
+
y: clientY - rect.top,
|
|
562
|
+
});
|
|
409
563
|
const hr = 8 / st.network.getScale();
|
|
410
564
|
|
|
411
565
|
// ── Resize handle detection ──────────────────────────────────────────────
|
|
412
|
-
for (const edgeId of
|
|
566
|
+
for (const edgeId of st.selectedEdgeIds || []) {
|
|
413
567
|
const edge = st.edges && st.edges.get(edgeId);
|
|
414
568
|
if (!edge || !edge.label) continue;
|
|
415
569
|
const bbox = st.edgeLabelBBox[edgeId];
|
|
416
570
|
if (!bbox) continue;
|
|
417
571
|
|
|
418
|
-
const r
|
|
419
|
-
const dx = cp.x - bbox.cx,
|
|
572
|
+
const r = -(bbox.rotation || 0);
|
|
573
|
+
const dx = cp.x - bbox.cx,
|
|
574
|
+
dy = cp.y - bbox.cy;
|
|
420
575
|
const lx = dx * Math.cos(r) - dy * Math.sin(r);
|
|
421
576
|
const ly = dx * Math.sin(r) + dy * Math.cos(r);
|
|
422
577
|
|
|
423
|
-
if (
|
|
424
|
-
|
|
578
|
+
if (
|
|
579
|
+
Math.hypot(lx - -bbox.w / 2, ly) < hr ||
|
|
580
|
+
Math.hypot(lx - bbox.w / 2, ly) < hr
|
|
581
|
+
) {
|
|
582
|
+
return {
|
|
583
|
+
type: "handle",
|
|
584
|
+
edgeId,
|
|
585
|
+
bboxCx: bbox.cx,
|
|
586
|
+
bboxCy: bbox.cy,
|
|
587
|
+
rotation: bbox.rotation || 0,
|
|
588
|
+
};
|
|
425
589
|
}
|
|
426
590
|
}
|
|
427
591
|
|
|
@@ -432,12 +596,13 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
432
596
|
if (edge && edge.label) {
|
|
433
597
|
const bbox = st.edgeLabelBBox && st.edgeLabelBBox[edgeId];
|
|
434
598
|
if (bbox) {
|
|
435
|
-
const r
|
|
436
|
-
const dx = cp.x - bbox.cx,
|
|
599
|
+
const r = -(bbox.rotation || 0);
|
|
600
|
+
const dx = cp.x - bbox.cx,
|
|
601
|
+
dy = cp.y - bbox.cy;
|
|
437
602
|
const lx = dx * Math.cos(r) - dy * Math.sin(r);
|
|
438
603
|
const ly = dx * Math.sin(r) + dy * Math.cos(r);
|
|
439
604
|
if (Math.abs(lx) <= bbox.w / 2 && Math.abs(ly) <= bbox.h / 2) {
|
|
440
|
-
return { type:
|
|
605
|
+
return { type: "label", edgeId };
|
|
441
606
|
}
|
|
442
607
|
}
|
|
443
608
|
}
|
|
@@ -447,19 +612,30 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
447
612
|
}
|
|
448
613
|
|
|
449
614
|
// Track hover during mousemove — computed before any mousedown fires.
|
|
450
|
-
container.addEventListener(
|
|
615
|
+
container.addEventListener("mousemove", (e) => {
|
|
451
616
|
const hit = edgeLabelHitAt(e.clientX, e.clientY);
|
|
452
|
-
const found =
|
|
453
|
-
|
|
454
|
-
|
|
617
|
+
const found =
|
|
618
|
+
hit && hit.type === "handle"
|
|
619
|
+
? {
|
|
620
|
+
edgeId: hit.edgeId,
|
|
621
|
+
bboxCx: hit.bboxCx,
|
|
622
|
+
bboxCy: hit.bboxCy,
|
|
623
|
+
rotation: hit.rotation,
|
|
624
|
+
}
|
|
625
|
+
: null;
|
|
455
626
|
if (Boolean(found) !== Boolean(_hoverHandle)) {
|
|
456
|
-
container.style.cursor = found
|
|
627
|
+
container.style.cursor = found
|
|
628
|
+
? "ew-resize"
|
|
629
|
+
: _hoverLabelDrag
|
|
630
|
+
? "grab"
|
|
631
|
+
: "";
|
|
457
632
|
}
|
|
458
633
|
_hoverHandle = found;
|
|
459
634
|
|
|
460
|
-
const labelFound =
|
|
635
|
+
const labelFound =
|
|
636
|
+
!found && hit && hit.type === "label" ? { edgeId: hit.edgeId } : null;
|
|
461
637
|
if (Boolean(labelFound) !== Boolean(_hoverLabelDrag)) {
|
|
462
|
-
if (!found) container.style.cursor = labelFound ?
|
|
638
|
+
if (!found) container.style.cursor = labelFound ? "grab" : "";
|
|
463
639
|
}
|
|
464
640
|
_hoverLabelDrag = labelFound;
|
|
465
641
|
});
|
|
@@ -468,14 +644,19 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
468
644
|
if (e.button !== 0) return false;
|
|
469
645
|
if (!container.contains(e.target)) return false;
|
|
470
646
|
const hit = edgeLabelHitAt(e.clientX, e.clientY);
|
|
471
|
-
if (hit && hit.type ===
|
|
647
|
+
if (hit && hit.type === "handle") {
|
|
472
648
|
e.preventDefault();
|
|
473
649
|
e.stopImmediatePropagation();
|
|
474
|
-
_hoverHandle = {
|
|
650
|
+
_hoverHandle = {
|
|
651
|
+
edgeId: hit.edgeId,
|
|
652
|
+
bboxCx: hit.bboxCx,
|
|
653
|
+
bboxCy: hit.bboxCy,
|
|
654
|
+
rotation: hit.rotation,
|
|
655
|
+
};
|
|
475
656
|
_lr = { ..._hoverHandle, dragging: false };
|
|
476
657
|
st.network.setOptions({ interaction: { dragView: false } });
|
|
477
658
|
return true;
|
|
478
|
-
} else if (hit && hit.type ===
|
|
659
|
+
} else if (hit && hit.type === "label") {
|
|
479
660
|
const edge = st.edges && st.edges.get(hit.edgeId);
|
|
480
661
|
if (!edge) return false;
|
|
481
662
|
e.preventDefault();
|
|
@@ -500,69 +681,103 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
500
681
|
// Mousedown: start resize (handles take priority) or label drag.
|
|
501
682
|
// The document-level capture listener runs before vis-network's internal
|
|
502
683
|
// pointer handlers, so a node underneath the edge label cannot start moving.
|
|
503
|
-
document.addEventListener(
|
|
684
|
+
document.addEventListener("pointerdown", startEdgeLabelPointerInteraction, {
|
|
504
685
|
capture: true,
|
|
505
686
|
signal: _edgeLabelPointerAbort.signal,
|
|
506
687
|
});
|
|
507
|
-
document.addEventListener(
|
|
688
|
+
document.addEventListener("mousedown", startEdgeLabelPointerInteraction, {
|
|
508
689
|
capture: true,
|
|
509
690
|
signal: _edgeLabelPointerAbort.signal,
|
|
510
691
|
});
|
|
511
|
-
container.addEventListener(
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
692
|
+
container.addEventListener(
|
|
693
|
+
"pointerdown",
|
|
694
|
+
(e) => {
|
|
695
|
+
startEdgeLabelPointerInteraction(e);
|
|
696
|
+
},
|
|
697
|
+
{ capture: true, signal: _edgeLabelPointerAbort.signal },
|
|
698
|
+
);
|
|
699
|
+
container.addEventListener(
|
|
700
|
+
"mousedown",
|
|
701
|
+
(e) => {
|
|
702
|
+
startEdgeLabelPointerInteraction(e);
|
|
703
|
+
},
|
|
704
|
+
{ capture: true, signal: _edgeLabelPointerAbort.signal },
|
|
705
|
+
);
|
|
522
706
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
707
|
+
document.addEventListener(
|
|
708
|
+
"dblclick",
|
|
709
|
+
(e) => {
|
|
710
|
+
if (!container.contains(e.target)) return;
|
|
711
|
+
const hit = edgeLabelHitAt(e.clientX, e.clientY);
|
|
712
|
+
if (!hit || hit.type !== "label") return;
|
|
713
|
+
|
|
714
|
+
e.preventDefault();
|
|
715
|
+
e.stopImmediatePropagation();
|
|
716
|
+
_lr = null;
|
|
717
|
+
_ld = null;
|
|
718
|
+
st.network.setOptions({ interaction: { dragView: true } });
|
|
719
|
+
st.selectedNodeIds = [];
|
|
720
|
+
st.selectedEdgeIds = [hit.edgeId];
|
|
721
|
+
st.network.setSelection({ nodes: [], edges: [hit.edgeId] });
|
|
722
|
+
hideNodePanel();
|
|
723
|
+
showEdgePanel();
|
|
724
|
+
startEdgeLabelEdit();
|
|
725
|
+
},
|
|
726
|
+
{ capture: true, signal: _edgeLabelPointerAbort.signal },
|
|
727
|
+
);
|
|
535
728
|
|
|
536
729
|
// Update width (resize) or offset (label drag) while dragging.
|
|
537
730
|
function onEdgeLabelPointerMove(e) {
|
|
538
731
|
if (!st.network) return;
|
|
539
732
|
if (_lr) {
|
|
540
|
-
if (!_lr.dragging) {
|
|
733
|
+
if (!_lr.dragging) {
|
|
734
|
+
_lr.dragging = true;
|
|
735
|
+
pushSnapshot();
|
|
736
|
+
}
|
|
541
737
|
const rect = container.getBoundingClientRect();
|
|
542
|
-
const cp
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
738
|
+
const cp = st.network.DOMtoCanvas({
|
|
739
|
+
x: e.clientX - rect.left,
|
|
740
|
+
y: e.clientY - rect.top,
|
|
741
|
+
});
|
|
742
|
+
const r = -_lr.rotation;
|
|
743
|
+
const dx = cp.x - _lr.bboxCx,
|
|
744
|
+
dy = cp.y - _lr.bboxCy;
|
|
745
|
+
const lx = dx * Math.cos(r) - dy * Math.sin(r);
|
|
746
|
+
st.edges.update({
|
|
747
|
+
id: _lr.edgeId,
|
|
748
|
+
edgeLabelWidth: Math.max(40, Math.abs(lx) * 2),
|
|
749
|
+
});
|
|
547
750
|
st.network.redraw();
|
|
548
751
|
}
|
|
549
752
|
if (_ld) {
|
|
550
753
|
if (!_ld.dragging) {
|
|
551
|
-
if (
|
|
754
|
+
if (
|
|
755
|
+
Math.hypot(
|
|
756
|
+
e.clientX - _ld.startMouse.x,
|
|
757
|
+
e.clientY - _ld.startMouse.y,
|
|
758
|
+
) < 4
|
|
759
|
+
)
|
|
760
|
+
return;
|
|
552
761
|
_ld.dragging = true;
|
|
553
762
|
pushSnapshot();
|
|
554
763
|
}
|
|
555
764
|
const scale = st.network.getScale();
|
|
556
765
|
st.edges.update({
|
|
557
766
|
id: _ld.edgeId,
|
|
558
|
-
edgeLabelOffsetX:
|
|
559
|
-
|
|
767
|
+
edgeLabelOffsetX:
|
|
768
|
+
_ld.startOffsetX + (e.clientX - _ld.startMouse.x) / scale,
|
|
769
|
+
edgeLabelOffsetY:
|
|
770
|
+
_ld.startOffsetY + (e.clientY - _ld.startMouse.y) / scale,
|
|
560
771
|
});
|
|
561
772
|
st.network.redraw();
|
|
562
773
|
}
|
|
563
774
|
}
|
|
564
|
-
document.addEventListener(
|
|
565
|
-
|
|
775
|
+
document.addEventListener("pointermove", onEdgeLabelPointerMove, {
|
|
776
|
+
signal: _edgeLabelPointerAbort.signal,
|
|
777
|
+
});
|
|
778
|
+
document.addEventListener("mousemove", onEdgeLabelPointerMove, {
|
|
779
|
+
signal: _edgeLabelPointerAbort.signal,
|
|
780
|
+
});
|
|
566
781
|
|
|
567
782
|
// Commit on mouseup.
|
|
568
783
|
function onEdgeLabelPointerUp() {
|
|
@@ -576,10 +791,18 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
576
791
|
if (_ld.dragging) markDirty();
|
|
577
792
|
_ld = null;
|
|
578
793
|
}
|
|
579
|
-
container.style.cursor = _hoverHandle
|
|
794
|
+
container.style.cursor = _hoverHandle
|
|
795
|
+
? "ew-resize"
|
|
796
|
+
: _hoverLabelDrag
|
|
797
|
+
? "grab"
|
|
798
|
+
: "";
|
|
580
799
|
}
|
|
581
|
-
document.addEventListener(
|
|
582
|
-
|
|
800
|
+
document.addEventListener("pointerup", onEdgeLabelPointerUp, {
|
|
801
|
+
signal: _edgeLabelPointerAbort.signal,
|
|
802
|
+
});
|
|
803
|
+
document.addEventListener("mouseup", onEdgeLabelPointerUp, {
|
|
804
|
+
signal: _edgeLabelPointerAbort.signal,
|
|
805
|
+
});
|
|
583
806
|
}
|
|
584
807
|
|
|
585
808
|
// ── Free-arrow body drag ──────────────────────────────────────────────────────
|
|
@@ -588,25 +811,43 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
588
811
|
// Fix: capture mousedown on the edge body, disable dragView, move anchors manually.
|
|
589
812
|
{
|
|
590
813
|
let _fad = null; // free-arrow drag state
|
|
591
|
-
container.addEventListener(
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
814
|
+
container.addEventListener(
|
|
815
|
+
"mousedown",
|
|
816
|
+
(e) => {
|
|
817
|
+
if (e.button !== 0 || !st.network) return;
|
|
818
|
+
const domPos = { x: e.offsetX, y: e.offsetY };
|
|
819
|
+
// If the click landed on a node (including an anchor endpoint), don't intercept:
|
|
820
|
+
// the user wants to move only that endpoint (pivot), not the whole arrow.
|
|
821
|
+
if (st.network.getNodeAt(domPos)) return;
|
|
822
|
+
const edgeId = st.network.getEdgeAt(domPos);
|
|
823
|
+
if (!edgeId) return;
|
|
824
|
+
const edge = st.edges.get(edgeId);
|
|
825
|
+
if (!edge) return;
|
|
826
|
+
const fromN = st.nodes.get(edge.from);
|
|
827
|
+
const toN = st.nodes.get(edge.to);
|
|
828
|
+
if (
|
|
829
|
+
!(
|
|
830
|
+
fromN &&
|
|
831
|
+
fromN.shapeType === "anchor" &&
|
|
832
|
+
toN &&
|
|
833
|
+
toN.shapeType === "anchor"
|
|
834
|
+
)
|
|
835
|
+
)
|
|
836
|
+
return;
|
|
837
|
+
const startPos = st.network.getPositions([edge.from, edge.to]);
|
|
838
|
+
_fad = {
|
|
839
|
+
edgeId,
|
|
840
|
+
fromId: edge.from,
|
|
841
|
+
toId: edge.to,
|
|
842
|
+
startMouse: { x: e.clientX, y: e.clientY },
|
|
843
|
+
startPos,
|
|
844
|
+
dragging: false,
|
|
845
|
+
};
|
|
846
|
+
},
|
|
847
|
+
{ capture: true },
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
document.addEventListener("mousemove", (e) => {
|
|
610
851
|
if (!_fad || !st.network) return;
|
|
611
852
|
const dx = e.clientX - _fad.startMouse.x;
|
|
612
853
|
const dy = e.clientY - _fad.startMouse.y;
|
|
@@ -619,13 +860,19 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
619
860
|
const scale = st.network.getScale();
|
|
620
861
|
const fp = _fad.startPos[_fad.fromId];
|
|
621
862
|
const tp = _fad.startPos[_fad.toId];
|
|
622
|
-
const snap = (x, y) => st.gridEnabled ? snapToGrid(x, y) : { x, y };
|
|
623
|
-
if (fp) {
|
|
624
|
-
|
|
863
|
+
const snap = (x, y) => (st.gridEnabled ? snapToGrid(x, y) : { x, y });
|
|
864
|
+
if (fp) {
|
|
865
|
+
const p = snap(fp.x + dx / scale, fp.y + dy / scale);
|
|
866
|
+
st.network.moveNode(_fad.fromId, p.x, p.y);
|
|
867
|
+
}
|
|
868
|
+
if (tp) {
|
|
869
|
+
const p = snap(tp.x + dx / scale, tp.y + dy / scale);
|
|
870
|
+
st.network.moveNode(_fad.toId, p.x, p.y);
|
|
871
|
+
}
|
|
625
872
|
st.network.redraw();
|
|
626
873
|
});
|
|
627
874
|
|
|
628
|
-
document.addEventListener(
|
|
875
|
+
document.addEventListener("mouseup", () => {
|
|
629
876
|
if (!_fad) return;
|
|
630
877
|
if (_fad.dragging) {
|
|
631
878
|
st.network.setOptions({ interaction: { dragView: true } });
|
|
@@ -642,67 +889,90 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
642
889
|
// Path B — two successive CLICKS on empty canvas → free-standing anchor→anchor.
|
|
643
890
|
// Uses vis-network's own 'click' event: Hammer.js fires 'click' only
|
|
644
891
|
// for taps, NOT for drags, so Path A drags never bleed into Path B.
|
|
645
|
-
let _addEdgeFromId
|
|
646
|
-
let _addEdgeFromPort = null;
|
|
892
|
+
let _addEdgeFromId = null;
|
|
893
|
+
let _addEdgeFromPort = null; // port key on the source node (null = no port)
|
|
647
894
|
// _hoveredPortNodeId, _hoveredPortKey, _draggingAnchorIds are module-level.
|
|
648
|
-
const visCanvas = document.getElementById(
|
|
895
|
+
const visCanvas = document.getElementById("vis-canvas");
|
|
649
896
|
|
|
650
897
|
// Track hovered node + nearest port while in addEdge mode OR while dragging an anchor.
|
|
651
|
-
visCanvas.addEventListener(
|
|
652
|
-
const inAddEdge
|
|
898
|
+
visCanvas.addEventListener("mousemove", (e) => {
|
|
899
|
+
const inAddEdge = st.currentTool === "addEdge";
|
|
653
900
|
const anchorDragging = _draggingAnchorIds.size > 0;
|
|
654
901
|
if ((!inAddEdge && !anchorDragging) || !st.network) return;
|
|
655
902
|
|
|
656
903
|
const pos = { x: e.offsetX, y: e.offsetY };
|
|
657
|
-
const cp
|
|
904
|
+
const cp = st.network.DOMtoCanvas(pos);
|
|
658
905
|
|
|
659
906
|
let newPortNodeId = null;
|
|
660
907
|
|
|
661
908
|
if (inAddEdge) {
|
|
662
909
|
// addEdge mode: use getNodeAt as before
|
|
663
|
-
const nodeId
|
|
910
|
+
const nodeId = st.network.getNodeAt(pos) || null;
|
|
664
911
|
const nodeData = nodeId && st.nodes.get(nodeId);
|
|
665
|
-
const isAnchor = nodeData && nodeData.shapeType ===
|
|
912
|
+
const isAnchor = nodeData && nodeData.shapeType === "anchor";
|
|
666
913
|
const isLocked = nodeData && nodeData.locked;
|
|
667
|
-
newPortNodeId
|
|
914
|
+
newPortNodeId = nodeId && !isAnchor && !isLocked ? nodeId : null;
|
|
668
915
|
} else {
|
|
669
916
|
// Anchor drag: getNodeAt returns the dragged anchor itself — do a canvas-space
|
|
670
917
|
// bounding-box search for any non-anchor node that contains the cursor.
|
|
671
918
|
const SNAP_THRESHOLD = 30;
|
|
672
|
-
let bestId
|
|
919
|
+
let bestId = null;
|
|
673
920
|
let bestDist = Infinity;
|
|
674
|
-
for (const [candidateId, candidatePos] of Object.entries(
|
|
921
|
+
for (const [candidateId, candidatePos] of Object.entries(
|
|
922
|
+
st.network.getPositions(),
|
|
923
|
+
)) {
|
|
675
924
|
if (_draggingAnchorIds.has(candidateId)) continue;
|
|
676
925
|
const candidate = st.nodes.get(candidateId);
|
|
677
|
-
if (!candidate || candidate.shapeType ===
|
|
926
|
+
if (!candidate || candidate.shapeType === "anchor") continue;
|
|
678
927
|
const bodyNode = st.network.body.nodes[candidateId];
|
|
679
928
|
if (!bodyNode) continue;
|
|
680
|
-
const w =
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
929
|
+
const w =
|
|
930
|
+
(bodyNode.shape && bodyNode.shape.width) ||
|
|
931
|
+
SHAPE_DEFAULTS[candidate.shapeType]?.width ||
|
|
932
|
+
120;
|
|
933
|
+
const h =
|
|
934
|
+
(bodyNode.shape && bodyNode.shape.height) ||
|
|
935
|
+
SHAPE_DEFAULTS[candidate.shapeType]?.height ||
|
|
936
|
+
60;
|
|
937
|
+
const inBox =
|
|
938
|
+
cp.x >= candidatePos.x - w / 2 - SNAP_THRESHOLD &&
|
|
939
|
+
cp.x <= candidatePos.x + w / 2 + SNAP_THRESHOLD &&
|
|
940
|
+
cp.y >= candidatePos.y - h / 2 - SNAP_THRESHOLD &&
|
|
941
|
+
cp.y <= candidatePos.y + h / 2 + SNAP_THRESHOLD;
|
|
686
942
|
if (inBox) {
|
|
687
943
|
const dist = Math.hypot(candidatePos.x - cp.x, candidatePos.y - cp.y);
|
|
688
|
-
if (dist < bestDist) {
|
|
944
|
+
if (dist < bestDist) {
|
|
945
|
+
bestDist = dist;
|
|
946
|
+
bestId = candidateId;
|
|
947
|
+
}
|
|
689
948
|
}
|
|
690
949
|
}
|
|
691
950
|
newPortNodeId = bestId;
|
|
692
951
|
}
|
|
693
952
|
|
|
694
953
|
const newPortKey = newPortNodeId ? getNearestPort(newPortNodeId, cp) : null;
|
|
695
|
-
if (
|
|
954
|
+
if (
|
|
955
|
+
newPortNodeId !== _hoveredPortNodeId ||
|
|
956
|
+
newPortKey !== _hoveredPortKey
|
|
957
|
+
) {
|
|
696
958
|
_hoveredPortNodeId = newPortNodeId;
|
|
697
|
-
_hoveredPortKey
|
|
959
|
+
_hoveredPortKey = newPortKey;
|
|
698
960
|
st.network.redraw();
|
|
699
961
|
}
|
|
700
962
|
});
|
|
701
963
|
|
|
702
964
|
// Rehook hover: track the nearest port on the from/to nodes of the selected port edge.
|
|
703
|
-
visCanvas.addEventListener(
|
|
704
|
-
if (
|
|
705
|
-
|
|
965
|
+
visCanvas.addEventListener("mousemove", (e) => {
|
|
966
|
+
if (
|
|
967
|
+
!st.network ||
|
|
968
|
+
st.currentTool === "addEdge" ||
|
|
969
|
+
_draggingAnchorIds.size > 0
|
|
970
|
+
)
|
|
971
|
+
return;
|
|
972
|
+
const selectedEdge =
|
|
973
|
+
st.selectedEdgeIds.length === 1
|
|
974
|
+
? st.edges.get(st.selectedEdgeIds[0])
|
|
975
|
+
: null;
|
|
706
976
|
if (!isRehookable(selectedEdge)) {
|
|
707
977
|
if (_rehookEdgeId !== null) {
|
|
708
978
|
_rehookEdgeId = _rehookHoveredNodeId = _rehookHoveredPortKey = null;
|
|
@@ -714,49 +984,58 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
714
984
|
_rehookEdgeId = selectedEdge.id;
|
|
715
985
|
const cp = st.network.DOMtoCanvas({ x: e.offsetX, y: e.offsetY });
|
|
716
986
|
const REHOOK_THRESHOLD = 15; // world units
|
|
717
|
-
let newNodeId
|
|
718
|
-
let newPortKey
|
|
987
|
+
let newNodeId = null;
|
|
988
|
+
let newPortKey = null;
|
|
719
989
|
let closestDist = Infinity;
|
|
720
990
|
|
|
721
991
|
for (const nodeId of [selectedEdge.from, selectedEdge.to]) {
|
|
722
992
|
const nodeData = st.nodes.get(nodeId);
|
|
723
|
-
if (!nodeData || nodeData.shapeType ===
|
|
993
|
+
if (!nodeData || nodeData.shapeType === "anchor") continue;
|
|
724
994
|
const portKey = getNearestPort(nodeId, cp);
|
|
725
995
|
const portPos = getPortPosition(nodeId, portKey);
|
|
726
996
|
if (!portPos) continue;
|
|
727
997
|
const dist = Math.hypot(cp.x - portPos.x, cp.y - portPos.y);
|
|
728
998
|
if (dist <= REHOOK_THRESHOLD && dist < closestDist) {
|
|
729
999
|
closestDist = dist;
|
|
730
|
-
newNodeId
|
|
1000
|
+
newNodeId = nodeId;
|
|
731
1001
|
newPortKey = portKey;
|
|
732
1002
|
}
|
|
733
1003
|
}
|
|
734
1004
|
|
|
735
|
-
if (
|
|
736
|
-
_rehookHoveredNodeId
|
|
1005
|
+
if (
|
|
1006
|
+
newNodeId !== _rehookHoveredNodeId ||
|
|
1007
|
+
newPortKey !== _rehookHoveredPortKey
|
|
1008
|
+
) {
|
|
1009
|
+
_rehookHoveredNodeId = newNodeId;
|
|
737
1010
|
_rehookHoveredPortKey = newPortKey;
|
|
738
1011
|
st.network.redraw();
|
|
739
1012
|
}
|
|
740
1013
|
});
|
|
741
1014
|
|
|
742
1015
|
// Path A – capture source node on mousedown.
|
|
743
|
-
visCanvas.addEventListener(
|
|
744
|
-
if (st.currentTool !==
|
|
745
|
-
const nodeId
|
|
1016
|
+
visCanvas.addEventListener("mousedown", (e) => {
|
|
1017
|
+
if (st.currentTool !== "addEdge") return;
|
|
1018
|
+
const nodeId = st.network.getNodeAt({ x: e.offsetX, y: e.offsetY }) || null;
|
|
746
1019
|
const nodeData = nodeId && st.nodes.get(nodeId);
|
|
747
1020
|
// Block starting an edge from a locked node or an anchor (free-arrow endpoint).
|
|
748
|
-
if (nodeData && (nodeData.locked || nodeData.shapeType ===
|
|
1021
|
+
if (nodeData && (nodeData.locked || nodeData.shapeType === "anchor")) {
|
|
1022
|
+
_addEdgeFromId = null;
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
749
1025
|
// Dragging from a real node cancels any pending two-click origin.
|
|
750
1026
|
if (nodeId) st.freeArrowFirstPoint = null;
|
|
751
|
-
_addEdgeFromId
|
|
1027
|
+
_addEdgeFromId = nodeId;
|
|
752
1028
|
_addEdgeFromPort = _hoveredPortKey;
|
|
753
1029
|
});
|
|
754
1030
|
|
|
755
1031
|
// Path A – release on empty canvas creates the anchor endpoint.
|
|
756
|
-
visCanvas.addEventListener(
|
|
757
|
-
if (st.currentTool !==
|
|
758
|
-
|
|
759
|
-
|
|
1032
|
+
visCanvas.addEventListener("mouseup", (e) => {
|
|
1033
|
+
if (st.currentTool !== "addEdge" || !_addEdgeFromId) {
|
|
1034
|
+
_addEdgeFromId = null;
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const pos = { x: e.offsetX, y: e.offsetY };
|
|
1038
|
+
const targetId = st.network.getNodeAt(pos);
|
|
760
1039
|
const targetData = targetId && st.nodes.get(targetId);
|
|
761
1040
|
// Locked targets are non-interactive: treat them exactly like empty canvas
|
|
762
1041
|
// so a free-arrow endpoint is created at the release point — matches the
|
|
@@ -764,26 +1043,40 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
764
1043
|
// Also: if the source sits on top of a lower-z target that contains it
|
|
765
1044
|
// (e.g. a post-it on a background image), don't bind to that background.
|
|
766
1045
|
const targetLocked = !!(targetData && targetData.locked);
|
|
767
|
-
const targetContainsSource =
|
|
1046
|
+
const targetContainsSource =
|
|
1047
|
+
targetId && lowerZTargetContainsSource(_addEdgeFromId, targetId);
|
|
768
1048
|
if (!targetId || targetLocked || targetContainsSource) {
|
|
769
1049
|
pushSnapshot();
|
|
770
|
-
const cp
|
|
771
|
-
const anchorId =
|
|
1050
|
+
const cp = st.network.DOMtoCanvas(pos);
|
|
1051
|
+
const anchorId = "a" + Date.now();
|
|
772
1052
|
st.nodes.add({
|
|
773
|
-
id: anchorId,
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
1053
|
+
id: anchorId,
|
|
1054
|
+
label: "",
|
|
1055
|
+
shapeType: "anchor",
|
|
1056
|
+
colorKey: "c-gray",
|
|
1057
|
+
nodeWidth: 8,
|
|
1058
|
+
nodeHeight: 8,
|
|
1059
|
+
fontSize: null,
|
|
1060
|
+
rotation: 0,
|
|
1061
|
+
labelRotation: 0,
|
|
1062
|
+
x: cp.x,
|
|
1063
|
+
y: cp.y,
|
|
1064
|
+
...visNodeProps("anchor", "c-gray", 8, 8, null, null, null),
|
|
777
1065
|
});
|
|
778
1066
|
st.edges.add({
|
|
779
|
-
id:
|
|
780
|
-
|
|
1067
|
+
id: "e" + Date.now(),
|
|
1068
|
+
from: _addEdgeFromId,
|
|
1069
|
+
to: anchorId,
|
|
1070
|
+
arrowDir: "to",
|
|
1071
|
+
dashes: false,
|
|
781
1072
|
smooth: { enabled: false },
|
|
782
|
-
...visEdgeProps(
|
|
1073
|
+
...visEdgeProps("to", false),
|
|
783
1074
|
});
|
|
784
1075
|
markDirty();
|
|
785
1076
|
st.freeArrowFirstPoint = null;
|
|
786
|
-
setTimeout(() => {
|
|
1077
|
+
setTimeout(() => {
|
|
1078
|
+
if (st.currentTool === "addEdge") st.network.addEdgeMode();
|
|
1079
|
+
}, 0);
|
|
787
1080
|
}
|
|
788
1081
|
_addEdgeFromId = null;
|
|
789
1082
|
});
|
|
@@ -791,8 +1084,8 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
791
1084
|
// Path B – two successive clicks on empty canvas → free-standing anchor→anchor arrow.
|
|
792
1085
|
// vis-network's 'click' event (via Hammer.js) only fires for taps, never for drags,
|
|
793
1086
|
// so Path A drag gestures cannot accidentally trigger Path B.
|
|
794
|
-
st.network.on(
|
|
795
|
-
if (st.currentTool !==
|
|
1087
|
+
st.network.on("click", (params) => {
|
|
1088
|
+
if (st.currentTool !== "addEdge") return;
|
|
796
1089
|
// Only handle clicks that landed on empty canvas (no node, no edge).
|
|
797
1090
|
if (params.nodes.length > 0 || params.edges.length > 0) return;
|
|
798
1091
|
const cp = params.pointer.canvas;
|
|
@@ -803,26 +1096,71 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
803
1096
|
} else {
|
|
804
1097
|
// Second click: create the free-standing anchor→anchor arrow.
|
|
805
1098
|
pushSnapshot();
|
|
806
|
-
const t
|
|
807
|
-
const fromId
|
|
808
|
-
const toId
|
|
809
|
-
const anchorProps = visNodeProps(
|
|
1099
|
+
const t = Date.now();
|
|
1100
|
+
const fromId = "a" + t;
|
|
1101
|
+
const toId = "a" + (t + 1);
|
|
1102
|
+
const anchorProps = visNodeProps(
|
|
1103
|
+
"anchor",
|
|
1104
|
+
"c-gray",
|
|
1105
|
+
8,
|
|
1106
|
+
8,
|
|
1107
|
+
null,
|
|
1108
|
+
null,
|
|
1109
|
+
null,
|
|
1110
|
+
);
|
|
810
1111
|
st.nodes.add([
|
|
811
|
-
{
|
|
812
|
-
|
|
1112
|
+
{
|
|
1113
|
+
id: fromId,
|
|
1114
|
+
label: "",
|
|
1115
|
+
shapeType: "anchor",
|
|
1116
|
+
colorKey: "c-gray",
|
|
1117
|
+
nodeWidth: 8,
|
|
1118
|
+
nodeHeight: 8,
|
|
1119
|
+
fontSize: null,
|
|
1120
|
+
rotation: 0,
|
|
1121
|
+
labelRotation: 0,
|
|
1122
|
+
x: st.freeArrowFirstPoint.x,
|
|
1123
|
+
y: st.freeArrowFirstPoint.y,
|
|
1124
|
+
...anchorProps,
|
|
1125
|
+
},
|
|
1126
|
+
{
|
|
1127
|
+
id: toId,
|
|
1128
|
+
label: "",
|
|
1129
|
+
shapeType: "anchor",
|
|
1130
|
+
colorKey: "c-gray",
|
|
1131
|
+
nodeWidth: 8,
|
|
1132
|
+
nodeHeight: 8,
|
|
1133
|
+
fontSize: null,
|
|
1134
|
+
rotation: 0,
|
|
1135
|
+
labelRotation: 0,
|
|
1136
|
+
x: cp.x,
|
|
1137
|
+
y: cp.y,
|
|
1138
|
+
...anchorProps,
|
|
1139
|
+
},
|
|
813
1140
|
]);
|
|
814
|
-
const edgeId
|
|
1141
|
+
const edgeId = "e" + t;
|
|
815
1142
|
const lastStyle = getLastFreeArrowStyle();
|
|
816
|
-
const arrowDir
|
|
817
|
-
const dashes
|
|
1143
|
+
const arrowDir = lastStyle.arrowDir || "to";
|
|
1144
|
+
const dashes = lastStyle.dashes || false;
|
|
818
1145
|
st.edges.add({
|
|
819
|
-
id: edgeId,
|
|
820
|
-
|
|
1146
|
+
id: edgeId,
|
|
1147
|
+
from: fromId,
|
|
1148
|
+
to: toId,
|
|
1149
|
+
arrowDir,
|
|
1150
|
+
dashes,
|
|
821
1151
|
edgeColor: lastStyle.edgeColor || null,
|
|
822
1152
|
edgeWidth: lastStyle.edgeWidth || null,
|
|
823
1153
|
smooth: { enabled: false },
|
|
824
1154
|
...visEdgeProps(arrowDir, dashes),
|
|
825
|
-
...(lastStyle.edgeColor
|
|
1155
|
+
...(lastStyle.edgeColor
|
|
1156
|
+
? {
|
|
1157
|
+
color: {
|
|
1158
|
+
color: lastStyle.edgeColor,
|
|
1159
|
+
highlight: "#f97316",
|
|
1160
|
+
hover: "#f97316",
|
|
1161
|
+
},
|
|
1162
|
+
}
|
|
1163
|
+
: {}),
|
|
826
1164
|
...(lastStyle.edgeWidth ? { width: lastStyle.edgeWidth } : {}),
|
|
827
1165
|
});
|
|
828
1166
|
st.freeArrowFirstPoint = null;
|
|
@@ -837,18 +1175,18 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
837
1175
|
}
|
|
838
1176
|
});
|
|
839
1177
|
|
|
840
|
-
document.getElementById(
|
|
1178
|
+
document.getElementById("emptyState").classList.add("hidden");
|
|
841
1179
|
updateZoomDisplay();
|
|
842
1180
|
|
|
843
1181
|
// vis-network initialises shape.width/height to 50 (not undefined), so
|
|
844
1182
|
// needsRefresh() returns false and resize() never runs on the first render.
|
|
845
1183
|
// Fix: reset shape.width/height to undefined so needsRefresh() returns true
|
|
846
1184
|
// and vis-network's own render loop runs resize() with the correct context.
|
|
847
|
-
st.network.once(
|
|
1185
|
+
st.network.once("afterDrawing", () => {
|
|
848
1186
|
for (const id of st.network.body.nodeIndices) {
|
|
849
1187
|
const bn = st.network.body.nodes[id];
|
|
850
1188
|
if (!bn || !bn.shape) continue;
|
|
851
|
-
bn.shape.width
|
|
1189
|
+
bn.shape.width = undefined;
|
|
852
1190
|
bn.shape.height = undefined;
|
|
853
1191
|
}
|
|
854
1192
|
// Patch CustomShape.distanceToBorder so anchor nodes report 0: arrow tips
|
|
@@ -864,7 +1202,7 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
864
1202
|
const orig = proto.distanceToBorder;
|
|
865
1203
|
proto.distanceToBorder = function (ctx, angle) {
|
|
866
1204
|
const data = st.nodes && st.nodes.get(this.options && this.options.id);
|
|
867
|
-
if (data && data.shapeType ===
|
|
1205
|
+
if (data && data.shapeType === "anchor") return 0;
|
|
868
1206
|
return orig.call(this, ctx, angle);
|
|
869
1207
|
};
|
|
870
1208
|
proto.__ldAnchorDistancePatched = true;
|
|
@@ -872,7 +1210,7 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
|
872
1210
|
}
|
|
873
1211
|
st.network.redraw();
|
|
874
1212
|
});
|
|
875
|
-
window.dispatchEvent(new CustomEvent(
|
|
1213
|
+
window.dispatchEvent(new CustomEvent("diagram:network-ready"));
|
|
876
1214
|
}
|
|
877
1215
|
|
|
878
1216
|
// ── Anchor snap-to-connect ────────────────────────────────────────────────────
|
|
@@ -888,10 +1226,12 @@ function _onAnchorSnapConnect(params) {
|
|
|
888
1226
|
|
|
889
1227
|
for (const anchorId of params.nodes) {
|
|
890
1228
|
const anchor = st.nodes.get(anchorId);
|
|
891
|
-
if (!anchor || anchor.shapeType !==
|
|
1229
|
+
if (!anchor || anchor.shapeType !== "anchor") continue;
|
|
892
1230
|
|
|
893
1231
|
// Find the edge this anchor belongs to (as from or to)
|
|
894
|
-
const connectedEdges = st.edges
|
|
1232
|
+
const connectedEdges = st.edges
|
|
1233
|
+
.get()
|
|
1234
|
+
.filter((e) => e.from === anchorId || e.to === anchorId);
|
|
895
1235
|
if (connectedEdges.length === 0) continue;
|
|
896
1236
|
|
|
897
1237
|
// Current position of dragged anchor
|
|
@@ -907,10 +1247,12 @@ function _onAnchorSnapConnect(params) {
|
|
|
907
1247
|
}
|
|
908
1248
|
siblingIds.delete(anchorId); // keep sibling (other endpoint) in set to block self-loop
|
|
909
1249
|
|
|
910
|
-
let bestId
|
|
1250
|
+
let bestId = null;
|
|
911
1251
|
let bestDist = Infinity;
|
|
912
1252
|
|
|
913
|
-
for (const [candidateId, candidatePos] of Object.entries(
|
|
1253
|
+
for (const [candidateId, candidatePos] of Object.entries(
|
|
1254
|
+
st.network.getPositions(),
|
|
1255
|
+
)) {
|
|
914
1256
|
if (candidateId === anchorId) continue;
|
|
915
1257
|
if (siblingIds.has(candidateId)) continue; // would create a self-loop
|
|
916
1258
|
|
|
@@ -919,26 +1261,33 @@ function _onAnchorSnapConnect(params) {
|
|
|
919
1261
|
|
|
920
1262
|
const dist = Math.hypot(candidatePos.x - pos.x, candidatePos.y - pos.y);
|
|
921
1263
|
|
|
922
|
-
if (candidate.shapeType ===
|
|
1264
|
+
if (candidate.shapeType === "anchor") {
|
|
923
1265
|
// Snap to another anchor if within threshold
|
|
924
1266
|
if (dist < SNAP_THRESHOLD && dist < bestDist) {
|
|
925
|
-
bestId
|
|
1267
|
+
bestId = candidateId;
|
|
926
1268
|
bestDist = dist;
|
|
927
1269
|
}
|
|
928
1270
|
} else {
|
|
929
1271
|
// Snap to a regular node if anchor falls within its bounding box (padded by threshold)
|
|
930
1272
|
const bodyNode = st.network.body.nodes[candidateId];
|
|
931
1273
|
if (!bodyNode) continue;
|
|
932
|
-
const w =
|
|
933
|
-
|
|
1274
|
+
const w =
|
|
1275
|
+
(bodyNode.shape && bodyNode.shape.width) ||
|
|
1276
|
+
SHAPE_DEFAULTS[candidate.shapeType]?.width ||
|
|
1277
|
+
120;
|
|
1278
|
+
const h =
|
|
1279
|
+
(bodyNode.shape && bodyNode.shape.height) ||
|
|
1280
|
+
SHAPE_DEFAULTS[candidate.shapeType]?.height ||
|
|
1281
|
+
60;
|
|
934
1282
|
const cx = candidatePos.x;
|
|
935
1283
|
const cy = candidatePos.y;
|
|
936
|
-
const inBox =
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1284
|
+
const inBox =
|
|
1285
|
+
pos.x >= cx - w / 2 - SNAP_THRESHOLD &&
|
|
1286
|
+
pos.x <= cx + w / 2 + SNAP_THRESHOLD &&
|
|
1287
|
+
pos.y >= cy - h / 2 - SNAP_THRESHOLD &&
|
|
1288
|
+
pos.y <= cy + h / 2 + SNAP_THRESHOLD;
|
|
940
1289
|
if (inBox && dist < bestDist) {
|
|
941
|
-
bestId
|
|
1290
|
+
bestId = candidateId;
|
|
942
1291
|
bestDist = dist;
|
|
943
1292
|
}
|
|
944
1293
|
}
|
|
@@ -946,17 +1295,21 @@ function _onAnchorSnapConnect(params) {
|
|
|
946
1295
|
|
|
947
1296
|
if (!bestId) continue;
|
|
948
1297
|
|
|
949
|
-
if (!snapped) {
|
|
1298
|
+
if (!snapped) {
|
|
1299
|
+
pushSnapshot();
|
|
1300
|
+
snapped = true;
|
|
1301
|
+
}
|
|
950
1302
|
|
|
951
1303
|
// Determine which port to attach to on the target node (non-anchor targets only).
|
|
952
1304
|
const targetNode = st.nodes.get(bestId);
|
|
953
|
-
const isAnchorTarget = targetNode && targetNode.shapeType ===
|
|
1305
|
+
const isAnchorTarget = targetNode && targetNode.shapeType === "anchor";
|
|
954
1306
|
// Use the hovered port (computed by mousemove) when available; fall back to nearest port.
|
|
955
1307
|
let portKey = null;
|
|
956
1308
|
if (!isAnchorTarget) {
|
|
957
|
-
portKey =
|
|
958
|
-
|
|
959
|
-
|
|
1309
|
+
portKey =
|
|
1310
|
+
_hoveredPortNodeId === bestId && _hoveredPortKey
|
|
1311
|
+
? _hoveredPortKey
|
|
1312
|
+
: getNearestPort(bestId, pos);
|
|
960
1313
|
}
|
|
961
1314
|
|
|
962
1315
|
// Reconnect every edge that uses this anchor as an endpoint
|
|
@@ -966,7 +1319,11 @@ function _onAnchorSnapConnect(params) {
|
|
|
966
1319
|
if (portKey) update.fromPort = portKey;
|
|
967
1320
|
// Port edges must be transparent ghosts so only drawPortEdge is visible.
|
|
968
1321
|
if (portKey || e.toPort) {
|
|
969
|
-
update.color
|
|
1322
|
+
update.color = {
|
|
1323
|
+
color: "rgba(0,0,0,0)",
|
|
1324
|
+
highlight: "rgba(0,0,0,0)",
|
|
1325
|
+
hover: "rgba(0,0,0,0)",
|
|
1326
|
+
};
|
|
970
1327
|
update.arrows = { to: { enabled: false }, from: { enabled: false } };
|
|
971
1328
|
}
|
|
972
1329
|
st.edges.update(update);
|
|
@@ -976,7 +1333,11 @@ function _onAnchorSnapConnect(params) {
|
|
|
976
1333
|
if (portKey) update.toPort = portKey;
|
|
977
1334
|
// Port edges must be transparent ghosts so only drawPortEdge is visible.
|
|
978
1335
|
if (portKey || e.fromPort) {
|
|
979
|
-
update.color
|
|
1336
|
+
update.color = {
|
|
1337
|
+
color: "rgba(0,0,0,0)",
|
|
1338
|
+
highlight: "rgba(0,0,0,0)",
|
|
1339
|
+
hover: "rgba(0,0,0,0)",
|
|
1340
|
+
};
|
|
980
1341
|
update.arrows = { to: { enabled: false }, from: { enabled: false } };
|
|
981
1342
|
}
|
|
982
1343
|
st.edges.update(update);
|
|
@@ -996,133 +1357,146 @@ function _onAnchorSnapConnect(params) {
|
|
|
996
1357
|
// labels to appear twice at different positions.
|
|
997
1358
|
function drawEdgeLabels(ctx) {
|
|
998
1359
|
try {
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
const ly = my + oy;
|
|
1360
|
+
if (!st.edges || !st.network) return;
|
|
1361
|
+
|
|
1362
|
+
const m = ctx.getTransform();
|
|
1363
|
+
const dpr = window.devicePixelRatio || 1;
|
|
1364
|
+
const canvasEl = ctx.canvas;
|
|
1365
|
+
const container = document.getElementById("vis-canvas").parentElement;
|
|
1366
|
+
const canvasRect = canvasEl.getBoundingClientRect();
|
|
1367
|
+
const containerRect = container.getBoundingClientRect();
|
|
1368
|
+
const offsetX = canvasRect.left - containerRect.left;
|
|
1369
|
+
const offsetY = canvasRect.top - containerRect.top;
|
|
1370
|
+
|
|
1371
|
+
st.edges.get().forEach((e) => {
|
|
1372
|
+
// Port edges draw their own labels inside drawPortEdge — skip here.
|
|
1373
|
+
if (e.fromPort || e.toPort) return;
|
|
1374
|
+
|
|
1375
|
+
// Compute bezier midpoint in layout space for every edge (labeled or not)
|
|
1376
|
+
// so the label editor always has an accurate DOM position to open at.
|
|
1377
|
+
const bodyEdge = st.network.body.edges[e.id];
|
|
1378
|
+
let mx, my;
|
|
1379
|
+
if (
|
|
1380
|
+
bodyEdge &&
|
|
1381
|
+
bodyEdge.edgeType &&
|
|
1382
|
+
typeof bodyEdge.edgeType.getPoint === "function"
|
|
1383
|
+
) {
|
|
1384
|
+
const pt = bodyEdge.edgeType.getPoint(0.5);
|
|
1385
|
+
mx = pt.x;
|
|
1386
|
+
my = pt.y;
|
|
1387
|
+
} else {
|
|
1388
|
+
const positions = st.network.getPositions([e.from, e.to]);
|
|
1389
|
+
const fp = positions[e.from];
|
|
1390
|
+
const tp = positions[e.to];
|
|
1391
|
+
if (!fp || !tp) return;
|
|
1392
|
+
mx = (fp.x + tp.x) / 2;
|
|
1393
|
+
my = (fp.y + tp.y) / 2;
|
|
1394
|
+
}
|
|
1035
1395
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1396
|
+
const ox = e.edgeLabelOffsetX || 0;
|
|
1397
|
+
const oy = e.edgeLabelOffsetY || 0;
|
|
1398
|
+
const lx = mx + ox;
|
|
1399
|
+
const ly = my + oy;
|
|
1040
1400
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1401
|
+
st.edgeLabelCanvasPos[e.id] = {
|
|
1402
|
+
x: (m.a * lx + m.e) / dpr + offsetX,
|
|
1403
|
+
y: (m.d * ly + m.f) / dpr + offsetY,
|
|
1404
|
+
};
|
|
1043
1405
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
const PAD_X = 6, PAD_Y = 4;
|
|
1406
|
+
// Only draw the label text for edges that have one.
|
|
1407
|
+
if (!e.label) return;
|
|
1047
1408
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
}
|
|
1053
|
-
ctx.font = `${fontSize}px system-ui,-apple-system,sans-serif`;
|
|
1054
|
-
|
|
1055
|
-
const fixedW = e.edgeLabelWidth || null;
|
|
1056
|
-
const innerW = fixedW ? fixedW - PAD_X * 2 : null;
|
|
1057
|
-
const lines = fixedW ? wrapText(ctx, e.label, innerW) : [e.label];
|
|
1058
|
-
const textW = fixedW ? innerW : ctx.measureText(e.label).width;
|
|
1059
|
-
const boxW = textW + PAD_X * 2;
|
|
1060
|
-
const boxH = lines.length * lineHeight + PAD_Y * 2;
|
|
1061
|
-
|
|
1062
|
-
// Always store bbox (needed for resize handles, even when not selected)
|
|
1063
|
-
st.edgeLabelBBox[e.id] = {
|
|
1064
|
-
cx: lx, cy: ly,
|
|
1065
|
-
w: boxW, h: boxH,
|
|
1066
|
-
rotation: e.labelRotation || 0,
|
|
1067
|
-
};
|
|
1409
|
+
const fontSize = e.fontSize || 11;
|
|
1410
|
+
const lineHeight = fontSize * 1.5;
|
|
1411
|
+
const PAD_X = 6,
|
|
1412
|
+
PAD_Y = 4;
|
|
1068
1413
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1414
|
+
ctx.save();
|
|
1415
|
+
ctx.translate(lx, ly);
|
|
1416
|
+
if (e.labelRotation && Math.abs(e.labelRotation) > 0.001) {
|
|
1417
|
+
ctx.rotate(e.labelRotation);
|
|
1418
|
+
}
|
|
1419
|
+
ctx.font = `${fontSize}px system-ui,-apple-system,sans-serif`;
|
|
1420
|
+
|
|
1421
|
+
const fixedW = e.edgeLabelWidth || null;
|
|
1422
|
+
const innerW = fixedW ? fixedW - PAD_X * 2 : null;
|
|
1423
|
+
const lines = fixedW ? wrapText(ctx, e.label, innerW) : [e.label];
|
|
1424
|
+
const textW = fixedW ? innerW : ctx.measureText(e.label).width;
|
|
1425
|
+
const boxW = textW + PAD_X * 2;
|
|
1426
|
+
const boxH = lines.length * lineHeight + PAD_Y * 2;
|
|
1427
|
+
|
|
1428
|
+
// Always store bbox (needed for resize handles, even when not selected)
|
|
1429
|
+
st.edgeLabelBBox[e.id] = {
|
|
1430
|
+
cx: lx,
|
|
1431
|
+
cy: ly,
|
|
1432
|
+
w: boxW,
|
|
1433
|
+
h: boxH,
|
|
1434
|
+
rotation: e.labelRotation || 0,
|
|
1435
|
+
};
|
|
1073
1436
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
textW + TEXT_PAD * 2, totalH + TEXT_PAD * 2);
|
|
1079
|
-
ctx.restore();
|
|
1437
|
+
const totalH = lines.length * lineHeight;
|
|
1438
|
+
const TEXT_PAD = 3;
|
|
1439
|
+
const isDark = document.documentElement.classList.contains("dark");
|
|
1440
|
+
const bgFill = isDark ? "rgba(3,7,18,0.82)" : "rgba(249,250,251,0.82)";
|
|
1080
1441
|
|
|
1081
|
-
|
|
1082
|
-
if (st.selectedEdgeIds && st.selectedEdgeIds.includes(e.id)) {
|
|
1442
|
+
// Opaque background tight around the text — makes the arrow appear to pass behind.
|
|
1083
1443
|
ctx.save();
|
|
1084
|
-
ctx.
|
|
1085
|
-
ctx.
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1444
|
+
ctx.fillStyle = bgFill;
|
|
1445
|
+
ctx.fillRect(
|
|
1446
|
+
-textW / 2 - TEXT_PAD,
|
|
1447
|
+
-totalH / 2 - TEXT_PAD,
|
|
1448
|
+
textW + TEXT_PAD * 2,
|
|
1449
|
+
totalH + TEXT_PAD * 2,
|
|
1450
|
+
);
|
|
1089
1451
|
ctx.restore();
|
|
1090
|
-
}
|
|
1091
1452
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1453
|
+
// Dashed border box — only when the edge is selected
|
|
1454
|
+
if (st.selectedEdgeIds && st.selectedEdgeIds.includes(e.id)) {
|
|
1455
|
+
ctx.save();
|
|
1456
|
+
ctx.strokeStyle = "#9ca3af";
|
|
1457
|
+
ctx.lineWidth = 0.8;
|
|
1458
|
+
ctx.setLineDash([3, 3]);
|
|
1459
|
+
ctx.strokeRect(-boxW / 2, -boxH / 2, boxW, boxH);
|
|
1460
|
+
ctx.setLineDash([]);
|
|
1461
|
+
ctx.restore();
|
|
1462
|
+
}
|
|
1099
1463
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1464
|
+
ctx.fillStyle = "#6b7280";
|
|
1465
|
+
ctx.textAlign = "center";
|
|
1466
|
+
ctx.textBaseline = "middle";
|
|
1467
|
+
lines.forEach((line, i) => {
|
|
1468
|
+
const y = -totalH / 2 + i * lineHeight + lineHeight / 2;
|
|
1469
|
+
ctx.fillText(line, 0, y);
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
ctx.restore();
|
|
1473
|
+
});
|
|
1474
|
+
} catch (err) {
|
|
1475
|
+
console.error("[draw] EXCEPTION:", err);
|
|
1476
|
+
}
|
|
1103
1477
|
}
|
|
1104
1478
|
|
|
1105
1479
|
// ── Network event handlers ────────────────────────────────────────────────────
|
|
1106
1480
|
|
|
1107
1481
|
// Distance from point (px,py) to segment (ax,ay)-(bx,by) in canvas space.
|
|
1108
1482
|
function _distToSegment(px, py, ax, ay, bx, by) {
|
|
1109
|
-
const dx = bx - ax,
|
|
1483
|
+
const dx = bx - ax,
|
|
1484
|
+
dy = by - ay;
|
|
1110
1485
|
const lenSq = dx * dx + dy * dy;
|
|
1111
1486
|
if (lenSq === 0) return Math.hypot(px - ax, py - ay);
|
|
1112
1487
|
const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / lenSq));
|
|
1113
1488
|
return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
|
|
1114
1489
|
}
|
|
1115
1490
|
|
|
1116
|
-
|
|
1117
1491
|
function nodeContainsCanvasPoint(id, canvasPos) {
|
|
1118
|
-
const n
|
|
1492
|
+
const n = st.nodes.get(id);
|
|
1119
1493
|
const bn = st.network && st.network.body.nodes[id];
|
|
1120
|
-
if (!n || !bn || n.shapeType ===
|
|
1494
|
+
if (!n || !bn || n.shapeType === "anchor") return false;
|
|
1121
1495
|
|
|
1122
|
-
const shapeType = n.shapeType ||
|
|
1123
|
-
const defaults
|
|
1496
|
+
const shapeType = n.shapeType || "box";
|
|
1497
|
+
const defaults = SHAPE_DEFAULTS[shapeType] || [60, 28];
|
|
1124
1498
|
const W = n.nodeWidth || defaults[0];
|
|
1125
|
-
const H = shapeType ===
|
|
1499
|
+
const H = shapeType === "circle" ? W : n.nodeHeight || defaults[1];
|
|
1126
1500
|
|
|
1127
1501
|
const dx = canvasPos.x - bn.x;
|
|
1128
1502
|
const dy = canvasPos.y - bn.y;
|
|
@@ -1130,10 +1504,12 @@ function nodeContainsCanvasPoint(id, canvasPos) {
|
|
|
1130
1504
|
const lx = rot ? dx * Math.cos(-rot) - dy * Math.sin(-rot) : dx;
|
|
1131
1505
|
const ly = rot ? dx * Math.sin(-rot) + dy * Math.cos(-rot) : dy;
|
|
1132
1506
|
|
|
1133
|
-
if (shapeType ===
|
|
1507
|
+
if (shapeType === "circle" || shapeType === "ellipse") {
|
|
1134
1508
|
const rx = W / 2;
|
|
1135
1509
|
const ry = H / 2;
|
|
1136
|
-
return
|
|
1510
|
+
return (
|
|
1511
|
+
rx > 0 && ry > 0 && (lx * lx) / (rx * rx) + (ly * ly) / (ry * ry) <= 1
|
|
1512
|
+
);
|
|
1137
1513
|
}
|
|
1138
1514
|
|
|
1139
1515
|
return Math.abs(lx) <= W / 2 && Math.abs(ly) <= H / 2;
|
|
@@ -1161,15 +1537,15 @@ function topmostNodeAt(canvasPos) {
|
|
|
1161
1537
|
function edgeDrawLevel(edgeData) {
|
|
1162
1538
|
if (!edgeData) return -1;
|
|
1163
1539
|
const fromLevel = st.canonicalOrder.indexOf(edgeData.from);
|
|
1164
|
-
const toLevel
|
|
1540
|
+
const toLevel = st.canonicalOrder.indexOf(edgeData.to);
|
|
1165
1541
|
if (fromLevel === -1 && toLevel === -1) return -1;
|
|
1166
1542
|
if (fromLevel === -1) return toLevel;
|
|
1167
1543
|
if (toLevel === -1) return fromLevel;
|
|
1168
1544
|
|
|
1169
1545
|
const fromNode = st.nodes.get(edgeData.from);
|
|
1170
|
-
const toNode
|
|
1171
|
-
const fromIsAnchor = fromNode && fromNode.shapeType ===
|
|
1172
|
-
const toIsAnchor
|
|
1546
|
+
const toNode = st.nodes.get(edgeData.to);
|
|
1547
|
+
const fromIsAnchor = fromNode && fromNode.shapeType === "anchor";
|
|
1548
|
+
const toIsAnchor = toNode && toNode.shapeType === "anchor";
|
|
1173
1549
|
if (toIsAnchor && !fromIsAnchor) return fromLevel;
|
|
1174
1550
|
if (fromIsAnchor && !toIsAnchor) return toLevel;
|
|
1175
1551
|
return Math.max(fromLevel, toLevel);
|
|
@@ -1199,7 +1575,7 @@ function edgeLabelAtCanvasPoint(canvasPos) {
|
|
|
1199
1575
|
const bbox = st.edgeLabelBBox[edge.id];
|
|
1200
1576
|
if (!bbox) continue;
|
|
1201
1577
|
|
|
1202
|
-
const r
|
|
1578
|
+
const r = -(bbox.rotation || 0);
|
|
1203
1579
|
const dx = canvasPos.x - bbox.cx;
|
|
1204
1580
|
const dy = canvasPos.y - bbox.cy;
|
|
1205
1581
|
const lx = dx * Math.cos(r) - dy * Math.sin(r);
|
|
@@ -1218,24 +1594,28 @@ function edgeLabelAtCanvasPoint(canvasPos) {
|
|
|
1218
1594
|
function isEdgeInteractive(edge) {
|
|
1219
1595
|
if (!edge) return false;
|
|
1220
1596
|
const fromN = st.nodes.get(edge.from);
|
|
1221
|
-
const toN
|
|
1222
|
-
const isFreeArrow =
|
|
1597
|
+
const toN = st.nodes.get(edge.to);
|
|
1598
|
+
const isFreeArrow =
|
|
1599
|
+
fromN && fromN.shapeType === "anchor" && toN && toN.shapeType === "anchor";
|
|
1223
1600
|
return isFreeArrow ? !(fromN.locked && toN.locked) : !edge.edgeLocked;
|
|
1224
1601
|
}
|
|
1225
1602
|
|
|
1226
1603
|
function selectableEdgesForNodes(nodeIds) {
|
|
1227
1604
|
const selectedSet = new Set(nodeIds);
|
|
1228
|
-
return st.edges
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1605
|
+
return st.edges
|
|
1606
|
+
.get()
|
|
1607
|
+
.filter((e) => {
|
|
1608
|
+
if (!selectedSet.has(e.from) || !selectedSet.has(e.to)) return false;
|
|
1609
|
+
return isEdgeInteractive(e);
|
|
1610
|
+
})
|
|
1611
|
+
.map((e) => e.id);
|
|
1232
1612
|
}
|
|
1233
1613
|
|
|
1234
1614
|
function selectNodesFromClick(nodeId, srcEvent) {
|
|
1235
1615
|
const additive = !!(srcEvent && (srcEvent.metaKey || srcEvent.ctrlKey));
|
|
1236
1616
|
const clicked = expandSelectionToGroup([nodeId]).filter((id) => {
|
|
1237
1617
|
const n = st.nodes.get(id);
|
|
1238
|
-
return n && !n.locked && n.shapeType !==
|
|
1618
|
+
return n && !n.locked && n.shapeType !== "anchor";
|
|
1239
1619
|
});
|
|
1240
1620
|
if (!clicked.length) return;
|
|
1241
1621
|
|
|
@@ -1249,7 +1629,7 @@ function selectNodesFromClick(nodeId, srcEvent) {
|
|
|
1249
1629
|
});
|
|
1250
1630
|
nodeIds = Array.from(next).filter((id) => {
|
|
1251
1631
|
const n = st.nodes.get(id);
|
|
1252
|
-
return n && !n.locked && n.shapeType !==
|
|
1632
|
+
return n && !n.locked && n.shapeType !== "anchor";
|
|
1253
1633
|
});
|
|
1254
1634
|
} else {
|
|
1255
1635
|
nodeIds = clicked;
|
|
@@ -1273,11 +1653,14 @@ function selectNodesFromClick(nodeId, srcEvent) {
|
|
|
1273
1653
|
|
|
1274
1654
|
function onDoubleClick(params) {
|
|
1275
1655
|
const srcEvent = params.event && params.event.srcEvent;
|
|
1276
|
-
const clientPos = srcEvent
|
|
1656
|
+
const clientPos = srcEvent
|
|
1657
|
+
? { x: srcEvent.clientX, y: srcEvent.clientY }
|
|
1658
|
+
: null;
|
|
1277
1659
|
const labelEdgeId = edgeLabelAtCanvasPoint(params.pointer.canvas);
|
|
1278
1660
|
const topNodeId = topmostNodeAt(params.pointer.canvas);
|
|
1279
1661
|
const topNode = topNodeId && st.nodes.get(topNodeId);
|
|
1280
|
-
const canEditTopNode =
|
|
1662
|
+
const canEditTopNode =
|
|
1663
|
+
topNode && !topNode.locked && topNode.shapeType !== "anchor";
|
|
1281
1664
|
|
|
1282
1665
|
// Direct node double-clicks must edit the node. Dense port-edge diagrams can
|
|
1283
1666
|
// have many visual edges crossing a node; those should not steal the edit.
|
|
@@ -1294,12 +1677,20 @@ function onDoubleClick(params) {
|
|
|
1294
1677
|
|
|
1295
1678
|
const nativeEdgeId = clientPos ? st.network.getEdgeAt(clientPos) : null;
|
|
1296
1679
|
const portEdge = nearestPortEdgeAt(params.pointer.canvas);
|
|
1297
|
-
const edgeCandidates = [
|
|
1680
|
+
const edgeCandidates = [
|
|
1681
|
+
labelEdgeId,
|
|
1682
|
+
nativeEdgeId,
|
|
1683
|
+
portEdge && portEdge.id,
|
|
1684
|
+
...params.edges,
|
|
1685
|
+
]
|
|
1298
1686
|
.filter((id, index, list) => id && list.indexOf(id) === index)
|
|
1299
1687
|
.filter((id) => isEdgeInteractive(st.edges.get(id)));
|
|
1300
1688
|
const edgeId = edgeCandidates.reduce((bestId, id) => {
|
|
1301
1689
|
if (!bestId) return id;
|
|
1302
|
-
return edgeDrawLevel(st.edges.get(id)) >=
|
|
1690
|
+
return edgeDrawLevel(st.edges.get(id)) >=
|
|
1691
|
+
edgeDrawLevel(st.edges.get(bestId))
|
|
1692
|
+
? id
|
|
1693
|
+
: bestId;
|
|
1303
1694
|
}, null);
|
|
1304
1695
|
|
|
1305
1696
|
if (edgeId) {
|
|
@@ -1309,34 +1700,60 @@ function onDoubleClick(params) {
|
|
|
1309
1700
|
hideNodePanel();
|
|
1310
1701
|
showEdgePanel();
|
|
1311
1702
|
startEdgeLabelEdit();
|
|
1312
|
-
} else if (st.currentTool ===
|
|
1703
|
+
} else if (st.currentTool === "addNode" && st.pendingShape === "image") {
|
|
1313
1704
|
const canvasPos = params.pointer.canvas;
|
|
1314
1705
|
pickAndCreateImageNode(canvasPos.x, canvasPos.y);
|
|
1315
|
-
} else if (st.currentTool ===
|
|
1706
|
+
} else if (st.currentTool === "addNode") {
|
|
1316
1707
|
pushSnapshot();
|
|
1317
|
-
const id
|
|
1708
|
+
const id = "n" + Date.now();
|
|
1318
1709
|
const customShapeId = customShapeIdFromTool(st.pendingShape);
|
|
1319
1710
|
const shapeType = customShapeId ? CUSTOM_SHAPE_TYPE : st.pendingShape;
|
|
1320
|
-
const customDef = customShapeId
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
const
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
const
|
|
1711
|
+
const customDef = customShapeId
|
|
1712
|
+
? getCustomShapeDefinition(customShapeId)
|
|
1713
|
+
: null;
|
|
1714
|
+
const defaults = customShapeId
|
|
1715
|
+
? getCustomShapeDefaultSize(customShapeId)
|
|
1716
|
+
: SHAPE_DEFAULTS[shapeType] || [100, 40];
|
|
1717
|
+
const fallbackColor = shapeType === "post-it" ? "c-amber" : "c-gray";
|
|
1718
|
+
const lastStyle = getLastNodeStyle(shapeType);
|
|
1719
|
+
const colorKey = lastStyle.colorKey || fallbackColor;
|
|
1720
|
+
const fontSize = lastStyle.fontSize || null;
|
|
1721
|
+
const textAlign = lastStyle.textAlign || null;
|
|
1327
1722
|
const textValign = lastStyle.textValign || null;
|
|
1328
|
-
const rawPos
|
|
1329
|
-
const pos
|
|
1723
|
+
const rawPos = params.pointer.canvas;
|
|
1724
|
+
const pos = st.gridEnabled ? snapToGrid(rawPos.x, rawPos.y) : rawPos;
|
|
1330
1725
|
st.nodes.add({
|
|
1331
|
-
id,
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1726
|
+
id,
|
|
1727
|
+
label:
|
|
1728
|
+
shapeType === "text-free"
|
|
1729
|
+
? t("diagram.label_input.placeholder")
|
|
1730
|
+
: customDef
|
|
1731
|
+
? customDef.name
|
|
1732
|
+
: "Node",
|
|
1733
|
+
shapeType,
|
|
1734
|
+
customShapeId: customShapeId || null,
|
|
1735
|
+
colorKey,
|
|
1736
|
+
nodeWidth: defaults[0],
|
|
1737
|
+
nodeHeight: defaults[1],
|
|
1738
|
+
fontSize,
|
|
1739
|
+
textAlign,
|
|
1740
|
+
textValign,
|
|
1741
|
+
rotation: 0,
|
|
1742
|
+
labelRotation: 0,
|
|
1743
|
+
x: pos.x,
|
|
1744
|
+
y: pos.y,
|
|
1745
|
+
...visNodeProps(
|
|
1746
|
+
shapeType,
|
|
1747
|
+
colorKey,
|
|
1748
|
+
defaults[0],
|
|
1749
|
+
defaults[1],
|
|
1750
|
+
fontSize,
|
|
1751
|
+
textAlign,
|
|
1752
|
+
textValign,
|
|
1753
|
+
),
|
|
1338
1754
|
});
|
|
1339
1755
|
markDirty();
|
|
1756
|
+
|
|
1340
1757
|
setTimeout(() => {
|
|
1341
1758
|
st.network.selectNodes([id]);
|
|
1342
1759
|
st.selectedNodeIds = [id];
|
|
@@ -1349,9 +1766,9 @@ function onDoubleClick(params) {
|
|
|
1349
1766
|
// ── Image node creation ───────────────────────────────────────────────────────
|
|
1350
1767
|
|
|
1351
1768
|
function pickAndCreateImageNode(canvasX, canvasY) {
|
|
1352
|
-
const input = document.createElement(
|
|
1353
|
-
input.type
|
|
1354
|
-
input.accept =
|
|
1769
|
+
const input = document.createElement("input");
|
|
1770
|
+
input.type = "file";
|
|
1771
|
+
input.accept = "image/*";
|
|
1355
1772
|
input.onchange = async () => {
|
|
1356
1773
|
const file = input.files && input.files[0];
|
|
1357
1774
|
if (!file) return;
|
|
@@ -1361,7 +1778,7 @@ function pickAndCreateImageNode(canvasX, canvasY) {
|
|
|
1361
1778
|
const src = await uploadImageFile(file, name);
|
|
1362
1779
|
createImageNode(src, canvasX, canvasY);
|
|
1363
1780
|
} catch {
|
|
1364
|
-
showToast(t(
|
|
1781
|
+
showToast(t("diagram.toast.image_import_error"), "error");
|
|
1365
1782
|
}
|
|
1366
1783
|
};
|
|
1367
1784
|
input.click();
|
|
@@ -1369,32 +1786,47 @@ function pickAndCreateImageNode(canvasX, canvasY) {
|
|
|
1369
1786
|
|
|
1370
1787
|
export function createImageNode(imageSrc, canvasX, canvasY) {
|
|
1371
1788
|
if (!st.network) return;
|
|
1372
|
-
const id
|
|
1373
|
-
const captionId = id +
|
|
1789
|
+
const id = "n" + Date.now();
|
|
1790
|
+
const captionId = id + "c";
|
|
1374
1791
|
|
|
1375
1792
|
const addNode = (nW, nH) => {
|
|
1376
1793
|
pushSnapshot();
|
|
1377
|
-
const filename
|
|
1378
|
-
const textDefs
|
|
1379
|
-
const captionH
|
|
1380
|
-
const GAP
|
|
1794
|
+
const filename = imageSrc.split("/").pop() || "";
|
|
1795
|
+
const textDefs = SHAPE_DEFAULTS["text-free"];
|
|
1796
|
+
const captionH = textDefs[1];
|
|
1797
|
+
const GAP = 8;
|
|
1381
1798
|
|
|
1382
|
-
const groupId =
|
|
1799
|
+
const groupId = "g" + Date.now();
|
|
1383
1800
|
st.nodes.add({
|
|
1384
|
-
id,
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1801
|
+
id,
|
|
1802
|
+
label: "",
|
|
1803
|
+
imageSrc,
|
|
1804
|
+
groupId,
|
|
1805
|
+
shapeType: "image",
|
|
1806
|
+
colorKey: "c-gray",
|
|
1807
|
+
nodeWidth: nW,
|
|
1808
|
+
nodeHeight: nH,
|
|
1809
|
+
fontSize: null,
|
|
1810
|
+
rotation: 0,
|
|
1811
|
+
labelRotation: 0,
|
|
1812
|
+
x: canvasX,
|
|
1813
|
+
y: canvasY,
|
|
1814
|
+
...visNodeProps("image", "c-gray", nW, nH, null, null, null),
|
|
1390
1815
|
});
|
|
1391
1816
|
st.nodes.add({
|
|
1392
|
-
id: captionId,
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1817
|
+
id: captionId,
|
|
1818
|
+
label: filename,
|
|
1819
|
+
groupId,
|
|
1820
|
+
shapeType: "text-free",
|
|
1821
|
+
colorKey: "c-gray",
|
|
1822
|
+
nodeWidth: nW,
|
|
1823
|
+
nodeHeight: captionH,
|
|
1824
|
+
fontSize: null,
|
|
1825
|
+
rotation: 0,
|
|
1826
|
+
labelRotation: 0,
|
|
1827
|
+
x: canvasX,
|
|
1828
|
+
y: canvasY + nH / 2 + GAP + captionH / 2,
|
|
1829
|
+
...visNodeProps("text-free", "c-gray", nW, captionH, null, null, null),
|
|
1398
1830
|
});
|
|
1399
1831
|
markDirty();
|
|
1400
1832
|
setTimeout(() => {
|
|
@@ -1408,12 +1840,16 @@ export function createImageNode(imageSrc, canvasX, canvasY) {
|
|
|
1408
1840
|
img.onload = () => {
|
|
1409
1841
|
const MAX = 300;
|
|
1410
1842
|
const ratio = img.naturalWidth / img.naturalHeight;
|
|
1411
|
-
let nW = img.naturalWidth,
|
|
1412
|
-
|
|
1843
|
+
let nW = img.naturalWidth,
|
|
1844
|
+
nH = img.naturalHeight;
|
|
1845
|
+
if (nW > MAX) {
|
|
1846
|
+
nW = MAX;
|
|
1847
|
+
nH = Math.round(MAX / ratio);
|
|
1848
|
+
}
|
|
1413
1849
|
addNode(nW, nH);
|
|
1414
1850
|
};
|
|
1415
1851
|
img.onerror = () => {
|
|
1416
|
-
const d = SHAPE_DEFAULTS[
|
|
1852
|
+
const d = SHAPE_DEFAULTS["image"];
|
|
1417
1853
|
addNode(d[0], d[1]);
|
|
1418
1854
|
};
|
|
1419
1855
|
img.src = imageSrc;
|
|
@@ -1424,17 +1860,20 @@ export function toggleEdgeStraight() {
|
|
|
1424
1860
|
if (!st.network) return;
|
|
1425
1861
|
pushSnapshot();
|
|
1426
1862
|
st.edgesStraight = !st.edgesStraight;
|
|
1427
|
-
const smooth = st.edgesStraight ? { enabled: false } : { type:
|
|
1863
|
+
const smooth = st.edgesStraight ? { enabled: false } : { type: "continuous" };
|
|
1428
1864
|
// Update global network option first (overrides per-edge inherited defaults).
|
|
1429
1865
|
st.network.setOptions({ edges: { smooth } });
|
|
1430
1866
|
// Then update each edge individually, keeping anchor edges always straight.
|
|
1431
1867
|
const updates = st.edges.get().map((e) => {
|
|
1432
1868
|
const toData = st.nodes.get(e.to);
|
|
1433
|
-
const s =
|
|
1869
|
+
const s =
|
|
1870
|
+
toData && toData.shapeType === "anchor" ? { enabled: false } : smooth;
|
|
1434
1871
|
return { id: e.id, smooth: s };
|
|
1435
1872
|
});
|
|
1436
1873
|
if (updates.length) st.edges.update(updates);
|
|
1437
|
-
document
|
|
1874
|
+
document
|
|
1875
|
+
.getElementById("btnEdgeStraight")
|
|
1876
|
+
.classList.toggle("tool-active", st.edgesStraight);
|
|
1438
1877
|
markDirty();
|
|
1439
1878
|
}
|
|
1440
1879
|
|
|
@@ -1449,15 +1888,24 @@ function onClickNode(params) {
|
|
|
1449
1888
|
// endpoint nodes, reconnect that end to the new port without losing selection.
|
|
1450
1889
|
if (_rehookEdgeId && _rehookHoveredNodeId && _rehookHoveredPortKey) {
|
|
1451
1890
|
const edgeData = st.edges.get(_rehookEdgeId);
|
|
1452
|
-
if (
|
|
1891
|
+
if (
|
|
1892
|
+
edgeData &&
|
|
1893
|
+
(edgeData.from === _rehookHoveredNodeId ||
|
|
1894
|
+
edgeData.to === _rehookHoveredNodeId)
|
|
1895
|
+
) {
|
|
1453
1896
|
const wasPortEdge = !!(edgeData.fromPort || edgeData.toPort);
|
|
1454
1897
|
const update = { id: edgeData.id };
|
|
1455
|
-
if (edgeData.from === _rehookHoveredNodeId)
|
|
1456
|
-
|
|
1898
|
+
if (edgeData.from === _rehookHoveredNodeId)
|
|
1899
|
+
update.fromPort = _rehookHoveredPortKey;
|
|
1900
|
+
else update.toPort = _rehookHoveredPortKey;
|
|
1457
1901
|
// First port assigned on a native edge: hide vis-network's rendering so
|
|
1458
1902
|
// drawPortEdge() takes over without double-rendering.
|
|
1459
1903
|
if (!wasPortEdge) {
|
|
1460
|
-
update.color
|
|
1904
|
+
update.color = {
|
|
1905
|
+
color: "rgba(0,0,0,0)",
|
|
1906
|
+
highlight: "rgba(0,0,0,0)",
|
|
1907
|
+
hover: "rgba(0,0,0,0)",
|
|
1908
|
+
};
|
|
1461
1909
|
update.arrows = { to: { enabled: false }, from: { enabled: false } };
|
|
1462
1910
|
}
|
|
1463
1911
|
pushSnapshot();
|
|
@@ -1485,7 +1933,12 @@ function onClickNode(params) {
|
|
|
1485
1933
|
const top = topmostNodeAt(params.pointer.canvas);
|
|
1486
1934
|
if (top) {
|
|
1487
1935
|
const topNode = st.nodes.get(top);
|
|
1488
|
-
if (
|
|
1936
|
+
if (
|
|
1937
|
+
topNode &&
|
|
1938
|
+
!topNode.locked &&
|
|
1939
|
+
topNode.shapeType !== "anchor" &&
|
|
1940
|
+
!labelEdgeId
|
|
1941
|
+
) {
|
|
1489
1942
|
setTimeout(() => {
|
|
1490
1943
|
selectNodesFromClick(top, params.event.srcEvent);
|
|
1491
1944
|
}, 0);
|
|
@@ -1493,18 +1946,28 @@ function onClickNode(params) {
|
|
|
1493
1946
|
}
|
|
1494
1947
|
}
|
|
1495
1948
|
|
|
1496
|
-
const clientPos = {
|
|
1949
|
+
const clientPos = {
|
|
1950
|
+
x: params.event.srcEvent.clientX,
|
|
1951
|
+
y: params.event.srcEvent.clientY,
|
|
1952
|
+
};
|
|
1497
1953
|
const nativeEdgeId = st.network.getEdgeAt(clientPos);
|
|
1498
1954
|
const portEdge = nearestPortEdgeAt(params.pointer.canvas);
|
|
1499
1955
|
const edgeId = labelEdgeId || nativeEdgeId || (portEdge && portEdge.id);
|
|
1500
1956
|
if (edgeId) {
|
|
1501
|
-
const edge
|
|
1957
|
+
const edge = st.edges.get(edgeId);
|
|
1502
1958
|
const fromN = edge && st.nodes.get(edge.from);
|
|
1503
|
-
const toN
|
|
1504
|
-
const isFreeArrow =
|
|
1959
|
+
const toN = edge && st.nodes.get(edge.to);
|
|
1960
|
+
const isFreeArrow =
|
|
1961
|
+
fromN &&
|
|
1962
|
+
fromN.shapeType === "anchor" &&
|
|
1963
|
+
toN &&
|
|
1964
|
+
toN.shapeType === "anchor";
|
|
1505
1965
|
// If the user clicked directly on an anchor node (not the edge body), keep only
|
|
1506
1966
|
// that single anchor selected so dragging pivots the arrow instead of moving it whole.
|
|
1507
|
-
const clickedAnAnchor = params.nodes.some(id => {
|
|
1967
|
+
const clickedAnAnchor = params.nodes.some((id) => {
|
|
1968
|
+
const n = st.nodes.get(id);
|
|
1969
|
+
return n && n.shapeType === "anchor";
|
|
1970
|
+
});
|
|
1508
1971
|
if (isFreeArrow && clickedAnAnchor) return;
|
|
1509
1972
|
setTimeout(() => {
|
|
1510
1973
|
const sel = isFreeArrow
|
|
@@ -1523,9 +1986,9 @@ function onClickNode(params) {
|
|
|
1523
1986
|
}
|
|
1524
1987
|
// No edge at click position — apply z-order correction for the topmost node.
|
|
1525
1988
|
const fallbackTop = topmostNodeAt(params.pointer.canvas);
|
|
1526
|
-
const clickable = params.nodes.filter(id => {
|
|
1989
|
+
const clickable = params.nodes.filter((id) => {
|
|
1527
1990
|
const n = st.nodes.get(id);
|
|
1528
|
-
return n && !n.locked && n.shapeType !==
|
|
1991
|
+
return n && !n.locked && n.shapeType !== "anchor";
|
|
1529
1992
|
});
|
|
1530
1993
|
if (!fallbackTop || !clickable.includes(fallbackTop)) {
|
|
1531
1994
|
const fallbackEdgeId = params.edges.length > 0 ? params.edges[0] : null;
|
|
@@ -1549,7 +2012,7 @@ function onClickNode(params) {
|
|
|
1549
2012
|
// misses them when they diverge from the centre-to-centre line.
|
|
1550
2013
|
// Also check the canvas for port-edge proximity when nothing was hit.
|
|
1551
2014
|
if (params.nodes.length === 0 && params.edges.length === 0) {
|
|
1552
|
-
const cp
|
|
2015
|
+
const cp = params.pointer.canvas;
|
|
1553
2016
|
|
|
1554
2017
|
// ── Port edge proximity check ──────────────────────────────────────────
|
|
1555
2018
|
// vis-network's hit detection uses the invisible centre-to-centre ghost,
|
|
@@ -1578,7 +2041,7 @@ function onDragStart(params) {
|
|
|
1578
2041
|
_draggingAnchorIds.clear();
|
|
1579
2042
|
for (const id of params.nodes) {
|
|
1580
2043
|
const n = st.nodes && st.nodes.get(id);
|
|
1581
|
-
if (n && n.shapeType ===
|
|
2044
|
+
if (n && n.shapeType === "anchor") _draggingAnchorIds.add(id);
|
|
1582
2045
|
}
|
|
1583
2046
|
const expanded = expandSelectionToGroup(params.nodes);
|
|
1584
2047
|
if (expanded.length > params.nodes.length) {
|
|
@@ -1592,17 +2055,32 @@ let _addingEdgesToSelection = false;
|
|
|
1592
2055
|
function onSelectNode(params) {
|
|
1593
2056
|
if (_expandingGroup || _addingEdgesToSelection) return;
|
|
1594
2057
|
// Filter out anchor nodes — they have no formatting panel.
|
|
1595
|
-
const nonAnchors = params.nodes.filter((id) => {
|
|
2058
|
+
const nonAnchors = params.nodes.filter((id) => {
|
|
2059
|
+
const n = st.nodes.get(id);
|
|
2060
|
+
return !(n && n.shapeType === "anchor");
|
|
2061
|
+
});
|
|
1596
2062
|
// Drop locked nodes: they are non-interactive until unlocked via long-press.
|
|
1597
|
-
const usable = nonAnchors.filter((id) => {
|
|
2063
|
+
const usable = nonAnchors.filter((id) => {
|
|
2064
|
+
const n = st.nodes.get(id);
|
|
2065
|
+
return n && !n.locked;
|
|
2066
|
+
});
|
|
1598
2067
|
if (usable.length !== nonAnchors.length) {
|
|
1599
2068
|
_addingEdgesToSelection = true;
|
|
1600
|
-
const anchorIds = params.nodes.filter((id) => {
|
|
1601
|
-
|
|
2069
|
+
const anchorIds = params.nodes.filter((id) => {
|
|
2070
|
+
const n = st.nodes.get(id);
|
|
2071
|
+
return n && n.shapeType === "anchor";
|
|
2072
|
+
});
|
|
2073
|
+
st.network.setSelection({
|
|
2074
|
+
nodes: [...usable, ...anchorIds],
|
|
2075
|
+
edges: st.network.getSelectedEdges(),
|
|
2076
|
+
});
|
|
1602
2077
|
_addingEdgesToSelection = false;
|
|
1603
2078
|
}
|
|
1604
2079
|
if (!usable.length) {
|
|
1605
|
-
const anchorIds = params.nodes.filter((id) => {
|
|
2080
|
+
const anchorIds = params.nodes.filter((id) => {
|
|
2081
|
+
const n = st.nodes.get(id);
|
|
2082
|
+
return n && n.shapeType === "anchor";
|
|
2083
|
+
});
|
|
1606
2084
|
if (anchorIds.length) {
|
|
1607
2085
|
// Only anchors selected (individual endpoint drag) — keep selection but no panel.
|
|
1608
2086
|
st.selectedNodeIds = anchorIds;
|
|
@@ -1616,7 +2094,10 @@ function onSelectNode(params) {
|
|
|
1616
2094
|
hideEdgePanel();
|
|
1617
2095
|
return;
|
|
1618
2096
|
}
|
|
1619
|
-
const expanded = expandSelectionToGroup(usable).filter((id) => {
|
|
2097
|
+
const expanded = expandSelectionToGroup(usable).filter((id) => {
|
|
2098
|
+
const n = st.nodes.get(id);
|
|
2099
|
+
return n && !n.locked;
|
|
2100
|
+
});
|
|
1620
2101
|
if (expanded.length > usable.length) {
|
|
1621
2102
|
_expandingGroup = true;
|
|
1622
2103
|
st.network.selectNodes(expanded);
|
|
@@ -1629,18 +2110,28 @@ function onSelectNode(params) {
|
|
|
1629
2110
|
// This makes rubber-band select and multi-select automatically include connected edges.
|
|
1630
2111
|
// Exclude locked edges — they are non-interactive.
|
|
1631
2112
|
const selectedSet = new Set(st.selectedNodeIds);
|
|
1632
|
-
st.selectedEdgeIds = st.edges
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
2113
|
+
st.selectedEdgeIds = st.edges
|
|
2114
|
+
.get()
|
|
2115
|
+
.filter((e) => {
|
|
2116
|
+
if (!selectedSet.has(e.from) || !selectedSet.has(e.to)) return false;
|
|
2117
|
+
const fromN = st.nodes.get(e.from);
|
|
2118
|
+
const toN = st.nodes.get(e.to);
|
|
2119
|
+
const isFreeArrow =
|
|
2120
|
+
fromN &&
|
|
2121
|
+
fromN.shapeType === "anchor" &&
|
|
2122
|
+
toN &&
|
|
2123
|
+
toN.shapeType === "anchor";
|
|
2124
|
+
return isFreeArrow ? !(fromN.locked && toN.locked) : !e.edgeLocked;
|
|
2125
|
+
})
|
|
2126
|
+
.map((e) => e.id);
|
|
1639
2127
|
// Tell vis-network to visually highlight the free-arrow edges.
|
|
1640
2128
|
// Use a guard to prevent the resulting selectNode event from re-entering.
|
|
1641
2129
|
if (st.selectedEdgeIds.length > 0) {
|
|
1642
2130
|
_addingEdgesToSelection = true;
|
|
1643
|
-
st.network.setSelection({
|
|
2131
|
+
st.network.setSelection({
|
|
2132
|
+
nodes: st.network.getSelectedNodes(),
|
|
2133
|
+
edges: st.selectedEdgeIds,
|
|
2134
|
+
});
|
|
1644
2135
|
_addingEdgesToSelection = false;
|
|
1645
2136
|
}
|
|
1646
2137
|
hideEdgePanel();
|
|
@@ -1655,12 +2146,19 @@ function onSelectEdge(params) {
|
|
|
1655
2146
|
const e = st.edges.get(id);
|
|
1656
2147
|
if (!e) return false;
|
|
1657
2148
|
const fromN = st.nodes.get(e.from);
|
|
1658
|
-
const toN
|
|
1659
|
-
const isFreeArrow =
|
|
2149
|
+
const toN = st.nodes.get(e.to);
|
|
2150
|
+
const isFreeArrow =
|
|
2151
|
+
fromN &&
|
|
2152
|
+
fromN.shapeType === "anchor" &&
|
|
2153
|
+
toN &&
|
|
2154
|
+
toN.shapeType === "anchor";
|
|
1660
2155
|
return isFreeArrow ? !(fromN.locked && toN.locked) : !e.edgeLocked;
|
|
1661
2156
|
});
|
|
1662
2157
|
if (usable.length !== params.edges.length) {
|
|
1663
|
-
st.network.setSelection({
|
|
2158
|
+
st.network.setSelection({
|
|
2159
|
+
nodes: st.network.getSelectedNodes(),
|
|
2160
|
+
edges: usable,
|
|
2161
|
+
});
|
|
1664
2162
|
}
|
|
1665
2163
|
if (!usable.length) {
|
|
1666
2164
|
st.selectedEdgeIds = [];
|
|
@@ -1680,8 +2178,13 @@ function onSelectEdge(params) {
|
|
|
1680
2178
|
const e = st.edges.get(edgeId);
|
|
1681
2179
|
if (!e) continue;
|
|
1682
2180
|
const fromN = st.nodes.get(e.from);
|
|
1683
|
-
const toN
|
|
1684
|
-
if (
|
|
2181
|
+
const toN = st.nodes.get(e.to);
|
|
2182
|
+
if (
|
|
2183
|
+
fromN &&
|
|
2184
|
+
fromN.shapeType === "anchor" &&
|
|
2185
|
+
toN &&
|
|
2186
|
+
toN.shapeType === "anchor"
|
|
2187
|
+
) {
|
|
1685
2188
|
freeAnchors.push(e.from, e.to);
|
|
1686
2189
|
}
|
|
1687
2190
|
}
|