@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.
@@ -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