@fxhash/open-form-graph 0.0.2 → 0.0.4
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/OpenFormGraph-Ccizmxbo.js +2527 -0
- package/dist/OpenFormGraph-Ccizmxbo.js.map +1 -0
- package/dist/OpenFormGraph-Dr0GdNk0.d.ts +581 -0
- package/dist/components.d.ts +1 -1
- package/dist/components.js +2 -2
- package/dist/index.d.ts +2 -117
- package/dist/index.js +3 -3
- package/dist/{provider-CTDz6ZQd.js → provider-CPq89n3a.js} +21 -10
- package/dist/provider-CPq89n3a.js.map +1 -0
- package/dist/provider.d.ts +2 -2
- package/dist/provider.js +2 -2
- package/package.json +6 -6
- package/dist/OpenFormGraph-0EqYrhjv.d.ts +0 -17
- package/dist/OpenFormGraph-wo_Y20C6.js +0 -1277
- package/dist/OpenFormGraph-wo_Y20C6.js.map +0 -1
- package/dist/constants-DU_wYtaU.d.ts +0 -242
- package/dist/provider-CTDz6ZQd.js.map +0 -1
|
@@ -0,0 +1,2527 @@
|
|
|
1
|
+
import { DEFAULT_GRAPH_CONFIG, VOID_DETACH_ID, VOID_ROOT_ID, useOpenFormGraph } from "./provider-CPq89n3a.js";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { forceCenter, forceCollide, forceManyBody, forceSimulation } from "d3-force";
|
|
5
|
+
import { EventEmitter, MappedArray, xorshiftString } from "@fxhash/utils";
|
|
6
|
+
import groupBy from "lodash.groupby";
|
|
7
|
+
import { scaleLinear, scaleLog } from "d3-scale";
|
|
8
|
+
|
|
9
|
+
//#region src/util/canvas.ts
|
|
10
|
+
/**
|
|
11
|
+
* draws a circle on the canvas
|
|
12
|
+
* @param ctx - The canvas rendering context
|
|
13
|
+
* @param x - The x-coordinate of the circle's center
|
|
14
|
+
* @param y - The y-coordinate of the circle's center
|
|
15
|
+
* @param radius - The radius of the circle (default is 5)
|
|
16
|
+
* @param options - Optional parameters for styling the circle
|
|
17
|
+
* @param options.fill - Whether to fill the circle (default is true)
|
|
18
|
+
* @param options.fillStyle - The fill color of the circle
|
|
19
|
+
* @param options.stroke - Whether to stroke the circle (default is false)
|
|
20
|
+
* @param options.strokeStyle - The stroke color of the circle
|
|
21
|
+
* @param options.lineWidth - The width of the stroke (default is 0.2)
|
|
22
|
+
* @returns void
|
|
23
|
+
*/
|
|
24
|
+
function circle(ctx, x, y, radius = 5, options) {
|
|
25
|
+
const { fill = true, fillStyle, stroke = false, strokeStyle, lineWidth = .2 } = options || {};
|
|
26
|
+
ctx.save();
|
|
27
|
+
if (fillStyle !== void 0) ctx.fillStyle = fillStyle;
|
|
28
|
+
if (strokeStyle !== void 0) ctx.strokeStyle = strokeStyle;
|
|
29
|
+
if (lineWidth !== void 0) ctx.lineWidth = lineWidth;
|
|
30
|
+
ctx.beginPath();
|
|
31
|
+
ctx.arc(x, y, radius, 0, 2 * Math.PI);
|
|
32
|
+
ctx.closePath();
|
|
33
|
+
if (fill) ctx.fill();
|
|
34
|
+
if (stroke) ctx.stroke();
|
|
35
|
+
ctx.restore();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* draws a rectangle on the canvas
|
|
39
|
+
* @param ctx - The canvas rendering context
|
|
40
|
+
* @param x - The x-coordinate of the rectangle's top-left corner
|
|
41
|
+
* @param y - The y-coordinate of the rectangle's top-left corner
|
|
42
|
+
* @param width - The width of the rectangle
|
|
43
|
+
* @param height - The height of the rectangle
|
|
44
|
+
* @param options - Optional parameters for styling the rectangle
|
|
45
|
+
* @param options.fill - Whether to fill the rectangle (default is true)
|
|
46
|
+
* @param options.fillStyle - The fill color of the rectangle
|
|
47
|
+
* @param options.stroke - Whether to stroke the rectangle (default is false)
|
|
48
|
+
* @param options.strokeStyle - The stroke color of the rectangle
|
|
49
|
+
* @param options.lineWidth - The width of the stroke (default is 0.2)
|
|
50
|
+
* @param options.borderRadius - The radius of the corners (default is 0)
|
|
51
|
+
* @returns void
|
|
52
|
+
*/
|
|
53
|
+
function rect(ctx, x, y, width, height, options) {
|
|
54
|
+
const { fill = true, fillStyle, stroke = false, strokeStyle, lineWidth = .2, borderRadius = 0 } = options || {};
|
|
55
|
+
ctx.save();
|
|
56
|
+
if (fillStyle !== void 0) ctx.fillStyle = fillStyle;
|
|
57
|
+
if (strokeStyle !== void 0) ctx.strokeStyle = strokeStyle;
|
|
58
|
+
if (lineWidth !== void 0) ctx.lineWidth = lineWidth;
|
|
59
|
+
const r = Math.min(borderRadius, width / 2, height / 2);
|
|
60
|
+
ctx.beginPath();
|
|
61
|
+
if (r > 0) {
|
|
62
|
+
ctx.moveTo(x + r, y);
|
|
63
|
+
ctx.lineTo(x + width - r, y);
|
|
64
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + r);
|
|
65
|
+
ctx.lineTo(x + width, y + height - r);
|
|
66
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
|
|
67
|
+
ctx.lineTo(x + r, y + height);
|
|
68
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - r);
|
|
69
|
+
ctx.lineTo(x, y + r);
|
|
70
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
71
|
+
} else ctx.rect(x, y, width, height);
|
|
72
|
+
ctx.closePath();
|
|
73
|
+
if (fill) ctx.fill();
|
|
74
|
+
if (stroke) ctx.stroke();
|
|
75
|
+
ctx.restore();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* draws an image on the canvas with optional border radius and opacity
|
|
79
|
+
* @param ctx - The canvas rendering context
|
|
80
|
+
* @param image - The HTMLImageElement to draw
|
|
81
|
+
* @param x - The x-coordinate of the image's top-left corner
|
|
82
|
+
* @param y - The y-coordinate of the image's top-left corner
|
|
83
|
+
* @param width - The width of the image
|
|
84
|
+
* @param height - The height of the image
|
|
85
|
+
* @param borderRadius - The radius of the corners (default is 0)
|
|
86
|
+
* @param opacity - The opacity of the image (default is 1.0)
|
|
87
|
+
* @param bgColor - Optional background color to fill the clipped area
|
|
88
|
+
* @returns void
|
|
89
|
+
*/
|
|
90
|
+
function img(ctx, image, x, y, width, height, borderRadius = 0, opacity = 1, bgColor) {
|
|
91
|
+
ctx.save();
|
|
92
|
+
ctx.beginPath();
|
|
93
|
+
ctx.globalAlpha = 1;
|
|
94
|
+
if (borderRadius > 0) {
|
|
95
|
+
const r = Math.min(borderRadius, width / 2, height / 2);
|
|
96
|
+
ctx.moveTo(x + r, y);
|
|
97
|
+
ctx.lineTo(x + width - r, y);
|
|
98
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + r);
|
|
99
|
+
ctx.lineTo(x + width, y + height - r);
|
|
100
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
|
|
101
|
+
ctx.lineTo(x + r, y + height);
|
|
102
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - r);
|
|
103
|
+
ctx.lineTo(x, y + r);
|
|
104
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
105
|
+
} else ctx.rect(x, y, width, height);
|
|
106
|
+
ctx.closePath();
|
|
107
|
+
if (bgColor && opacity < 1) {
|
|
108
|
+
ctx.save();
|
|
109
|
+
ctx.fillStyle = bgColor;
|
|
110
|
+
ctx.fill();
|
|
111
|
+
ctx.restore();
|
|
112
|
+
}
|
|
113
|
+
ctx.clip();
|
|
114
|
+
ctx.globalAlpha = opacity;
|
|
115
|
+
ctx.drawImage(image, x, y, width, height);
|
|
116
|
+
ctx.restore();
|
|
117
|
+
}
|
|
118
|
+
function hexagon(ctx, x, y, radius, options) {
|
|
119
|
+
const { fill = true, fillStyle, stroke = false, strokeStyle, lineWidth = .2, rotation = 0, borderRadius = 0 } = options || {};
|
|
120
|
+
ctx.save();
|
|
121
|
+
if (fillStyle !== void 0) ctx.fillStyle = fillStyle;
|
|
122
|
+
if (strokeStyle !== void 0) ctx.strokeStyle = strokeStyle;
|
|
123
|
+
if (lineWidth !== void 0) ctx.lineWidth = lineWidth;
|
|
124
|
+
const sides = 6;
|
|
125
|
+
const angleStep = Math.PI * 2 / sides;
|
|
126
|
+
ctx.beginPath();
|
|
127
|
+
const points = [];
|
|
128
|
+
for (let i = 0; i < sides; i++) {
|
|
129
|
+
const angle = rotation + i * angleStep;
|
|
130
|
+
points.push({
|
|
131
|
+
x: x + radius * Math.cos(angle),
|
|
132
|
+
y: y + radius * Math.sin(angle)
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (borderRadius > 0) {
|
|
136
|
+
const maxBorderRadius = Math.min(borderRadius, radius / 3);
|
|
137
|
+
for (let i = 0; i < sides; i++) {
|
|
138
|
+
const current = points[i];
|
|
139
|
+
const next = points[(i + 1) % sides];
|
|
140
|
+
const prev = points[(i - 1 + sides) % sides];
|
|
141
|
+
const toPrev = {
|
|
142
|
+
x: prev.x - current.x,
|
|
143
|
+
y: prev.y - current.y
|
|
144
|
+
};
|
|
145
|
+
const toNext = {
|
|
146
|
+
x: next.x - current.x,
|
|
147
|
+
y: next.y - current.y
|
|
148
|
+
};
|
|
149
|
+
const lenPrev = Math.sqrt(toPrev.x * toPrev.x + toPrev.y * toPrev.y);
|
|
150
|
+
const lenNext = Math.sqrt(toNext.x * toNext.x + toNext.y * toNext.y);
|
|
151
|
+
const normPrev = {
|
|
152
|
+
x: toPrev.x / lenPrev,
|
|
153
|
+
y: toPrev.y / lenPrev
|
|
154
|
+
};
|
|
155
|
+
const normNext = {
|
|
156
|
+
x: toNext.x / lenNext,
|
|
157
|
+
y: toNext.y / lenNext
|
|
158
|
+
};
|
|
159
|
+
const cpPrev = {
|
|
160
|
+
x: current.x + normPrev.x * maxBorderRadius,
|
|
161
|
+
y: current.y + normPrev.y * maxBorderRadius
|
|
162
|
+
};
|
|
163
|
+
const cpNext = {
|
|
164
|
+
x: current.x + normNext.x * maxBorderRadius,
|
|
165
|
+
y: current.y + normNext.y * maxBorderRadius
|
|
166
|
+
};
|
|
167
|
+
if (i === 0) ctx.moveTo(cpPrev.x, cpPrev.y);
|
|
168
|
+
else ctx.lineTo(cpPrev.x, cpPrev.y);
|
|
169
|
+
ctx.quadraticCurveTo(current.x, current.y, cpNext.x, cpNext.y);
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
ctx.moveTo(points[0].x, points[0].y);
|
|
173
|
+
for (let i = 1; i < sides; i++) ctx.lineTo(points[i].x, points[i].y);
|
|
174
|
+
}
|
|
175
|
+
ctx.closePath();
|
|
176
|
+
if (fill) ctx.fill();
|
|
177
|
+
if (stroke) ctx.stroke();
|
|
178
|
+
ctx.restore();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/util/color.ts
|
|
183
|
+
/**
|
|
184
|
+
* Some utility functions to handle colors
|
|
185
|
+
*/
|
|
186
|
+
function color(rgb) {
|
|
187
|
+
const colorHandler = function(arg) {
|
|
188
|
+
if (typeof arg === "number") return `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${arg})`;
|
|
189
|
+
if (typeof arg === "function") {
|
|
190
|
+
const transformedRGB = arg(rgb);
|
|
191
|
+
return color(transformedRGB);
|
|
192
|
+
}
|
|
193
|
+
return `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
|
|
194
|
+
};
|
|
195
|
+
colorHandler.rgb = rgb;
|
|
196
|
+
return colorHandler;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Dims a color to white or black
|
|
200
|
+
* @param color An array of 3 numbers representing RGB values (0-255)
|
|
201
|
+
* @param dimFactor A value between 0 and 1 where 0 is completely black and 1 is the original color
|
|
202
|
+
* @returns A new array with the dimmed RGB values
|
|
203
|
+
*/
|
|
204
|
+
function dim(factor = .6, white = true) {
|
|
205
|
+
const base = white ? 255 : 0;
|
|
206
|
+
return function(rgb) {
|
|
207
|
+
return [
|
|
208
|
+
Math.round(rgb[0] + (base - rgb[0]) * (1 - factor)),
|
|
209
|
+
Math.round(rgb[1] + (base - rgb[1]) * (1 - factor)),
|
|
210
|
+
Math.round(rgb[2] + (base - rgb[2]) * (1 - factor))
|
|
211
|
+
];
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/util/types.ts
|
|
217
|
+
function isSimNode(node) {
|
|
218
|
+
return typeof node === "object" && "id" in node;
|
|
219
|
+
}
|
|
220
|
+
function getNodeId(node) {
|
|
221
|
+
return isSimNode(node) ? node.id : node.toString();
|
|
222
|
+
}
|
|
223
|
+
function getLinkId(link) {
|
|
224
|
+
return `${getNodeId(link.target)}-${getNodeId(link.source)}`;
|
|
225
|
+
}
|
|
226
|
+
function isSimLink(link) {
|
|
227
|
+
return typeof link === "object" && "source" in link && typeof link.source !== "string";
|
|
228
|
+
}
|
|
229
|
+
function isCustomHighlight(highlight) {
|
|
230
|
+
if (typeof highlight === "string") return false;
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/util/graph.ts
|
|
236
|
+
function getParent(id, links) {
|
|
237
|
+
const linkToParent = links.maps.targetId.get(id);
|
|
238
|
+
if (!linkToParent) return null;
|
|
239
|
+
return getNodeId(linkToParent.source);
|
|
240
|
+
}
|
|
241
|
+
function getAllParentsUntil(nodeId, links, stopAtId) {
|
|
242
|
+
const parent = getParent(nodeId, links);
|
|
243
|
+
if (parent === null || parent === stopAtId) return [];
|
|
244
|
+
return [parent, ...getAllParentsUntil(parent, links, stopAtId)];
|
|
245
|
+
}
|
|
246
|
+
function getChildren(id, links) {
|
|
247
|
+
return (links.maps.sourceId.get(id) || []).map((link) => getNodeId(link.target));
|
|
248
|
+
}
|
|
249
|
+
function getClusterSize(id, links) {
|
|
250
|
+
const children = getChildren(id, links);
|
|
251
|
+
return children.reduce((acc, childId) => {
|
|
252
|
+
return acc + getClusterSize(childId, links);
|
|
253
|
+
}, children.length || 0);
|
|
254
|
+
}
|
|
255
|
+
function getNodeDepth(id, links) {
|
|
256
|
+
function getDepth(id$1, depth) {
|
|
257
|
+
const parent = getParent(id$1, links);
|
|
258
|
+
if (parent === null) return depth;
|
|
259
|
+
return getDepth(parent, depth + 1);
|
|
260
|
+
}
|
|
261
|
+
return getDepth(id, 0);
|
|
262
|
+
}
|
|
263
|
+
function getRootParent(id, links, stop) {
|
|
264
|
+
let currentId = id;
|
|
265
|
+
while (true) {
|
|
266
|
+
const parent = getParent(currentId, links);
|
|
267
|
+
if (stop && parent === stop) return currentId;
|
|
268
|
+
if (parent === null) return currentId;
|
|
269
|
+
currentId = parent;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function hasOnlyLeafs(id, links) {
|
|
273
|
+
return getChildren(id, links).every((childId) => getChildren(childId, links).length === 0);
|
|
274
|
+
}
|
|
275
|
+
function getNodeSubgraph(nodeId, nodes, links, rootId) {
|
|
276
|
+
const parentSet = /* @__PURE__ */ new Set();
|
|
277
|
+
const childSet = /* @__PURE__ */ new Set();
|
|
278
|
+
const subLinks = /* @__PURE__ */ new Set();
|
|
279
|
+
let currentId = nodeId;
|
|
280
|
+
while (currentId !== rootId) {
|
|
281
|
+
const parentLink = links.maps.targetId.get(currentId);
|
|
282
|
+
if (!parentLink) break;
|
|
283
|
+
const parentId = getNodeId(parentLink.source);
|
|
284
|
+
if (parentSet.has(parentId)) break;
|
|
285
|
+
parentSet.add(parentId);
|
|
286
|
+
subLinks.add(parentLink);
|
|
287
|
+
currentId = parentId;
|
|
288
|
+
}
|
|
289
|
+
function collectChildren(id) {
|
|
290
|
+
for (const link of links.values) {
|
|
291
|
+
const sourceId = getNodeId(link.source);
|
|
292
|
+
const targetId = getNodeId(link.target);
|
|
293
|
+
if (sourceId === id && !childSet.has(targetId)) {
|
|
294
|
+
childSet.add(targetId);
|
|
295
|
+
subLinks.add(link);
|
|
296
|
+
collectChildren(targetId);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
collectChildren(nodeId);
|
|
301
|
+
const validIds = new Set([
|
|
302
|
+
...parentSet,
|
|
303
|
+
nodeId,
|
|
304
|
+
...childSet
|
|
305
|
+
]);
|
|
306
|
+
const filteredLinks = Array.from(subLinks).filter((link) => {
|
|
307
|
+
const sourceId = getNodeId(link.source);
|
|
308
|
+
const targetId = getNodeId(link.target);
|
|
309
|
+
return validIds.has(sourceId.toString()) && validIds.has(targetId.toString());
|
|
310
|
+
});
|
|
311
|
+
const subNodes = Array.from(validIds).map((id) => nodes.maps.id.get(id)).filter(Boolean);
|
|
312
|
+
return newGraphData({
|
|
313
|
+
nodes: subNodes,
|
|
314
|
+
links: filteredLinks
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/util/data.ts
|
|
320
|
+
const images = [];
|
|
321
|
+
function generateTree(maxNodes, maxChildren) {
|
|
322
|
+
const nodes = [];
|
|
323
|
+
const links = [];
|
|
324
|
+
let index$1 = 0;
|
|
325
|
+
function createNode(label) {
|
|
326
|
+
const isRoot = label === VOID_ROOT_ID;
|
|
327
|
+
return {
|
|
328
|
+
id: (isRoot ? VOID_ROOT_ID : index$1++).toString(),
|
|
329
|
+
label,
|
|
330
|
+
imgSrc: isRoot ? void 0 : images[index$1 % images.length]
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const root = createNode(VOID_ROOT_ID);
|
|
334
|
+
nodes.push(root);
|
|
335
|
+
const queue = [root];
|
|
336
|
+
while (queue.length > 0 && nodes.length < maxNodes) {
|
|
337
|
+
const parent = queue.shift();
|
|
338
|
+
const rand = Math.random();
|
|
339
|
+
const biased = Math.floor(Math.pow(rand, 2) * (maxChildren + 1));
|
|
340
|
+
const childrenCount = Math.min(biased, maxChildren);
|
|
341
|
+
for (let i = 0; i < childrenCount; i++) {
|
|
342
|
+
if (nodes.length >= maxNodes) break;
|
|
343
|
+
const child = createNode(`Node ${nodes.length}`);
|
|
344
|
+
nodes.push(child);
|
|
345
|
+
links.push({
|
|
346
|
+
source: parent.id,
|
|
347
|
+
target: child.id
|
|
348
|
+
});
|
|
349
|
+
queue.push(child);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
nodes,
|
|
354
|
+
links
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function isSpecialNode(node) {
|
|
358
|
+
const state = node.state;
|
|
359
|
+
return !!(state?.emitterNode || state?.sessionNode || state?.rootNode || state?.groupNode);
|
|
360
|
+
}
|
|
361
|
+
function groupGraphNodes(startId, nodes, links, options = {
|
|
362
|
+
skip: false,
|
|
363
|
+
existingGroups: [],
|
|
364
|
+
openedGroups: []
|
|
365
|
+
}) {
|
|
366
|
+
if (options.skip) return {
|
|
367
|
+
nodes,
|
|
368
|
+
links
|
|
369
|
+
};
|
|
370
|
+
const visited = /* @__PURE__ */ new Set();
|
|
371
|
+
const nodesToRemove = /* @__PURE__ */ new Set();
|
|
372
|
+
function markRemovableNodes(id) {
|
|
373
|
+
if (visited.has(id)) return;
|
|
374
|
+
visited.add(id);
|
|
375
|
+
const children = getChildren(id, links);
|
|
376
|
+
let removableChildren = [];
|
|
377
|
+
for (const childId of children) {
|
|
378
|
+
const child = nodes.maps.id.get(childId);
|
|
379
|
+
if (!child) continue;
|
|
380
|
+
if ((child.clusterSize || 0) < 1 && !isSpecialNode(child)) removableChildren.push(childId);
|
|
381
|
+
}
|
|
382
|
+
if (removableChildren.length > 3) removableChildren.forEach((id$1) => nodesToRemove.add(id$1));
|
|
383
|
+
}
|
|
384
|
+
markRemovableNodes(startId);
|
|
385
|
+
const groupedNodesToRemove = groupBy(Array.from(nodesToRemove), (id) => {
|
|
386
|
+
const sourceLink = links.maps.targetId.get(id);
|
|
387
|
+
return isSimNode(sourceLink?.source) ? sourceLink?.source.id : sourceLink?.source.toString();
|
|
388
|
+
});
|
|
389
|
+
const GROUP_CHUNK_SIZE = 100;
|
|
390
|
+
const groupNodesToAdd = [];
|
|
391
|
+
const groupLinksToAdd = [];
|
|
392
|
+
function createGroupChunkNodes(groupSourceId, groupContent) {
|
|
393
|
+
const chunks = [];
|
|
394
|
+
for (let i = 0; i < groupContent.length; i += GROUP_CHUNK_SIZE) chunks.push(groupContent.slice(i, i + GROUP_CHUNK_SIZE));
|
|
395
|
+
chunks.forEach((chunk, index$1) => {
|
|
396
|
+
const chunkId = `${groupSourceId}-group-${index$1}`;
|
|
397
|
+
if (options.openedGroups?.includes(chunkId)) {
|
|
398
|
+
chunk.forEach((id) => {
|
|
399
|
+
nodesToRemove.delete(id);
|
|
400
|
+
});
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const existingGroup = options.existingGroups?.find((n) => n.id === chunkId);
|
|
404
|
+
const originNode = nodes.maps.id.get(groupSourceId);
|
|
405
|
+
const groupNode = {
|
|
406
|
+
...existingGroup,
|
|
407
|
+
x: existingGroup?.x || originNode?.x || 0,
|
|
408
|
+
y: existingGroup?.y || originNode?.y || 0,
|
|
409
|
+
id: chunkId,
|
|
410
|
+
clusterSize: chunk.length,
|
|
411
|
+
state: {
|
|
412
|
+
...existingGroup?.state,
|
|
413
|
+
groupNode: true,
|
|
414
|
+
groupContent: chunk,
|
|
415
|
+
collapsed: true
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
groupNodesToAdd.push(groupNode);
|
|
419
|
+
groupLinksToAdd.push({
|
|
420
|
+
source: nodes.maps.id.get(groupSourceId),
|
|
421
|
+
target: groupNode
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
Object.keys(groupedNodesToRemove).forEach((groupSourceId) => {
|
|
426
|
+
const groupContent = groupedNodesToRemove[groupSourceId];
|
|
427
|
+
createGroupChunkNodes(groupSourceId, groupContent);
|
|
428
|
+
});
|
|
429
|
+
const _nodes = nodes.values.filter((n) => !nodesToRemove.has(n.id));
|
|
430
|
+
const _links = links.values.filter((l) => {
|
|
431
|
+
const targetId = isSimNode(l.target) ? l.target.id : l.target.toString();
|
|
432
|
+
return !nodesToRemove.has(targetId);
|
|
433
|
+
});
|
|
434
|
+
return newGraphData({
|
|
435
|
+
nodes: [..._nodes, ...groupNodesToAdd],
|
|
436
|
+
links: [..._links, ...groupLinksToAdd]
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function getPrunedData(startId, nodes, links, highlights = [], options = {
|
|
440
|
+
nodeVisibility: "all",
|
|
441
|
+
emittedNodes: [],
|
|
442
|
+
groupNodes: [],
|
|
443
|
+
skipGrouping: false,
|
|
444
|
+
openedGroups: []
|
|
445
|
+
}) {
|
|
446
|
+
const visibleNodes = [];
|
|
447
|
+
const visibleLinks = [];
|
|
448
|
+
const visited = /* @__PURE__ */ new Set();
|
|
449
|
+
const isLocked = (node) => node.status === "LOCKED" || node.status === "EVOLVED";
|
|
450
|
+
const isEmitted = (node) => options.emittedNodes.includes(node.id);
|
|
451
|
+
const isOwnedHighlight = (nodeId) => highlights.find((h) => h.id === nodeId && h.type === "mine");
|
|
452
|
+
const isOnSale = (node) => highlights.find((h) => h.id === node.id && h.type === "on-sale");
|
|
453
|
+
const isHighlighted = (nodeId, type) => highlights.some((h) => type ? h.id === nodeId && h.type === type : h.id === nodeId);
|
|
454
|
+
const isChildHighlighted = (nodeId, type) => {
|
|
455
|
+
const children = getChildren(nodeId, links);
|
|
456
|
+
if (children.some((h) => isHighlighted(h, type))) return true;
|
|
457
|
+
else return children.some((c) => isChildHighlighted(c, type));
|
|
458
|
+
};
|
|
459
|
+
function visbilityFilter() {
|
|
460
|
+
switch (options.nodeVisibility) {
|
|
461
|
+
case "on-sale": return (node) => !isEmitted(node) && !isSpecialNode(node) && !isOnSale(node) && !isChildHighlighted(node.id, "on-sale");
|
|
462
|
+
case "locked": return (node) => !isEmitted(node) && !isSpecialNode(node) && !isLocked(node) && !isChildHighlighted(node.id, "locked");
|
|
463
|
+
case "mine": return (node) => {
|
|
464
|
+
return !isEmitted(node) && !isSpecialNode(node) && !isOwnedHighlight(node.id) && !isChildHighlighted(node.id, "mine");
|
|
465
|
+
};
|
|
466
|
+
case "all":
|
|
467
|
+
default: return () => false;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const liquidatedFilter = (node) => {
|
|
471
|
+
return node.status === "LIQUIDATED" && !highlights.find((h) => h.id === node.id);
|
|
472
|
+
};
|
|
473
|
+
(function traverseTree(node = nodes.maps.id.get(startId)) {
|
|
474
|
+
if (!node || visited.has(node.id)) return;
|
|
475
|
+
visited.add(node.id);
|
|
476
|
+
if (liquidatedFilter(node)) return;
|
|
477
|
+
if (visbilityFilter()(node)) return;
|
|
478
|
+
visibleNodes.push(node);
|
|
479
|
+
if (node?.state?.collapsed) return;
|
|
480
|
+
const childLinks = links.maps.sourceId.get(node.id) || [];
|
|
481
|
+
for (const link of childLinks) {
|
|
482
|
+
const targetNode = isSimNode(link.target) ? link.target : nodes.maps.id.get(link.target.toString());
|
|
483
|
+
if (!targetNode) continue;
|
|
484
|
+
if (liquidatedFilter(targetNode)) continue;
|
|
485
|
+
if (visbilityFilter()(targetNode)) continue;
|
|
486
|
+
visibleLinks.push(link);
|
|
487
|
+
traverseTree(targetNode);
|
|
488
|
+
}
|
|
489
|
+
})();
|
|
490
|
+
const visible = newGraphData({
|
|
491
|
+
nodes: visibleNodes,
|
|
492
|
+
links: visibleLinks
|
|
493
|
+
});
|
|
494
|
+
return groupGraphNodes(startId, visible.nodes, visible.links, {
|
|
495
|
+
skip: options.skipGrouping || false,
|
|
496
|
+
existingGroups: options.groupNodes,
|
|
497
|
+
openedGroups: options.openedGroups
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Automatically identifies root nodes and builds a nested structure
|
|
502
|
+
* @param nodes Array of raw nodes
|
|
503
|
+
* @param links Array of links between nodes
|
|
504
|
+
* @returns Array of nested nodes starting from identified roots
|
|
505
|
+
*/
|
|
506
|
+
function buildTreeFromGraphData(nodes, links) {
|
|
507
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
508
|
+
nodes.forEach((node) => nodeMap.set(node.id, node));
|
|
509
|
+
const childrenMap = /* @__PURE__ */ new Map();
|
|
510
|
+
const parentMap = /* @__PURE__ */ new Map();
|
|
511
|
+
nodes.forEach((node) => {
|
|
512
|
+
childrenMap.set(node.id, []);
|
|
513
|
+
parentMap.set(node.id, []);
|
|
514
|
+
});
|
|
515
|
+
links.forEach((link) => {
|
|
516
|
+
if (nodeMap.has(link.source) && nodeMap.has(link.target)) {
|
|
517
|
+
const children = childrenMap.get(link.source) || [];
|
|
518
|
+
if (!children.includes(link.target)) {
|
|
519
|
+
children.push(link.target);
|
|
520
|
+
childrenMap.set(link.source, children);
|
|
521
|
+
}
|
|
522
|
+
const parents = parentMap.get(link.target) || [];
|
|
523
|
+
if (!parents.includes(link.source)) {
|
|
524
|
+
parents.push(link.source);
|
|
525
|
+
parentMap.set(link.target, parents);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
const rootHashes = [];
|
|
530
|
+
nodeMap.forEach((_, hash) => {
|
|
531
|
+
if ((parentMap.get(hash) || []).length === 0) rootHashes.push(hash);
|
|
532
|
+
});
|
|
533
|
+
const buildNode = (hash, visited = /* @__PURE__ */ new Set()) => {
|
|
534
|
+
if (visited.has(hash)) return null;
|
|
535
|
+
visited.add(hash);
|
|
536
|
+
const node = nodeMap.get(hash);
|
|
537
|
+
if (!node) return null;
|
|
538
|
+
const childHashes = childrenMap.get(hash) || [];
|
|
539
|
+
const nestedChildren = [];
|
|
540
|
+
childHashes.forEach((childHash) => {
|
|
541
|
+
const childNode = buildNode(childHash, new Set([...visited]));
|
|
542
|
+
if (childNode) nestedChildren.push(childNode);
|
|
543
|
+
});
|
|
544
|
+
return {
|
|
545
|
+
...node,
|
|
546
|
+
children: nestedChildren
|
|
547
|
+
};
|
|
548
|
+
};
|
|
549
|
+
const result = [];
|
|
550
|
+
rootHashes.forEach((rootHash) => {
|
|
551
|
+
const nestedRoot = buildNode(rootHash);
|
|
552
|
+
if (nestedRoot) result.push(nestedRoot);
|
|
553
|
+
});
|
|
554
|
+
const processedNodes = /* @__PURE__ */ new Set();
|
|
555
|
+
const markProcessed = (node) => {
|
|
556
|
+
processedNodes.add(node.id);
|
|
557
|
+
node.children.forEach(markProcessed);
|
|
558
|
+
};
|
|
559
|
+
result.forEach(markProcessed);
|
|
560
|
+
nodeMap.forEach((_, hash) => {
|
|
561
|
+
if (!processedNodes.has(hash)) {
|
|
562
|
+
const orphanedRoot = buildNode(hash);
|
|
563
|
+
if (orphanedRoot) result.push(orphanedRoot);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
return result;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Recursively retrieves all parents of a node from a graph data structure
|
|
570
|
+
* @param {string} nodeHash - The hash of the node to find parents for
|
|
571
|
+
* @param {RawNode[]} nodes - Array of nodes in the graph
|
|
572
|
+
* @param {RawLink[]} links - Array of links connecting the nodes
|
|
573
|
+
* @returns {RawNode[]} - Array of parent nodes
|
|
574
|
+
*/
|
|
575
|
+
function searchParents(nodeHash, nodes, links) {
|
|
576
|
+
const visited = /* @__PURE__ */ new Set();
|
|
577
|
+
function findParents(hash) {
|
|
578
|
+
if (visited.has(hash)) return [];
|
|
579
|
+
visited.add(hash);
|
|
580
|
+
const immediateParents = links.filter((link) => link.target === hash).map((link) => link.source);
|
|
581
|
+
const parentNodes = nodes.filter((node) => immediateParents.includes(node.id));
|
|
582
|
+
const ancestorNodes = immediateParents.flatMap((parentHash) => findParents(parentHash));
|
|
583
|
+
return [...parentNodes, ...ancestorNodes];
|
|
584
|
+
}
|
|
585
|
+
return findParents(nodeHash);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
//#endregion
|
|
589
|
+
//#region src/util/img.ts
|
|
590
|
+
function loadHTMLImageElement(src) {
|
|
591
|
+
return new Promise((resolve, reject) => {
|
|
592
|
+
const img$1 = new Image();
|
|
593
|
+
img$1.onload = () => resolve(img$1);
|
|
594
|
+
img$1.onerror = reject;
|
|
595
|
+
img$1.src = src;
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/util/highlights.ts
|
|
601
|
+
const blue = [
|
|
602
|
+
94,
|
|
603
|
+
112,
|
|
604
|
+
235
|
|
605
|
+
];
|
|
606
|
+
const red = [
|
|
607
|
+
238,
|
|
608
|
+
125,
|
|
609
|
+
121
|
|
610
|
+
];
|
|
611
|
+
const redred = [
|
|
612
|
+
255,
|
|
613
|
+
0,
|
|
614
|
+
0
|
|
615
|
+
];
|
|
616
|
+
var Highlight = class {
|
|
617
|
+
static owner = (id) => {
|
|
618
|
+
return {
|
|
619
|
+
id,
|
|
620
|
+
type: "mine",
|
|
621
|
+
strokeColor: red
|
|
622
|
+
};
|
|
623
|
+
};
|
|
624
|
+
static onSale = (id) => {
|
|
625
|
+
return {
|
|
626
|
+
id,
|
|
627
|
+
type: "on-sale",
|
|
628
|
+
strokeColor: blue
|
|
629
|
+
};
|
|
630
|
+
};
|
|
631
|
+
static primary = (id) => {
|
|
632
|
+
return {
|
|
633
|
+
id,
|
|
634
|
+
type: "any",
|
|
635
|
+
linkTo: id,
|
|
636
|
+
scale: 4,
|
|
637
|
+
strokeColor: redred,
|
|
638
|
+
linkColor: redred,
|
|
639
|
+
onTop: true,
|
|
640
|
+
isDetached: true
|
|
641
|
+
};
|
|
642
|
+
};
|
|
643
|
+
static minted = (id) => {
|
|
644
|
+
return {
|
|
645
|
+
id,
|
|
646
|
+
type: "any",
|
|
647
|
+
linkTo: id,
|
|
648
|
+
scale: 1.5,
|
|
649
|
+
strokeColor: redred,
|
|
650
|
+
linkColor: redred,
|
|
651
|
+
isDetached: true,
|
|
652
|
+
onTop: true
|
|
653
|
+
};
|
|
654
|
+
};
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
//#endregion
|
|
658
|
+
//#region src/sim/_interfaces.ts
|
|
659
|
+
var OpenGraphEventEmitter = class extends EventEmitter {};
|
|
660
|
+
|
|
661
|
+
//#endregion
|
|
662
|
+
//#region src/sim/TransformCanvas.ts
|
|
663
|
+
const MIN_ZOOM = .1;
|
|
664
|
+
const MAX_ZOOM = 10;
|
|
665
|
+
const CLICK_THRESHOLD = 5;
|
|
666
|
+
const ANIMATION_SPEED = .07;
|
|
667
|
+
const DRAG_ANIMATION_SPEED = .5;
|
|
668
|
+
const ANIMATION_THRESHOLD = {
|
|
669
|
+
x: .5,
|
|
670
|
+
y: .5,
|
|
671
|
+
scale: .001
|
|
672
|
+
};
|
|
673
|
+
const MOMENTUM_DAMPING = .91;
|
|
674
|
+
const MIN_VELOCITY = .5;
|
|
675
|
+
var TransformCanvas = class {
|
|
676
|
+
canvas;
|
|
677
|
+
transform = {
|
|
678
|
+
x: 0,
|
|
679
|
+
y: 0,
|
|
680
|
+
scale: 1
|
|
681
|
+
};
|
|
682
|
+
targetTransform = {
|
|
683
|
+
x: 0,
|
|
684
|
+
y: 0,
|
|
685
|
+
scale: 1
|
|
686
|
+
};
|
|
687
|
+
isAnimating = false;
|
|
688
|
+
animationFrame = null;
|
|
689
|
+
focus = null;
|
|
690
|
+
offset = {
|
|
691
|
+
x: 0,
|
|
692
|
+
y: 0
|
|
693
|
+
};
|
|
694
|
+
isDragging = false;
|
|
695
|
+
dragStart = null;
|
|
696
|
+
hasMoved = false;
|
|
697
|
+
lastPointerPos = {
|
|
698
|
+
x: 0,
|
|
699
|
+
y: 0
|
|
700
|
+
};
|
|
701
|
+
lastMovePos = {
|
|
702
|
+
x: 0,
|
|
703
|
+
y: 0
|
|
704
|
+
};
|
|
705
|
+
velocity = {
|
|
706
|
+
x: 0,
|
|
707
|
+
y: 0
|
|
708
|
+
};
|
|
709
|
+
lastDragTime = 0;
|
|
710
|
+
momentumFrame = null;
|
|
711
|
+
touchStart = null;
|
|
712
|
+
pinchStartDist = null;
|
|
713
|
+
pinchStartScale = 1;
|
|
714
|
+
noInteraction = false;
|
|
715
|
+
dpr = 1;
|
|
716
|
+
resizeObserver = null;
|
|
717
|
+
mediaQueryList = null;
|
|
718
|
+
onUpdate;
|
|
719
|
+
onClick;
|
|
720
|
+
onMove;
|
|
721
|
+
constructor(canvas, options) {
|
|
722
|
+
this.canvas = canvas;
|
|
723
|
+
this.onUpdate = options?.onUpdate;
|
|
724
|
+
this.onClick = options?.onClick;
|
|
725
|
+
this.onMove = options?.onMove;
|
|
726
|
+
this.offset = options?.offset || {
|
|
727
|
+
x: 0,
|
|
728
|
+
y: 0
|
|
729
|
+
};
|
|
730
|
+
this.dpr = window.devicePixelRatio || 1;
|
|
731
|
+
this.bindEventHandlers();
|
|
732
|
+
this.attachEventListeners();
|
|
733
|
+
this.setupDPRMonitoring();
|
|
734
|
+
}
|
|
735
|
+
bindEventHandlers() {
|
|
736
|
+
this.handleWheel = this.handleWheel.bind(this);
|
|
737
|
+
this.handleMouseDown = this.handleMouseDown.bind(this);
|
|
738
|
+
this.handleMouseMove = this.handleMouseMove.bind(this);
|
|
739
|
+
this.handleMouseUp = this.handleMouseUp.bind(this);
|
|
740
|
+
this.handleCanvasClick = this.handleCanvasClick.bind(this);
|
|
741
|
+
this.handleTouchStart = this.handleTouchStart.bind(this);
|
|
742
|
+
this.handleTouchMove = this.handleTouchMove.bind(this);
|
|
743
|
+
this.handleTouchEnd = this.handleTouchEnd.bind(this);
|
|
744
|
+
}
|
|
745
|
+
attachEventListeners() {
|
|
746
|
+
this.canvas.addEventListener("wheel", this.handleWheel, { passive: false });
|
|
747
|
+
this.canvas.addEventListener("mousedown", this.handleMouseDown);
|
|
748
|
+
this.canvas.addEventListener("click", this.handleCanvasClick);
|
|
749
|
+
window.addEventListener("mousemove", this.handleMouseMove);
|
|
750
|
+
window.addEventListener("mouseup", this.handleMouseUp);
|
|
751
|
+
this.canvas.addEventListener("touchstart", this.handleTouchStart, { passive: false });
|
|
752
|
+
this.canvas.addEventListener("touchmove", this.handleTouchMove, { passive: false });
|
|
753
|
+
this.canvas.addEventListener("touchend", this.handleTouchEnd, { passive: false });
|
|
754
|
+
this.canvas.addEventListener("touchcancel", this.handleTouchEnd, { passive: false });
|
|
755
|
+
}
|
|
756
|
+
setupDPRMonitoring() {
|
|
757
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
758
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
759
|
+
this.updateDPR();
|
|
760
|
+
});
|
|
761
|
+
this.resizeObserver.observe(this.canvas);
|
|
762
|
+
}
|
|
763
|
+
const updateDPRFromMediaQuery = () => {
|
|
764
|
+
this.updateDPR();
|
|
765
|
+
};
|
|
766
|
+
const dpr = window.devicePixelRatio || 1;
|
|
767
|
+
this.mediaQueryList = window.matchMedia(`(resolution: ${dpr}dppx)`);
|
|
768
|
+
if (this.mediaQueryList.addEventListener) this.mediaQueryList.addEventListener("change", updateDPRFromMediaQuery);
|
|
769
|
+
}
|
|
770
|
+
updateDPR() {
|
|
771
|
+
const newDPR = window.devicePixelRatio || 1;
|
|
772
|
+
if (newDPR !== this.dpr) {
|
|
773
|
+
const oldDPR = this.dpr;
|
|
774
|
+
this.dpr = newDPR;
|
|
775
|
+
const scale = newDPR / oldDPR;
|
|
776
|
+
this.transform.x *= scale;
|
|
777
|
+
this.transform.y *= scale;
|
|
778
|
+
this.targetTransform.x *= scale;
|
|
779
|
+
this.targetTransform.y *= scale;
|
|
780
|
+
this.onUpdate?.(this.transform);
|
|
781
|
+
if (this.mediaQueryList) {
|
|
782
|
+
const updateDPRFromMediaQuery = () => {
|
|
783
|
+
this.updateDPR();
|
|
784
|
+
};
|
|
785
|
+
if (this.mediaQueryList.removeEventListener) this.mediaQueryList.removeEventListener("change", updateDPRFromMediaQuery);
|
|
786
|
+
this.mediaQueryList = window.matchMedia(`(resolution: ${newDPR}dppx)`);
|
|
787
|
+
if (this.mediaQueryList.addEventListener) this.mediaQueryList.addEventListener("change", updateDPRFromMediaQuery);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
toCanvasCoords(clientX, clientY) {
|
|
792
|
+
const rect$1 = this.canvas.getBoundingClientRect();
|
|
793
|
+
return {
|
|
794
|
+
x: (clientX - rect$1.left) * this.dpr,
|
|
795
|
+
y: (clientY - rect$1.top) * this.dpr
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
toCSSCoords(clientX, clientY) {
|
|
799
|
+
const rect$1 = this.canvas.getBoundingClientRect();
|
|
800
|
+
return {
|
|
801
|
+
x: clientX - rect$1.left,
|
|
802
|
+
y: clientY - rect$1.top
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
startMomentum() {
|
|
806
|
+
if (Math.abs(this.velocity.x) > MIN_VELOCITY || Math.abs(this.velocity.y) > MIN_VELOCITY) {
|
|
807
|
+
if (!this.momentumFrame) this.momentumFrame = requestAnimationFrame(this.applyMomentum);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
applyMomentum = () => {
|
|
811
|
+
this.targetTransform.x += this.velocity.x * this.dpr;
|
|
812
|
+
this.targetTransform.y += this.velocity.y * this.dpr;
|
|
813
|
+
this.velocity.x *= MOMENTUM_DAMPING;
|
|
814
|
+
this.velocity.y *= MOMENTUM_DAMPING;
|
|
815
|
+
if (Math.abs(this.velocity.x) > MIN_VELOCITY || Math.abs(this.velocity.y) > MIN_VELOCITY) {
|
|
816
|
+
this.startAnimation();
|
|
817
|
+
this.momentumFrame = requestAnimationFrame(this.applyMomentum);
|
|
818
|
+
} else {
|
|
819
|
+
this.velocity = {
|
|
820
|
+
x: 0,
|
|
821
|
+
y: 0
|
|
822
|
+
};
|
|
823
|
+
this.momentumFrame = null;
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
lerp(a, b, t) {
|
|
827
|
+
return a + (b - a) * t;
|
|
828
|
+
}
|
|
829
|
+
animateTransform = () => {
|
|
830
|
+
const target = this.focus?.() || this.targetTransform;
|
|
831
|
+
const prev = this.transform;
|
|
832
|
+
const animationSpeed = this.isDragging ? DRAG_ANIMATION_SPEED : ANIMATION_SPEED;
|
|
833
|
+
const next = {
|
|
834
|
+
x: this.lerp(prev.x, target.x, animationSpeed / this.dpr),
|
|
835
|
+
y: this.lerp(prev.y, target.y, animationSpeed / this.dpr),
|
|
836
|
+
scale: this.lerp(prev.scale, target.scale, animationSpeed / this.dpr)
|
|
837
|
+
};
|
|
838
|
+
if (Math.abs(next.x - target.x) < ANIMATION_THRESHOLD.x && Math.abs(next.y - target.y) < ANIMATION_THRESHOLD.y && Math.abs(next.scale - target.scale) < ANIMATION_THRESHOLD.scale) {
|
|
839
|
+
this.transform = { ...target };
|
|
840
|
+
this.stopAnimation();
|
|
841
|
+
} else {
|
|
842
|
+
this.transform = next;
|
|
843
|
+
this.onUpdate?.(this.transform);
|
|
844
|
+
this.animationFrame = requestAnimationFrame(this.animateTransform);
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
startAnimation() {
|
|
848
|
+
if (!this.isAnimating) {
|
|
849
|
+
this.isAnimating = true;
|
|
850
|
+
this.animationFrame = requestAnimationFrame(this.animateTransform);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
stopAnimation() {
|
|
854
|
+
this.isAnimating = false;
|
|
855
|
+
this.focus = null;
|
|
856
|
+
if (this.animationFrame) {
|
|
857
|
+
cancelAnimationFrame(this.animationFrame);
|
|
858
|
+
this.animationFrame = null;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
interruptAnimation() {
|
|
862
|
+
this.targetTransform = { ...this.transform };
|
|
863
|
+
if (this.isAnimating) this.stopAnimation();
|
|
864
|
+
if (this.momentumFrame) {
|
|
865
|
+
cancelAnimationFrame(this.momentumFrame);
|
|
866
|
+
this.momentumFrame = null;
|
|
867
|
+
this.velocity = {
|
|
868
|
+
x: 0,
|
|
869
|
+
y: 0
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
handleWheel(e) {
|
|
874
|
+
e.preventDefault();
|
|
875
|
+
e.stopPropagation();
|
|
876
|
+
if (this.noInteraction) return;
|
|
877
|
+
if (this.momentumFrame) {
|
|
878
|
+
cancelAnimationFrame(this.momentumFrame);
|
|
879
|
+
this.momentumFrame = null;
|
|
880
|
+
this.velocity = {
|
|
881
|
+
x: 0,
|
|
882
|
+
y: 0
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
if (this.focus) {
|
|
886
|
+
this.interruptAnimation();
|
|
887
|
+
this.focus = null;
|
|
888
|
+
}
|
|
889
|
+
const canvasCoords = this.toCanvasCoords(e.clientX, e.clientY);
|
|
890
|
+
const scaleFactor = e.deltaY > 0 ? .95 : 1.05;
|
|
891
|
+
const newScale = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, this.targetTransform.scale * scaleFactor));
|
|
892
|
+
const { x: currentX, y: currentY, scale: currentScale } = this.transform;
|
|
893
|
+
const worldX = (canvasCoords.x - currentX - this.offset.x * currentScale) / currentScale;
|
|
894
|
+
const worldY = (canvasCoords.y - currentY - this.offset.y * currentScale) / currentScale;
|
|
895
|
+
const newX = canvasCoords.x - worldX * newScale - this.offset.x * newScale;
|
|
896
|
+
const newY = canvasCoords.y - worldY * newScale - this.offset.y * newScale;
|
|
897
|
+
this.targetTransform = {
|
|
898
|
+
x: newX,
|
|
899
|
+
y: newY,
|
|
900
|
+
scale: newScale
|
|
901
|
+
};
|
|
902
|
+
this.startAnimation();
|
|
903
|
+
}
|
|
904
|
+
handleMouseDown(e) {
|
|
905
|
+
if (this.noInteraction) return;
|
|
906
|
+
this.interruptAnimation();
|
|
907
|
+
this.isDragging = true;
|
|
908
|
+
this.hasMoved = false;
|
|
909
|
+
this.dragStart = {
|
|
910
|
+
x: e.clientX,
|
|
911
|
+
y: e.clientY
|
|
912
|
+
};
|
|
913
|
+
this.lastPointerPos = {
|
|
914
|
+
x: e.clientX,
|
|
915
|
+
y: e.clientY
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
handleMouseMove(e) {
|
|
919
|
+
if (this.noInteraction) return;
|
|
920
|
+
this.lastMovePos = {
|
|
921
|
+
x: e.clientX,
|
|
922
|
+
y: e.clientY
|
|
923
|
+
};
|
|
924
|
+
const cssCoords = this.toCSSCoords(e.clientX, e.clientY);
|
|
925
|
+
this.onMove?.(cssCoords.x, cssCoords.y);
|
|
926
|
+
if (!this.isDragging || !this.dragStart) return;
|
|
927
|
+
const dx = e.clientX - this.dragStart.x;
|
|
928
|
+
const dy = e.clientY - this.dragStart.y;
|
|
929
|
+
if (Math.abs(dx) > CLICK_THRESHOLD || Math.abs(dy) > CLICK_THRESHOLD) {
|
|
930
|
+
this.hasMoved = true;
|
|
931
|
+
const deltaX = (e.clientX - this.lastPointerPos.x) * this.dpr;
|
|
932
|
+
const deltaY = (e.clientY - this.lastPointerPos.y) * this.dpr;
|
|
933
|
+
this.targetTransform.x += deltaX;
|
|
934
|
+
this.targetTransform.y += deltaY;
|
|
935
|
+
const now = Date.now();
|
|
936
|
+
const dt = now - this.lastDragTime;
|
|
937
|
+
if (dt > 0 && dt < 100) {
|
|
938
|
+
this.velocity.x = deltaX / dt * 16;
|
|
939
|
+
this.velocity.y = deltaY / dt * 16;
|
|
940
|
+
}
|
|
941
|
+
this.lastPointerPos = {
|
|
942
|
+
x: e.clientX,
|
|
943
|
+
y: e.clientY
|
|
944
|
+
};
|
|
945
|
+
this.lastDragTime = now;
|
|
946
|
+
this.startAnimation();
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
handleMouseUp(e) {
|
|
950
|
+
if (this.isDragging && this.hasMoved) this.startMomentum();
|
|
951
|
+
this.isDragging = false;
|
|
952
|
+
this.dragStart = null;
|
|
953
|
+
}
|
|
954
|
+
handleCanvasClick(e) {
|
|
955
|
+
if (this.noInteraction || this.hasMoved) return;
|
|
956
|
+
const cssCoords = this.toCSSCoords(e.clientX, e.clientY);
|
|
957
|
+
this.onClick?.(cssCoords.x, cssCoords.y);
|
|
958
|
+
}
|
|
959
|
+
handleTouchStart(e) {
|
|
960
|
+
if (this.noInteraction) return;
|
|
961
|
+
e.preventDefault();
|
|
962
|
+
this.interruptAnimation();
|
|
963
|
+
if (e.touches.length === 1) {
|
|
964
|
+
const touch = e.touches[0];
|
|
965
|
+
this.isDragging = true;
|
|
966
|
+
this.hasMoved = false;
|
|
967
|
+
this.touchStart = {
|
|
968
|
+
x: touch.clientX,
|
|
969
|
+
y: touch.clientY
|
|
970
|
+
};
|
|
971
|
+
this.lastPointerPos = {
|
|
972
|
+
x: touch.clientX,
|
|
973
|
+
y: touch.clientY
|
|
974
|
+
};
|
|
975
|
+
this.lastMovePos = {
|
|
976
|
+
x: touch.clientX,
|
|
977
|
+
y: touch.clientY
|
|
978
|
+
};
|
|
979
|
+
} else if (e.touches.length === 2) {
|
|
980
|
+
this.isDragging = false;
|
|
981
|
+
const [t1, t2] = Array.from(e.touches);
|
|
982
|
+
this.pinchStartDist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
|
|
983
|
+
this.pinchStartScale = this.targetTransform.scale;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
handleTouchMove(e) {
|
|
987
|
+
if (this.noInteraction) return;
|
|
988
|
+
e.preventDefault();
|
|
989
|
+
if (e.touches.length === 1 && this.isDragging && this.touchStart) {
|
|
990
|
+
const touch = e.touches[0];
|
|
991
|
+
const dx = touch.clientX - this.touchStart.x;
|
|
992
|
+
const dy = touch.clientY - this.touchStart.y;
|
|
993
|
+
if (Math.abs(dx) > CLICK_THRESHOLD || Math.abs(dy) > CLICK_THRESHOLD) {
|
|
994
|
+
this.hasMoved = true;
|
|
995
|
+
const deltaX = touch.clientX - this.lastPointerPos.x;
|
|
996
|
+
const deltaY = touch.clientY - this.lastPointerPos.y;
|
|
997
|
+
this.targetTransform.x += deltaX * this.dpr;
|
|
998
|
+
this.targetTransform.y += deltaY * this.dpr;
|
|
999
|
+
const now = Date.now();
|
|
1000
|
+
const dt = now - this.lastDragTime;
|
|
1001
|
+
if (dt > 0 && dt < 100) {
|
|
1002
|
+
this.velocity.x = deltaX / dt * 16;
|
|
1003
|
+
this.velocity.y = deltaY / dt * 16;
|
|
1004
|
+
}
|
|
1005
|
+
this.lastPointerPos = {
|
|
1006
|
+
x: touch.clientX,
|
|
1007
|
+
y: touch.clientY
|
|
1008
|
+
};
|
|
1009
|
+
this.lastMovePos = {
|
|
1010
|
+
x: touch.clientX,
|
|
1011
|
+
y: touch.clientY
|
|
1012
|
+
};
|
|
1013
|
+
this.lastDragTime = now;
|
|
1014
|
+
this.startAnimation();
|
|
1015
|
+
const cssCoords = this.toCSSCoords(touch.clientX, touch.clientY);
|
|
1016
|
+
this.onMove?.(cssCoords.x, cssCoords.y);
|
|
1017
|
+
}
|
|
1018
|
+
} else if (e.touches.length === 2 && this.pinchStartDist != null) {
|
|
1019
|
+
if (this.momentumFrame) {
|
|
1020
|
+
cancelAnimationFrame(this.momentumFrame);
|
|
1021
|
+
this.momentumFrame = null;
|
|
1022
|
+
this.velocity = {
|
|
1023
|
+
x: 0,
|
|
1024
|
+
y: 0
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
const [t1, t2] = Array.from(e.touches);
|
|
1028
|
+
let newScale = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY) / this.pinchStartDist * this.pinchStartScale;
|
|
1029
|
+
newScale = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newScale));
|
|
1030
|
+
const centerX = (t1.clientX + t2.clientX) / 2;
|
|
1031
|
+
const centerY = (t1.clientY + t2.clientY) / 2;
|
|
1032
|
+
const canvasCenter = this.toCanvasCoords(centerX, centerY);
|
|
1033
|
+
const { x: currentX, y: currentY, scale: currentScale } = this.transform;
|
|
1034
|
+
const worldX = (canvasCenter.x - currentX - this.offset.x * currentScale) / currentScale;
|
|
1035
|
+
const worldY = (canvasCenter.y - currentY - this.offset.y * currentScale) / currentScale;
|
|
1036
|
+
const newX = canvasCenter.x - worldX * newScale - this.offset.x * newScale;
|
|
1037
|
+
const newY = canvasCenter.y - worldY * newScale - this.offset.y * newScale;
|
|
1038
|
+
this.targetTransform = {
|
|
1039
|
+
x: newX,
|
|
1040
|
+
y: newY,
|
|
1041
|
+
scale: newScale
|
|
1042
|
+
};
|
|
1043
|
+
this.startAnimation();
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
handleTouchEnd(e) {
|
|
1047
|
+
if (this.noInteraction) return;
|
|
1048
|
+
if (e.touches.length === 0) {
|
|
1049
|
+
if (this.isDragging && this.hasMoved) this.startMomentum();
|
|
1050
|
+
else if (this.isDragging && !this.hasMoved && this.onClick && this.touchStart) {
|
|
1051
|
+
const cssCoords = this.toCSSCoords(this.touchStart.x, this.touchStart.y);
|
|
1052
|
+
this.onClick(cssCoords.x, cssCoords.y);
|
|
1053
|
+
}
|
|
1054
|
+
this.isDragging = false;
|
|
1055
|
+
this.touchStart = null;
|
|
1056
|
+
this.hasMoved = false;
|
|
1057
|
+
this.pinchStartDist = null;
|
|
1058
|
+
} else if (e.touches.length === 1) {
|
|
1059
|
+
const touch = e.touches[0];
|
|
1060
|
+
this.isDragging = true;
|
|
1061
|
+
this.hasMoved = false;
|
|
1062
|
+
this.touchStart = {
|
|
1063
|
+
x: touch.clientX,
|
|
1064
|
+
y: touch.clientY
|
|
1065
|
+
};
|
|
1066
|
+
this.lastPointerPos = {
|
|
1067
|
+
x: touch.clientX,
|
|
1068
|
+
y: touch.clientY
|
|
1069
|
+
};
|
|
1070
|
+
this.pinchStartDist = null;
|
|
1071
|
+
this.velocity = {
|
|
1072
|
+
x: 0,
|
|
1073
|
+
y: 0
|
|
1074
|
+
};
|
|
1075
|
+
this.lastDragTime = Date.now();
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
resetZoom() {
|
|
1079
|
+
this.targetTransform = {
|
|
1080
|
+
x: 0,
|
|
1081
|
+
y: 0,
|
|
1082
|
+
scale: 1
|
|
1083
|
+
};
|
|
1084
|
+
this.startAnimation();
|
|
1085
|
+
}
|
|
1086
|
+
transformTo(update) {
|
|
1087
|
+
this.targetTransform = {
|
|
1088
|
+
...this.targetTransform,
|
|
1089
|
+
...update
|
|
1090
|
+
};
|
|
1091
|
+
this.startAnimation();
|
|
1092
|
+
}
|
|
1093
|
+
getTransformationFromWorld(worldX, worldY, newScale) {
|
|
1094
|
+
const scale = newScale ?? this.transform.scale;
|
|
1095
|
+
const x = this.canvas.width / 2 + this.offset.x * this.dpr - worldX * scale - (this.canvas.width / 2 + this.offset.x * this.dpr) * scale;
|
|
1096
|
+
const y = this.canvas.height / 2 + this.offset.y * this.dpr - worldY * scale - (this.canvas.height / 2 + this.offset.y * this.dpr) * scale;
|
|
1097
|
+
return {
|
|
1098
|
+
x,
|
|
1099
|
+
y,
|
|
1100
|
+
scale
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
transformToWorld(worldX, worldY, newScale) {
|
|
1104
|
+
const transform = this.getTransformationFromWorld(worldX, worldY, newScale);
|
|
1105
|
+
this.transformTo(transform);
|
|
1106
|
+
}
|
|
1107
|
+
trackCursor() {
|
|
1108
|
+
const cssCoords = this.toCSSCoords(this.lastMovePos.x, this.lastMovePos.y);
|
|
1109
|
+
this.onMove?.(cssCoords.x, cssCoords.y);
|
|
1110
|
+
}
|
|
1111
|
+
setNoInteraction(noInteraction) {
|
|
1112
|
+
this.noInteraction = noInteraction;
|
|
1113
|
+
if (noInteraction) {
|
|
1114
|
+
this.isDragging = false;
|
|
1115
|
+
this.dragStart = null;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
focusOn(focus) {
|
|
1119
|
+
this.focus = focus;
|
|
1120
|
+
if (focus) {
|
|
1121
|
+
const _focus = focus;
|
|
1122
|
+
this.focus = () => {
|
|
1123
|
+
const worldFocus = _focus();
|
|
1124
|
+
if (!worldFocus) return null;
|
|
1125
|
+
return this.getTransformationFromWorld(worldFocus.x, worldFocus.y, worldFocus.scale);
|
|
1126
|
+
};
|
|
1127
|
+
this.startAnimation();
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
resetFocus() {
|
|
1131
|
+
this.focus = null;
|
|
1132
|
+
}
|
|
1133
|
+
destroy() {
|
|
1134
|
+
this.stopAnimation();
|
|
1135
|
+
if (this.momentumFrame) {
|
|
1136
|
+
cancelAnimationFrame(this.momentumFrame);
|
|
1137
|
+
this.momentumFrame = null;
|
|
1138
|
+
}
|
|
1139
|
+
this.canvas.removeEventListener("wheel", this.handleWheel);
|
|
1140
|
+
this.canvas.removeEventListener("mousedown", this.handleMouseDown);
|
|
1141
|
+
this.canvas.removeEventListener("click", this.handleCanvasClick);
|
|
1142
|
+
this.canvas.removeEventListener("touchstart", this.handleTouchStart);
|
|
1143
|
+
this.canvas.removeEventListener("touchmove", this.handleTouchMove);
|
|
1144
|
+
this.canvas.removeEventListener("touchend", this.handleTouchEnd);
|
|
1145
|
+
this.canvas.removeEventListener("touchcancel", this.handleTouchEnd);
|
|
1146
|
+
window.removeEventListener("mousemove", this.handleMouseMove);
|
|
1147
|
+
window.removeEventListener("mouseup", this.handleMouseUp);
|
|
1148
|
+
if (this.resizeObserver) {
|
|
1149
|
+
this.resizeObserver.disconnect();
|
|
1150
|
+
this.resizeObserver = null;
|
|
1151
|
+
}
|
|
1152
|
+
if (this.mediaQueryList) {
|
|
1153
|
+
const updateDPRFromMediaQuery = () => {
|
|
1154
|
+
this.updateDPR();
|
|
1155
|
+
};
|
|
1156
|
+
if (this.mediaQueryList.removeEventListener) this.mediaQueryList.removeEventListener("change", updateDPRFromMediaQuery);
|
|
1157
|
+
this.mediaQueryList = null;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
getTransform() {
|
|
1161
|
+
return { ...this.transform };
|
|
1162
|
+
}
|
|
1163
|
+
getTargetTransform() {
|
|
1164
|
+
return { ...this.targetTransform };
|
|
1165
|
+
}
|
|
1166
|
+
getFocus() {
|
|
1167
|
+
return this.focus ? { ...this.focus } : null;
|
|
1168
|
+
}
|
|
1169
|
+
getIsDragging() {
|
|
1170
|
+
return this.isDragging;
|
|
1171
|
+
}
|
|
1172
|
+
setOffset(offset) {
|
|
1173
|
+
this.offset = offset;
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
//#endregion
|
|
1178
|
+
//#region src/_types.ts
|
|
1179
|
+
function newGraphData(data) {
|
|
1180
|
+
return {
|
|
1181
|
+
nodes: new MappedArray({
|
|
1182
|
+
id: {
|
|
1183
|
+
getKey: (node) => node.id,
|
|
1184
|
+
multi: false
|
|
1185
|
+
},
|
|
1186
|
+
collapsed: {
|
|
1187
|
+
getKey: (node) => (!!node.state?.collapsed).toString(),
|
|
1188
|
+
multi: true
|
|
1189
|
+
},
|
|
1190
|
+
groupNode: {
|
|
1191
|
+
getKey: (node) => (!!node.state?.groupNode).toString(),
|
|
1192
|
+
multi: true
|
|
1193
|
+
}
|
|
1194
|
+
}, data?.nodes),
|
|
1195
|
+
links: new MappedArray({
|
|
1196
|
+
targetId: {
|
|
1197
|
+
getKey: (link) => (isSimNode(link.target) ? link.target.id : link.target).toString(),
|
|
1198
|
+
multi: false
|
|
1199
|
+
},
|
|
1200
|
+
linkId: {
|
|
1201
|
+
getKey: (link) => getLinkId(link),
|
|
1202
|
+
multi: false
|
|
1203
|
+
},
|
|
1204
|
+
sourceId: {
|
|
1205
|
+
getKey: (link) => (isSimNode(link.source) ? link.source.id : link.source).toString(),
|
|
1206
|
+
multi: true
|
|
1207
|
+
}
|
|
1208
|
+
}, data?.links)
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
//#endregion
|
|
1213
|
+
//#region src/util/math.ts
|
|
1214
|
+
function getRadialPoint(r, cx, cy, angle = Math.random() * 2 * Math.PI) {
|
|
1215
|
+
return {
|
|
1216
|
+
x: cx + r * Math.cos(angle),
|
|
1217
|
+
y: cy + r * Math.sin(angle)
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
function getAngle(cx, cy, x, y) {
|
|
1221
|
+
return Math.atan2(y - cy, x - cx);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
//#endregion
|
|
1225
|
+
//#region src/sim/asymmetric-link.ts
|
|
1226
|
+
function constant(x) {
|
|
1227
|
+
return function() {
|
|
1228
|
+
return x;
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
function jiggle(random) {
|
|
1232
|
+
return (random() - .5) * 1e-6;
|
|
1233
|
+
}
|
|
1234
|
+
function index(d) {
|
|
1235
|
+
return d.index;
|
|
1236
|
+
}
|
|
1237
|
+
function find(nodeById, nodeId) {
|
|
1238
|
+
const node = nodeById.get(nodeId);
|
|
1239
|
+
if (!node) throw new Error("node not found: " + nodeId);
|
|
1240
|
+
return node;
|
|
1241
|
+
}
|
|
1242
|
+
function asymmetricLinks(links) {
|
|
1243
|
+
let id = index;
|
|
1244
|
+
let strength = defaultStrength;
|
|
1245
|
+
let strengths;
|
|
1246
|
+
let distance = constant(30);
|
|
1247
|
+
let distances;
|
|
1248
|
+
let nodes;
|
|
1249
|
+
let count;
|
|
1250
|
+
let bias;
|
|
1251
|
+
let random;
|
|
1252
|
+
let iterations = 1;
|
|
1253
|
+
if (links == null) links = [];
|
|
1254
|
+
function defaultStrength(link) {
|
|
1255
|
+
return 1 / Math.min(count[link.source.index], count[link.target.index]);
|
|
1256
|
+
}
|
|
1257
|
+
function force(alpha) {
|
|
1258
|
+
for (let k = 0, n = links.length; k < iterations; ++k) for (let i = 0; i < n; ++i) {
|
|
1259
|
+
const link = links[i];
|
|
1260
|
+
const source = link.source;
|
|
1261
|
+
const target = link.target;
|
|
1262
|
+
let x = target.x + target.vx - source.x - source.vx || jiggle(random);
|
|
1263
|
+
let y = target.y + target.vy - source.y - source.vy || jiggle(random);
|
|
1264
|
+
let l = Math.sqrt(x * x + y * y);
|
|
1265
|
+
l = (l - distances[i]) / l * alpha;
|
|
1266
|
+
x *= l;
|
|
1267
|
+
y *= l;
|
|
1268
|
+
const b = bias[i];
|
|
1269
|
+
const strengthValue = strengths[i];
|
|
1270
|
+
let s0, s1;
|
|
1271
|
+
if (Array.isArray(strengthValue)) [s0, s1] = strengthValue;
|
|
1272
|
+
else s0 = s1 = strengthValue;
|
|
1273
|
+
target.vx -= x * b * s0;
|
|
1274
|
+
target.vy -= y * b * s0;
|
|
1275
|
+
source.vx += x * (1 - b) * s1;
|
|
1276
|
+
source.vy += y * (1 - b) * s1;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
function initialize() {
|
|
1280
|
+
if (!nodes) return;
|
|
1281
|
+
const n = nodes.length;
|
|
1282
|
+
const m = links.length;
|
|
1283
|
+
const nodeById = new Map(nodes.map((d, i) => [id(d, i, nodes), d]));
|
|
1284
|
+
count = new Array(n).fill(0);
|
|
1285
|
+
for (let i = 0; i < m; ++i) {
|
|
1286
|
+
const link = links[i];
|
|
1287
|
+
link.index = i;
|
|
1288
|
+
if (typeof link.source !== "object") link.source = find(nodeById, link.source);
|
|
1289
|
+
if (typeof link.target !== "object") link.target = find(nodeById, link.target);
|
|
1290
|
+
count[link.source.index]++;
|
|
1291
|
+
count[link.target.index]++;
|
|
1292
|
+
}
|
|
1293
|
+
bias = new Array(m);
|
|
1294
|
+
for (let i = 0; i < m; ++i) {
|
|
1295
|
+
const link = links[i];
|
|
1296
|
+
const sourceCount = count[link.source.index];
|
|
1297
|
+
const targetCount = count[link.target.index];
|
|
1298
|
+
bias[i] = sourceCount / (sourceCount + targetCount);
|
|
1299
|
+
}
|
|
1300
|
+
strengths = new Array(m);
|
|
1301
|
+
initializeStrength();
|
|
1302
|
+
distances = new Array(m);
|
|
1303
|
+
initializeDistance();
|
|
1304
|
+
}
|
|
1305
|
+
function initializeStrength() {
|
|
1306
|
+
if (!nodes) return;
|
|
1307
|
+
for (let i = 0, n = links.length; i < n; ++i) strengths[i] = strength(links[i], i, links);
|
|
1308
|
+
}
|
|
1309
|
+
function initializeDistance() {
|
|
1310
|
+
if (!nodes) return;
|
|
1311
|
+
for (let i = 0, n = links.length; i < n; ++i) distances[i] = +distance(links[i], i, links);
|
|
1312
|
+
}
|
|
1313
|
+
force.initialize = function(_nodes, _random) {
|
|
1314
|
+
nodes = _nodes;
|
|
1315
|
+
random = _random;
|
|
1316
|
+
initialize();
|
|
1317
|
+
};
|
|
1318
|
+
force.links = function(_) {
|
|
1319
|
+
return arguments.length ? (links = _, initialize(), force) : links;
|
|
1320
|
+
};
|
|
1321
|
+
force.id = function(_) {
|
|
1322
|
+
return arguments.length ? (id = _, force) : id;
|
|
1323
|
+
};
|
|
1324
|
+
force.iterations = function(_) {
|
|
1325
|
+
return arguments.length ? (iterations = +_, force) : iterations;
|
|
1326
|
+
};
|
|
1327
|
+
force.strength = function(_) {
|
|
1328
|
+
return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initializeStrength(), force) : strength;
|
|
1329
|
+
};
|
|
1330
|
+
force.distance = function(_) {
|
|
1331
|
+
return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), initializeDistance(), force) : distance;
|
|
1332
|
+
};
|
|
1333
|
+
return force;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
//#endregion
|
|
1337
|
+
//#region src/util/hash.ts
|
|
1338
|
+
function quickHash(data) {
|
|
1339
|
+
return xorshiftString(data).toString(16);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
//#endregion
|
|
1343
|
+
//#region src/sim/measurements.ts
|
|
1344
|
+
/**
|
|
1345
|
+
* @dev Temp utility to make performance measurements on the graph.
|
|
1346
|
+
*/
|
|
1347
|
+
const arr = {
|
|
1348
|
+
sum(A, getV = (v) => v) {
|
|
1349
|
+
return A.reduce((acc, val) => acc + getV(val), 0);
|
|
1350
|
+
},
|
|
1351
|
+
avg(A, getV = (v) => v) {
|
|
1352
|
+
return this.sum(A, getV) / A.length;
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
const Measure = {
|
|
1356
|
+
measures: {},
|
|
1357
|
+
samples: {},
|
|
1358
|
+
collect(name, value) {
|
|
1359
|
+
if (!this.samples[name]) this.samples[name] = [];
|
|
1360
|
+
this.samples[name].push(value);
|
|
1361
|
+
},
|
|
1362
|
+
start(name) {
|
|
1363
|
+
this.measures[name] = performance.now();
|
|
1364
|
+
},
|
|
1365
|
+
end(name, ignoreError = false) {
|
|
1366
|
+
if (!(name in this.measures)) {
|
|
1367
|
+
if (ignoreError) return;
|
|
1368
|
+
throw `invalid timer end on "${name}"`;
|
|
1369
|
+
}
|
|
1370
|
+
this.collect(name, performance.now() - this.measures[name]);
|
|
1371
|
+
},
|
|
1372
|
+
export() {
|
|
1373
|
+
const samples = Object.entries(this.samples);
|
|
1374
|
+
const rows = [samples.map((s) => s[0]).join(",")];
|
|
1375
|
+
rows.push(samples.map((s) => arr.avg(s[1])).join(","));
|
|
1376
|
+
downloadAsCsvFile("samples.csv", rows.join("\n"));
|
|
1377
|
+
},
|
|
1378
|
+
getSamples() {
|
|
1379
|
+
return this.samples;
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
function downloadAsCsvFile(filename, content) {
|
|
1383
|
+
const blob = new Blob([content], { type: "text/csv" });
|
|
1384
|
+
const url = URL.createObjectURL(blob);
|
|
1385
|
+
const a = document.createElement("a");
|
|
1386
|
+
a.href = url;
|
|
1387
|
+
a.download = filename.endsWith(".csv") ? filename : filename + ".csv";
|
|
1388
|
+
document.body.appendChild(a);
|
|
1389
|
+
a.click();
|
|
1390
|
+
document.body.removeChild(a);
|
|
1391
|
+
URL.revokeObjectURL(url);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
//#endregion
|
|
1395
|
+
//#region src/sim/OpenGraphSimulation.ts
|
|
1396
|
+
const RENDER_EMITTER_NODES = false;
|
|
1397
|
+
const INITIAL_RADIUS = 300;
|
|
1398
|
+
const INCREMENTAL = 200;
|
|
1399
|
+
function getRadius(depth) {
|
|
1400
|
+
if (depth === 0) return INITIAL_RADIUS;
|
|
1401
|
+
return INCREMENTAL * depth + INITIAL_RADIUS;
|
|
1402
|
+
}
|
|
1403
|
+
var OpenGraphSimulation = class {
|
|
1404
|
+
width;
|
|
1405
|
+
height;
|
|
1406
|
+
config;
|
|
1407
|
+
rootImageSources;
|
|
1408
|
+
canvas;
|
|
1409
|
+
transformCanvas;
|
|
1410
|
+
theme;
|
|
1411
|
+
groupRootOrphans = false;
|
|
1412
|
+
rawData;
|
|
1413
|
+
emitter;
|
|
1414
|
+
translate = {
|
|
1415
|
+
x: 0,
|
|
1416
|
+
y: 0
|
|
1417
|
+
};
|
|
1418
|
+
data = newGraphData();
|
|
1419
|
+
prunedData = newGraphData();
|
|
1420
|
+
subGraph = newGraphData();
|
|
1421
|
+
rootId = "";
|
|
1422
|
+
simulation = null;
|
|
1423
|
+
clusterSizeRange = [0, 1];
|
|
1424
|
+
emitterClusterSizeRange = [0, 1];
|
|
1425
|
+
maxDepth = 0;
|
|
1426
|
+
isTicking = false;
|
|
1427
|
+
isDrawing = false;
|
|
1428
|
+
tickCount = 0;
|
|
1429
|
+
loadNodeImage;
|
|
1430
|
+
imageCache = /* @__PURE__ */ new Map();
|
|
1431
|
+
rootImages = [];
|
|
1432
|
+
hideThumbnails = false;
|
|
1433
|
+
noInteraction = false;
|
|
1434
|
+
lockedNodeId;
|
|
1435
|
+
selectedNode = null;
|
|
1436
|
+
hoveredNode = null;
|
|
1437
|
+
highlights;
|
|
1438
|
+
nodeVisibility = "all";
|
|
1439
|
+
secondaryNodes = /* @__PURE__ */ new Map();
|
|
1440
|
+
emittedNodes = [];
|
|
1441
|
+
openedGroups = [];
|
|
1442
|
+
renderLayers = {
|
|
1443
|
+
links: {
|
|
1444
|
+
regular: [],
|
|
1445
|
+
highlighted: []
|
|
1446
|
+
},
|
|
1447
|
+
nodes: {
|
|
1448
|
+
regular: [],
|
|
1449
|
+
highlighted: []
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
constructor(props) {
|
|
1453
|
+
this.emitter = new OpenGraphEventEmitter();
|
|
1454
|
+
this.theme = props.theme || "light";
|
|
1455
|
+
this.width = props.width;
|
|
1456
|
+
this.height = props.height;
|
|
1457
|
+
this.config = props.config || DEFAULT_GRAPH_CONFIG;
|
|
1458
|
+
this.rootImageSources = props.rootImageSources || [];
|
|
1459
|
+
this.canvas = props.canvas;
|
|
1460
|
+
this.lockedNodeId = props.lockedNodeId;
|
|
1461
|
+
this.loadNodeImage = props.loadNodeImage;
|
|
1462
|
+
this.translate = props.translate || {
|
|
1463
|
+
x: 0,
|
|
1464
|
+
y: 0
|
|
1465
|
+
};
|
|
1466
|
+
this.transformCanvas = new TransformCanvas(this.canvas, {
|
|
1467
|
+
onUpdate: this.handleTransform,
|
|
1468
|
+
onClick: this.handleClick,
|
|
1469
|
+
onMove: this.handleMove
|
|
1470
|
+
});
|
|
1471
|
+
this.rootImageSources.forEach((src, idx) => {
|
|
1472
|
+
if (src && !this.imageCache.get(src)) loadHTMLImageElement(src).then((img$1) => {
|
|
1473
|
+
this.imageCache.set(src, img$1);
|
|
1474
|
+
this.rootImages[idx] = img$1;
|
|
1475
|
+
});
|
|
1476
|
+
});
|
|
1477
|
+
this.groupRootOrphans = props.groupRootOrphans || false;
|
|
1478
|
+
this.nodeVisibility = props.nodeVisibility || "all";
|
|
1479
|
+
this.highlights = new MappedArray({
|
|
1480
|
+
nodeId: {
|
|
1481
|
+
getKey: (highlight) => highlight.id,
|
|
1482
|
+
multi: false
|
|
1483
|
+
},
|
|
1484
|
+
detached: {
|
|
1485
|
+
getKey: (highlight) => (highlight.isDetached || false).toString(),
|
|
1486
|
+
multi: true
|
|
1487
|
+
}
|
|
1488
|
+
}, props.highlights || []);
|
|
1489
|
+
}
|
|
1490
|
+
get center() {
|
|
1491
|
+
return {
|
|
1492
|
+
x: 0,
|
|
1493
|
+
y: 0
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
get origin() {
|
|
1497
|
+
const dpi = devicePixelRatio || 1;
|
|
1498
|
+
return {
|
|
1499
|
+
x: (this.translate.x + this.width / 2) * dpi,
|
|
1500
|
+
y: (this.translate.y + this.height / 2) * dpi
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
getNodeAtPosition = (cx, cy) => {
|
|
1504
|
+
const { x: tx, y: ty, scale } = this.transformCanvas.getTransform();
|
|
1505
|
+
const dpi = devicePixelRatio || 1;
|
|
1506
|
+
const canvasX = cx * dpi;
|
|
1507
|
+
const canvasY = cy * dpi;
|
|
1508
|
+
const scaledX = (canvasX - tx) / scale;
|
|
1509
|
+
const scaledY = (canvasY - ty) / scale;
|
|
1510
|
+
const graphX = scaledX - this.origin.x;
|
|
1511
|
+
const graphY = scaledY - this.origin.y;
|
|
1512
|
+
const candidates = [];
|
|
1513
|
+
for (let node of this.prunedData.nodes.values) {
|
|
1514
|
+
const r = this.getNodeSize(node.id) / 2;
|
|
1515
|
+
if (node.x == null || node.y == null) continue;
|
|
1516
|
+
const dx = node.x - graphX;
|
|
1517
|
+
const dy = node.y - graphY;
|
|
1518
|
+
if (dx * dx + dy * dy < r * r) {
|
|
1519
|
+
if (!this.prunedData.nodes.maps.id.get(node.id)) continue;
|
|
1520
|
+
candidates.push(node);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
if (candidates.length === 0) return null;
|
|
1524
|
+
if (candidates.length === 1) return candidates[0];
|
|
1525
|
+
for (let i = this.renderLayers.nodes.highlighted.length - 1; i >= 0; i--) {
|
|
1526
|
+
const node = this.renderLayers.nodes.highlighted[i];
|
|
1527
|
+
if (candidates.find((c) => c.id === node.id)) return node;
|
|
1528
|
+
}
|
|
1529
|
+
for (let node of this.renderLayers.nodes.regular) if (candidates.find((c) => c.id === node.id)) return node;
|
|
1530
|
+
return candidates[0];
|
|
1531
|
+
};
|
|
1532
|
+
getNodeScreenPosition = (node) => {
|
|
1533
|
+
const transform = this.transformCanvas.getTransform();
|
|
1534
|
+
const x = transform.x + (node.x || 0 + this.origin.x) * transform.scale;
|
|
1535
|
+
const y = transform.y + (node.y || 0 + this.origin.y) * transform.scale;
|
|
1536
|
+
return {
|
|
1537
|
+
x,
|
|
1538
|
+
y
|
|
1539
|
+
};
|
|
1540
|
+
};
|
|
1541
|
+
getNodeCanvasPosition = (node) => {
|
|
1542
|
+
const _x = node.x || 0;
|
|
1543
|
+
const _y = node.y || 0;
|
|
1544
|
+
const transform = this.transformCanvas.getTransform();
|
|
1545
|
+
const x = this.origin.x - _x * transform.scale;
|
|
1546
|
+
const y = this.origin.y - _y * transform.scale;
|
|
1547
|
+
return {
|
|
1548
|
+
x,
|
|
1549
|
+
y
|
|
1550
|
+
};
|
|
1551
|
+
};
|
|
1552
|
+
screenToWorld(_x, _y) {
|
|
1553
|
+
const { x: tx, y: ty, scale } = this.transformCanvas.getTransform();
|
|
1554
|
+
const dpi = devicePixelRatio || 1;
|
|
1555
|
+
const canvasX = _x * dpi;
|
|
1556
|
+
const canvasY = _y * dpi;
|
|
1557
|
+
const scaledX = (canvasX - tx) / scale;
|
|
1558
|
+
const scaledY = (canvasY - ty) / scale;
|
|
1559
|
+
const x = scaledX - this.origin.x;
|
|
1560
|
+
const y = scaledY - this.origin.y;
|
|
1561
|
+
return {
|
|
1562
|
+
x,
|
|
1563
|
+
y
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
worldToScreen(worldX, worldY) {
|
|
1567
|
+
const { x: tx, y: ty, scale } = this.transformCanvas.getTransform();
|
|
1568
|
+
const dpi = devicePixelRatio || 1;
|
|
1569
|
+
const scaledX = (worldX + this.origin.x) * scale;
|
|
1570
|
+
const scaledY = (worldY + this.origin.y) * scale;
|
|
1571
|
+
const canvasX = scaledX + tx;
|
|
1572
|
+
const canvasY = scaledY + ty;
|
|
1573
|
+
const screenX = canvasX / dpi;
|
|
1574
|
+
const screenY = canvasY / dpi;
|
|
1575
|
+
return {
|
|
1576
|
+
x: screenX,
|
|
1577
|
+
y: screenY
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
handleClick = (x, y) => {
|
|
1581
|
+
let node = this.getNodeAtPosition(x, y);
|
|
1582
|
+
if (node?.state?.sessionNode) return;
|
|
1583
|
+
if (node?.state?.groupNode) {
|
|
1584
|
+
this.data.nodes.values.forEach((n) => {
|
|
1585
|
+
if (node?.state?.groupContent?.includes(n.id)) {
|
|
1586
|
+
const circlePos = getRadialPoint(getRadius(0), this.rootNode?.x, this.rootNode?.y);
|
|
1587
|
+
n.x = circlePos.x;
|
|
1588
|
+
n.y = circlePos.y;
|
|
1589
|
+
}
|
|
1590
|
+
});
|
|
1591
|
+
this.openedGroups.push(node.id);
|
|
1592
|
+
this.updateHighlights();
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
if (this.lockedNodeId && !node) node = this.getNodeById(this.lockedNodeId);
|
|
1596
|
+
if (node === this.selectedNode) return;
|
|
1597
|
+
if (node?.state?.emitterNode && RENDER_EMITTER_NODES);
|
|
1598
|
+
else this.handleClickNode(node);
|
|
1599
|
+
};
|
|
1600
|
+
emitNodeAt = (depth) => {
|
|
1601
|
+
const nodes = this.secondaryNodes[depth].filter((n) => !this.emittedNodes.includes(n.id)) || [];
|
|
1602
|
+
const randomIndex = Math.floor(Math.random() * nodes.length);
|
|
1603
|
+
if (randomIndex < 0 || randomIndex >= nodes.length) return;
|
|
1604
|
+
const depthNode = this.emitterNodes[depth];
|
|
1605
|
+
const node = nodes[randomIndex];
|
|
1606
|
+
node.x = depthNode.x;
|
|
1607
|
+
node.y = depthNode.y;
|
|
1608
|
+
const parents = getAllParentsUntil(node.id, this.data.links, this.rootId);
|
|
1609
|
+
this.emittedNodes.push(node.id);
|
|
1610
|
+
node.state.collapsed = false;
|
|
1611
|
+
if (parents.length > 0) {
|
|
1612
|
+
parents.forEach((parentId) => {
|
|
1613
|
+
const parentNode = this.data.nodes.maps.id.get(parentId);
|
|
1614
|
+
if (parentNode) {
|
|
1615
|
+
if (parentNode.state) parentNode.state.collapsed = false;
|
|
1616
|
+
}
|
|
1617
|
+
});
|
|
1618
|
+
this.emittedNodes.push(...parents);
|
|
1619
|
+
}
|
|
1620
|
+
console.log("Emitting node at depth", depth, ":", node.id);
|
|
1621
|
+
this.updateHighlights();
|
|
1622
|
+
};
|
|
1623
|
+
handleClickNode = (node, options = {
|
|
1624
|
+
noToggle: false,
|
|
1625
|
+
triggerFocus: false,
|
|
1626
|
+
triggerRestart: false
|
|
1627
|
+
}) => {
|
|
1628
|
+
let wasOpened = false;
|
|
1629
|
+
if (node) {
|
|
1630
|
+
if (node.id === this.rootId) {
|
|
1631
|
+
this.selectedNode = null;
|
|
1632
|
+
this.emitter.emit("selected-node-changed", null);
|
|
1633
|
+
this.subGraph = newGraphData();
|
|
1634
|
+
this.updateRenderLayers();
|
|
1635
|
+
this.updateScene();
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
if (node.state) {
|
|
1639
|
+
const children = getChildren(node.id, this.data.links);
|
|
1640
|
+
if (children.length > 0) {
|
|
1641
|
+
if (this.selectedNode?.id !== node.id) {
|
|
1642
|
+
if (node.state.collapsed) wasOpened = true;
|
|
1643
|
+
node.state.collapsed = false;
|
|
1644
|
+
} else if (!options.noToggle) node.state.collapsed = !node.state.collapsed;
|
|
1645
|
+
if (!node.state.collapsed && wasOpened) {
|
|
1646
|
+
const clusterDistance = 100;
|
|
1647
|
+
const clusterRadius = 50;
|
|
1648
|
+
const parentX = node.x || this.center.x;
|
|
1649
|
+
const parentY = node.y || this.center.y;
|
|
1650
|
+
const dirX = parentX - this.center.x;
|
|
1651
|
+
const dirY = parentY - this.center.y;
|
|
1652
|
+
const length = Math.sqrt(dirX * dirX + dirY * dirY) || 1;
|
|
1653
|
+
const normX = dirX / length;
|
|
1654
|
+
const normY = dirY / length;
|
|
1655
|
+
parentX + normX * clusterDistance;
|
|
1656
|
+
parentY + normY * clusterDistance;
|
|
1657
|
+
children.forEach((childId) => {
|
|
1658
|
+
const childNode = this.data.nodes.maps.id.get(childId);
|
|
1659
|
+
if (childNode?.state?.groupNode) return;
|
|
1660
|
+
if (childNode && isSimNode(childNode)) {
|
|
1661
|
+
Math.random() * 2 * Math.PI;
|
|
1662
|
+
Math.random() * clusterRadius;
|
|
1663
|
+
}
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
this.subGraph = getNodeSubgraph(node.id, this.prunedData.nodes, this.prunedData.links, this.rootId);
|
|
1669
|
+
if (this.selectedNode?.id !== node?.id) {
|
|
1670
|
+
this.selectedNode = node;
|
|
1671
|
+
this.emitter.emit("selected-node-changed", node);
|
|
1672
|
+
}
|
|
1673
|
+
if (wasOpened || options?.triggerFocus) this.transformCanvas.focusOn(() => {
|
|
1674
|
+
const t = this.transformCanvas.getTransform();
|
|
1675
|
+
const _node = this.getNodeById(node.id);
|
|
1676
|
+
return {
|
|
1677
|
+
x: _node?.x,
|
|
1678
|
+
y: _node?.y,
|
|
1679
|
+
scale: t.scale
|
|
1680
|
+
};
|
|
1681
|
+
});
|
|
1682
|
+
} else if (!node && this.selectedNode) {
|
|
1683
|
+
this.selectedNode = null;
|
|
1684
|
+
this.emitter.emit("selected-node-changed", null);
|
|
1685
|
+
this.subGraph = newGraphData();
|
|
1686
|
+
this.updateRenderLayers();
|
|
1687
|
+
this.updateScene();
|
|
1688
|
+
}
|
|
1689
|
+
};
|
|
1690
|
+
updateEmitterNodes() {
|
|
1691
|
+
this.emitterNodes.forEach((node, depth) => {
|
|
1692
|
+
node.clusterSize = (this.secondaryNodes[depth] || []).filter((n) => this.emittedNodes.indexOf(n.id) === -1).length;
|
|
1693
|
+
});
|
|
1694
|
+
this.emitterClusterSizeRange = this.emitterNodes.reduce((acc, node) => [Math.min(acc[0], node.clusterSize || 1), Math.max(acc[1], node.clusterSize || 1)], [Infinity, -Infinity]);
|
|
1695
|
+
}
|
|
1696
|
+
get emitterNodes() {
|
|
1697
|
+
return this.data.nodes.values.filter((n) => n.state?.emitterNode).sort((a, b) => (a.depth || 0) - (b.depth || 0)) || [];
|
|
1698
|
+
}
|
|
1699
|
+
updateScene = () => {
|
|
1700
|
+
if (this.isTicking) return;
|
|
1701
|
+
this.onDraw();
|
|
1702
|
+
};
|
|
1703
|
+
handleMove = (x, y) => {
|
|
1704
|
+
if (this.transformCanvas.getIsDragging()) return;
|
|
1705
|
+
const world = this.screenToWorld(x, y);
|
|
1706
|
+
const node = this.simulation?.find(world.x, world.y, 10) || null;
|
|
1707
|
+
if (node?.state?.sessionNode) return;
|
|
1708
|
+
if (this.hoveredNode === node) return;
|
|
1709
|
+
this.hoveredNode = node;
|
|
1710
|
+
this.emitter.emit("hovered-node-changed", node);
|
|
1711
|
+
this.canvas.style.cursor = node ? "pointer" : "default";
|
|
1712
|
+
this.updateScene();
|
|
1713
|
+
};
|
|
1714
|
+
handleTransform = (t) => {
|
|
1715
|
+
this.emitter.emit("transform-changed", t);
|
|
1716
|
+
this.updateScene();
|
|
1717
|
+
};
|
|
1718
|
+
updateHighlights = () => {
|
|
1719
|
+
const detachedHighlights = this.highlights.maps.detached.get("true") || [];
|
|
1720
|
+
const validSessionIds = /* @__PURE__ */ new Set();
|
|
1721
|
+
let focusSessionNode = null;
|
|
1722
|
+
detachedHighlights.forEach((h) => {
|
|
1723
|
+
const highlightedNode = this.data.nodes.maps.id.get(h.id);
|
|
1724
|
+
if (!highlightedNode) return;
|
|
1725
|
+
const existingLink = this.data.links.maps.targetId.get(h.id);
|
|
1726
|
+
if (!existingLink) return;
|
|
1727
|
+
const id = isSimNode(existingLink.source) ? existingLink.source.id : existingLink.source.toString();
|
|
1728
|
+
const parentNode = this.getNodeById(id);
|
|
1729
|
+
if (!parentNode) return;
|
|
1730
|
+
const _sessionId = h.sessionId || parentNode.id;
|
|
1731
|
+
const sessionId = parentNode.state?.sessionNode ? parentNode.id : `${VOID_DETACH_ID}-${_sessionId}`;
|
|
1732
|
+
validSessionIds.add(sessionId);
|
|
1733
|
+
let sessionNode = this.data.nodes.maps.id.get(sessionId);
|
|
1734
|
+
if (!sessionNode) {
|
|
1735
|
+
const depth = (highlightedNode?.depth || 1) + 1;
|
|
1736
|
+
const angle = getAngle(this.rootNode?.x, this.rootNode?.y, parentNode?.x, parentNode?.y);
|
|
1737
|
+
const circlePos = getRadialPoint(getRadius(depth), this.rootNode?.x, this.rootNode?.y, angle);
|
|
1738
|
+
sessionNode = {
|
|
1739
|
+
id: sessionId,
|
|
1740
|
+
state: {
|
|
1741
|
+
collapsed: false,
|
|
1742
|
+
image: void 0,
|
|
1743
|
+
sessionNode: true
|
|
1744
|
+
},
|
|
1745
|
+
depth,
|
|
1746
|
+
clusterSize: 1,
|
|
1747
|
+
x: circlePos.x,
|
|
1748
|
+
y: circlePos.y
|
|
1749
|
+
};
|
|
1750
|
+
this.data.nodes.push(sessionNode);
|
|
1751
|
+
this.data.links.push({
|
|
1752
|
+
target: sessionNode,
|
|
1753
|
+
source: parentNode
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
const existingLinkIndex = this.data.links.values.findIndex((l) => l === existingLink);
|
|
1757
|
+
this.data.links.splice(existingLinkIndex, 1);
|
|
1758
|
+
this.data.links.push({
|
|
1759
|
+
target: highlightedNode,
|
|
1760
|
+
source: sessionNode
|
|
1761
|
+
});
|
|
1762
|
+
getAllParentsUntil(highlightedNode.id, this.data.links, this.rootId).forEach((parentId) => {
|
|
1763
|
+
if (this.data.nodes.maps.id.get(parentId)?.state) {}
|
|
1764
|
+
});
|
|
1765
|
+
if (highlightedNode.state) highlightedNode.state.collapsed = false;
|
|
1766
|
+
if (!focusSessionNode) focusSessionNode = sessionNode;
|
|
1767
|
+
});
|
|
1768
|
+
this.data.nodes.values.filter((n) => n.state?.sessionNode && !validSessionIds.has(n.id)).forEach((sessionNode) => {
|
|
1769
|
+
const childLinks = this.data.links.maps.sourceId.get(sessionNode.id);
|
|
1770
|
+
const incomingLink = this.data.links.maps.targetId.get(sessionNode.id);
|
|
1771
|
+
const parentNode = incomingLink ? isSimNode(incomingLink.source) ? incomingLink.source : this.getNodeById(incomingLink.source.toString()) : null;
|
|
1772
|
+
this.data.links.reset(this.data.links.values.filter((l) => !(isSimNode(l.source) && l.source.id === sessionNode.id) && !(isSimNode(l.target) && l.target.id === sessionNode.id)));
|
|
1773
|
+
if (parentNode) childLinks?.forEach((link) => {
|
|
1774
|
+
const targetNode = isSimNode(link.target) ? link.target : this.getNodeById(link.target.toString());
|
|
1775
|
+
if (targetNode) this.data.links.push({
|
|
1776
|
+
source: parentNode,
|
|
1777
|
+
target: targetNode
|
|
1778
|
+
});
|
|
1779
|
+
});
|
|
1780
|
+
const index$1 = this.data.nodes.values.findIndex((n) => n.id === sessionNode.id);
|
|
1781
|
+
if (index$1 !== -1) this.data.nodes.splice(index$1, 1);
|
|
1782
|
+
});
|
|
1783
|
+
const isHighlighted = (nodeId) => this.highlights.maps.nodeId.get(nodeId);
|
|
1784
|
+
const isChildHighlighted = (nodeId) => {
|
|
1785
|
+
const children = getChildren(nodeId, this.data.links);
|
|
1786
|
+
if (children.some(isHighlighted)) return true;
|
|
1787
|
+
else return children.some(isHighlighted);
|
|
1788
|
+
};
|
|
1789
|
+
const nodesWithoutHighlight = this.data.nodes.values.filter((n) => {
|
|
1790
|
+
if (n.state?.sessionNode) return false;
|
|
1791
|
+
if (n.id === this.rootId) return false;
|
|
1792
|
+
if (n.state?.emitterNode) return false;
|
|
1793
|
+
if (isHighlighted(n.id)) return false;
|
|
1794
|
+
if (isChildHighlighted(n.id)) return false;
|
|
1795
|
+
return true;
|
|
1796
|
+
});
|
|
1797
|
+
this.secondaryNodes = groupBy(nodesWithoutHighlight, (n) => n.depth || 0);
|
|
1798
|
+
if (this.selectedNode) this.subGraph = getNodeSubgraph(this.selectedNode.id, this.data.nodes, this.data.links, this.rootId);
|
|
1799
|
+
this.updateEmitterNodes();
|
|
1800
|
+
this.restart();
|
|
1801
|
+
if (focusSessionNode) this.transformCanvas.focusOn(() => {
|
|
1802
|
+
if (!focusSessionNode) return null;
|
|
1803
|
+
const t = this.transformCanvas.getTransform();
|
|
1804
|
+
const _node = this.getNodeById(focusSessionNode.id);
|
|
1805
|
+
if (!_node) return null;
|
|
1806
|
+
return {
|
|
1807
|
+
x: _node?.x,
|
|
1808
|
+
y: _node?.y,
|
|
1809
|
+
scale: t.scale
|
|
1810
|
+
};
|
|
1811
|
+
});
|
|
1812
|
+
else this.transformCanvas.focusOn(() => {
|
|
1813
|
+
if (!this.rootNode) return null;
|
|
1814
|
+
const t = this.transformCanvas.getTransform();
|
|
1815
|
+
return {
|
|
1816
|
+
x: this.rootNode.x,
|
|
1817
|
+
y: this.rootNode.y,
|
|
1818
|
+
scale: t.scale
|
|
1819
|
+
};
|
|
1820
|
+
});
|
|
1821
|
+
};
|
|
1822
|
+
setNodeVisibility = (visibility, groupRootOrphans) => {
|
|
1823
|
+
if (this.nodeVisibility === visibility && groupRootOrphans === this.groupRootOrphans) return;
|
|
1824
|
+
this.groupRootOrphans = groupRootOrphans;
|
|
1825
|
+
this.nodeVisibility = visibility;
|
|
1826
|
+
this.updateHighlights();
|
|
1827
|
+
};
|
|
1828
|
+
initialize = (data, rootId) => {
|
|
1829
|
+
Measure.start("initialize");
|
|
1830
|
+
this.emittedNodes = [];
|
|
1831
|
+
this.rawData = data;
|
|
1832
|
+
this.rootId = rootId;
|
|
1833
|
+
const newData = newGraphData({
|
|
1834
|
+
nodes: [],
|
|
1835
|
+
links: data.links.map((l) => ({ ...l }))
|
|
1836
|
+
});
|
|
1837
|
+
newData.nodes.reset(data.nodes.map((n) => {
|
|
1838
|
+
const existingData = this.data.nodes.maps.id.get(n.id);
|
|
1839
|
+
const parent = getParent(n.id, newData.links);
|
|
1840
|
+
const parentNode = parent ? newData.nodes.maps.id.get(parent) : null;
|
|
1841
|
+
const clusterSize = getClusterSize(n.id, newData.links);
|
|
1842
|
+
const depth = getNodeDepth(n.id, newData.links);
|
|
1843
|
+
const circlePos = getRadialPoint(getRadius(Math.max(depth, 2)), this.center.x, this.center.y);
|
|
1844
|
+
const x = depth > 0 ? void 0 : existingData?.x || parentNode?.x || circlePos.x;
|
|
1845
|
+
const y = depth > 0 ? void 0 : existingData?.y || parentNode?.y || circlePos.y;
|
|
1846
|
+
return {
|
|
1847
|
+
...n,
|
|
1848
|
+
state: {
|
|
1849
|
+
collapsed: false,
|
|
1850
|
+
...existingData?.state
|
|
1851
|
+
},
|
|
1852
|
+
clusterSize,
|
|
1853
|
+
depth,
|
|
1854
|
+
x,
|
|
1855
|
+
y
|
|
1856
|
+
};
|
|
1857
|
+
}).sort((a, b) => a.depth - b.depth));
|
|
1858
|
+
newData.nodes.values.forEach((n, i, arr$1) => {
|
|
1859
|
+
const parent = getParent(n.id, newData.links);
|
|
1860
|
+
const parentNode = parent ? newData.nodes.maps.id.get(parent) : null;
|
|
1861
|
+
const depth = Math.max(2, n.depth || 0);
|
|
1862
|
+
const parentAngle = getAngle(this.center.x, this.center.y, parentNode?.x, parentNode?.y);
|
|
1863
|
+
const circlePos = getRadialPoint(getRadius(depth), this.center.x, this.center.y, parentAngle);
|
|
1864
|
+
n.x = n.x || circlePos.x;
|
|
1865
|
+
n.y = n.y || circlePos.y;
|
|
1866
|
+
});
|
|
1867
|
+
if (!newData.nodes.maps.id.get(this.rootId)) newData.nodes.push({
|
|
1868
|
+
id: this.rootId,
|
|
1869
|
+
state: {
|
|
1870
|
+
collapsed: false,
|
|
1871
|
+
image: void 0,
|
|
1872
|
+
rootNode: true
|
|
1873
|
+
},
|
|
1874
|
+
depth: -1,
|
|
1875
|
+
clusterSize: 1,
|
|
1876
|
+
x: this.center.x,
|
|
1877
|
+
y: this.center.y
|
|
1878
|
+
});
|
|
1879
|
+
const rootNodes = newData.nodes.values.filter((node) => !newData.links.maps.targetId.get(node.id));
|
|
1880
|
+
for (const node of rootNodes) newData.links.push({
|
|
1881
|
+
source: this.rootId,
|
|
1882
|
+
target: node.id
|
|
1883
|
+
});
|
|
1884
|
+
this.maxDepth = Math.max(...newData.nodes.values.map((n) => n.depth || 0));
|
|
1885
|
+
this.data = newData;
|
|
1886
|
+
this.loadNodeImages();
|
|
1887
|
+
this.updateHighlights();
|
|
1888
|
+
this.triggerSelected(true);
|
|
1889
|
+
Measure.end("initialize");
|
|
1890
|
+
};
|
|
1891
|
+
get lockedNode() {
|
|
1892
|
+
return this.lockedNodeId ? this.getNodeById(this.lockedNodeId) : null;
|
|
1893
|
+
}
|
|
1894
|
+
triggerSelected = (triggerFocus = false) => {
|
|
1895
|
+
if (this.selectedNode) this.handleClickNode(this.selectedNode, {
|
|
1896
|
+
noToggle: true,
|
|
1897
|
+
triggerFocus
|
|
1898
|
+
});
|
|
1899
|
+
else if (this.lockedNode) this.handleClickNode(this.lockedNode, {
|
|
1900
|
+
noToggle: true,
|
|
1901
|
+
triggerFocus,
|
|
1902
|
+
triggerRestart: true
|
|
1903
|
+
});
|
|
1904
|
+
else this.setSelectedNode(null);
|
|
1905
|
+
};
|
|
1906
|
+
get groupNodes() {
|
|
1907
|
+
return this.prunedData.nodes.maps.groupNode.get("true");
|
|
1908
|
+
}
|
|
1909
|
+
restart = (alpha = .3) => {
|
|
1910
|
+
this.tickCount = 0;
|
|
1911
|
+
console.log("Restarting simulation with alpha:", this.groupNodes);
|
|
1912
|
+
this.prunedData = getPrunedData(this.rootId, this.data.nodes, this.data.links, this.highlights.values, {
|
|
1913
|
+
nodeVisibility: this.nodeVisibility,
|
|
1914
|
+
emittedNodes: this.emittedNodes,
|
|
1915
|
+
groupNodes: this.groupNodes,
|
|
1916
|
+
skipGrouping: !this.groupRootOrphans || this.data.nodes.values.length < 200,
|
|
1917
|
+
openedGroups: this.openedGroups
|
|
1918
|
+
});
|
|
1919
|
+
this.updateRenderLayers();
|
|
1920
|
+
this.clusterSizeRange = (this.prunedData.nodes.maps.collapsed.get("true") || []).reduce((acc, node) => [Math.min(acc[0], node.clusterSize || 1), Math.max(acc[1], node.clusterSize || 1)], [Infinity, -Infinity]);
|
|
1921
|
+
if (this.simulation) {
|
|
1922
|
+
this.simulation.stop();
|
|
1923
|
+
this.simulation.on("tick", null);
|
|
1924
|
+
this.simulation.on("end", null);
|
|
1925
|
+
}
|
|
1926
|
+
this.simulation = forceSimulation(this.prunedData.nodes.values).alpha(this.simulation ? alpha : .5).force("collide", forceCollide((n) => {
|
|
1927
|
+
return this.getNodeSize(n.id) / 2 + 2;
|
|
1928
|
+
})).force("link", asymmetricLinks(this.prunedData.links.values).id((d) => d.id).distance((l) => {
|
|
1929
|
+
const size = this.getNodeSize(isSimNode(l.target) ? l.target.id : l.target.toString());
|
|
1930
|
+
if (isSimNode(l.target)) {
|
|
1931
|
+
const state = l.target?.state;
|
|
1932
|
+
if (state?.emitterNode) {
|
|
1933
|
+
if (isSimNode(l.source) && l.source.id === this.rootId) return 100;
|
|
1934
|
+
return 1;
|
|
1935
|
+
}
|
|
1936
|
+
if (!state?.collapsed) return size;
|
|
1937
|
+
if (state.groupNode) return 5;
|
|
1938
|
+
}
|
|
1939
|
+
return size * 3;
|
|
1940
|
+
}).strength((l) => {
|
|
1941
|
+
return [.66, .08];
|
|
1942
|
+
})).force("charge", forceManyBody().strength((node) => {
|
|
1943
|
+
return -150;
|
|
1944
|
+
})).force("center", forceCenter(this.center.x, this.center.y).strength(.1)).restart();
|
|
1945
|
+
this.simulation.restart();
|
|
1946
|
+
Measure.start("simulation");
|
|
1947
|
+
this.simulation.on("tick", () => {
|
|
1948
|
+
Measure.end("tick", true);
|
|
1949
|
+
this.handleTick();
|
|
1950
|
+
Measure.start("tick");
|
|
1951
|
+
});
|
|
1952
|
+
this.simulation.on("end", () => {
|
|
1953
|
+
Measure.end("simulation");
|
|
1954
|
+
this.onEnd();
|
|
1955
|
+
});
|
|
1956
|
+
};
|
|
1957
|
+
get rootNode() {
|
|
1958
|
+
return this.data.nodes.maps.id.get(this.rootId) || null;
|
|
1959
|
+
}
|
|
1960
|
+
handleTick = () => {
|
|
1961
|
+
this.isTicking = true;
|
|
1962
|
+
if (this.rootNode) {
|
|
1963
|
+
this.rootNode.vx = 0;
|
|
1964
|
+
this.rootNode.vy = 0;
|
|
1965
|
+
}
|
|
1966
|
+
this.onDraw();
|
|
1967
|
+
this.tickCount++;
|
|
1968
|
+
};
|
|
1969
|
+
setTranslate({ x, y }) {
|
|
1970
|
+
this.translate = {
|
|
1971
|
+
x,
|
|
1972
|
+
y
|
|
1973
|
+
};
|
|
1974
|
+
this.transformCanvas.setOffset(this.translate);
|
|
1975
|
+
}
|
|
1976
|
+
get visiblityScale() {
|
|
1977
|
+
return scaleLog().domain(this.clusterSizeRange).range([1.5, .9]).clamp(true);
|
|
1978
|
+
}
|
|
1979
|
+
get color() {
|
|
1980
|
+
return color(this.theme === "light" ? this.config.theme.dark : this.config.theme.light);
|
|
1981
|
+
}
|
|
1982
|
+
get colorContrast() {
|
|
1983
|
+
return color(this.theme === "light" ? this.config.theme.light : this.config.theme.dark);
|
|
1984
|
+
}
|
|
1985
|
+
getNodeSize = (nodeId) => {
|
|
1986
|
+
const { nodeSize } = this.config;
|
|
1987
|
+
const sizeScale = this.highlights.maps.nodeId.get(nodeId)?.scale || 1;
|
|
1988
|
+
if (nodeId === this.rootId) return nodeSize * 2 * sizeScale;
|
|
1989
|
+
const node = this.prunedData.nodes.maps.id.get(nodeId);
|
|
1990
|
+
if (node?.state?.sessionNode) return 5;
|
|
1991
|
+
const isEmitterNode = node?.state?.emitterNode;
|
|
1992
|
+
if (isEmitterNode) {
|
|
1993
|
+
if (node.clusterSize === 0) return 5;
|
|
1994
|
+
return scaleLinear().domain(this.emitterClusterSizeRange).range([nodeSize, nodeSize * 3])(node.clusterSize || 1);
|
|
1995
|
+
}
|
|
1996
|
+
const isCollapsed = !!node?.state?.collapsed;
|
|
1997
|
+
const isGroupNode = !!node?.state?.groupNode;
|
|
1998
|
+
if (isCollapsed || isEmitterNode || isGroupNode) return scaleLinear().domain(this.clusterSizeRange).range([nodeSize, nodeSize * 3])(node.clusterSize || 1);
|
|
1999
|
+
const isSelected = this.selectedNode?.id === nodeId;
|
|
2000
|
+
const _size = node?.status === "LIQUIDATED" || node?.status === "REGENERATED" ? nodeSize * .2 : nodeSize;
|
|
2001
|
+
return isSelected ? _size * 4 : _size * sizeScale;
|
|
2002
|
+
};
|
|
2003
|
+
updateRenderLayers() {
|
|
2004
|
+
const isHighlighted = (id) => {
|
|
2005
|
+
const highlight = this.highlights.maps.nodeId.get(id);
|
|
2006
|
+
return !this.selectedNode && highlight?.onTop || this.selectedNode?.id === id || this.subGraph.nodes.maps.id.get(id);
|
|
2007
|
+
};
|
|
2008
|
+
this.renderLayers.nodes.regular = [];
|
|
2009
|
+
this.renderLayers.nodes.highlighted = [];
|
|
2010
|
+
this.prunedData.nodes.values.forEach((node) => {
|
|
2011
|
+
if (isHighlighted(node.id)) this.renderLayers.nodes.highlighted.push(node);
|
|
2012
|
+
else this.renderLayers.nodes.regular.push(node);
|
|
2013
|
+
});
|
|
2014
|
+
this.renderLayers.links.regular = [];
|
|
2015
|
+
this.renderLayers.links.highlighted = [];
|
|
2016
|
+
const isLinkInSubgraph = (link) => {
|
|
2017
|
+
if (!this.selectedNode) return false;
|
|
2018
|
+
return this.subGraph.links.maps.linkId.has(getLinkId(link));
|
|
2019
|
+
};
|
|
2020
|
+
const isLinkInHighlights = (link) => {
|
|
2021
|
+
const sourceId = isSimNode(link.source) ? link.source.id : link.source;
|
|
2022
|
+
const targetId = isSimNode(link.target) ? link.target.id : link.target;
|
|
2023
|
+
return !this.selectedNode && !!this.highlights.maps.detached.get("true")?.some((h) => h.id === sourceId || h.id === targetId);
|
|
2024
|
+
};
|
|
2025
|
+
this.prunedData.links.values.forEach((link) => {
|
|
2026
|
+
const inSubgraph = isLinkInSubgraph(link);
|
|
2027
|
+
const inHighlights = isLinkInHighlights(link);
|
|
2028
|
+
if (inSubgraph || inHighlights) this.renderLayers.links.highlighted.push(link);
|
|
2029
|
+
else this.renderLayers.links.regular.push(link);
|
|
2030
|
+
});
|
|
2031
|
+
this.renderLayers.links.highlighted.sort((a, b) => {
|
|
2032
|
+
const inSubgraphA = isLinkInSubgraph(a);
|
|
2033
|
+
const inSubgraphB = isLinkInSubgraph(b);
|
|
2034
|
+
if (inSubgraphA || inSubgraphB) return 2;
|
|
2035
|
+
return -1;
|
|
2036
|
+
});
|
|
2037
|
+
this.renderLayers.nodes.highlighted.sort((a, b) => {
|
|
2038
|
+
const highlightA = this.highlights.maps.nodeId.get(a.id);
|
|
2039
|
+
const highlightB = this.highlights.maps.nodeId.get(b.id);
|
|
2040
|
+
if (this.subGraph.nodes.maps.id.get(a.id) || this.subGraph.nodes.maps.id.get(b.id)) return 2;
|
|
2041
|
+
if (a.id === this.selectedNode?.id || b.id === this.selectedNode?.id) return 2;
|
|
2042
|
+
if (highlightA?.onTop || highlightB?.onTop) return 1;
|
|
2043
|
+
return -1;
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
renderLink(ctx, link, options) {
|
|
2047
|
+
const sourceId = isSimNode(link.source) ? link.source.id : link.source;
|
|
2048
|
+
const sourceNode = this.prunedData.nodes.maps.id.get(sourceId.toString());
|
|
2049
|
+
if (isSimNode(link.target)) {
|
|
2050
|
+
if (this.nodeVisibility === "all" || true) {
|
|
2051
|
+
if (link.target.state?.emitterNode || isSimNode(link.source) && link.source.state?.emitterNode) return;
|
|
2052
|
+
}
|
|
2053
|
+
const isSourceNode = this.prunedData.links.maps.sourceId.has(getNodeId(link.target));
|
|
2054
|
+
if (link.target.state?.emitterNode && link.target.clusterSize === 0 && !isSourceNode) return;
|
|
2055
|
+
}
|
|
2056
|
+
const isLight = this.theme === "light";
|
|
2057
|
+
const { dim: _dim, hasSelection, highlight } = options;
|
|
2058
|
+
let stroke = _dim ? this.color(dim(.09, isLight))() : hasSelection ? this.color(dim(.4, isLight))() : this.color(dim(.18, isLight))();
|
|
2059
|
+
ctx.globalAlpha = highlight ? 1 : .5;
|
|
2060
|
+
const sx = sourceNode && sourceNode.x || 0;
|
|
2061
|
+
const sy = sourceNode && sourceNode.y || 0;
|
|
2062
|
+
const tx = isSimNode(link.target) && link.target.x || 0;
|
|
2063
|
+
const ty = isSimNode(link.target) && link.target.y || 0;
|
|
2064
|
+
if (highlight?.linkColor) {
|
|
2065
|
+
const gradient = ctx.createLinearGradient(sx, sy, tx, ty);
|
|
2066
|
+
gradient.addColorStop(0, stroke);
|
|
2067
|
+
gradient.addColorStop(1, color(highlight.linkColor)());
|
|
2068
|
+
ctx.strokeStyle = gradient;
|
|
2069
|
+
} else ctx.strokeStyle = stroke;
|
|
2070
|
+
ctx.lineWidth = _dim ? .3 : .8;
|
|
2071
|
+
ctx.beginPath();
|
|
2072
|
+
ctx.moveTo(sx, sy);
|
|
2073
|
+
ctx.lineTo(tx, ty);
|
|
2074
|
+
ctx.stroke();
|
|
2075
|
+
ctx.closePath();
|
|
2076
|
+
}
|
|
2077
|
+
renderNode(ctx, node, options) {
|
|
2078
|
+
const x = node.x || 0;
|
|
2079
|
+
const y = node.y || 0;
|
|
2080
|
+
const isSelected = this.selectedNode?.id === node.id;
|
|
2081
|
+
const isHovered = this.hoveredNode?.id === node.id;
|
|
2082
|
+
const isCollapsed = !!node.state?.collapsed;
|
|
2083
|
+
const isLiquidated = node.status === "LIQUIDATED" || node.status === "REGENERATED";
|
|
2084
|
+
const isLight = this.theme === "light";
|
|
2085
|
+
const { dim: _dim, transform } = options;
|
|
2086
|
+
const fill = _dim ? this.color(dim(.075, isLight))() : isCollapsed ? this.color(dim(.18, isLight))() : isHovered ? this.color(dim(.4, isLight))() : this.color();
|
|
2087
|
+
const stroke = this.colorContrast();
|
|
2088
|
+
const nodeSize = this.getNodeSize(node.id);
|
|
2089
|
+
const highlight = this.highlights.maps.nodeId.get(node.id);
|
|
2090
|
+
const highlighted = !!highlight;
|
|
2091
|
+
let highlightedStroke = _dim ? color(highlight?.strokeColor || red)(dim(.4, isLight))() : color(highlight?.strokeColor || red)();
|
|
2092
|
+
if (node.id === this.rootId) this.renderRootNode(ctx, x, y, nodeSize, _dim, isLight);
|
|
2093
|
+
else if (node.state?.emitterNode) return;
|
|
2094
|
+
else if (node.state?.sessionNode) this.renderSessionNode(ctx, x, y, nodeSize);
|
|
2095
|
+
else if (isCollapsed || node.state?.groupNode) this.renderCollapsedNode(ctx, x, y, nodeSize, {
|
|
2096
|
+
fill,
|
|
2097
|
+
stroke,
|
|
2098
|
+
highlighted,
|
|
2099
|
+
highlightedStroke,
|
|
2100
|
+
isSelected,
|
|
2101
|
+
dim: _dim,
|
|
2102
|
+
transform,
|
|
2103
|
+
clusterSize: node.clusterSize || 1,
|
|
2104
|
+
isLight
|
|
2105
|
+
});
|
|
2106
|
+
else this.renderExpandedNode(ctx, x, y, nodeSize, {
|
|
2107
|
+
fill,
|
|
2108
|
+
stroke,
|
|
2109
|
+
highlighted,
|
|
2110
|
+
highlightedStroke,
|
|
2111
|
+
isHovered,
|
|
2112
|
+
isLiquidated,
|
|
2113
|
+
dim: _dim,
|
|
2114
|
+
image: node.state?.image
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
renderEmitterNode(ctx, x, y, size, node, dimmed) {
|
|
2118
|
+
if (this.nodeVisibility === "all") return;
|
|
2119
|
+
if (node.clusterSize === 0) return;
|
|
2120
|
+
this.theme;
|
|
2121
|
+
circle(ctx, x, y, size / 2, {
|
|
2122
|
+
stroke: true,
|
|
2123
|
+
strokeStyle: this.colorContrast(),
|
|
2124
|
+
lineWidth: .2,
|
|
2125
|
+
fill: true,
|
|
2126
|
+
fillStyle: "#0000ff"
|
|
2127
|
+
});
|
|
2128
|
+
const transform = this.transformCanvas.getTransform();
|
|
2129
|
+
const clusterSize = node.clusterSize || 1;
|
|
2130
|
+
if (transform.scale - .5 >= this.visiblityScale(clusterSize) ? 1 : 0) {
|
|
2131
|
+
ctx.font = `${14 / transform.scale}px Sans-Serif`;
|
|
2132
|
+
ctx.textAlign = "center";
|
|
2133
|
+
ctx.textBaseline = "middle";
|
|
2134
|
+
ctx.fillStyle = this.colorContrast();
|
|
2135
|
+
ctx.fillText(clusterSize.toString(), x, y);
|
|
2136
|
+
ctx.font = `${10 / transform.scale}px Sans-Serif`;
|
|
2137
|
+
ctx.fillText("click to emit", x, y + 5);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
renderSessionNode(ctx, x, y, size) {
|
|
2141
|
+
const isLight = this.theme === "light";
|
|
2142
|
+
circle(ctx, x, y, size / 2, {
|
|
2143
|
+
stroke: true,
|
|
2144
|
+
strokeStyle: this.colorContrast(),
|
|
2145
|
+
lineWidth: .2,
|
|
2146
|
+
fill: true,
|
|
2147
|
+
fillStyle: this.color(dim(.18, isLight))()
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
2150
|
+
renderRootNode(ctx, x, y, size, dimmed, isLight) {
|
|
2151
|
+
circle(ctx, x, y, size / 2, {
|
|
2152
|
+
stroke: false,
|
|
2153
|
+
strokeStyle: this.colorContrast(),
|
|
2154
|
+
lineWidth: .2,
|
|
2155
|
+
fill: true,
|
|
2156
|
+
fillStyle: this.color(dim(.18, isLight))()
|
|
2157
|
+
});
|
|
2158
|
+
if (this.rootImages) {
|
|
2159
|
+
const _idx = Math.min(isLight ? 0 : 1, this.rootImages.length - 1);
|
|
2160
|
+
const _img = this.rootImages[_idx];
|
|
2161
|
+
const _imgSize = size * .55;
|
|
2162
|
+
if (_img) img(ctx, _img, x - _imgSize / 2, y - _imgSize / 2, _imgSize, _imgSize, 0, dimmed ? .1 : 1);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
renderCollapsedNode(ctx, x, y, size, options) {
|
|
2166
|
+
const { fill, stroke, highlighted, highlightedStroke, isSelected, dim: _dim, transform, clusterSize, isLight } = options;
|
|
2167
|
+
if (highlighted) {
|
|
2168
|
+
const _size = size + 4;
|
|
2169
|
+
circle(ctx, x, y, _size / 2, {
|
|
2170
|
+
stroke: true,
|
|
2171
|
+
strokeStyle: highlightedStroke,
|
|
2172
|
+
lineWidth: 1,
|
|
2173
|
+
fill: false,
|
|
2174
|
+
fillStyle: fill
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
circle(ctx, x, y, size / 2, {
|
|
2178
|
+
stroke: true,
|
|
2179
|
+
strokeStyle: stroke,
|
|
2180
|
+
lineWidth: isSelected ? 1 : .2,
|
|
2181
|
+
fill: true,
|
|
2182
|
+
fillStyle: fill
|
|
2183
|
+
});
|
|
2184
|
+
if (transform.scale >= this.visiblityScale(clusterSize) ? 1 : 0) {
|
|
2185
|
+
ctx.font = `${14 / transform.scale}px Sans-Serif`;
|
|
2186
|
+
ctx.textAlign = "center";
|
|
2187
|
+
ctx.textBaseline = "middle";
|
|
2188
|
+
ctx.fillStyle = this.color(dim(_dim ? .2 : .5, isLight))();
|
|
2189
|
+
ctx.fillText(clusterSize.toString(), x, y);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
renderExpandedNode(ctx, x, y, size, options) {
|
|
2193
|
+
const { fill, highlighted, highlightedStroke, isHovered, isLiquidated, dim: _dim, image } = options;
|
|
2194
|
+
const _size = size + 1;
|
|
2195
|
+
rect(ctx, x - _size / 2, y - _size / 2, _size, _size, {
|
|
2196
|
+
stroke: highlighted || isHovered,
|
|
2197
|
+
strokeStyle: isHovered ? fill : highlightedStroke,
|
|
2198
|
+
lineWidth: 1,
|
|
2199
|
+
fill: this.hideThumbnails || isLiquidated || !image,
|
|
2200
|
+
fillStyle: fill,
|
|
2201
|
+
borderRadius: 1
|
|
2202
|
+
});
|
|
2203
|
+
if (image && !this.hideThumbnails && !isLiquidated) img(ctx, image, x - size / 2, y - size / 2, size, size, 1, _dim ? .1 : 1, fill);
|
|
2204
|
+
}
|
|
2205
|
+
onDraw = () => {
|
|
2206
|
+
Measure.start("draw");
|
|
2207
|
+
this.isDrawing = true;
|
|
2208
|
+
const context = this.canvas?.getContext("2d");
|
|
2209
|
+
const transform = this.transformCanvas.getTransform();
|
|
2210
|
+
if (!context) {
|
|
2211
|
+
this.isDrawing = false;
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
const dpi = devicePixelRatio || 1;
|
|
2215
|
+
context.save();
|
|
2216
|
+
context.scale(dpi, dpi);
|
|
2217
|
+
context.clearRect(0, 0, this.width, this.height);
|
|
2218
|
+
context.setTransform(transform.scale, 0, 0, transform.scale, transform.x, transform.y);
|
|
2219
|
+
context.translate(this.origin.x, this.origin.y);
|
|
2220
|
+
context.save();
|
|
2221
|
+
const dimmedByHighlights = (nodeId) => {
|
|
2222
|
+
if (this.nodeVisibility === "all") return false;
|
|
2223
|
+
if (nodeId === this.rootId) return false;
|
|
2224
|
+
if (this.prunedData.nodes.maps.id.get(nodeId)?.state?.groupNode) return false;
|
|
2225
|
+
if (this.nodeVisibility === "locked") {
|
|
2226
|
+
const node = this.getNodeById(nodeId);
|
|
2227
|
+
return !(node?.status === "LOCKED" || node?.status === "EVOLVED");
|
|
2228
|
+
}
|
|
2229
|
+
return !this.highlights.maps.nodeId.get(nodeId);
|
|
2230
|
+
};
|
|
2231
|
+
const hasSelection = !!this.selectedNode;
|
|
2232
|
+
this.renderLayers.links.regular.forEach((link) => {
|
|
2233
|
+
const sourceId = getNodeId(link.source);
|
|
2234
|
+
const targetId = getNodeId(link.target);
|
|
2235
|
+
const _dim = hasSelection ? !this.subGraph.links.maps.linkId.has(getLinkId(link)) : dimmedByHighlights(sourceId) || dimmedByHighlights(targetId);
|
|
2236
|
+
this.renderLink(context, link, {
|
|
2237
|
+
dim: _dim,
|
|
2238
|
+
hasSelection
|
|
2239
|
+
});
|
|
2240
|
+
});
|
|
2241
|
+
context.globalAlpha = 1;
|
|
2242
|
+
this.renderLayers.nodes.regular.forEach((node) => {
|
|
2243
|
+
const _dim = hasSelection ? !this.subGraph.nodes.maps.id.has(node.id) : dimmedByHighlights(node.id);
|
|
2244
|
+
this.renderNode(context, node, {
|
|
2245
|
+
dim: _dim,
|
|
2246
|
+
transform
|
|
2247
|
+
});
|
|
2248
|
+
});
|
|
2249
|
+
this.renderLayers.links.highlighted.forEach((link) => {
|
|
2250
|
+
const sourceId = getNodeId(link.source);
|
|
2251
|
+
const targetId = getNodeId(link.target);
|
|
2252
|
+
const _dim = hasSelection ? !this.subGraph.links.maps.linkId.has(getLinkId(link)) : dimmedByHighlights(sourceId) || dimmedByHighlights(targetId);
|
|
2253
|
+
const highlight = this.highlights.maps.nodeId.get(sourceId) || this.highlights.maps.nodeId.get(targetId);
|
|
2254
|
+
this.renderLink(context, link, {
|
|
2255
|
+
dim: _dim,
|
|
2256
|
+
hasSelection,
|
|
2257
|
+
highlight
|
|
2258
|
+
});
|
|
2259
|
+
});
|
|
2260
|
+
context.globalAlpha = 1;
|
|
2261
|
+
this.renderLayers.nodes.highlighted.forEach((node) => {
|
|
2262
|
+
const _dim = hasSelection ? !this.subGraph.nodes.maps.id.has(node.id) : dimmedByHighlights(node.id);
|
|
2263
|
+
this.renderNode(context, node, {
|
|
2264
|
+
dim: _dim,
|
|
2265
|
+
transform
|
|
2266
|
+
});
|
|
2267
|
+
});
|
|
2268
|
+
context.restore();
|
|
2269
|
+
context.restore();
|
|
2270
|
+
this.transformCanvas.trackCursor();
|
|
2271
|
+
this.isDrawing = false;
|
|
2272
|
+
Measure.end("draw");
|
|
2273
|
+
};
|
|
2274
|
+
drawDebug(context) {
|
|
2275
|
+
const transform = this.transformCanvas.getTransform();
|
|
2276
|
+
context.font = `14px Sans-Serif`;
|
|
2277
|
+
context.textAlign = "left";
|
|
2278
|
+
context.fillStyle = "#000000";
|
|
2279
|
+
context.fillText(`${transform.x}, ${transform.y}, ${transform.scale}`, 0, 15);
|
|
2280
|
+
const center = this.worldToScreen(this.rootNode?.x, this.rootNode?.y);
|
|
2281
|
+
context.fillText(`${center.x}, ${center.y}`, 0, 30);
|
|
2282
|
+
}
|
|
2283
|
+
drawDebugDepthCircles(context) {
|
|
2284
|
+
const transform = this.transformCanvas.getTransform();
|
|
2285
|
+
for (let i = 0; i < this.maxDepth; i++) {
|
|
2286
|
+
const depth = i;
|
|
2287
|
+
const r = getRadius(depth);
|
|
2288
|
+
const x = this.center.x;
|
|
2289
|
+
const y = this.center.y;
|
|
2290
|
+
circle(context, x, y, r, {
|
|
2291
|
+
fill: false,
|
|
2292
|
+
stroke: true,
|
|
2293
|
+
strokeStyle: "#00ff00"
|
|
2294
|
+
});
|
|
2295
|
+
context.font = `${40 / transform.scale}px Sans-Serif`;
|
|
2296
|
+
context.textAlign = "center";
|
|
2297
|
+
context.textBaseline = "middle";
|
|
2298
|
+
context.fillStyle = this.color();
|
|
2299
|
+
context.fillText(depth.toString(), x + r, y);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
onEnd = () => {
|
|
2303
|
+
this.isTicking = false;
|
|
2304
|
+
this.emitter.emit("simulation:ended", this);
|
|
2305
|
+
};
|
|
2306
|
+
loadNodeImages = () => {
|
|
2307
|
+
this.data.nodes.values.forEach((node) => {
|
|
2308
|
+
if (node.id === this.rootId) return;
|
|
2309
|
+
if (node.imgSrc && this.imageCache.get(node.imgSrc)) {
|
|
2310
|
+
const html = this.imageCache.get(node.imgSrc);
|
|
2311
|
+
node.state = node.state || {};
|
|
2312
|
+
node.state.image = html;
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
const loadImage = async () => {
|
|
2316
|
+
const src = this.loadNodeImage ? await this.loadNodeImage(node) : node.imgSrc;
|
|
2317
|
+
if (!src) return;
|
|
2318
|
+
const html = this.imageCache.get(src) || await loadHTMLImageElement(src);
|
|
2319
|
+
this.imageCache.set(src, html);
|
|
2320
|
+
node.state = node.state || {};
|
|
2321
|
+
node.state.image = html;
|
|
2322
|
+
};
|
|
2323
|
+
loadImage();
|
|
2324
|
+
});
|
|
2325
|
+
};
|
|
2326
|
+
destroy = () => {
|
|
2327
|
+
this.simulation?.stop();
|
|
2328
|
+
this.simulation?.on("tick", null);
|
|
2329
|
+
this.simulation?.on("end", null);
|
|
2330
|
+
this.transformCanvas.destroy();
|
|
2331
|
+
};
|
|
2332
|
+
resize = (width, height) => {
|
|
2333
|
+
this.width = width;
|
|
2334
|
+
this.height = height;
|
|
2335
|
+
this.updateScene();
|
|
2336
|
+
};
|
|
2337
|
+
setTheme = (theme) => {
|
|
2338
|
+
this.theme = theme;
|
|
2339
|
+
this.updateScene();
|
|
2340
|
+
};
|
|
2341
|
+
setNodeImage = (nodeId, src) => {
|
|
2342
|
+
const node = this.getNodeById(nodeId);
|
|
2343
|
+
if (!node) return;
|
|
2344
|
+
const load = async () => {
|
|
2345
|
+
const html = this.imageCache.get(src) || await loadHTMLImageElement(src);
|
|
2346
|
+
this.imageCache.set(src, html);
|
|
2347
|
+
node.state = node.state || {};
|
|
2348
|
+
node.state.image = html;
|
|
2349
|
+
if (!this.isTicking && !this.isDrawing) this.updateScene();
|
|
2350
|
+
};
|
|
2351
|
+
load();
|
|
2352
|
+
};
|
|
2353
|
+
setHideThumbnails = (hide) => {
|
|
2354
|
+
this.hideThumbnails = hide;
|
|
2355
|
+
this.updateScene();
|
|
2356
|
+
};
|
|
2357
|
+
setSelectedNode = (node) => {
|
|
2358
|
+
this.selectedNode = node;
|
|
2359
|
+
this.updateRenderLayers();
|
|
2360
|
+
this.updateScene();
|
|
2361
|
+
};
|
|
2362
|
+
_highlightHash;
|
|
2363
|
+
setHighlights = (highlights) => {
|
|
2364
|
+
const hash = quickHash(JSON.stringify(highlights));
|
|
2365
|
+
if (hash === this._highlightHash) return;
|
|
2366
|
+
this._highlightHash = hash;
|
|
2367
|
+
this.highlights.reset(highlights);
|
|
2368
|
+
this.updateHighlights();
|
|
2369
|
+
};
|
|
2370
|
+
setNoInteraction = (noInteraction) => {
|
|
2371
|
+
this.noInteraction = noInteraction;
|
|
2372
|
+
this.transformCanvas.setNoInteraction(this.noInteraction);
|
|
2373
|
+
this.updateScene();
|
|
2374
|
+
};
|
|
2375
|
+
getNodeById = (nodeId) => {
|
|
2376
|
+
return this.data.nodes.maps.id.get(nodeId) || null;
|
|
2377
|
+
};
|
|
2378
|
+
setLockedNodeId = (nodeId) => {
|
|
2379
|
+
this.lockedNodeId = nodeId || void 0;
|
|
2380
|
+
};
|
|
2381
|
+
handleClickDebug = (x, y) => {
|
|
2382
|
+
const p = this.screenToWorld(x, y);
|
|
2383
|
+
this.circles.push(p);
|
|
2384
|
+
this.transformCanvas.focusOn(() => {
|
|
2385
|
+
const circle$1 = this.circles[this.circles.length - 1];
|
|
2386
|
+
return {
|
|
2387
|
+
x: circle$1.x,
|
|
2388
|
+
y: circle$1.y,
|
|
2389
|
+
scale: this.transformCanvas.getTransform().scale
|
|
2390
|
+
};
|
|
2391
|
+
});
|
|
2392
|
+
this.updateScene();
|
|
2393
|
+
};
|
|
2394
|
+
circles = [];
|
|
2395
|
+
onDrawDebug = () => {
|
|
2396
|
+
const context = this.canvas?.getContext("2d");
|
|
2397
|
+
const transform = this.transformCanvas.getTransform();
|
|
2398
|
+
if (!context) return;
|
|
2399
|
+
const dpi = devicePixelRatio || 1;
|
|
2400
|
+
context.save();
|
|
2401
|
+
context.scale(dpi, dpi);
|
|
2402
|
+
context.clearRect(0, 0, this.width, this.height);
|
|
2403
|
+
context.setTransform(transform.scale, 0, 0, transform.scale, transform.x, transform.y);
|
|
2404
|
+
context.translate(this.origin.x, this.origin.y);
|
|
2405
|
+
context.save();
|
|
2406
|
+
this.circles.map((c) => {
|
|
2407
|
+
circle(context, c.x, c.y, 5, {
|
|
2408
|
+
fill: true,
|
|
2409
|
+
fillStyle: "#ff0000",
|
|
2410
|
+
stroke: false
|
|
2411
|
+
});
|
|
2412
|
+
});
|
|
2413
|
+
circle(context, 0, 0, 5, {
|
|
2414
|
+
fill: true,
|
|
2415
|
+
fillStyle: "#ff0000",
|
|
2416
|
+
stroke: false
|
|
2417
|
+
});
|
|
2418
|
+
context.restore();
|
|
2419
|
+
context.restore();
|
|
2420
|
+
};
|
|
2421
|
+
get measures() {
|
|
2422
|
+
return Measure.getSamples();
|
|
2423
|
+
}
|
|
2424
|
+
};
|
|
2425
|
+
|
|
2426
|
+
//#endregion
|
|
2427
|
+
//#region src/components/OpenFormGraph.tsx
|
|
2428
|
+
function OpenFormGraph(props) {
|
|
2429
|
+
const { width, height, highlights = [], className, noInteraction = false, loadNodeImage, translate, onTransform, onSimulationEnded, nodeVisibility = "all", children, groupRootOrphans = false } = props;
|
|
2430
|
+
const { simulation, data, rootId, rootImageSources, theme, hideThumbnails, setHoveredNode, setSelectedNode, lockedNodeId } = useOpenFormGraph();
|
|
2431
|
+
const canvasRef = useRef(null);
|
|
2432
|
+
useEffect(() => {
|
|
2433
|
+
if (!canvasRef.current) return;
|
|
2434
|
+
simulation.current = new OpenGraphSimulation({
|
|
2435
|
+
width,
|
|
2436
|
+
height,
|
|
2437
|
+
canvas: canvasRef.current,
|
|
2438
|
+
rootImageSources,
|
|
2439
|
+
loadNodeImage,
|
|
2440
|
+
theme,
|
|
2441
|
+
translate,
|
|
2442
|
+
lockedNodeId,
|
|
2443
|
+
nodeVisibility,
|
|
2444
|
+
highlights,
|
|
2445
|
+
groupRootOrphans
|
|
2446
|
+
});
|
|
2447
|
+
return () => {
|
|
2448
|
+
simulation.current?.destroy();
|
|
2449
|
+
};
|
|
2450
|
+
}, []);
|
|
2451
|
+
useEffect(() => {
|
|
2452
|
+
if (!simulation.current) return;
|
|
2453
|
+
simulation.current.emitter.on("selected-node-changed", setSelectedNode);
|
|
2454
|
+
simulation.current.emitter.on("hovered-node-changed", setHoveredNode);
|
|
2455
|
+
return () => {
|
|
2456
|
+
simulation.current?.emitter.off("selected-node-changed", setSelectedNode);
|
|
2457
|
+
simulation.current?.emitter.off("hovered-node-changed", setHoveredNode);
|
|
2458
|
+
};
|
|
2459
|
+
}, [setSelectedNode, setHoveredNode]);
|
|
2460
|
+
useEffect(() => {
|
|
2461
|
+
if (!simulation.current) return;
|
|
2462
|
+
if (!onTransform) return;
|
|
2463
|
+
simulation.current.emitter.on("transform-changed", onTransform);
|
|
2464
|
+
return () => {
|
|
2465
|
+
simulation.current?.emitter.off("transform-changed", onTransform);
|
|
2466
|
+
};
|
|
2467
|
+
}, [onTransform]);
|
|
2468
|
+
useEffect(() => {
|
|
2469
|
+
if (!simulation.current || !onSimulationEnded) return;
|
|
2470
|
+
return simulation.current.emitter.on("simulation:ended", onSimulationEnded);
|
|
2471
|
+
}, [onSimulationEnded]);
|
|
2472
|
+
useEffect(() => {
|
|
2473
|
+
if (!simulation.current) return;
|
|
2474
|
+
simulation.current.resize(width, height);
|
|
2475
|
+
}, [width, height]);
|
|
2476
|
+
useEffect(() => {
|
|
2477
|
+
if (!simulation.current || !theme) return;
|
|
2478
|
+
simulation.current.setTheme(theme);
|
|
2479
|
+
}, [theme]);
|
|
2480
|
+
useEffect(() => {
|
|
2481
|
+
if (!simulation.current) return;
|
|
2482
|
+
simulation.current.setHideThumbnails(hideThumbnails);
|
|
2483
|
+
}, [hideThumbnails]);
|
|
2484
|
+
useEffect(() => {
|
|
2485
|
+
if (!simulation.current) return;
|
|
2486
|
+
simulation.current.setNodeVisibility(nodeVisibility, groupRootOrphans);
|
|
2487
|
+
}, [nodeVisibility, groupRootOrphans]);
|
|
2488
|
+
useEffect(() => {
|
|
2489
|
+
if (!simulation.current) return;
|
|
2490
|
+
simulation.current.setHighlights(highlights);
|
|
2491
|
+
}, [highlights]);
|
|
2492
|
+
useEffect(() => {
|
|
2493
|
+
if (!simulation.current) return;
|
|
2494
|
+
simulation.current.setNoInteraction(noInteraction);
|
|
2495
|
+
}, [noInteraction]);
|
|
2496
|
+
useEffect(() => {
|
|
2497
|
+
if (!simulation.current) return;
|
|
2498
|
+
simulation.current.initialize(data, rootId);
|
|
2499
|
+
}, [data, rootId]);
|
|
2500
|
+
useEffect(() => {
|
|
2501
|
+
if (!simulation.current) return;
|
|
2502
|
+
if (!translate) return;
|
|
2503
|
+
simulation.current.setTranslate(translate);
|
|
2504
|
+
}, [translate?.y, translate?.x]);
|
|
2505
|
+
useEffect(() => {
|
|
2506
|
+
if (!simulation.current) return;
|
|
2507
|
+
if (simulation.current.lockedNodeId === lockedNodeId) return;
|
|
2508
|
+
simulation.current.setLockedNodeId(lockedNodeId);
|
|
2509
|
+
}, [lockedNodeId]);
|
|
2510
|
+
const dpi = devicePixelRatio || 1;
|
|
2511
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("canvas", {
|
|
2512
|
+
onMouseEnter: props.onMouseEnter,
|
|
2513
|
+
onMouseLeave: props.onMouseLeave,
|
|
2514
|
+
ref: canvasRef,
|
|
2515
|
+
className,
|
|
2516
|
+
width: `${width * dpi}px`,
|
|
2517
|
+
height: `${height * dpi}px`,
|
|
2518
|
+
style: {
|
|
2519
|
+
width: `${width}px`,
|
|
2520
|
+
height: `${height}px`
|
|
2521
|
+
}
|
|
2522
|
+
}), children] });
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
//#endregion
|
|
2526
|
+
export { Highlight, OpenFormGraph, OpenGraphEventEmitter, OpenGraphSimulation, TransformCanvas, buildTreeFromGraphData, circle, color, dim, generateTree, getAllParentsUntil, getChildren, getClusterSize, getLinkId, getNodeDepth, getNodeId, getNodeSubgraph, getParent, getPrunedData, getRootParent, groupGraphNodes, hasOnlyLeafs, hexagon, img, isCustomHighlight, isSimLink, isSimNode, loadHTMLImageElement, newGraphData, rect, red, searchParents };
|
|
2527
|
+
//# sourceMappingURL=OpenFormGraph-Ccizmxbo.js.map
|