@fxhash/open-form-graph 0.0.3 → 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.
@@ -1,14 +1,228 @@
1
- import { DEFAULT_GRAPH_CONFIG, VOID_DETACH_ID, VOID_ROOT_ID, useOpenFormGraph } from "./provider-PqOen2FE.js";
1
+ import { DEFAULT_GRAPH_CONFIG, VOID_DETACH_ID, VOID_ROOT_ID, useOpenFormGraph } from "./provider-CPq89n3a.js";
2
2
  import { useEffect, useRef } from "react";
3
- import { jsx } from "react/jsx-runtime";
4
- import { forceCenter, forceCollide, forceManyBody, forceRadial, forceSimulation } from "d3-force";
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";
5
7
  import { scaleLinear, scaleLog } from "d3-scale";
6
- import { EventEmitter, float2hex, xorshiftString } from "@fxhash/utils";
7
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
8
216
  //#region src/util/types.ts
9
217
  function isSimNode(node) {
10
218
  return typeof node === "object" && "id" in node;
11
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
+ }
12
226
  function isSimLink(link) {
13
227
  return typeof link === "object" && "source" in link && typeof link.source !== "string";
14
228
  }
@@ -19,25 +233,18 @@ function isCustomHighlight(highlight) {
19
233
 
20
234
  //#endregion
21
235
  //#region src/util/graph.ts
22
- function getNodeId(n) {
23
- return typeof n === "object" && n !== null && "id" in n ? n.id : n;
24
- }
25
- function getParents(id, links) {
26
- return links.filter((l) => {
27
- const targetId = isSimNode(l.target) ? l.target.id : l.target;
28
- return targetId === id;
29
- }).map((link) => isSimNode(link.source) ? link.source.id : link.source.toString());
236
+ function getParent(id, links) {
237
+ const linkToParent = links.maps.targetId.get(id);
238
+ if (!linkToParent) return null;
239
+ return getNodeId(linkToParent.source);
30
240
  }
31
241
  function getAllParentsUntil(nodeId, links, stopAtId) {
32
- const parent = getParents(nodeId, links)[0];
33
- if (!parent || parent === stopAtId) return [];
242
+ const parent = getParent(nodeId, links);
243
+ if (parent === null || parent === stopAtId) return [];
34
244
  return [parent, ...getAllParentsUntil(parent, links, stopAtId)];
35
245
  }
36
246
  function getChildren(id, links) {
37
- return links.filter((l) => {
38
- const sourceId = isSimNode(l.source) ? l.source.id : l.source;
39
- return sourceId === id;
40
- }).map((link) => link.target.toString());
247
+ return (links.maps.sourceId.get(id) || []).map((link) => getNodeId(link.target));
41
248
  }
42
249
  function getClusterSize(id, links) {
43
250
  const children = getChildren(id, links);
@@ -47,51 +254,46 @@ function getClusterSize(id, links) {
47
254
  }
48
255
  function getNodeDepth(id, links) {
49
256
  function getDepth(id$1, depth) {
50
- const parents = getParents(id$1, links);
51
- if (parents.length === 0) return depth;
52
- return getDepth(parents[0], depth + 1);
257
+ const parent = getParent(id$1, links);
258
+ if (parent === null) return depth;
259
+ return getDepth(parent, depth + 1);
53
260
  }
54
261
  return getDepth(id, 0);
55
262
  }
56
263
  function getRootParent(id, links, stop) {
57
264
  let currentId = id;
58
265
  while (true) {
59
- const parents = getParents(currentId, links);
60
- if (stop && parents.includes(stop)) return currentId;
61
- if (parents.length === 0) return currentId;
62
- currentId = parents[0];
266
+ const parent = getParent(currentId, links);
267
+ if (stop && parent === stop) return currentId;
268
+ if (parent === null) return currentId;
269
+ currentId = parent;
63
270
  }
64
271
  }
65
272
  function hasOnlyLeafs(id, links) {
66
- const children = getChildren(id, links);
67
- return children.every((childId) => getChildren(childId, links).length === 0);
273
+ return getChildren(id, links).every((childId) => getChildren(childId, links).length === 0);
68
274
  }
69
275
  function getNodeSubgraph(nodeId, nodes, links, rootId) {
70
- const nodesById = Object.fromEntries(nodes.map((n) => [n.id, n]));
71
- const parentSet = new Set();
72
- const childSet = new Set();
73
- const subLinks = new Set();
276
+ const parentSet = /* @__PURE__ */ new Set();
277
+ const childSet = /* @__PURE__ */ new Set();
278
+ const subLinks = /* @__PURE__ */ new Set();
74
279
  let currentId = nodeId;
75
280
  while (currentId !== rootId) {
76
- const parentLink = links.find((l) => {
77
- const targetId = isSimNode(l.target) ? l.target.id : l.target;
78
- return targetId === currentId;
79
- });
281
+ const parentLink = links.maps.targetId.get(currentId);
80
282
  if (!parentLink) break;
81
- const parentId = isSimNode(parentLink.source) ? parentLink.source.id : parentLink.source;
82
- if (parentSet.has(parentId.toString())) break;
83
- parentSet.add(parentId.toString());
283
+ const parentId = getNodeId(parentLink.source);
284
+ if (parentSet.has(parentId)) break;
285
+ parentSet.add(parentId);
84
286
  subLinks.add(parentLink);
85
- currentId = parentId.toString();
287
+ currentId = parentId;
86
288
  }
87
289
  function collectChildren(id) {
88
- for (const link of links) {
89
- const sourceId = isSimNode(link.source) ? link.source.id : link.source;
90
- const targetId = isSimNode(link.target) ? link.target.id : link.target;
91
- if (sourceId === id && !childSet.has(targetId.toString())) {
92
- childSet.add(targetId.toString());
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);
93
295
  subLinks.add(link);
94
- collectChildren(targetId.toString());
296
+ collectChildren(targetId);
95
297
  }
96
298
  }
97
299
  }
@@ -102,59 +304,390 @@ function getNodeSubgraph(nodeId, nodes, links, rootId) {
102
304
  ...childSet
103
305
  ]);
104
306
  const filteredLinks = Array.from(subLinks).filter((link) => {
105
- const sourceId = isSimNode(link.source) ? link.source.id : link.source;
106
- const targetId = isSimNode(link.target) ? link.target.id : link.target;
307
+ const sourceId = getNodeId(link.source);
308
+ const targetId = getNodeId(link.target);
107
309
  return validIds.has(sourceId.toString()) && validIds.has(targetId.toString());
108
310
  });
109
- const allNodeIds = Array.from(validIds);
110
- const subNodes = allNodeIds.map((id) => nodesById[id]).filter(Boolean);
111
- return {
311
+ const subNodes = Array.from(validIds).map((id) => nodes.maps.id.get(id)).filter(Boolean);
312
+ return newGraphData({
112
313
  nodes: subNodes,
113
314
  links: filteredLinks
114
- };
115
- }
116
-
117
- //#endregion
118
- //#region src/util/img.ts
119
- function loadHTMLImageElement(src) {
120
- return new Promise((resolve, reject) => {
121
- const img$1 = new Image();
122
- img$1.onload = () => resolve(img$1);
123
- img$1.onerror = reject;
124
- img$1.src = src;
125
315
  });
126
316
  }
127
317
 
128
318
  //#endregion
129
- //#region src/sim/TransformCanvas.ts
130
- const MIN_ZOOM = .1;
131
- const MAX_ZOOM = 10;
132
- const CLICK_THRESHOLD = 5;
133
- const ANIMATION_SPEED = .07;
134
- const DRAG_ANIMATION_SPEED = .5;
135
- const ANIMATION_THRESHOLD = {
136
- x: .5,
137
- y: .5,
138
- scale: .001
139
- };
140
- const MOMENTUM_DAMPING = .91;
141
- const MIN_VELOCITY = .5;
142
- var TransformCanvas = class {
143
- canvas;
144
- transform = {
145
- x: 0,
146
- y: 0,
147
- scale: 1
148
- };
149
- targetTransform = {
150
- x: 0,
151
- y: 0,
152
- scale: 1
153
- };
154
- isAnimating = false;
155
- animationFrame = null;
156
- focus = null;
157
- offset = {
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 = {
158
691
  x: 0,
159
692
  y: 0
160
693
  };
@@ -302,8 +835,7 @@ var TransformCanvas = class {
302
835
  y: this.lerp(prev.y, target.y, animationSpeed / this.dpr),
303
836
  scale: this.lerp(prev.scale, target.scale, animationSpeed / this.dpr)
304
837
  };
305
- const done = 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;
306
- if (done) {
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) {
307
839
  this.transform = { ...target };
308
840
  this.stopAnimation();
309
841
  } else {
@@ -493,8 +1025,7 @@ var TransformCanvas = class {
493
1025
  };
494
1026
  }
495
1027
  const [t1, t2] = Array.from(e.touches);
496
- const dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
497
- let newScale = dist / this.pinchStartDist * this.pinchStartScale;
1028
+ let newScale = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY) / this.pinchStartDist * this.pinchStartScale;
498
1029
  newScale = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newScale));
499
1030
  const centerX = (t1.clientX + t2.clientX) / 2;
500
1031
  const centerY = (t1.clientY + t2.clientY) / 2;
@@ -591,8 +1122,7 @@ var TransformCanvas = class {
591
1122
  this.focus = () => {
592
1123
  const worldFocus = _focus();
593
1124
  if (!worldFocus) return null;
594
- const transform = this.getTransformationFromWorld(worldFocus.x, worldFocus.y, worldFocus.scale);
595
- return transform;
1125
+ return this.getTransformationFromWorld(worldFocus.x, worldFocus.y, worldFocus.scale);
596
1126
  };
597
1127
  this.startAnimation();
598
1128
  }
@@ -606,404 +1136,78 @@ var TransformCanvas = class {
606
1136
  cancelAnimationFrame(this.momentumFrame);
607
1137
  this.momentumFrame = null;
608
1138
  }
609
- this.canvas.removeEventListener("wheel", this.handleWheel);
610
- this.canvas.removeEventListener("mousedown", this.handleMouseDown);
611
- this.canvas.removeEventListener("click", this.handleCanvasClick);
612
- this.canvas.removeEventListener("touchstart", this.handleTouchStart);
613
- this.canvas.removeEventListener("touchmove", this.handleTouchMove);
614
- this.canvas.removeEventListener("touchend", this.handleTouchEnd);
615
- this.canvas.removeEventListener("touchcancel", this.handleTouchEnd);
616
- window.removeEventListener("mousemove", this.handleMouseMove);
617
- window.removeEventListener("mouseup", this.handleMouseUp);
618
- if (this.resizeObserver) {
619
- this.resizeObserver.disconnect();
620
- this.resizeObserver = null;
621
- }
622
- if (this.mediaQueryList) {
623
- const updateDPRFromMediaQuery = () => {
624
- this.updateDPR();
625
- };
626
- if (this.mediaQueryList.removeEventListener) this.mediaQueryList.removeEventListener("change", updateDPRFromMediaQuery);
627
- this.mediaQueryList = null;
628
- }
629
- }
630
- getTransform() {
631
- return { ...this.transform };
632
- }
633
- getTargetTransform() {
634
- return { ...this.targetTransform };
635
- }
636
- getFocus() {
637
- return this.focus ? { ...this.focus } : null;
638
- }
639
- setOffset(offset) {
640
- this.offset = offset;
641
- }
642
- };
643
-
644
- //#endregion
645
- //#region src/util/canvas.ts
646
- /**
647
- * draws a circle on the canvas
648
- * @param ctx - The canvas rendering context
649
- * @param x - The x-coordinate of the circle's center
650
- * @param y - The y-coordinate of the circle's center
651
- * @param radius - The radius of the circle (default is 5)
652
- * @param options - Optional parameters for styling the circle
653
- * @param options.fill - Whether to fill the circle (default is true)
654
- * @param options.fillStyle - The fill color of the circle
655
- * @param options.stroke - Whether to stroke the circle (default is false)
656
- * @param options.strokeStyle - The stroke color of the circle
657
- * @param options.lineWidth - The width of the stroke (default is 0.2)
658
- * @returns void
659
- */
660
- function circle(ctx, x, y, radius = 5, options) {
661
- const { fill = true, fillStyle, stroke = false, strokeStyle, lineWidth = .2 } = options || {};
662
- ctx.save();
663
- if (fillStyle !== void 0) ctx.fillStyle = fillStyle;
664
- if (strokeStyle !== void 0) ctx.strokeStyle = strokeStyle;
665
- if (lineWidth !== void 0) ctx.lineWidth = lineWidth;
666
- ctx.beginPath();
667
- ctx.arc(x, y, radius, 0, 2 * Math.PI);
668
- ctx.closePath();
669
- if (fill) ctx.fill();
670
- if (stroke) ctx.stroke();
671
- ctx.restore();
672
- }
673
- /**
674
- * draws a rectangle on the canvas
675
- * @param ctx - The canvas rendering context
676
- * @param x - The x-coordinate of the rectangle's top-left corner
677
- * @param y - The y-coordinate of the rectangle's top-left corner
678
- * @param width - The width of the rectangle
679
- * @param height - The height of the rectangle
680
- * @param options - Optional parameters for styling the rectangle
681
- * @param options.fill - Whether to fill the rectangle (default is true)
682
- * @param options.fillStyle - The fill color of the rectangle
683
- * @param options.stroke - Whether to stroke the rectangle (default is false)
684
- * @param options.strokeStyle - The stroke color of the rectangle
685
- * @param options.lineWidth - The width of the stroke (default is 0.2)
686
- * @param options.borderRadius - The radius of the corners (default is 0)
687
- * @returns void
688
- */
689
- function rect(ctx, x, y, width, height, options) {
690
- const { fill = true, fillStyle, stroke = false, strokeStyle, lineWidth = .2, borderRadius = 0 } = options || {};
691
- ctx.save();
692
- if (fillStyle !== void 0) ctx.fillStyle = fillStyle;
693
- if (strokeStyle !== void 0) ctx.strokeStyle = strokeStyle;
694
- if (lineWidth !== void 0) ctx.lineWidth = lineWidth;
695
- const r = Math.min(borderRadius, width / 2, height / 2);
696
- ctx.beginPath();
697
- if (r > 0) {
698
- ctx.moveTo(x + r, y);
699
- ctx.lineTo(x + width - r, y);
700
- ctx.quadraticCurveTo(x + width, y, x + width, y + r);
701
- ctx.lineTo(x + width, y + height - r);
702
- ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
703
- ctx.lineTo(x + r, y + height);
704
- ctx.quadraticCurveTo(x, y + height, x, y + height - r);
705
- ctx.lineTo(x, y + r);
706
- ctx.quadraticCurveTo(x, y, x + r, y);
707
- } else ctx.rect(x, y, width, height);
708
- ctx.closePath();
709
- if (fill) ctx.fill();
710
- if (stroke) ctx.stroke();
711
- ctx.restore();
712
- }
713
- /**
714
- * draws an image on the canvas with optional border radius and opacity
715
- * @param ctx - The canvas rendering context
716
- * @param image - The HTMLImageElement to draw
717
- * @param x - The x-coordinate of the image's top-left corner
718
- * @param y - The y-coordinate of the image's top-left corner
719
- * @param width - The width of the image
720
- * @param height - The height of the image
721
- * @param borderRadius - The radius of the corners (default is 0)
722
- * @param opacity - The opacity of the image (default is 1.0)
723
- * @param bgColor - Optional background color to fill the clipped area
724
- * @returns void
725
- */
726
- function img(ctx, image, x, y, width, height, borderRadius = 0, opacity = 1, bgColor) {
727
- ctx.save();
728
- ctx.beginPath();
729
- ctx.globalAlpha = 1;
730
- if (borderRadius > 0) {
731
- const r = Math.min(borderRadius, width / 2, height / 2);
732
- ctx.moveTo(x + r, y);
733
- ctx.lineTo(x + width - r, y);
734
- ctx.quadraticCurveTo(x + width, y, x + width, y + r);
735
- ctx.lineTo(x + width, y + height - r);
736
- ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
737
- ctx.lineTo(x + r, y + height);
738
- ctx.quadraticCurveTo(x, y + height, x, y + height - r);
739
- ctx.lineTo(x, y + r);
740
- ctx.quadraticCurveTo(x, y, x + r, y);
741
- } else ctx.rect(x, y, width, height);
742
- ctx.closePath();
743
- if (bgColor && opacity < 1) {
744
- ctx.save();
745
- ctx.fillStyle = bgColor;
746
- ctx.fill();
747
- ctx.restore();
748
- }
749
- ctx.clip();
750
- ctx.globalAlpha = opacity;
751
- ctx.drawImage(image, x, y, width, height);
752
- ctx.restore();
753
- }
754
- function hexagon(ctx, x, y, radius, options) {
755
- const { fill = true, fillStyle, stroke = false, strokeStyle, lineWidth = .2, rotation = 0, borderRadius = 0 } = options || {};
756
- ctx.save();
757
- if (fillStyle !== void 0) ctx.fillStyle = fillStyle;
758
- if (strokeStyle !== void 0) ctx.strokeStyle = strokeStyle;
759
- if (lineWidth !== void 0) ctx.lineWidth = lineWidth;
760
- const sides = 6;
761
- const angleStep = Math.PI * 2 / sides;
762
- ctx.beginPath();
763
- const points = [];
764
- for (let i = 0; i < sides; i++) {
765
- const angle = rotation + i * angleStep;
766
- points.push({
767
- x: x + radius * Math.cos(angle),
768
- y: y + radius * Math.sin(angle)
769
- });
770
- }
771
- if (borderRadius > 0) {
772
- const maxBorderRadius = Math.min(borderRadius, radius / 3);
773
- for (let i = 0; i < sides; i++) {
774
- const current = points[i];
775
- const next = points[(i + 1) % sides];
776
- const prev = points[(i - 1 + sides) % sides];
777
- const toPrev = {
778
- x: prev.x - current.x,
779
- y: prev.y - current.y
780
- };
781
- const toNext = {
782
- x: next.x - current.x,
783
- y: next.y - current.y
784
- };
785
- const lenPrev = Math.sqrt(toPrev.x * toPrev.x + toPrev.y * toPrev.y);
786
- const lenNext = Math.sqrt(toNext.x * toNext.x + toNext.y * toNext.y);
787
- const normPrev = {
788
- x: toPrev.x / lenPrev,
789
- y: toPrev.y / lenPrev
790
- };
791
- const normNext = {
792
- x: toNext.x / lenNext,
793
- y: toNext.y / lenNext
794
- };
795
- const cpPrev = {
796
- x: current.x + normPrev.x * maxBorderRadius,
797
- y: current.y + normPrev.y * maxBorderRadius
798
- };
799
- const cpNext = {
800
- x: current.x + normNext.x * maxBorderRadius,
801
- y: current.y + normNext.y * maxBorderRadius
802
- };
803
- if (i === 0) ctx.moveTo(cpPrev.x, cpPrev.y);
804
- else ctx.lineTo(cpPrev.x, cpPrev.y);
805
- ctx.quadraticCurveTo(current.x, current.y, cpNext.x, cpNext.y);
806
- }
807
- } else {
808
- ctx.moveTo(points[0].x, points[0].y);
809
- for (let i = 1; i < sides; i++) ctx.lineTo(points[i].x, points[i].y);
810
- }
811
- ctx.closePath();
812
- if (fill) ctx.fill();
813
- if (stroke) ctx.stroke();
814
- ctx.restore();
815
- }
816
-
817
- //#endregion
818
- //#region src/util/color.ts
819
- /**
820
- * Some utility functions to handle colors
821
- */
822
- function color(rgb) {
823
- const colorHandler = function(arg) {
824
- if (typeof arg === "number") return `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${arg})`;
825
- if (typeof arg === "function") {
826
- const transformedRGB = arg(rgb);
827
- return color(transformedRGB);
828
- }
829
- return `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
830
- };
831
- colorHandler.rgb = rgb;
832
- return colorHandler;
833
- }
834
- /**
835
- * Dims a color to white or black
836
- * @param color An array of 3 numbers representing RGB values (0-255)
837
- * @param dimFactor A value between 0 and 1 where 0 is completely black and 1 is the original color
838
- * @returns A new array with the dimmed RGB values
839
- */
840
- function dim(factor = .6, white = true) {
841
- const base = white ? 255 : 0;
842
- return function(rgb) {
843
- return [
844
- Math.round(rgb[0] + (base - rgb[0]) * (1 - factor)),
845
- Math.round(rgb[1] + (base - rgb[1]) * (1 - factor)),
846
- Math.round(rgb[2] + (base - rgb[2]) * (1 - factor))
847
- ];
848
- };
849
- }
850
-
851
- //#endregion
852
- //#region src/util/data.ts
853
- const images = [];
854
- function generateTree(maxNodes, maxChildren) {
855
- const nodes = [];
856
- const links = [];
857
- let index$1 = 0;
858
- function createNode(label) {
859
- const isRoot = label === VOID_ROOT_ID;
860
- const idx = isRoot ? VOID_ROOT_ID : index$1++;
861
- return {
862
- id: idx.toString(),
863
- label,
864
- imgSrc: isRoot ? void 0 : images[index$1 % images.length]
865
- };
866
- }
867
- const root = createNode(VOID_ROOT_ID);
868
- nodes.push(root);
869
- const queue = [root];
870
- while (queue.length > 0 && nodes.length < maxNodes) {
871
- const parent = queue.shift();
872
- const rand = Math.random();
873
- const biased = Math.floor(Math.pow(rand, 2) * (maxChildren + 1));
874
- const childrenCount = Math.min(biased, maxChildren);
875
- for (let i = 0; i < childrenCount; i++) {
876
- if (nodes.length >= maxNodes) break;
877
- const child = createNode(`Node ${nodes.length}`);
878
- nodes.push(child);
879
- links.push({
880
- source: parent.id,
881
- target: child.id
882
- });
883
- queue.push(child);
884
- }
885
- }
886
- return {
887
- nodes,
888
- links
889
- };
890
- }
891
- function getPrunedData(startId, nodes, links, highlights = []) {
892
- const nodesById = Object.fromEntries(nodes.map((node) => [node.id, node]));
893
- const visibleNodes = [];
894
- const visibleLinks = [];
895
- const visited = new Set();
896
- (function traverseTree(node = nodesById[startId]) {
897
- if (!node || visited.has(node.id)) return;
898
- visited.add(node.id);
899
- if (node.status === "LIQUIDATED" && !highlights.find((h) => h.id === node.id)) return;
900
- visibleNodes.push(node);
901
- if (node?.state?.collapsed) return;
902
- const childLinks = links.filter((l) => (isSimNode(l.source) ? l.source.id : l.source) === node.id);
903
- for (const link of childLinks) {
904
- const targetNode = isSimNode(link.target) ? link.target : nodesById[link.target.toString()];
905
- if (targetNode?.status === "LIQUIDATED" && !highlights.find((h) => h.id === targetNode.id)) continue;
906
- visibleLinks.push(link);
907
- traverseTree(targetNode);
908
- }
909
- })();
910
- return {
911
- nodes: visibleNodes,
912
- links: visibleLinks
913
- };
914
- }
915
- /**
916
- * Automatically identifies root nodes and builds a nested structure
917
- * @param nodes Array of raw nodes
918
- * @param links Array of links between nodes
919
- * @returns Array of nested nodes starting from identified roots
920
- */
921
- function buildTreeFromGraphData(nodes, links) {
922
- const nodeMap = new Map();
923
- nodes.forEach((node) => nodeMap.set(node.id, node));
924
- const childrenMap = new Map();
925
- const parentMap = new Map();
926
- nodes.forEach((node) => {
927
- childrenMap.set(node.id, []);
928
- parentMap.set(node.id, []);
929
- });
930
- links.forEach((link) => {
931
- if (nodeMap.has(link.source) && nodeMap.has(link.target)) {
932
- const children = childrenMap.get(link.source) || [];
933
- if (!children.includes(link.target)) {
934
- children.push(link.target);
935
- childrenMap.set(link.source, children);
936
- }
937
- const parents = parentMap.get(link.target) || [];
938
- if (!parents.includes(link.source)) {
939
- parents.push(link.source);
940
- parentMap.set(link.target, parents);
941
- }
942
- }
943
- });
944
- const rootHashes = [];
945
- nodeMap.forEach((_, hash) => {
946
- const parents = parentMap.get(hash) || [];
947
- if (parents.length === 0) rootHashes.push(hash);
948
- });
949
- const buildNode = (hash, visited = new Set()) => {
950
- if (visited.has(hash)) return null;
951
- visited.add(hash);
952
- const node = nodeMap.get(hash);
953
- if (!node) return null;
954
- const childHashes = childrenMap.get(hash) || [];
955
- const nestedChildren = [];
956
- childHashes.forEach((childHash) => {
957
- const childNode = buildNode(childHash, new Set([...visited]));
958
- if (childNode) nestedChildren.push(childNode);
959
- });
960
- return {
961
- ...node,
962
- children: nestedChildren
963
- };
964
- };
965
- const result = [];
966
- rootHashes.forEach((rootHash) => {
967
- const nestedRoot = buildNode(rootHash);
968
- if (nestedRoot) result.push(nestedRoot);
969
- });
970
- const processedNodes = new Set();
971
- const markProcessed = (node) => {
972
- processedNodes.add(node.id);
973
- node.children.forEach(markProcessed);
974
- };
975
- result.forEach(markProcessed);
976
- nodeMap.forEach((_, hash) => {
977
- if (!processedNodes.has(hash)) {
978
- const orphanedRoot = buildNode(hash);
979
- if (orphanedRoot) result.push(orphanedRoot);
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;
980
1158
  }
981
- });
982
- return result;
983
- }
984
- /**
985
- * Recursively retrieves all parents of a node from a graph data structure
986
- * @param {string} nodeHash - The hash of the node to find parents for
987
- * @param {RawNode[]} nodes - Array of nodes in the graph
988
- * @param {RawLink[]} links - Array of links connecting the nodes
989
- * @returns {RawNode[]} - Array of parent nodes
990
- */
991
- function searchParents(nodeHash, nodes, links) {
992
- const visited = new Set();
993
- function findParents(hash) {
994
- if (visited.has(hash)) return [];
995
- visited.add(hash);
996
- const immediateParents = links.filter((link) => link.target === hash).map((link) => link.source);
997
- const parentNodes = nodes.filter((node) => immediateParents.includes(node.id));
998
- const ancestorNodes = immediateParents.flatMap((parentHash) => findParents(parentHash));
999
- return [...parentNodes, ...ancestorNodes];
1000
1159
  }
1001
- return findParents(nodeHash);
1002
- }
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
+ };
1003
1176
 
1004
1177
  //#endregion
1005
- //#region src/sim/_interfaces.ts
1006
- var OpenGraphEventEmitter = class extends EventEmitter {};
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
+ }
1007
1211
 
1008
1212
  //#endregion
1009
1213
  //#region src/util/math.ts
@@ -1017,60 +1221,6 @@ function getAngle(cx, cy, x, y) {
1017
1221
  return Math.atan2(y - cy, x - cx);
1018
1222
  }
1019
1223
 
1020
- //#endregion
1021
- //#region src/util/highlights.ts
1022
- const red = [
1023
- 238,
1024
- 125,
1025
- 121
1026
- ];
1027
- const redred = [
1028
- 255,
1029
- 0,
1030
- 0
1031
- ];
1032
- var Highlight = class {
1033
- static owner = (id) => {
1034
- return {
1035
- id,
1036
- strokeColor: red
1037
- };
1038
- };
1039
- static primary = (id) => {
1040
- return {
1041
- id,
1042
- linkTo: id,
1043
- scale: 4,
1044
- strokeColor: redred,
1045
- linkColor: redred,
1046
- onTop: true,
1047
- isDetached: true
1048
- };
1049
- };
1050
- static evolved = (id) => {
1051
- return {
1052
- id,
1053
- linkTo: id,
1054
- scale: 2.1,
1055
- strokeColor: redred,
1056
- linkColor: redred,
1057
- onTop: true,
1058
- isDetached: true
1059
- };
1060
- };
1061
- static minted = (id) => {
1062
- return {
1063
- id,
1064
- linkTo: id,
1065
- scale: 1.5,
1066
- strokeColor: redred,
1067
- linkColor: redred,
1068
- isDetached: true,
1069
- onTop: true
1070
- };
1071
- };
1072
- };
1073
-
1074
1224
  //#endregion
1075
1225
  //#region src/sim/asymmetric-link.ts
1076
1226
  function constant(x) {
@@ -1186,12 +1336,64 @@ function asymmetricLinks(links) {
1186
1336
  //#endregion
1187
1337
  //#region src/util/hash.ts
1188
1338
  function quickHash(data) {
1189
- return float2hex(xorshiftString(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);
1190
1392
  }
1191
1393
 
1192
1394
  //#endregion
1193
1395
  //#region src/sim/OpenGraphSimulation.ts
1194
- const RADIAL_FORCES = false;
1396
+ const RENDER_EMITTER_NODES = false;
1195
1397
  const INITIAL_RADIUS = 300;
1196
1398
  const INCREMENTAL = 200;
1197
1399
  function getRadius(depth) {
@@ -1206,39 +1408,37 @@ var OpenGraphSimulation = class {
1206
1408
  canvas;
1207
1409
  transformCanvas;
1208
1410
  theme;
1411
+ groupRootOrphans = false;
1209
1412
  rawData;
1210
1413
  emitter;
1211
1414
  translate = {
1212
1415
  x: 0,
1213
1416
  y: 0
1214
1417
  };
1215
- data = {
1216
- nodes: [],
1217
- links: []
1218
- };
1219
- prunedData = {
1220
- nodes: [],
1221
- links: []
1222
- };
1223
- subGraph = {
1224
- nodes: [],
1225
- links: []
1226
- };
1418
+ data = newGraphData();
1419
+ prunedData = newGraphData();
1420
+ subGraph = newGraphData();
1227
1421
  rootId = "";
1228
1422
  simulation = null;
1229
1423
  clusterSizeRange = [0, 1];
1424
+ emitterClusterSizeRange = [0, 1];
1230
1425
  maxDepth = 0;
1231
1426
  isTicking = false;
1427
+ isDrawing = false;
1232
1428
  tickCount = 0;
1233
1429
  loadNodeImage;
1234
- imageCache = new Map();
1430
+ imageCache = /* @__PURE__ */ new Map();
1235
1431
  rootImages = [];
1236
1432
  hideThumbnails = false;
1237
1433
  noInteraction = false;
1238
1434
  lockedNodeId;
1239
1435
  selectedNode = null;
1240
1436
  hoveredNode = null;
1241
- highlights = [];
1437
+ highlights;
1438
+ nodeVisibility = "all";
1439
+ secondaryNodes = /* @__PURE__ */ new Map();
1440
+ emittedNodes = [];
1441
+ openedGroups = [];
1242
1442
  renderLayers = {
1243
1443
  links: {
1244
1444
  regular: [],
@@ -1274,6 +1474,18 @@ var OpenGraphSimulation = class {
1274
1474
  this.rootImages[idx] = img$1;
1275
1475
  });
1276
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 || []);
1277
1489
  }
1278
1490
  get center() {
1279
1491
  return {
@@ -1289,8 +1501,7 @@ var OpenGraphSimulation = class {
1289
1501
  };
1290
1502
  }
1291
1503
  getNodeAtPosition = (cx, cy) => {
1292
- const transform = this.transformCanvas.getTransform();
1293
- const { x: tx, y: ty, scale } = transform;
1504
+ const { x: tx, y: ty, scale } = this.transformCanvas.getTransform();
1294
1505
  const dpi = devicePixelRatio || 1;
1295
1506
  const canvasX = cx * dpi;
1296
1507
  const canvasY = cy * dpi;
@@ -1299,13 +1510,13 @@ var OpenGraphSimulation = class {
1299
1510
  const graphX = scaledX - this.origin.x;
1300
1511
  const graphY = scaledY - this.origin.y;
1301
1512
  const candidates = [];
1302
- for (let node of this.data.nodes) {
1513
+ for (let node of this.prunedData.nodes.values) {
1303
1514
  const r = this.getNodeSize(node.id) / 2;
1304
1515
  if (node.x == null || node.y == null) continue;
1305
1516
  const dx = node.x - graphX;
1306
1517
  const dy = node.y - graphY;
1307
1518
  if (dx * dx + dy * dy < r * r) {
1308
- if (!this.prunedData.nodes.find((n) => n.id === node.id)) continue;
1519
+ if (!this.prunedData.nodes.maps.id.get(node.id)) continue;
1309
1520
  candidates.push(node);
1310
1521
  }
1311
1522
  }
@@ -1339,8 +1550,7 @@ var OpenGraphSimulation = class {
1339
1550
  };
1340
1551
  };
1341
1552
  screenToWorld(_x, _y) {
1342
- const transform = this.transformCanvas.getTransform();
1343
- const { x: tx, y: ty, scale } = transform;
1553
+ const { x: tx, y: ty, scale } = this.transformCanvas.getTransform();
1344
1554
  const dpi = devicePixelRatio || 1;
1345
1555
  const canvasX = _x * dpi;
1346
1556
  const canvasY = _y * dpi;
@@ -1354,8 +1564,7 @@ var OpenGraphSimulation = class {
1354
1564
  };
1355
1565
  }
1356
1566
  worldToScreen(worldX, worldY) {
1357
- const transform = this.transformCanvas.getTransform();
1358
- const { x: tx, y: ty, scale } = transform;
1567
+ const { x: tx, y: ty, scale } = this.transformCanvas.getTransform();
1359
1568
  const dpi = devicePixelRatio || 1;
1360
1569
  const scaledX = (worldX + this.origin.x) * scale;
1361
1570
  const scaledY = (worldY + this.origin.y) * scale;
@@ -1371,34 +1580,69 @@ var OpenGraphSimulation = class {
1371
1580
  handleClick = (x, y) => {
1372
1581
  let node = this.getNodeAtPosition(x, y);
1373
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
+ }
1374
1595
  if (this.lockedNodeId && !node) node = this.getNodeById(this.lockedNodeId);
1375
- this.handleClickNode(node);
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();
1376
1622
  };
1377
1623
  handleClickNode = (node, options = {
1378
1624
  noToggle: false,
1379
- triggerFocus: false
1625
+ triggerFocus: false,
1626
+ triggerRestart: false
1380
1627
  }) => {
1381
1628
  let wasOpened = false;
1382
1629
  if (node) {
1383
1630
  if (node.id === this.rootId) {
1384
1631
  this.selectedNode = null;
1385
1632
  this.emitter.emit("selected-node-changed", null);
1386
- this.subGraph = {
1387
- nodes: [],
1388
- links: []
1389
- };
1633
+ this.subGraph = newGraphData();
1390
1634
  this.updateRenderLayers();
1391
1635
  this.updateScene();
1392
1636
  return;
1393
1637
  }
1394
1638
  if (node.state) {
1395
1639
  const children = getChildren(node.id, this.data.links);
1396
- if (children.length > 0 && !options?.noToggle) {
1640
+ if (children.length > 0) {
1397
1641
  if (this.selectedNode?.id !== node.id) {
1398
1642
  if (node.state.collapsed) wasOpened = true;
1399
1643
  node.state.collapsed = false;
1400
- } else node.state.collapsed = !node.state.collapsed;
1401
- if (!node.state.collapsed) {
1644
+ } else if (!options.noToggle) node.state.collapsed = !node.state.collapsed;
1645
+ if (!node.state.collapsed && wasOpened) {
1402
1646
  const clusterDistance = 100;
1403
1647
  const clusterRadius = 50;
1404
1648
  const parentX = node.x || this.center.x;
@@ -1408,36 +1652,24 @@ var OpenGraphSimulation = class {
1408
1652
  const length = Math.sqrt(dirX * dirX + dirY * dirY) || 1;
1409
1653
  const normX = dirX / length;
1410
1654
  const normY = dirY / length;
1411
- const clusterX = parentX + normX * clusterDistance;
1412
- const clusterY = parentY + normY * clusterDistance;
1655
+ parentX + normX * clusterDistance;
1656
+ parentY + normY * clusterDistance;
1413
1657
  children.forEach((childId) => {
1414
- const childNode = this.data.nodes.find((n) => n.id === childId);
1658
+ const childNode = this.data.nodes.maps.id.get(childId);
1659
+ if (childNode?.state?.groupNode) return;
1415
1660
  if (childNode && isSimNode(childNode)) {
1416
- const angle = Math.random() * 2 * Math.PI;
1417
- const radius = Math.random() * clusterRadius;
1418
- childNode.x = clusterX + Math.cos(angle) * radius;
1419
- childNode.y = clusterY + Math.sin(angle) * radius;
1661
+ Math.random() * 2 * Math.PI;
1662
+ Math.random() * clusterRadius;
1420
1663
  }
1421
1664
  });
1422
1665
  }
1423
1666
  }
1424
1667
  }
1425
- if (!node.state?.collapsed) {
1426
- const parents = getParents(node.id, this.data.links);
1427
- parents.forEach((parentId) => {
1428
- const parentNode = this.data.nodes.find((n) => n.id === parentId);
1429
- if (parentNode && isSimNode(parentNode) && parentNode.state) parentNode.state.collapsed = false;
1430
- });
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);
1431
1672
  }
1432
- this.subGraph = getNodeSubgraph(node.id, this.data.nodes, this.data.links, this.rootId);
1433
- }
1434
- if (this.selectedNode?.id !== node?.id) {
1435
- this.selectedNode = node;
1436
- this.emitter.emit("selected-node-changed", node);
1437
- this.updateRenderLayers();
1438
- }
1439
- if (node) {
1440
- this.restart(wasOpened ? .05 : 0);
1441
1673
  if (wasOpened || options?.triggerFocus) this.transformCanvas.focusOn(() => {
1442
1674
  const t = this.transformCanvas.getTransform();
1443
1675
  const _node = this.getNodeById(node.id);
@@ -1450,19 +1682,26 @@ var OpenGraphSimulation = class {
1450
1682
  } else if (!node && this.selectedNode) {
1451
1683
  this.selectedNode = null;
1452
1684
  this.emitter.emit("selected-node-changed", null);
1453
- this.subGraph = {
1454
- nodes: [],
1455
- links: []
1456
- };
1685
+ this.subGraph = newGraphData();
1457
1686
  this.updateRenderLayers();
1458
1687
  this.updateScene();
1459
1688
  }
1460
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
+ }
1461
1699
  updateScene = () => {
1462
1700
  if (this.isTicking) return;
1463
1701
  this.onDraw();
1464
1702
  };
1465
1703
  handleMove = (x, y) => {
1704
+ if (this.transformCanvas.getIsDragging()) return;
1466
1705
  const world = this.screenToWorld(x, y);
1467
1706
  const node = this.simulation?.find(world.x, world.y, 10) || null;
1468
1707
  if (node?.state?.sessionNode) return;
@@ -1477,15 +1716,13 @@ var OpenGraphSimulation = class {
1477
1716
  this.updateScene();
1478
1717
  };
1479
1718
  updateHighlights = () => {
1480
- const detachedHighlights = this.highlights.filter((h) => h.isDetached);
1481
- const validSessionIds = new Set();
1719
+ const detachedHighlights = this.highlights.maps.detached.get("true") || [];
1720
+ const validSessionIds = /* @__PURE__ */ new Set();
1482
1721
  let focusSessionNode = null;
1483
1722
  detachedHighlights.forEach((h) => {
1484
- const highlightedNode = this.data.nodes.find((n) => n.id === h.id);
1723
+ const highlightedNode = this.data.nodes.maps.id.get(h.id);
1485
1724
  if (!highlightedNode) return;
1486
- const existingLink = this.data.links.find((l) => {
1487
- return isSimNode(l.target) ? l.target.id === h.id : l.target === h.id;
1488
- });
1725
+ const existingLink = this.data.links.maps.targetId.get(h.id);
1489
1726
  if (!existingLink) return;
1490
1727
  const id = isSimNode(existingLink.source) ? existingLink.source.id : existingLink.source.toString();
1491
1728
  const parentNode = this.getNodeById(id);
@@ -1493,11 +1730,11 @@ var OpenGraphSimulation = class {
1493
1730
  const _sessionId = h.sessionId || parentNode.id;
1494
1731
  const sessionId = parentNode.state?.sessionNode ? parentNode.id : `${VOID_DETACH_ID}-${_sessionId}`;
1495
1732
  validSessionIds.add(sessionId);
1496
- let sessionNode = this.data.nodes.find((n) => n.id === sessionId);
1733
+ let sessionNode = this.data.nodes.maps.id.get(sessionId);
1497
1734
  if (!sessionNode) {
1498
1735
  const depth = (highlightedNode?.depth || 1) + 1;
1499
- const angle = getAngle(this.center.x, this.center.y, parentNode?.x, parentNode?.y);
1500
- const circlePos = getRadialPoint(getRadius(depth), this.center.x, this.center.y, angle);
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);
1501
1738
  sessionNode = {
1502
1739
  id: sessionId,
1503
1740
  state: {
@@ -1516,37 +1753,50 @@ var OpenGraphSimulation = class {
1516
1753
  source: parentNode
1517
1754
  });
1518
1755
  }
1519
- const existingLinkIndex = this.data.links.findIndex((l) => l === existingLink);
1756
+ const existingLinkIndex = this.data.links.values.findIndex((l) => l === existingLink);
1520
1757
  this.data.links.splice(existingLinkIndex, 1);
1521
1758
  this.data.links.push({
1522
1759
  target: highlightedNode,
1523
1760
  source: sessionNode
1524
1761
  });
1525
- const parents = getAllParentsUntil(highlightedNode.id, this.data.links, this.rootId);
1526
- parents.forEach((parentId) => {
1527
- const node = this.data.nodes.find((n) => n.id === parentId);
1528
- if (node?.state) node.state.collapsed = false;
1762
+ getAllParentsUntil(highlightedNode.id, this.data.links, this.rootId).forEach((parentId) => {
1763
+ if (this.data.nodes.maps.id.get(parentId)?.state) {}
1529
1764
  });
1530
1765
  if (highlightedNode.state) highlightedNode.state.collapsed = false;
1531
1766
  if (!focusSessionNode) focusSessionNode = sessionNode;
1532
1767
  });
1533
- const sessionNodesToRemove = this.data.nodes.filter((n) => n.state?.sessionNode && !validSessionIds.has(n.id));
1534
- sessionNodesToRemove.forEach((sessionNode) => {
1535
- const childLinks = this.data.links.filter((l) => isSimNode(l.source) && l.source.id === sessionNode.id);
1536
- const incomingLink = this.data.links.find((l) => isSimNode(l.target) && l.target.id === sessionNode.id);
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);
1537
1771
  const parentNode = incomingLink ? isSimNode(incomingLink.source) ? incomingLink.source : this.getNodeById(incomingLink.source.toString()) : null;
1538
- this.data.links = this.data.links.filter((l) => !(isSimNode(l.source) && l.source.id === sessionNode.id) && !(isSimNode(l.target) && l.target.id === sessionNode.id));
1539
- if (parentNode) childLinks.forEach((link) => {
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) => {
1540
1774
  const targetNode = isSimNode(link.target) ? link.target : this.getNodeById(link.target.toString());
1541
1775
  if (targetNode) this.data.links.push({
1542
1776
  source: parentNode,
1543
1777
  target: targetNode
1544
1778
  });
1545
1779
  });
1546
- const index$1 = this.data.nodes.findIndex((n) => n.id === sessionNode.id);
1780
+ const index$1 = this.data.nodes.values.findIndex((n) => n.id === sessionNode.id);
1547
1781
  if (index$1 !== -1) this.data.nodes.splice(index$1, 1);
1548
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);
1549
1798
  if (this.selectedNode) this.subGraph = getNodeSubgraph(this.selectedNode.id, this.data.nodes, this.data.links, this.rootId);
1799
+ this.updateEmitterNodes();
1550
1800
  this.restart();
1551
1801
  if (focusSessionNode) this.transformCanvas.focusOn(() => {
1552
1802
  if (!focusSessionNode) return null;
@@ -1559,24 +1809,44 @@ var OpenGraphSimulation = class {
1559
1809
  scale: t.scale
1560
1810
  };
1561
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();
1562
1827
  };
1563
1828
  initialize = (data, rootId) => {
1829
+ Measure.start("initialize");
1830
+ this.emittedNodes = [];
1564
1831
  this.rawData = data;
1565
1832
  this.rootId = rootId;
1566
- const _links = data.links.map((l) => ({ ...l }));
1567
- const _nodes = data.nodes.map((n) => {
1568
- const existingData = this.data.nodes.find((x$1) => x$1.id === n.id);
1569
- const parents = getParents(n.id, _links);
1570
- const parentNode = this.data.nodes.find((p) => p.id === parents[0]);
1571
- const clusterSize = getClusterSize(n.id, _links);
1572
- const depth = getNodeDepth(n.id, _links);
1573
- const circlePos = getRadialPoint(getRadius(depth), this.center.x, this.center.y);
1574
- const x = depth > 0 ? null : existingData?.x || parentNode?.x || circlePos.x;
1575
- const y = depth > 0 ? null : existingData?.y || parentNode?.y || circlePos.y;
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;
1576
1846
  return {
1577
1847
  ...n,
1578
1848
  state: {
1579
- collapsed: hasOnlyLeafs(n.id, _links) && getChildren(n.id, _links).length > 1,
1849
+ collapsed: false,
1580
1850
  ...existingData?.state
1581
1851
  },
1582
1852
  clusterSize,
@@ -1584,44 +1854,39 @@ var OpenGraphSimulation = class {
1584
1854
  x,
1585
1855
  y
1586
1856
  };
1587
- }).sort((a, b) => a.depth - b.depth);
1588
- _nodes.forEach((n, i, arr) => {
1589
- const existingData = _nodes.find((x$1) => x$1.id === n.id);
1590
- const parents = getParents(n.id, _links);
1591
- const parentNode = _nodes.find((p) => p.id === parents[0]);
1592
- const depth = n.depth;
1593
- const parentAngle = depth > 0 ? getAngle(this.center.x, this.center.y, parentNode?.x, parentNode?.y) : void 0;
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);
1594
1863
  const circlePos = getRadialPoint(getRadius(depth), this.center.x, this.center.y, parentAngle);
1595
- const x = depth > 0 ? existingData?.x || circlePos.x : existingData?.x || circlePos.x;
1596
- const y = depth > 0 ? existingData?.y || circlePos.y : existingData?.y || circlePos.y;
1597
- _nodes[i].x = x;
1598
- _nodes[i].y = y;
1864
+ n.x = n.x || circlePos.x;
1865
+ n.y = n.y || circlePos.y;
1599
1866
  });
1600
- if (!data.nodes.find((n) => n.id === this.rootId)) _nodes.push({
1867
+ if (!newData.nodes.maps.id.get(this.rootId)) newData.nodes.push({
1601
1868
  id: this.rootId,
1602
1869
  state: {
1603
1870
  collapsed: false,
1604
- image: void 0
1871
+ image: void 0,
1872
+ rootNode: true
1605
1873
  },
1606
1874
  depth: -1,
1607
1875
  clusterSize: 1,
1608
1876
  x: this.center.x,
1609
1877
  y: this.center.y
1610
1878
  });
1611
- const targetIds = new Set(_links.map((link) => link.target));
1612
- const rootNodes = _nodes.filter((node) => !targetIds.has(node.id));
1613
- for (const node of rootNodes) _links.push({
1879
+ const rootNodes = newData.nodes.values.filter((node) => !newData.links.maps.targetId.get(node.id));
1880
+ for (const node of rootNodes) newData.links.push({
1614
1881
  source: this.rootId,
1615
1882
  target: node.id
1616
1883
  });
1617
- this.maxDepth = Math.max(..._nodes.map((n) => n.depth || 0));
1618
- this.data = {
1619
- nodes: _nodes,
1620
- links: _links
1621
- };
1884
+ this.maxDepth = Math.max(...newData.nodes.values.map((n) => n.depth || 0));
1885
+ this.data = newData;
1622
1886
  this.loadNodeImages();
1623
1887
  this.updateHighlights();
1624
1888
  this.triggerSelected(true);
1889
+ Measure.end("initialize");
1625
1890
  };
1626
1891
  get lockedNode() {
1627
1892
  return this.lockedNodeId ? this.getNodeById(this.lockedNodeId) : null;
@@ -1633,25 +1898,43 @@ var OpenGraphSimulation = class {
1633
1898
  });
1634
1899
  else if (this.lockedNode) this.handleClickNode(this.lockedNode, {
1635
1900
  noToggle: true,
1636
- triggerFocus
1901
+ triggerFocus,
1902
+ triggerRestart: true
1637
1903
  });
1638
1904
  else this.setSelectedNode(null);
1639
1905
  };
1640
- restart = (alpha = .1) => {
1906
+ get groupNodes() {
1907
+ return this.prunedData.nodes.maps.groupNode.get("true");
1908
+ }
1909
+ restart = (alpha = .3) => {
1641
1910
  this.tickCount = 0;
1642
- this.prunedData = getPrunedData(this.rootId, this.data.nodes, this.data.links, this.highlights);
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
+ });
1643
1919
  this.updateRenderLayers();
1644
- this.clusterSizeRange = this.prunedData.nodes.filter((n) => n.state?.collapsed).reduce((acc, node) => [Math.min(acc[0], node.clusterSize || 1), Math.max(acc[1], node.clusterSize || 1)], [Infinity, -Infinity]);
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]);
1645
1921
  if (this.simulation) {
1646
1922
  this.simulation.stop();
1647
1923
  this.simulation.on("tick", null);
1648
1924
  this.simulation.on("end", null);
1649
1925
  }
1650
- this.simulation = forceSimulation(this.prunedData.nodes).alpha(this.simulation ? alpha : .5).force("collide", forceCollide((n) => this.getNodeSize(n.id) / 2)).force("link", asymmetricLinks(this.prunedData.links).id((d) => d.id).distance((l) => {
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) => {
1651
1929
  const size = this.getNodeSize(isSimNode(l.target) ? l.target.id : l.target.toString());
1652
1930
  if (isSimNode(l.target)) {
1653
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
+ }
1654
1936
  if (!state?.collapsed) return size;
1937
+ if (state.groupNode) return 5;
1655
1938
  }
1656
1939
  return size * 3;
1657
1940
  }).strength((l) => {
@@ -1659,27 +1942,27 @@ var OpenGraphSimulation = class {
1659
1942
  })).force("charge", forceManyBody().strength((node) => {
1660
1943
  return -150;
1661
1944
  })).force("center", forceCenter(this.center.x, this.center.y).strength(.1)).restart();
1662
- if (RADIAL_FORCES) for (let i = 0; i < this.maxDepth; i++) {
1663
- const depth = i;
1664
- const r = getRadius(depth);
1665
- const x = this.center.x;
1666
- const y = this.center.y;
1667
- console.log("Adding radial force for depth", depth, "with radius", r, x, y);
1668
- this.simulation.force(`radial-${depth}`, forceRadial(r, x, y).strength((n) => {
1669
- if (n.id === this.rootId) return 0;
1670
- if (n.depth === 0) return 0;
1671
- if (n.depth === depth) return .01;
1672
- return 0;
1673
- }));
1674
- }
1675
- this.simulation.on("tick", this.handleTick);
1676
- this.simulation.on("end", this.onEnd);
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
+ });
1677
1956
  };
1678
1957
  get rootNode() {
1679
- return this.data.nodes.find((n) => n.id === this.rootId) || null;
1958
+ return this.data.nodes.maps.id.get(this.rootId) || null;
1680
1959
  }
1681
1960
  handleTick = () => {
1682
1961
  this.isTicking = true;
1962
+ if (this.rootNode) {
1963
+ this.rootNode.vx = 0;
1964
+ this.rootNode.vy = 0;
1965
+ }
1683
1966
  this.onDraw();
1684
1967
  this.tickCount++;
1685
1968
  };
@@ -1701,29 +1984,30 @@ var OpenGraphSimulation = class {
1701
1984
  }
1702
1985
  getNodeSize = (nodeId) => {
1703
1986
  const { nodeSize } = this.config;
1704
- const highlight = this.highlights.find((h) => h.id === nodeId);
1705
- const sizeScale = highlight?.scale || 1;
1987
+ const sizeScale = this.highlights.maps.nodeId.get(nodeId)?.scale || 1;
1706
1988
  if (nodeId === this.rootId) return nodeSize * 2 * sizeScale;
1707
- const node = this.data.nodes.find((n) => n.id === nodeId);
1989
+ const node = this.prunedData.nodes.maps.id.get(nodeId);
1708
1990
  if (node?.state?.sessionNode) return 5;
1709
- const isCollapsed = !!node?.state?.collapsed;
1710
- if (isCollapsed) {
1711
- const scale = scaleLinear().domain(this.clusterSizeRange).range([nodeSize, nodeSize * 3]);
1712
- return scale(node.clusterSize || 1);
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);
1713
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);
1714
1999
  const isSelected = this.selectedNode?.id === nodeId;
1715
- const isLiquidated = node?.status === "LIQUIDATED" || node?.status === "REGENERATED";
1716
- const _size = isLiquidated ? nodeSize * .2 : nodeSize;
2000
+ const _size = node?.status === "LIQUIDATED" || node?.status === "REGENERATED" ? nodeSize * .2 : nodeSize;
1717
2001
  return isSelected ? _size * 4 : _size * sizeScale;
1718
2002
  };
1719
2003
  updateRenderLayers() {
1720
2004
  const isHighlighted = (id) => {
1721
- const highlight = this.highlights.find((h) => h.id === id);
1722
- return !this.selectedNode && highlight?.onTop || this.selectedNode?.id === id || this.subGraph.nodes.find((n) => n.id === 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);
1723
2007
  };
1724
2008
  this.renderLayers.nodes.regular = [];
1725
2009
  this.renderLayers.nodes.highlighted = [];
1726
- this.prunedData.nodes.forEach((node) => {
2010
+ this.prunedData.nodes.values.forEach((node) => {
1727
2011
  if (isHighlighted(node.id)) this.renderLayers.nodes.highlighted.push(node);
1728
2012
  else this.renderLayers.nodes.regular.push(node);
1729
2013
  });
@@ -1731,20 +2015,14 @@ var OpenGraphSimulation = class {
1731
2015
  this.renderLayers.links.highlighted = [];
1732
2016
  const isLinkInSubgraph = (link) => {
1733
2017
  if (!this.selectedNode) return false;
1734
- const sourceId = isSimNode(link.source) ? link.source.id : link.source;
1735
- const targetId = isSimNode(link.target) ? link.target.id : link.target;
1736
- return this.subGraph.links.some((l) => {
1737
- const lSourceId = getNodeId(l.source);
1738
- const lTargetId = getNodeId(l.target);
1739
- return lSourceId === sourceId && lTargetId === targetId || lSourceId === targetId && lTargetId === sourceId;
1740
- });
2018
+ return this.subGraph.links.maps.linkId.has(getLinkId(link));
1741
2019
  };
1742
2020
  const isLinkInHighlights = (link) => {
1743
2021
  const sourceId = isSimNode(link.source) ? link.source.id : link.source;
1744
2022
  const targetId = isSimNode(link.target) ? link.target.id : link.target;
1745
- return !this.selectedNode && !!this.highlights.find((h) => h.isDetached && (h.id === sourceId || h.id === targetId));
2023
+ return !this.selectedNode && !!this.highlights.maps.detached.get("true")?.some((h) => h.id === sourceId || h.id === targetId);
1746
2024
  };
1747
- this.prunedData.links.forEach((link) => {
2025
+ this.prunedData.links.values.forEach((link) => {
1748
2026
  const inSubgraph = isLinkInSubgraph(link);
1749
2027
  const inHighlights = isLinkInHighlights(link);
1750
2028
  if (inSubgraph || inHighlights) this.renderLayers.links.highlighted.push(link);
@@ -1757,9 +2035,9 @@ var OpenGraphSimulation = class {
1757
2035
  return -1;
1758
2036
  });
1759
2037
  this.renderLayers.nodes.highlighted.sort((a, b) => {
1760
- const highlightA = this.highlights.find((h) => h.id === a.id);
1761
- const highlightB = this.highlights.find((h) => h.id === b.id);
1762
- if (this.subGraph.nodes.find((n) => n.id === a.id) || this.subGraph.nodes.find((n) => n.id === b.id)) return 2;
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;
1763
2041
  if (a.id === this.selectedNode?.id || b.id === this.selectedNode?.id) return 2;
1764
2042
  if (highlightA?.onTop || highlightB?.onTop) return 1;
1765
2043
  return -1;
@@ -1767,8 +2045,14 @@ var OpenGraphSimulation = class {
1767
2045
  }
1768
2046
  renderLink(ctx, link, options) {
1769
2047
  const sourceId = isSimNode(link.source) ? link.source.id : link.source;
1770
- const targetId = isSimNode(link.target) ? link.target.id : link.target;
1771
- let sourceNode = this.data.nodes.find((n) => n.id === sourceId);
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
+ }
1772
2056
  const isLight = this.theme === "light";
1773
2057
  const { dim: _dim, hasSelection, highlight } = options;
1774
2058
  let stroke = _dim ? this.color(dim(.09, isLight))() : hasSelection ? this.color(dim(.4, isLight))() : this.color(dim(.18, isLight))();
@@ -1802,12 +2086,13 @@ var OpenGraphSimulation = class {
1802
2086
  const fill = _dim ? this.color(dim(.075, isLight))() : isCollapsed ? this.color(dim(.18, isLight))() : isHovered ? this.color(dim(.4, isLight))() : this.color();
1803
2087
  const stroke = this.colorContrast();
1804
2088
  const nodeSize = this.getNodeSize(node.id);
1805
- const highlight = this.highlights.find((h) => h.id === node.id);
2089
+ const highlight = this.highlights.maps.nodeId.get(node.id);
1806
2090
  const highlighted = !!highlight;
1807
2091
  let highlightedStroke = _dim ? color(highlight?.strokeColor || red)(dim(.4, isLight))() : color(highlight?.strokeColor || red)();
1808
2092
  if (node.id === this.rootId) this.renderRootNode(ctx, x, y, nodeSize, _dim, isLight);
2093
+ else if (node.state?.emitterNode) return;
1809
2094
  else if (node.state?.sessionNode) this.renderSessionNode(ctx, x, y, nodeSize);
1810
- else if (isCollapsed) this.renderCollapsedNode(ctx, x, y, nodeSize, {
2095
+ else if (isCollapsed || node.state?.groupNode) this.renderCollapsedNode(ctx, x, y, nodeSize, {
1811
2096
  fill,
1812
2097
  stroke,
1813
2098
  highlighted,
@@ -1829,6 +2114,29 @@ var OpenGraphSimulation = class {
1829
2114
  image: node.state?.image
1830
2115
  });
1831
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
+ }
1832
2140
  renderSessionNode(ctx, x, y, size) {
1833
2141
  const isLight = this.theme === "light";
1834
2142
  circle(ctx, x, y, size / 2, {
@@ -1873,8 +2181,7 @@ var OpenGraphSimulation = class {
1873
2181
  fill: true,
1874
2182
  fillStyle: fill
1875
2183
  });
1876
- const showLabel = transform.scale >= this.visiblityScale(clusterSize) ? 1 : 0;
1877
- if (showLabel) {
2184
+ if (transform.scale >= this.visiblityScale(clusterSize) ? 1 : 0) {
1878
2185
  ctx.font = `${14 / transform.scale}px Sans-Serif`;
1879
2186
  ctx.textAlign = "center";
1880
2187
  ctx.textBaseline = "middle";
@@ -1888,7 +2195,7 @@ var OpenGraphSimulation = class {
1888
2195
  rect(ctx, x - _size / 2, y - _size / 2, _size, _size, {
1889
2196
  stroke: highlighted || isHovered,
1890
2197
  strokeStyle: isHovered ? fill : highlightedStroke,
1891
- lineWidth: .5,
2198
+ lineWidth: 1,
1892
2199
  fill: this.hideThumbnails || isLiquidated || !image,
1893
2200
  fillStyle: fill,
1894
2201
  borderRadius: 1
@@ -1896,9 +2203,14 @@ var OpenGraphSimulation = class {
1896
2203
  if (image && !this.hideThumbnails && !isLiquidated) img(ctx, image, x - size / 2, y - size / 2, size, size, 1, _dim ? .1 : 1, fill);
1897
2204
  }
1898
2205
  onDraw = () => {
2206
+ Measure.start("draw");
2207
+ this.isDrawing = true;
1899
2208
  const context = this.canvas?.getContext("2d");
1900
2209
  const transform = this.transformCanvas.getTransform();
1901
- if (!context) return;
2210
+ if (!context) {
2211
+ this.isDrawing = false;
2212
+ return;
2213
+ }
1902
2214
  const dpi = devicePixelRatio || 1;
1903
2215
  context.save();
1904
2216
  context.scale(dpi, dpi);
@@ -1906,11 +2218,21 @@ var OpenGraphSimulation = class {
1906
2218
  context.setTransform(transform.scale, 0, 0, transform.scale, transform.x, transform.y);
1907
2219
  context.translate(this.origin.x, this.origin.y);
1908
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
+ };
1909
2231
  const hasSelection = !!this.selectedNode;
1910
2232
  this.renderLayers.links.regular.forEach((link) => {
1911
- const sourceId = isSimNode(link.source) ? link.source.id : link.source;
1912
- const targetId = isSimNode(link.target) ? link.target.id : link.target;
1913
- const _dim = hasSelection && !this.subGraph.links.find((sl) => getNodeId(sl.source) === sourceId && getNodeId(sl.target) === targetId);
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);
1914
2236
  this.renderLink(context, link, {
1915
2237
  dim: _dim,
1916
2238
  hasSelection
@@ -1918,17 +2240,17 @@ var OpenGraphSimulation = class {
1918
2240
  });
1919
2241
  context.globalAlpha = 1;
1920
2242
  this.renderLayers.nodes.regular.forEach((node) => {
1921
- const _dim = hasSelection && !this.subGraph.nodes.some((n) => n.id === node.id);
2243
+ const _dim = hasSelection ? !this.subGraph.nodes.maps.id.has(node.id) : dimmedByHighlights(node.id);
1922
2244
  this.renderNode(context, node, {
1923
2245
  dim: _dim,
1924
2246
  transform
1925
2247
  });
1926
2248
  });
1927
2249
  this.renderLayers.links.highlighted.forEach((link) => {
1928
- const sourceId = isSimNode(link.source) ? link.source.id : link.source;
1929
- const targetId = isSimNode(link.target) ? link.target.id : link.target;
1930
- const _dim = hasSelection && !this.subGraph.links.find((sl) => getNodeId(sl.source) === sourceId && getNodeId(sl.target) === targetId);
1931
- const highlight = this.highlights.find((h) => h.id === sourceId || h.id === targetId);
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);
1932
2254
  this.renderLink(context, link, {
1933
2255
  dim: _dim,
1934
2256
  hasSelection,
@@ -1937,7 +2259,7 @@ var OpenGraphSimulation = class {
1937
2259
  });
1938
2260
  context.globalAlpha = 1;
1939
2261
  this.renderLayers.nodes.highlighted.forEach((node) => {
1940
- const _dim = hasSelection && !this.subGraph.nodes.some((n) => n.id === node.id);
2262
+ const _dim = hasSelection ? !this.subGraph.nodes.maps.id.has(node.id) : dimmedByHighlights(node.id);
1941
2263
  this.renderNode(context, node, {
1942
2264
  dim: _dim,
1943
2265
  transform
@@ -1946,6 +2268,8 @@ var OpenGraphSimulation = class {
1946
2268
  context.restore();
1947
2269
  context.restore();
1948
2270
  this.transformCanvas.trackCursor();
2271
+ this.isDrawing = false;
2272
+ Measure.end("draw");
1949
2273
  };
1950
2274
  drawDebug(context) {
1951
2275
  const transform = this.transformCanvas.getTransform();
@@ -1977,13 +2301,15 @@ var OpenGraphSimulation = class {
1977
2301
  }
1978
2302
  onEnd = () => {
1979
2303
  this.isTicking = false;
2304
+ this.emitter.emit("simulation:ended", this);
1980
2305
  };
1981
2306
  loadNodeImages = () => {
1982
- this.data.nodes.forEach((node) => {
2307
+ this.data.nodes.values.forEach((node) => {
1983
2308
  if (node.id === this.rootId) return;
1984
2309
  if (node.imgSrc && this.imageCache.get(node.imgSrc)) {
2310
+ const html = this.imageCache.get(node.imgSrc);
1985
2311
  node.state = node.state || {};
1986
- node.state.image = this.imageCache.get(node.imgSrc);
2312
+ node.state.image = html;
1987
2313
  return;
1988
2314
  }
1989
2315
  const loadImage = async () => {
@@ -2012,6 +2338,18 @@ var OpenGraphSimulation = class {
2012
2338
  this.theme = theme;
2013
2339
  this.updateScene();
2014
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
+ };
2015
2353
  setHideThumbnails = (hide) => {
2016
2354
  this.hideThumbnails = hide;
2017
2355
  this.updateScene();
@@ -2026,7 +2364,7 @@ var OpenGraphSimulation = class {
2026
2364
  const hash = quickHash(JSON.stringify(highlights));
2027
2365
  if (hash === this._highlightHash) return;
2028
2366
  this._highlightHash = hash;
2029
- this.highlights = highlights;
2367
+ this.highlights.reset(highlights);
2030
2368
  this.updateHighlights();
2031
2369
  };
2032
2370
  setNoInteraction = (noInteraction) => {
@@ -2035,7 +2373,7 @@ var OpenGraphSimulation = class {
2035
2373
  this.updateScene();
2036
2374
  };
2037
2375
  getNodeById = (nodeId) => {
2038
- return this.data.nodes.find((n) => n.id === nodeId) || null;
2376
+ return this.data.nodes.maps.id.get(nodeId) || null;
2039
2377
  };
2040
2378
  setLockedNodeId = (nodeId) => {
2041
2379
  this.lockedNodeId = nodeId || void 0;
@@ -2080,12 +2418,15 @@ var OpenGraphSimulation = class {
2080
2418
  context.restore();
2081
2419
  context.restore();
2082
2420
  };
2421
+ get measures() {
2422
+ return Measure.getSamples();
2423
+ }
2083
2424
  };
2084
2425
 
2085
2426
  //#endregion
2086
2427
  //#region src/components/OpenFormGraph.tsx
2087
2428
  function OpenFormGraph(props) {
2088
- const { width, height, highlights = [], className, noInteraction = false, loadNodeImage, translate, onTransform } = props;
2429
+ const { width, height, highlights = [], className, noInteraction = false, loadNodeImage, translate, onTransform, onSimulationEnded, nodeVisibility = "all", children, groupRootOrphans = false } = props;
2089
2430
  const { simulation, data, rootId, rootImageSources, theme, hideThumbnails, setHoveredNode, setSelectedNode, lockedNodeId } = useOpenFormGraph();
2090
2431
  const canvasRef = useRef(null);
2091
2432
  useEffect(() => {
@@ -2098,16 +2439,24 @@ function OpenFormGraph(props) {
2098
2439
  loadNodeImage,
2099
2440
  theme,
2100
2441
  translate,
2101
- lockedNodeId
2442
+ lockedNodeId,
2443
+ nodeVisibility,
2444
+ highlights,
2445
+ groupRootOrphans
2102
2446
  });
2447
+ return () => {
2448
+ simulation.current?.destroy();
2449
+ };
2450
+ }, []);
2451
+ useEffect(() => {
2452
+ if (!simulation.current) return;
2103
2453
  simulation.current.emitter.on("selected-node-changed", setSelectedNode);
2104
2454
  simulation.current.emitter.on("hovered-node-changed", setHoveredNode);
2105
2455
  return () => {
2106
2456
  simulation.current?.emitter.off("selected-node-changed", setSelectedNode);
2107
2457
  simulation.current?.emitter.off("hovered-node-changed", setHoveredNode);
2108
- simulation.current?.destroy();
2109
2458
  };
2110
- }, []);
2459
+ }, [setSelectedNode, setHoveredNode]);
2111
2460
  useEffect(() => {
2112
2461
  if (!simulation.current) return;
2113
2462
  if (!onTransform) return;
@@ -2116,6 +2465,10 @@ function OpenFormGraph(props) {
2116
2465
  simulation.current?.emitter.off("transform-changed", onTransform);
2117
2466
  };
2118
2467
  }, [onTransform]);
2468
+ useEffect(() => {
2469
+ if (!simulation.current || !onSimulationEnded) return;
2470
+ return simulation.current.emitter.on("simulation:ended", onSimulationEnded);
2471
+ }, [onSimulationEnded]);
2119
2472
  useEffect(() => {
2120
2473
  if (!simulation.current) return;
2121
2474
  simulation.current.resize(width, height);
@@ -2128,6 +2481,10 @@ function OpenFormGraph(props) {
2128
2481
  if (!simulation.current) return;
2129
2482
  simulation.current.setHideThumbnails(hideThumbnails);
2130
2483
  }, [hideThumbnails]);
2484
+ useEffect(() => {
2485
+ if (!simulation.current) return;
2486
+ simulation.current.setNodeVisibility(nodeVisibility, groupRootOrphans);
2487
+ }, [nodeVisibility, groupRootOrphans]);
2131
2488
  useEffect(() => {
2132
2489
  if (!simulation.current) return;
2133
2490
  simulation.current.setHighlights(highlights);
@@ -2139,7 +2496,7 @@ function OpenFormGraph(props) {
2139
2496
  useEffect(() => {
2140
2497
  if (!simulation.current) return;
2141
2498
  simulation.current.initialize(data, rootId);
2142
- }, [data]);
2499
+ }, [data, rootId]);
2143
2500
  useEffect(() => {
2144
2501
  if (!simulation.current) return;
2145
2502
  if (!translate) return;
@@ -2149,9 +2506,9 @@ function OpenFormGraph(props) {
2149
2506
  if (!simulation.current) return;
2150
2507
  if (simulation.current.lockedNodeId === lockedNodeId) return;
2151
2508
  simulation.current.setLockedNodeId(lockedNodeId);
2152
- }, [translate?.y, translate?.x]);
2509
+ }, [lockedNodeId]);
2153
2510
  const dpi = devicePixelRatio || 1;
2154
- return /* @__PURE__ */ jsx("canvas", {
2511
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("canvas", {
2155
2512
  onMouseEnter: props.onMouseEnter,
2156
2513
  onMouseLeave: props.onMouseLeave,
2157
2514
  ref: canvasRef,
@@ -2162,9 +2519,9 @@ function OpenFormGraph(props) {
2162
2519
  width: `${width}px`,
2163
2520
  height: `${height}px`
2164
2521
  }
2165
- });
2522
+ }), children] });
2166
2523
  }
2167
2524
 
2168
2525
  //#endregion
2169
- export { Highlight, OpenFormGraph, OpenGraphEventEmitter, OpenGraphSimulation, TransformCanvas, buildTreeFromGraphData, circle, color, dim, generateTree, getAllParentsUntil, getChildren, getClusterSize, getNodeDepth, getNodeId, getNodeSubgraph, getParents, getPrunedData, getRootParent, hasOnlyLeafs, hexagon, img, isCustomHighlight, isSimLink, isSimNode, loadHTMLImageElement, rect, red, searchParents };
2170
- //# sourceMappingURL=OpenFormGraph-i8VSTamk.js.map
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