@fxhash/open-form-graph 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2170 @@
1
+ import { DEFAULT_GRAPH_CONFIG, VOID_DETACH_ID, VOID_ROOT_ID, useOpenFormGraph } from "./provider-PqOen2FE.js";
2
+ import { useEffect, useRef } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ import { forceCenter, forceCollide, forceManyBody, forceRadial, forceSimulation } from "d3-force";
5
+ import { scaleLinear, scaleLog } from "d3-scale";
6
+ import { EventEmitter, float2hex, xorshiftString } from "@fxhash/utils";
7
+
8
+ //#region src/util/types.ts
9
+ function isSimNode(node) {
10
+ return typeof node === "object" && "id" in node;
11
+ }
12
+ function isSimLink(link) {
13
+ return typeof link === "object" && "source" in link && typeof link.source !== "string";
14
+ }
15
+ function isCustomHighlight(highlight) {
16
+ if (typeof highlight === "string") return false;
17
+ return true;
18
+ }
19
+
20
+ //#endregion
21
+ //#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());
30
+ }
31
+ function getAllParentsUntil(nodeId, links, stopAtId) {
32
+ const parent = getParents(nodeId, links)[0];
33
+ if (!parent || parent === stopAtId) return [];
34
+ return [parent, ...getAllParentsUntil(parent, links, stopAtId)];
35
+ }
36
+ 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());
41
+ }
42
+ function getClusterSize(id, links) {
43
+ const children = getChildren(id, links);
44
+ return children.reduce((acc, childId) => {
45
+ return acc + getClusterSize(childId, links);
46
+ }, children.length || 0);
47
+ }
48
+ function getNodeDepth(id, links) {
49
+ 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);
53
+ }
54
+ return getDepth(id, 0);
55
+ }
56
+ function getRootParent(id, links, stop) {
57
+ let currentId = id;
58
+ 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];
63
+ }
64
+ }
65
+ function hasOnlyLeafs(id, links) {
66
+ const children = getChildren(id, links);
67
+ return children.every((childId) => getChildren(childId, links).length === 0);
68
+ }
69
+ 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();
74
+ let currentId = nodeId;
75
+ 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
+ });
80
+ 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());
84
+ subLinks.add(parentLink);
85
+ currentId = parentId.toString();
86
+ }
87
+ 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());
93
+ subLinks.add(link);
94
+ collectChildren(targetId.toString());
95
+ }
96
+ }
97
+ }
98
+ collectChildren(nodeId);
99
+ const validIds = new Set([
100
+ ...parentSet,
101
+ nodeId,
102
+ ...childSet
103
+ ]);
104
+ 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;
107
+ return validIds.has(sourceId.toString()) && validIds.has(targetId.toString());
108
+ });
109
+ const allNodeIds = Array.from(validIds);
110
+ const subNodes = allNodeIds.map((id) => nodesById[id]).filter(Boolean);
111
+ return {
112
+ nodes: subNodes,
113
+ 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
+ });
126
+ }
127
+
128
+ //#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 = {
158
+ x: 0,
159
+ y: 0
160
+ };
161
+ isDragging = false;
162
+ dragStart = null;
163
+ hasMoved = false;
164
+ lastPointerPos = {
165
+ x: 0,
166
+ y: 0
167
+ };
168
+ lastMovePos = {
169
+ x: 0,
170
+ y: 0
171
+ };
172
+ velocity = {
173
+ x: 0,
174
+ y: 0
175
+ };
176
+ lastDragTime = 0;
177
+ momentumFrame = null;
178
+ touchStart = null;
179
+ pinchStartDist = null;
180
+ pinchStartScale = 1;
181
+ noInteraction = false;
182
+ dpr = 1;
183
+ resizeObserver = null;
184
+ mediaQueryList = null;
185
+ onUpdate;
186
+ onClick;
187
+ onMove;
188
+ constructor(canvas, options) {
189
+ this.canvas = canvas;
190
+ this.onUpdate = options?.onUpdate;
191
+ this.onClick = options?.onClick;
192
+ this.onMove = options?.onMove;
193
+ this.offset = options?.offset || {
194
+ x: 0,
195
+ y: 0
196
+ };
197
+ this.dpr = window.devicePixelRatio || 1;
198
+ this.bindEventHandlers();
199
+ this.attachEventListeners();
200
+ this.setupDPRMonitoring();
201
+ }
202
+ bindEventHandlers() {
203
+ this.handleWheel = this.handleWheel.bind(this);
204
+ this.handleMouseDown = this.handleMouseDown.bind(this);
205
+ this.handleMouseMove = this.handleMouseMove.bind(this);
206
+ this.handleMouseUp = this.handleMouseUp.bind(this);
207
+ this.handleCanvasClick = this.handleCanvasClick.bind(this);
208
+ this.handleTouchStart = this.handleTouchStart.bind(this);
209
+ this.handleTouchMove = this.handleTouchMove.bind(this);
210
+ this.handleTouchEnd = this.handleTouchEnd.bind(this);
211
+ }
212
+ attachEventListeners() {
213
+ this.canvas.addEventListener("wheel", this.handleWheel, { passive: false });
214
+ this.canvas.addEventListener("mousedown", this.handleMouseDown);
215
+ this.canvas.addEventListener("click", this.handleCanvasClick);
216
+ window.addEventListener("mousemove", this.handleMouseMove);
217
+ window.addEventListener("mouseup", this.handleMouseUp);
218
+ this.canvas.addEventListener("touchstart", this.handleTouchStart, { passive: false });
219
+ this.canvas.addEventListener("touchmove", this.handleTouchMove, { passive: false });
220
+ this.canvas.addEventListener("touchend", this.handleTouchEnd, { passive: false });
221
+ this.canvas.addEventListener("touchcancel", this.handleTouchEnd, { passive: false });
222
+ }
223
+ setupDPRMonitoring() {
224
+ if (typeof ResizeObserver !== "undefined") {
225
+ this.resizeObserver = new ResizeObserver(() => {
226
+ this.updateDPR();
227
+ });
228
+ this.resizeObserver.observe(this.canvas);
229
+ }
230
+ const updateDPRFromMediaQuery = () => {
231
+ this.updateDPR();
232
+ };
233
+ const dpr = window.devicePixelRatio || 1;
234
+ this.mediaQueryList = window.matchMedia(`(resolution: ${dpr}dppx)`);
235
+ if (this.mediaQueryList.addEventListener) this.mediaQueryList.addEventListener("change", updateDPRFromMediaQuery);
236
+ }
237
+ updateDPR() {
238
+ const newDPR = window.devicePixelRatio || 1;
239
+ if (newDPR !== this.dpr) {
240
+ const oldDPR = this.dpr;
241
+ this.dpr = newDPR;
242
+ const scale = newDPR / oldDPR;
243
+ this.transform.x *= scale;
244
+ this.transform.y *= scale;
245
+ this.targetTransform.x *= scale;
246
+ this.targetTransform.y *= scale;
247
+ this.onUpdate?.(this.transform);
248
+ if (this.mediaQueryList) {
249
+ const updateDPRFromMediaQuery = () => {
250
+ this.updateDPR();
251
+ };
252
+ if (this.mediaQueryList.removeEventListener) this.mediaQueryList.removeEventListener("change", updateDPRFromMediaQuery);
253
+ this.mediaQueryList = window.matchMedia(`(resolution: ${newDPR}dppx)`);
254
+ if (this.mediaQueryList.addEventListener) this.mediaQueryList.addEventListener("change", updateDPRFromMediaQuery);
255
+ }
256
+ }
257
+ }
258
+ toCanvasCoords(clientX, clientY) {
259
+ const rect$1 = this.canvas.getBoundingClientRect();
260
+ return {
261
+ x: (clientX - rect$1.left) * this.dpr,
262
+ y: (clientY - rect$1.top) * this.dpr
263
+ };
264
+ }
265
+ toCSSCoords(clientX, clientY) {
266
+ const rect$1 = this.canvas.getBoundingClientRect();
267
+ return {
268
+ x: clientX - rect$1.left,
269
+ y: clientY - rect$1.top
270
+ };
271
+ }
272
+ startMomentum() {
273
+ if (Math.abs(this.velocity.x) > MIN_VELOCITY || Math.abs(this.velocity.y) > MIN_VELOCITY) {
274
+ if (!this.momentumFrame) this.momentumFrame = requestAnimationFrame(this.applyMomentum);
275
+ }
276
+ }
277
+ applyMomentum = () => {
278
+ this.targetTransform.x += this.velocity.x * this.dpr;
279
+ this.targetTransform.y += this.velocity.y * this.dpr;
280
+ this.velocity.x *= MOMENTUM_DAMPING;
281
+ this.velocity.y *= MOMENTUM_DAMPING;
282
+ if (Math.abs(this.velocity.x) > MIN_VELOCITY || Math.abs(this.velocity.y) > MIN_VELOCITY) {
283
+ this.startAnimation();
284
+ this.momentumFrame = requestAnimationFrame(this.applyMomentum);
285
+ } else {
286
+ this.velocity = {
287
+ x: 0,
288
+ y: 0
289
+ };
290
+ this.momentumFrame = null;
291
+ }
292
+ };
293
+ lerp(a, b, t) {
294
+ return a + (b - a) * t;
295
+ }
296
+ animateTransform = () => {
297
+ const target = this.focus?.() || this.targetTransform;
298
+ const prev = this.transform;
299
+ const animationSpeed = this.isDragging ? DRAG_ANIMATION_SPEED : ANIMATION_SPEED;
300
+ const next = {
301
+ x: this.lerp(prev.x, target.x, animationSpeed / this.dpr),
302
+ y: this.lerp(prev.y, target.y, animationSpeed / this.dpr),
303
+ scale: this.lerp(prev.scale, target.scale, animationSpeed / this.dpr)
304
+ };
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) {
307
+ this.transform = { ...target };
308
+ this.stopAnimation();
309
+ } else {
310
+ this.transform = next;
311
+ this.onUpdate?.(this.transform);
312
+ this.animationFrame = requestAnimationFrame(this.animateTransform);
313
+ }
314
+ };
315
+ startAnimation() {
316
+ if (!this.isAnimating) {
317
+ this.isAnimating = true;
318
+ this.animationFrame = requestAnimationFrame(this.animateTransform);
319
+ }
320
+ }
321
+ stopAnimation() {
322
+ this.isAnimating = false;
323
+ this.focus = null;
324
+ if (this.animationFrame) {
325
+ cancelAnimationFrame(this.animationFrame);
326
+ this.animationFrame = null;
327
+ }
328
+ }
329
+ interruptAnimation() {
330
+ this.targetTransform = { ...this.transform };
331
+ if (this.isAnimating) this.stopAnimation();
332
+ if (this.momentumFrame) {
333
+ cancelAnimationFrame(this.momentumFrame);
334
+ this.momentumFrame = null;
335
+ this.velocity = {
336
+ x: 0,
337
+ y: 0
338
+ };
339
+ }
340
+ }
341
+ handleWheel(e) {
342
+ e.preventDefault();
343
+ e.stopPropagation();
344
+ if (this.noInteraction) return;
345
+ if (this.momentumFrame) {
346
+ cancelAnimationFrame(this.momentumFrame);
347
+ this.momentumFrame = null;
348
+ this.velocity = {
349
+ x: 0,
350
+ y: 0
351
+ };
352
+ }
353
+ if (this.focus) {
354
+ this.interruptAnimation();
355
+ this.focus = null;
356
+ }
357
+ const canvasCoords = this.toCanvasCoords(e.clientX, e.clientY);
358
+ const scaleFactor = e.deltaY > 0 ? .95 : 1.05;
359
+ const newScale = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, this.targetTransform.scale * scaleFactor));
360
+ const { x: currentX, y: currentY, scale: currentScale } = this.transform;
361
+ const worldX = (canvasCoords.x - currentX - this.offset.x * currentScale) / currentScale;
362
+ const worldY = (canvasCoords.y - currentY - this.offset.y * currentScale) / currentScale;
363
+ const newX = canvasCoords.x - worldX * newScale - this.offset.x * newScale;
364
+ const newY = canvasCoords.y - worldY * newScale - this.offset.y * newScale;
365
+ this.targetTransform = {
366
+ x: newX,
367
+ y: newY,
368
+ scale: newScale
369
+ };
370
+ this.startAnimation();
371
+ }
372
+ handleMouseDown(e) {
373
+ if (this.noInteraction) return;
374
+ this.interruptAnimation();
375
+ this.isDragging = true;
376
+ this.hasMoved = false;
377
+ this.dragStart = {
378
+ x: e.clientX,
379
+ y: e.clientY
380
+ };
381
+ this.lastPointerPos = {
382
+ x: e.clientX,
383
+ y: e.clientY
384
+ };
385
+ }
386
+ handleMouseMove(e) {
387
+ if (this.noInteraction) return;
388
+ this.lastMovePos = {
389
+ x: e.clientX,
390
+ y: e.clientY
391
+ };
392
+ const cssCoords = this.toCSSCoords(e.clientX, e.clientY);
393
+ this.onMove?.(cssCoords.x, cssCoords.y);
394
+ if (!this.isDragging || !this.dragStart) return;
395
+ const dx = e.clientX - this.dragStart.x;
396
+ const dy = e.clientY - this.dragStart.y;
397
+ if (Math.abs(dx) > CLICK_THRESHOLD || Math.abs(dy) > CLICK_THRESHOLD) {
398
+ this.hasMoved = true;
399
+ const deltaX = (e.clientX - this.lastPointerPos.x) * this.dpr;
400
+ const deltaY = (e.clientY - this.lastPointerPos.y) * this.dpr;
401
+ this.targetTransform.x += deltaX;
402
+ this.targetTransform.y += deltaY;
403
+ const now = Date.now();
404
+ const dt = now - this.lastDragTime;
405
+ if (dt > 0 && dt < 100) {
406
+ this.velocity.x = deltaX / dt * 16;
407
+ this.velocity.y = deltaY / dt * 16;
408
+ }
409
+ this.lastPointerPos = {
410
+ x: e.clientX,
411
+ y: e.clientY
412
+ };
413
+ this.lastDragTime = now;
414
+ this.startAnimation();
415
+ }
416
+ }
417
+ handleMouseUp(e) {
418
+ if (this.isDragging && this.hasMoved) this.startMomentum();
419
+ this.isDragging = false;
420
+ this.dragStart = null;
421
+ }
422
+ handleCanvasClick(e) {
423
+ if (this.noInteraction || this.hasMoved) return;
424
+ const cssCoords = this.toCSSCoords(e.clientX, e.clientY);
425
+ this.onClick?.(cssCoords.x, cssCoords.y);
426
+ }
427
+ handleTouchStart(e) {
428
+ if (this.noInteraction) return;
429
+ e.preventDefault();
430
+ this.interruptAnimation();
431
+ if (e.touches.length === 1) {
432
+ const touch = e.touches[0];
433
+ this.isDragging = true;
434
+ this.hasMoved = false;
435
+ this.touchStart = {
436
+ x: touch.clientX,
437
+ y: touch.clientY
438
+ };
439
+ this.lastPointerPos = {
440
+ x: touch.clientX,
441
+ y: touch.clientY
442
+ };
443
+ this.lastMovePos = {
444
+ x: touch.clientX,
445
+ y: touch.clientY
446
+ };
447
+ } else if (e.touches.length === 2) {
448
+ this.isDragging = false;
449
+ const [t1, t2] = Array.from(e.touches);
450
+ this.pinchStartDist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
451
+ this.pinchStartScale = this.targetTransform.scale;
452
+ }
453
+ }
454
+ handleTouchMove(e) {
455
+ if (this.noInteraction) return;
456
+ e.preventDefault();
457
+ if (e.touches.length === 1 && this.isDragging && this.touchStart) {
458
+ const touch = e.touches[0];
459
+ const dx = touch.clientX - this.touchStart.x;
460
+ const dy = touch.clientY - this.touchStart.y;
461
+ if (Math.abs(dx) > CLICK_THRESHOLD || Math.abs(dy) > CLICK_THRESHOLD) {
462
+ this.hasMoved = true;
463
+ const deltaX = touch.clientX - this.lastPointerPos.x;
464
+ const deltaY = touch.clientY - this.lastPointerPos.y;
465
+ this.targetTransform.x += deltaX * this.dpr;
466
+ this.targetTransform.y += deltaY * this.dpr;
467
+ const now = Date.now();
468
+ const dt = now - this.lastDragTime;
469
+ if (dt > 0 && dt < 100) {
470
+ this.velocity.x = deltaX / dt * 16;
471
+ this.velocity.y = deltaY / dt * 16;
472
+ }
473
+ this.lastPointerPos = {
474
+ x: touch.clientX,
475
+ y: touch.clientY
476
+ };
477
+ this.lastMovePos = {
478
+ x: touch.clientX,
479
+ y: touch.clientY
480
+ };
481
+ this.lastDragTime = now;
482
+ this.startAnimation();
483
+ const cssCoords = this.toCSSCoords(touch.clientX, touch.clientY);
484
+ this.onMove?.(cssCoords.x, cssCoords.y);
485
+ }
486
+ } else if (e.touches.length === 2 && this.pinchStartDist != null) {
487
+ if (this.momentumFrame) {
488
+ cancelAnimationFrame(this.momentumFrame);
489
+ this.momentumFrame = null;
490
+ this.velocity = {
491
+ x: 0,
492
+ y: 0
493
+ };
494
+ }
495
+ 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;
498
+ newScale = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newScale));
499
+ const centerX = (t1.clientX + t2.clientX) / 2;
500
+ const centerY = (t1.clientY + t2.clientY) / 2;
501
+ const canvasCenter = this.toCanvasCoords(centerX, centerY);
502
+ const { x: currentX, y: currentY, scale: currentScale } = this.transform;
503
+ const worldX = (canvasCenter.x - currentX - this.offset.x * currentScale) / currentScale;
504
+ const worldY = (canvasCenter.y - currentY - this.offset.y * currentScale) / currentScale;
505
+ const newX = canvasCenter.x - worldX * newScale - this.offset.x * newScale;
506
+ const newY = canvasCenter.y - worldY * newScale - this.offset.y * newScale;
507
+ this.targetTransform = {
508
+ x: newX,
509
+ y: newY,
510
+ scale: newScale
511
+ };
512
+ this.startAnimation();
513
+ }
514
+ }
515
+ handleTouchEnd(e) {
516
+ if (this.noInteraction) return;
517
+ if (e.touches.length === 0) {
518
+ if (this.isDragging && this.hasMoved) this.startMomentum();
519
+ else if (this.isDragging && !this.hasMoved && this.onClick && this.touchStart) {
520
+ const cssCoords = this.toCSSCoords(this.touchStart.x, this.touchStart.y);
521
+ this.onClick(cssCoords.x, cssCoords.y);
522
+ }
523
+ this.isDragging = false;
524
+ this.touchStart = null;
525
+ this.hasMoved = false;
526
+ this.pinchStartDist = null;
527
+ } else if (e.touches.length === 1) {
528
+ const touch = e.touches[0];
529
+ this.isDragging = true;
530
+ this.hasMoved = false;
531
+ this.touchStart = {
532
+ x: touch.clientX,
533
+ y: touch.clientY
534
+ };
535
+ this.lastPointerPos = {
536
+ x: touch.clientX,
537
+ y: touch.clientY
538
+ };
539
+ this.pinchStartDist = null;
540
+ this.velocity = {
541
+ x: 0,
542
+ y: 0
543
+ };
544
+ this.lastDragTime = Date.now();
545
+ }
546
+ }
547
+ resetZoom() {
548
+ this.targetTransform = {
549
+ x: 0,
550
+ y: 0,
551
+ scale: 1
552
+ };
553
+ this.startAnimation();
554
+ }
555
+ transformTo(update) {
556
+ this.targetTransform = {
557
+ ...this.targetTransform,
558
+ ...update
559
+ };
560
+ this.startAnimation();
561
+ }
562
+ getTransformationFromWorld(worldX, worldY, newScale) {
563
+ const scale = newScale ?? this.transform.scale;
564
+ const x = this.canvas.width / 2 + this.offset.x * this.dpr - worldX * scale - (this.canvas.width / 2 + this.offset.x * this.dpr) * scale;
565
+ const y = this.canvas.height / 2 + this.offset.y * this.dpr - worldY * scale - (this.canvas.height / 2 + this.offset.y * this.dpr) * scale;
566
+ return {
567
+ x,
568
+ y,
569
+ scale
570
+ };
571
+ }
572
+ transformToWorld(worldX, worldY, newScale) {
573
+ const transform = this.getTransformationFromWorld(worldX, worldY, newScale);
574
+ this.transformTo(transform);
575
+ }
576
+ trackCursor() {
577
+ const cssCoords = this.toCSSCoords(this.lastMovePos.x, this.lastMovePos.y);
578
+ this.onMove?.(cssCoords.x, cssCoords.y);
579
+ }
580
+ setNoInteraction(noInteraction) {
581
+ this.noInteraction = noInteraction;
582
+ if (noInteraction) {
583
+ this.isDragging = false;
584
+ this.dragStart = null;
585
+ }
586
+ }
587
+ focusOn(focus) {
588
+ this.focus = focus;
589
+ if (focus) {
590
+ const _focus = focus;
591
+ this.focus = () => {
592
+ const worldFocus = _focus();
593
+ if (!worldFocus) return null;
594
+ const transform = this.getTransformationFromWorld(worldFocus.x, worldFocus.y, worldFocus.scale);
595
+ return transform;
596
+ };
597
+ this.startAnimation();
598
+ }
599
+ }
600
+ resetFocus() {
601
+ this.focus = null;
602
+ }
603
+ destroy() {
604
+ this.stopAnimation();
605
+ if (this.momentumFrame) {
606
+ cancelAnimationFrame(this.momentumFrame);
607
+ this.momentumFrame = null;
608
+ }
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);
980
+ }
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
+ }
1001
+ return findParents(nodeHash);
1002
+ }
1003
+
1004
+ //#endregion
1005
+ //#region src/sim/_interfaces.ts
1006
+ var OpenGraphEventEmitter = class extends EventEmitter {};
1007
+
1008
+ //#endregion
1009
+ //#region src/util/math.ts
1010
+ function getRadialPoint(r, cx, cy, angle = Math.random() * 2 * Math.PI) {
1011
+ return {
1012
+ x: cx + r * Math.cos(angle),
1013
+ y: cy + r * Math.sin(angle)
1014
+ };
1015
+ }
1016
+ function getAngle(cx, cy, x, y) {
1017
+ return Math.atan2(y - cy, x - cx);
1018
+ }
1019
+
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
+ //#endregion
1075
+ //#region src/sim/asymmetric-link.ts
1076
+ function constant(x) {
1077
+ return function() {
1078
+ return x;
1079
+ };
1080
+ }
1081
+ function jiggle(random) {
1082
+ return (random() - .5) * 1e-6;
1083
+ }
1084
+ function index(d) {
1085
+ return d.index;
1086
+ }
1087
+ function find(nodeById, nodeId) {
1088
+ const node = nodeById.get(nodeId);
1089
+ if (!node) throw new Error("node not found: " + nodeId);
1090
+ return node;
1091
+ }
1092
+ function asymmetricLinks(links) {
1093
+ let id = index;
1094
+ let strength = defaultStrength;
1095
+ let strengths;
1096
+ let distance = constant(30);
1097
+ let distances;
1098
+ let nodes;
1099
+ let count;
1100
+ let bias;
1101
+ let random;
1102
+ let iterations = 1;
1103
+ if (links == null) links = [];
1104
+ function defaultStrength(link) {
1105
+ return 1 / Math.min(count[link.source.index], count[link.target.index]);
1106
+ }
1107
+ function force(alpha) {
1108
+ for (let k = 0, n = links.length; k < iterations; ++k) for (let i = 0; i < n; ++i) {
1109
+ const link = links[i];
1110
+ const source = link.source;
1111
+ const target = link.target;
1112
+ let x = target.x + target.vx - source.x - source.vx || jiggle(random);
1113
+ let y = target.y + target.vy - source.y - source.vy || jiggle(random);
1114
+ let l = Math.sqrt(x * x + y * y);
1115
+ l = (l - distances[i]) / l * alpha;
1116
+ x *= l;
1117
+ y *= l;
1118
+ const b = bias[i];
1119
+ const strengthValue = strengths[i];
1120
+ let s0, s1;
1121
+ if (Array.isArray(strengthValue)) [s0, s1] = strengthValue;
1122
+ else s0 = s1 = strengthValue;
1123
+ target.vx -= x * b * s0;
1124
+ target.vy -= y * b * s0;
1125
+ source.vx += x * (1 - b) * s1;
1126
+ source.vy += y * (1 - b) * s1;
1127
+ }
1128
+ }
1129
+ function initialize() {
1130
+ if (!nodes) return;
1131
+ const n = nodes.length;
1132
+ const m = links.length;
1133
+ const nodeById = new Map(nodes.map((d, i) => [id(d, i, nodes), d]));
1134
+ count = new Array(n).fill(0);
1135
+ for (let i = 0; i < m; ++i) {
1136
+ const link = links[i];
1137
+ link.index = i;
1138
+ if (typeof link.source !== "object") link.source = find(nodeById, link.source);
1139
+ if (typeof link.target !== "object") link.target = find(nodeById, link.target);
1140
+ count[link.source.index]++;
1141
+ count[link.target.index]++;
1142
+ }
1143
+ bias = new Array(m);
1144
+ for (let i = 0; i < m; ++i) {
1145
+ const link = links[i];
1146
+ const sourceCount = count[link.source.index];
1147
+ const targetCount = count[link.target.index];
1148
+ bias[i] = sourceCount / (sourceCount + targetCount);
1149
+ }
1150
+ strengths = new Array(m);
1151
+ initializeStrength();
1152
+ distances = new Array(m);
1153
+ initializeDistance();
1154
+ }
1155
+ function initializeStrength() {
1156
+ if (!nodes) return;
1157
+ for (let i = 0, n = links.length; i < n; ++i) strengths[i] = strength(links[i], i, links);
1158
+ }
1159
+ function initializeDistance() {
1160
+ if (!nodes) return;
1161
+ for (let i = 0, n = links.length; i < n; ++i) distances[i] = +distance(links[i], i, links);
1162
+ }
1163
+ force.initialize = function(_nodes, _random) {
1164
+ nodes = _nodes;
1165
+ random = _random;
1166
+ initialize();
1167
+ };
1168
+ force.links = function(_) {
1169
+ return arguments.length ? (links = _, initialize(), force) : links;
1170
+ };
1171
+ force.id = function(_) {
1172
+ return arguments.length ? (id = _, force) : id;
1173
+ };
1174
+ force.iterations = function(_) {
1175
+ return arguments.length ? (iterations = +_, force) : iterations;
1176
+ };
1177
+ force.strength = function(_) {
1178
+ return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initializeStrength(), force) : strength;
1179
+ };
1180
+ force.distance = function(_) {
1181
+ return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), initializeDistance(), force) : distance;
1182
+ };
1183
+ return force;
1184
+ }
1185
+
1186
+ //#endregion
1187
+ //#region src/util/hash.ts
1188
+ function quickHash(data) {
1189
+ return float2hex(xorshiftString(data));
1190
+ }
1191
+
1192
+ //#endregion
1193
+ //#region src/sim/OpenGraphSimulation.ts
1194
+ const RADIAL_FORCES = false;
1195
+ const INITIAL_RADIUS = 300;
1196
+ const INCREMENTAL = 200;
1197
+ function getRadius(depth) {
1198
+ if (depth === 0) return INITIAL_RADIUS;
1199
+ return INCREMENTAL * depth + INITIAL_RADIUS;
1200
+ }
1201
+ var OpenGraphSimulation = class {
1202
+ width;
1203
+ height;
1204
+ config;
1205
+ rootImageSources;
1206
+ canvas;
1207
+ transformCanvas;
1208
+ theme;
1209
+ rawData;
1210
+ emitter;
1211
+ translate = {
1212
+ x: 0,
1213
+ y: 0
1214
+ };
1215
+ data = {
1216
+ nodes: [],
1217
+ links: []
1218
+ };
1219
+ prunedData = {
1220
+ nodes: [],
1221
+ links: []
1222
+ };
1223
+ subGraph = {
1224
+ nodes: [],
1225
+ links: []
1226
+ };
1227
+ rootId = "";
1228
+ simulation = null;
1229
+ clusterSizeRange = [0, 1];
1230
+ maxDepth = 0;
1231
+ isTicking = false;
1232
+ tickCount = 0;
1233
+ loadNodeImage;
1234
+ imageCache = new Map();
1235
+ rootImages = [];
1236
+ hideThumbnails = false;
1237
+ noInteraction = false;
1238
+ lockedNodeId;
1239
+ selectedNode = null;
1240
+ hoveredNode = null;
1241
+ highlights = [];
1242
+ renderLayers = {
1243
+ links: {
1244
+ regular: [],
1245
+ highlighted: []
1246
+ },
1247
+ nodes: {
1248
+ regular: [],
1249
+ highlighted: []
1250
+ }
1251
+ };
1252
+ constructor(props) {
1253
+ this.emitter = new OpenGraphEventEmitter();
1254
+ this.theme = props.theme || "light";
1255
+ this.width = props.width;
1256
+ this.height = props.height;
1257
+ this.config = props.config || DEFAULT_GRAPH_CONFIG;
1258
+ this.rootImageSources = props.rootImageSources || [];
1259
+ this.canvas = props.canvas;
1260
+ this.lockedNodeId = props.lockedNodeId;
1261
+ this.loadNodeImage = props.loadNodeImage;
1262
+ this.translate = props.translate || {
1263
+ x: 0,
1264
+ y: 0
1265
+ };
1266
+ this.transformCanvas = new TransformCanvas(this.canvas, {
1267
+ onUpdate: this.handleTransform,
1268
+ onClick: this.handleClick,
1269
+ onMove: this.handleMove
1270
+ });
1271
+ this.rootImageSources.forEach((src, idx) => {
1272
+ if (src && !this.imageCache.get(src)) loadHTMLImageElement(src).then((img$1) => {
1273
+ this.imageCache.set(src, img$1);
1274
+ this.rootImages[idx] = img$1;
1275
+ });
1276
+ });
1277
+ }
1278
+ get center() {
1279
+ return {
1280
+ x: 0,
1281
+ y: 0
1282
+ };
1283
+ }
1284
+ get origin() {
1285
+ const dpi = devicePixelRatio || 1;
1286
+ return {
1287
+ x: (this.translate.x + this.width / 2) * dpi,
1288
+ y: (this.translate.y + this.height / 2) * dpi
1289
+ };
1290
+ }
1291
+ getNodeAtPosition = (cx, cy) => {
1292
+ const transform = this.transformCanvas.getTransform();
1293
+ const { x: tx, y: ty, scale } = transform;
1294
+ const dpi = devicePixelRatio || 1;
1295
+ const canvasX = cx * dpi;
1296
+ const canvasY = cy * dpi;
1297
+ const scaledX = (canvasX - tx) / scale;
1298
+ const scaledY = (canvasY - ty) / scale;
1299
+ const graphX = scaledX - this.origin.x;
1300
+ const graphY = scaledY - this.origin.y;
1301
+ const candidates = [];
1302
+ for (let node of this.data.nodes) {
1303
+ const r = this.getNodeSize(node.id) / 2;
1304
+ if (node.x == null || node.y == null) continue;
1305
+ const dx = node.x - graphX;
1306
+ const dy = node.y - graphY;
1307
+ if (dx * dx + dy * dy < r * r) {
1308
+ if (!this.prunedData.nodes.find((n) => n.id === node.id)) continue;
1309
+ candidates.push(node);
1310
+ }
1311
+ }
1312
+ if (candidates.length === 0) return null;
1313
+ if (candidates.length === 1) return candidates[0];
1314
+ for (let i = this.renderLayers.nodes.highlighted.length - 1; i >= 0; i--) {
1315
+ const node = this.renderLayers.nodes.highlighted[i];
1316
+ if (candidates.find((c) => c.id === node.id)) return node;
1317
+ }
1318
+ for (let node of this.renderLayers.nodes.regular) if (candidates.find((c) => c.id === node.id)) return node;
1319
+ return candidates[0];
1320
+ };
1321
+ getNodeScreenPosition = (node) => {
1322
+ const transform = this.transformCanvas.getTransform();
1323
+ const x = transform.x + (node.x || 0 + this.origin.x) * transform.scale;
1324
+ const y = transform.y + (node.y || 0 + this.origin.y) * transform.scale;
1325
+ return {
1326
+ x,
1327
+ y
1328
+ };
1329
+ };
1330
+ getNodeCanvasPosition = (node) => {
1331
+ const _x = node.x || 0;
1332
+ const _y = node.y || 0;
1333
+ const transform = this.transformCanvas.getTransform();
1334
+ const x = this.origin.x - _x * transform.scale;
1335
+ const y = this.origin.y - _y * transform.scale;
1336
+ return {
1337
+ x,
1338
+ y
1339
+ };
1340
+ };
1341
+ screenToWorld(_x, _y) {
1342
+ const transform = this.transformCanvas.getTransform();
1343
+ const { x: tx, y: ty, scale } = transform;
1344
+ const dpi = devicePixelRatio || 1;
1345
+ const canvasX = _x * dpi;
1346
+ const canvasY = _y * dpi;
1347
+ const scaledX = (canvasX - tx) / scale;
1348
+ const scaledY = (canvasY - ty) / scale;
1349
+ const x = scaledX - this.origin.x;
1350
+ const y = scaledY - this.origin.y;
1351
+ return {
1352
+ x,
1353
+ y
1354
+ };
1355
+ }
1356
+ worldToScreen(worldX, worldY) {
1357
+ const transform = this.transformCanvas.getTransform();
1358
+ const { x: tx, y: ty, scale } = transform;
1359
+ const dpi = devicePixelRatio || 1;
1360
+ const scaledX = (worldX + this.origin.x) * scale;
1361
+ const scaledY = (worldY + this.origin.y) * scale;
1362
+ const canvasX = scaledX + tx;
1363
+ const canvasY = scaledY + ty;
1364
+ const screenX = canvasX / dpi;
1365
+ const screenY = canvasY / dpi;
1366
+ return {
1367
+ x: screenX,
1368
+ y: screenY
1369
+ };
1370
+ }
1371
+ handleClick = (x, y) => {
1372
+ let node = this.getNodeAtPosition(x, y);
1373
+ if (node?.state?.sessionNode) return;
1374
+ if (this.lockedNodeId && !node) node = this.getNodeById(this.lockedNodeId);
1375
+ this.handleClickNode(node);
1376
+ };
1377
+ handleClickNode = (node, options = {
1378
+ noToggle: false,
1379
+ triggerFocus: false
1380
+ }) => {
1381
+ let wasOpened = false;
1382
+ if (node) {
1383
+ if (node.id === this.rootId) {
1384
+ this.selectedNode = null;
1385
+ this.emitter.emit("selected-node-changed", null);
1386
+ this.subGraph = {
1387
+ nodes: [],
1388
+ links: []
1389
+ };
1390
+ this.updateRenderLayers();
1391
+ this.updateScene();
1392
+ return;
1393
+ }
1394
+ if (node.state) {
1395
+ const children = getChildren(node.id, this.data.links);
1396
+ if (children.length > 0 && !options?.noToggle) {
1397
+ if (this.selectedNode?.id !== node.id) {
1398
+ if (node.state.collapsed) wasOpened = true;
1399
+ node.state.collapsed = false;
1400
+ } else node.state.collapsed = !node.state.collapsed;
1401
+ if (!node.state.collapsed) {
1402
+ const clusterDistance = 100;
1403
+ const clusterRadius = 50;
1404
+ const parentX = node.x || this.center.x;
1405
+ const parentY = node.y || this.center.y;
1406
+ const dirX = parentX - this.center.x;
1407
+ const dirY = parentY - this.center.y;
1408
+ const length = Math.sqrt(dirX * dirX + dirY * dirY) || 1;
1409
+ const normX = dirX / length;
1410
+ const normY = dirY / length;
1411
+ const clusterX = parentX + normX * clusterDistance;
1412
+ const clusterY = parentY + normY * clusterDistance;
1413
+ children.forEach((childId) => {
1414
+ const childNode = this.data.nodes.find((n) => n.id === childId);
1415
+ 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;
1420
+ }
1421
+ });
1422
+ }
1423
+ }
1424
+ }
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
+ });
1431
+ }
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
+ if (wasOpened || options?.triggerFocus) this.transformCanvas.focusOn(() => {
1442
+ const t = this.transformCanvas.getTransform();
1443
+ const _node = this.getNodeById(node.id);
1444
+ return {
1445
+ x: _node?.x,
1446
+ y: _node?.y,
1447
+ scale: t.scale
1448
+ };
1449
+ });
1450
+ } else if (!node && this.selectedNode) {
1451
+ this.selectedNode = null;
1452
+ this.emitter.emit("selected-node-changed", null);
1453
+ this.subGraph = {
1454
+ nodes: [],
1455
+ links: []
1456
+ };
1457
+ this.updateRenderLayers();
1458
+ this.updateScene();
1459
+ }
1460
+ };
1461
+ updateScene = () => {
1462
+ if (this.isTicking) return;
1463
+ this.onDraw();
1464
+ };
1465
+ handleMove = (x, y) => {
1466
+ const world = this.screenToWorld(x, y);
1467
+ const node = this.simulation?.find(world.x, world.y, 10) || null;
1468
+ if (node?.state?.sessionNode) return;
1469
+ if (this.hoveredNode === node) return;
1470
+ this.hoveredNode = node;
1471
+ this.emitter.emit("hovered-node-changed", node);
1472
+ this.canvas.style.cursor = node ? "pointer" : "default";
1473
+ this.updateScene();
1474
+ };
1475
+ handleTransform = (t) => {
1476
+ this.emitter.emit("transform-changed", t);
1477
+ this.updateScene();
1478
+ };
1479
+ updateHighlights = () => {
1480
+ const detachedHighlights = this.highlights.filter((h) => h.isDetached);
1481
+ const validSessionIds = new Set();
1482
+ let focusSessionNode = null;
1483
+ detachedHighlights.forEach((h) => {
1484
+ const highlightedNode = this.data.nodes.find((n) => n.id === h.id);
1485
+ 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
+ });
1489
+ if (!existingLink) return;
1490
+ const id = isSimNode(existingLink.source) ? existingLink.source.id : existingLink.source.toString();
1491
+ const parentNode = this.getNodeById(id);
1492
+ if (!parentNode) return;
1493
+ const _sessionId = h.sessionId || parentNode.id;
1494
+ const sessionId = parentNode.state?.sessionNode ? parentNode.id : `${VOID_DETACH_ID}-${_sessionId}`;
1495
+ validSessionIds.add(sessionId);
1496
+ let sessionNode = this.data.nodes.find((n) => n.id === sessionId);
1497
+ if (!sessionNode) {
1498
+ 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);
1501
+ sessionNode = {
1502
+ id: sessionId,
1503
+ state: {
1504
+ collapsed: false,
1505
+ image: void 0,
1506
+ sessionNode: true
1507
+ },
1508
+ depth,
1509
+ clusterSize: 1,
1510
+ x: circlePos.x,
1511
+ y: circlePos.y
1512
+ };
1513
+ this.data.nodes.push(sessionNode);
1514
+ this.data.links.push({
1515
+ target: sessionNode,
1516
+ source: parentNode
1517
+ });
1518
+ }
1519
+ const existingLinkIndex = this.data.links.findIndex((l) => l === existingLink);
1520
+ this.data.links.splice(existingLinkIndex, 1);
1521
+ this.data.links.push({
1522
+ target: highlightedNode,
1523
+ source: sessionNode
1524
+ });
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;
1529
+ });
1530
+ if (highlightedNode.state) highlightedNode.state.collapsed = false;
1531
+ if (!focusSessionNode) focusSessionNode = sessionNode;
1532
+ });
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);
1537
+ 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) => {
1540
+ const targetNode = isSimNode(link.target) ? link.target : this.getNodeById(link.target.toString());
1541
+ if (targetNode) this.data.links.push({
1542
+ source: parentNode,
1543
+ target: targetNode
1544
+ });
1545
+ });
1546
+ const index$1 = this.data.nodes.findIndex((n) => n.id === sessionNode.id);
1547
+ if (index$1 !== -1) this.data.nodes.splice(index$1, 1);
1548
+ });
1549
+ if (this.selectedNode) this.subGraph = getNodeSubgraph(this.selectedNode.id, this.data.nodes, this.data.links, this.rootId);
1550
+ this.restart();
1551
+ if (focusSessionNode) this.transformCanvas.focusOn(() => {
1552
+ if (!focusSessionNode) return null;
1553
+ const t = this.transformCanvas.getTransform();
1554
+ const _node = this.getNodeById(focusSessionNode.id);
1555
+ if (!_node) return null;
1556
+ return {
1557
+ x: _node?.x,
1558
+ y: _node?.y,
1559
+ scale: t.scale
1560
+ };
1561
+ });
1562
+ };
1563
+ initialize = (data, rootId) => {
1564
+ this.rawData = data;
1565
+ 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;
1576
+ return {
1577
+ ...n,
1578
+ state: {
1579
+ collapsed: hasOnlyLeafs(n.id, _links) && getChildren(n.id, _links).length > 1,
1580
+ ...existingData?.state
1581
+ },
1582
+ clusterSize,
1583
+ depth,
1584
+ x,
1585
+ y
1586
+ };
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;
1594
+ 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;
1599
+ });
1600
+ if (!data.nodes.find((n) => n.id === this.rootId)) _nodes.push({
1601
+ id: this.rootId,
1602
+ state: {
1603
+ collapsed: false,
1604
+ image: void 0
1605
+ },
1606
+ depth: -1,
1607
+ clusterSize: 1,
1608
+ x: this.center.x,
1609
+ y: this.center.y
1610
+ });
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({
1614
+ source: this.rootId,
1615
+ target: node.id
1616
+ });
1617
+ this.maxDepth = Math.max(..._nodes.map((n) => n.depth || 0));
1618
+ this.data = {
1619
+ nodes: _nodes,
1620
+ links: _links
1621
+ };
1622
+ this.loadNodeImages();
1623
+ this.updateHighlights();
1624
+ this.triggerSelected(true);
1625
+ };
1626
+ get lockedNode() {
1627
+ return this.lockedNodeId ? this.getNodeById(this.lockedNodeId) : null;
1628
+ }
1629
+ triggerSelected = (triggerFocus = false) => {
1630
+ if (this.selectedNode) this.handleClickNode(this.selectedNode, {
1631
+ noToggle: true,
1632
+ triggerFocus
1633
+ });
1634
+ else if (this.lockedNode) this.handleClickNode(this.lockedNode, {
1635
+ noToggle: true,
1636
+ triggerFocus
1637
+ });
1638
+ else this.setSelectedNode(null);
1639
+ };
1640
+ restart = (alpha = .1) => {
1641
+ this.tickCount = 0;
1642
+ this.prunedData = getPrunedData(this.rootId, this.data.nodes, this.data.links, this.highlights);
1643
+ 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]);
1645
+ if (this.simulation) {
1646
+ this.simulation.stop();
1647
+ this.simulation.on("tick", null);
1648
+ this.simulation.on("end", null);
1649
+ }
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) => {
1651
+ const size = this.getNodeSize(isSimNode(l.target) ? l.target.id : l.target.toString());
1652
+ if (isSimNode(l.target)) {
1653
+ const state = l.target?.state;
1654
+ if (!state?.collapsed) return size;
1655
+ }
1656
+ return size * 3;
1657
+ }).strength((l) => {
1658
+ return [.66, .08];
1659
+ })).force("charge", forceManyBody().strength((node) => {
1660
+ return -150;
1661
+ })).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);
1677
+ };
1678
+ get rootNode() {
1679
+ return this.data.nodes.find((n) => n.id === this.rootId) || null;
1680
+ }
1681
+ handleTick = () => {
1682
+ this.isTicking = true;
1683
+ this.onDraw();
1684
+ this.tickCount++;
1685
+ };
1686
+ setTranslate({ x, y }) {
1687
+ this.translate = {
1688
+ x,
1689
+ y
1690
+ };
1691
+ this.transformCanvas.setOffset(this.translate);
1692
+ }
1693
+ get visiblityScale() {
1694
+ return scaleLog().domain(this.clusterSizeRange).range([1.5, .9]).clamp(true);
1695
+ }
1696
+ get color() {
1697
+ return color(this.theme === "light" ? this.config.theme.dark : this.config.theme.light);
1698
+ }
1699
+ get colorContrast() {
1700
+ return color(this.theme === "light" ? this.config.theme.light : this.config.theme.dark);
1701
+ }
1702
+ getNodeSize = (nodeId) => {
1703
+ const { nodeSize } = this.config;
1704
+ const highlight = this.highlights.find((h) => h.id === nodeId);
1705
+ const sizeScale = highlight?.scale || 1;
1706
+ if (nodeId === this.rootId) return nodeSize * 2 * sizeScale;
1707
+ const node = this.data.nodes.find((n) => n.id === nodeId);
1708
+ 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);
1713
+ }
1714
+ const isSelected = this.selectedNode?.id === nodeId;
1715
+ const isLiquidated = node?.status === "LIQUIDATED" || node?.status === "REGENERATED";
1716
+ const _size = isLiquidated ? nodeSize * .2 : nodeSize;
1717
+ return isSelected ? _size * 4 : _size * sizeScale;
1718
+ };
1719
+ updateRenderLayers() {
1720
+ 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);
1723
+ };
1724
+ this.renderLayers.nodes.regular = [];
1725
+ this.renderLayers.nodes.highlighted = [];
1726
+ this.prunedData.nodes.forEach((node) => {
1727
+ if (isHighlighted(node.id)) this.renderLayers.nodes.highlighted.push(node);
1728
+ else this.renderLayers.nodes.regular.push(node);
1729
+ });
1730
+ this.renderLayers.links.regular = [];
1731
+ this.renderLayers.links.highlighted = [];
1732
+ const isLinkInSubgraph = (link) => {
1733
+ 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
+ });
1741
+ };
1742
+ const isLinkInHighlights = (link) => {
1743
+ const sourceId = isSimNode(link.source) ? link.source.id : link.source;
1744
+ 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));
1746
+ };
1747
+ this.prunedData.links.forEach((link) => {
1748
+ const inSubgraph = isLinkInSubgraph(link);
1749
+ const inHighlights = isLinkInHighlights(link);
1750
+ if (inSubgraph || inHighlights) this.renderLayers.links.highlighted.push(link);
1751
+ else this.renderLayers.links.regular.push(link);
1752
+ });
1753
+ this.renderLayers.links.highlighted.sort((a, b) => {
1754
+ const inSubgraphA = isLinkInSubgraph(a);
1755
+ const inSubgraphB = isLinkInSubgraph(b);
1756
+ if (inSubgraphA || inSubgraphB) return 2;
1757
+ return -1;
1758
+ });
1759
+ 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;
1763
+ if (a.id === this.selectedNode?.id || b.id === this.selectedNode?.id) return 2;
1764
+ if (highlightA?.onTop || highlightB?.onTop) return 1;
1765
+ return -1;
1766
+ });
1767
+ }
1768
+ renderLink(ctx, link, options) {
1769
+ 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);
1772
+ const isLight = this.theme === "light";
1773
+ const { dim: _dim, hasSelection, highlight } = options;
1774
+ let stroke = _dim ? this.color(dim(.09, isLight))() : hasSelection ? this.color(dim(.4, isLight))() : this.color(dim(.18, isLight))();
1775
+ ctx.globalAlpha = highlight ? 1 : .5;
1776
+ const sx = sourceNode && sourceNode.x || 0;
1777
+ const sy = sourceNode && sourceNode.y || 0;
1778
+ const tx = isSimNode(link.target) && link.target.x || 0;
1779
+ const ty = isSimNode(link.target) && link.target.y || 0;
1780
+ if (highlight?.linkColor) {
1781
+ const gradient = ctx.createLinearGradient(sx, sy, tx, ty);
1782
+ gradient.addColorStop(0, stroke);
1783
+ gradient.addColorStop(1, color(highlight.linkColor)());
1784
+ ctx.strokeStyle = gradient;
1785
+ } else ctx.strokeStyle = stroke;
1786
+ ctx.lineWidth = _dim ? .3 : .8;
1787
+ ctx.beginPath();
1788
+ ctx.moveTo(sx, sy);
1789
+ ctx.lineTo(tx, ty);
1790
+ ctx.stroke();
1791
+ ctx.closePath();
1792
+ }
1793
+ renderNode(ctx, node, options) {
1794
+ const x = node.x || 0;
1795
+ const y = node.y || 0;
1796
+ const isSelected = this.selectedNode?.id === node.id;
1797
+ const isHovered = this.hoveredNode?.id === node.id;
1798
+ const isCollapsed = !!node.state?.collapsed;
1799
+ const isLiquidated = node.status === "LIQUIDATED" || node.status === "REGENERATED";
1800
+ const isLight = this.theme === "light";
1801
+ const { dim: _dim, transform } = options;
1802
+ const fill = _dim ? this.color(dim(.075, isLight))() : isCollapsed ? this.color(dim(.18, isLight))() : isHovered ? this.color(dim(.4, isLight))() : this.color();
1803
+ const stroke = this.colorContrast();
1804
+ const nodeSize = this.getNodeSize(node.id);
1805
+ const highlight = this.highlights.find((h) => h.id === node.id);
1806
+ const highlighted = !!highlight;
1807
+ let highlightedStroke = _dim ? color(highlight?.strokeColor || red)(dim(.4, isLight))() : color(highlight?.strokeColor || red)();
1808
+ if (node.id === this.rootId) this.renderRootNode(ctx, x, y, nodeSize, _dim, isLight);
1809
+ else if (node.state?.sessionNode) this.renderSessionNode(ctx, x, y, nodeSize);
1810
+ else if (isCollapsed) this.renderCollapsedNode(ctx, x, y, nodeSize, {
1811
+ fill,
1812
+ stroke,
1813
+ highlighted,
1814
+ highlightedStroke,
1815
+ isSelected,
1816
+ dim: _dim,
1817
+ transform,
1818
+ clusterSize: node.clusterSize || 1,
1819
+ isLight
1820
+ });
1821
+ else this.renderExpandedNode(ctx, x, y, nodeSize, {
1822
+ fill,
1823
+ stroke,
1824
+ highlighted,
1825
+ highlightedStroke,
1826
+ isHovered,
1827
+ isLiquidated,
1828
+ dim: _dim,
1829
+ image: node.state?.image
1830
+ });
1831
+ }
1832
+ renderSessionNode(ctx, x, y, size) {
1833
+ const isLight = this.theme === "light";
1834
+ circle(ctx, x, y, size / 2, {
1835
+ stroke: true,
1836
+ strokeStyle: this.colorContrast(),
1837
+ lineWidth: .2,
1838
+ fill: true,
1839
+ fillStyle: this.color(dim(.18, isLight))()
1840
+ });
1841
+ }
1842
+ renderRootNode(ctx, x, y, size, dimmed, isLight) {
1843
+ circle(ctx, x, y, size / 2, {
1844
+ stroke: false,
1845
+ strokeStyle: this.colorContrast(),
1846
+ lineWidth: .2,
1847
+ fill: true,
1848
+ fillStyle: this.color(dim(.18, isLight))()
1849
+ });
1850
+ if (this.rootImages) {
1851
+ const _idx = Math.min(isLight ? 0 : 1, this.rootImages.length - 1);
1852
+ const _img = this.rootImages[_idx];
1853
+ const _imgSize = size * .55;
1854
+ if (_img) img(ctx, _img, x - _imgSize / 2, y - _imgSize / 2, _imgSize, _imgSize, 0, dimmed ? .1 : 1);
1855
+ }
1856
+ }
1857
+ renderCollapsedNode(ctx, x, y, size, options) {
1858
+ const { fill, stroke, highlighted, highlightedStroke, isSelected, dim: _dim, transform, clusterSize, isLight } = options;
1859
+ if (highlighted) {
1860
+ const _size = size + 4;
1861
+ circle(ctx, x, y, _size / 2, {
1862
+ stroke: true,
1863
+ strokeStyle: highlightedStroke,
1864
+ lineWidth: 1,
1865
+ fill: false,
1866
+ fillStyle: fill
1867
+ });
1868
+ }
1869
+ circle(ctx, x, y, size / 2, {
1870
+ stroke: true,
1871
+ strokeStyle: stroke,
1872
+ lineWidth: isSelected ? 1 : .2,
1873
+ fill: true,
1874
+ fillStyle: fill
1875
+ });
1876
+ const showLabel = transform.scale >= this.visiblityScale(clusterSize) ? 1 : 0;
1877
+ if (showLabel) {
1878
+ ctx.font = `${14 / transform.scale}px Sans-Serif`;
1879
+ ctx.textAlign = "center";
1880
+ ctx.textBaseline = "middle";
1881
+ ctx.fillStyle = this.color(dim(_dim ? .2 : .5, isLight))();
1882
+ ctx.fillText(clusterSize.toString(), x, y);
1883
+ }
1884
+ }
1885
+ renderExpandedNode(ctx, x, y, size, options) {
1886
+ const { fill, highlighted, highlightedStroke, isHovered, isLiquidated, dim: _dim, image } = options;
1887
+ const _size = size + 1;
1888
+ rect(ctx, x - _size / 2, y - _size / 2, _size, _size, {
1889
+ stroke: highlighted || isHovered,
1890
+ strokeStyle: isHovered ? fill : highlightedStroke,
1891
+ lineWidth: .5,
1892
+ fill: this.hideThumbnails || isLiquidated || !image,
1893
+ fillStyle: fill,
1894
+ borderRadius: 1
1895
+ });
1896
+ if (image && !this.hideThumbnails && !isLiquidated) img(ctx, image, x - size / 2, y - size / 2, size, size, 1, _dim ? .1 : 1, fill);
1897
+ }
1898
+ onDraw = () => {
1899
+ const context = this.canvas?.getContext("2d");
1900
+ const transform = this.transformCanvas.getTransform();
1901
+ if (!context) return;
1902
+ const dpi = devicePixelRatio || 1;
1903
+ context.save();
1904
+ context.scale(dpi, dpi);
1905
+ context.clearRect(0, 0, this.width, this.height);
1906
+ context.setTransform(transform.scale, 0, 0, transform.scale, transform.x, transform.y);
1907
+ context.translate(this.origin.x, this.origin.y);
1908
+ context.save();
1909
+ const hasSelection = !!this.selectedNode;
1910
+ 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);
1914
+ this.renderLink(context, link, {
1915
+ dim: _dim,
1916
+ hasSelection
1917
+ });
1918
+ });
1919
+ context.globalAlpha = 1;
1920
+ this.renderLayers.nodes.regular.forEach((node) => {
1921
+ const _dim = hasSelection && !this.subGraph.nodes.some((n) => n.id === node.id);
1922
+ this.renderNode(context, node, {
1923
+ dim: _dim,
1924
+ transform
1925
+ });
1926
+ });
1927
+ 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);
1932
+ this.renderLink(context, link, {
1933
+ dim: _dim,
1934
+ hasSelection,
1935
+ highlight
1936
+ });
1937
+ });
1938
+ context.globalAlpha = 1;
1939
+ this.renderLayers.nodes.highlighted.forEach((node) => {
1940
+ const _dim = hasSelection && !this.subGraph.nodes.some((n) => n.id === node.id);
1941
+ this.renderNode(context, node, {
1942
+ dim: _dim,
1943
+ transform
1944
+ });
1945
+ });
1946
+ context.restore();
1947
+ context.restore();
1948
+ this.transformCanvas.trackCursor();
1949
+ };
1950
+ drawDebug(context) {
1951
+ const transform = this.transformCanvas.getTransform();
1952
+ context.font = `14px Sans-Serif`;
1953
+ context.textAlign = "left";
1954
+ context.fillStyle = "#000000";
1955
+ context.fillText(`${transform.x}, ${transform.y}, ${transform.scale}`, 0, 15);
1956
+ const center = this.worldToScreen(this.rootNode?.x, this.rootNode?.y);
1957
+ context.fillText(`${center.x}, ${center.y}`, 0, 30);
1958
+ }
1959
+ drawDebugDepthCircles(context) {
1960
+ const transform = this.transformCanvas.getTransform();
1961
+ for (let i = 0; i < this.maxDepth; i++) {
1962
+ const depth = i;
1963
+ const r = getRadius(depth);
1964
+ const x = this.center.x;
1965
+ const y = this.center.y;
1966
+ circle(context, x, y, r, {
1967
+ fill: false,
1968
+ stroke: true,
1969
+ strokeStyle: "#00ff00"
1970
+ });
1971
+ context.font = `${40 / transform.scale}px Sans-Serif`;
1972
+ context.textAlign = "center";
1973
+ context.textBaseline = "middle";
1974
+ context.fillStyle = this.color();
1975
+ context.fillText(depth.toString(), x + r, y);
1976
+ }
1977
+ }
1978
+ onEnd = () => {
1979
+ this.isTicking = false;
1980
+ };
1981
+ loadNodeImages = () => {
1982
+ this.data.nodes.forEach((node) => {
1983
+ if (node.id === this.rootId) return;
1984
+ if (node.imgSrc && this.imageCache.get(node.imgSrc)) {
1985
+ node.state = node.state || {};
1986
+ node.state.image = this.imageCache.get(node.imgSrc);
1987
+ return;
1988
+ }
1989
+ const loadImage = async () => {
1990
+ const src = this.loadNodeImage ? await this.loadNodeImage(node) : node.imgSrc;
1991
+ if (!src) return;
1992
+ const html = this.imageCache.get(src) || await loadHTMLImageElement(src);
1993
+ this.imageCache.set(src, html);
1994
+ node.state = node.state || {};
1995
+ node.state.image = html;
1996
+ };
1997
+ loadImage();
1998
+ });
1999
+ };
2000
+ destroy = () => {
2001
+ this.simulation?.stop();
2002
+ this.simulation?.on("tick", null);
2003
+ this.simulation?.on("end", null);
2004
+ this.transformCanvas.destroy();
2005
+ };
2006
+ resize = (width, height) => {
2007
+ this.width = width;
2008
+ this.height = height;
2009
+ this.updateScene();
2010
+ };
2011
+ setTheme = (theme) => {
2012
+ this.theme = theme;
2013
+ this.updateScene();
2014
+ };
2015
+ setHideThumbnails = (hide) => {
2016
+ this.hideThumbnails = hide;
2017
+ this.updateScene();
2018
+ };
2019
+ setSelectedNode = (node) => {
2020
+ this.selectedNode = node;
2021
+ this.updateRenderLayers();
2022
+ this.updateScene();
2023
+ };
2024
+ _highlightHash;
2025
+ setHighlights = (highlights) => {
2026
+ const hash = quickHash(JSON.stringify(highlights));
2027
+ if (hash === this._highlightHash) return;
2028
+ this._highlightHash = hash;
2029
+ this.highlights = highlights;
2030
+ this.updateHighlights();
2031
+ };
2032
+ setNoInteraction = (noInteraction) => {
2033
+ this.noInteraction = noInteraction;
2034
+ this.transformCanvas.setNoInteraction(this.noInteraction);
2035
+ this.updateScene();
2036
+ };
2037
+ getNodeById = (nodeId) => {
2038
+ return this.data.nodes.find((n) => n.id === nodeId) || null;
2039
+ };
2040
+ setLockedNodeId = (nodeId) => {
2041
+ this.lockedNodeId = nodeId || void 0;
2042
+ };
2043
+ handleClickDebug = (x, y) => {
2044
+ const p = this.screenToWorld(x, y);
2045
+ this.circles.push(p);
2046
+ this.transformCanvas.focusOn(() => {
2047
+ const circle$1 = this.circles[this.circles.length - 1];
2048
+ return {
2049
+ x: circle$1.x,
2050
+ y: circle$1.y,
2051
+ scale: this.transformCanvas.getTransform().scale
2052
+ };
2053
+ });
2054
+ this.updateScene();
2055
+ };
2056
+ circles = [];
2057
+ onDrawDebug = () => {
2058
+ const context = this.canvas?.getContext("2d");
2059
+ const transform = this.transformCanvas.getTransform();
2060
+ if (!context) return;
2061
+ const dpi = devicePixelRatio || 1;
2062
+ context.save();
2063
+ context.scale(dpi, dpi);
2064
+ context.clearRect(0, 0, this.width, this.height);
2065
+ context.setTransform(transform.scale, 0, 0, transform.scale, transform.x, transform.y);
2066
+ context.translate(this.origin.x, this.origin.y);
2067
+ context.save();
2068
+ this.circles.map((c) => {
2069
+ circle(context, c.x, c.y, 5, {
2070
+ fill: true,
2071
+ fillStyle: "#ff0000",
2072
+ stroke: false
2073
+ });
2074
+ });
2075
+ circle(context, 0, 0, 5, {
2076
+ fill: true,
2077
+ fillStyle: "#ff0000",
2078
+ stroke: false
2079
+ });
2080
+ context.restore();
2081
+ context.restore();
2082
+ };
2083
+ };
2084
+
2085
+ //#endregion
2086
+ //#region src/components/OpenFormGraph.tsx
2087
+ function OpenFormGraph(props) {
2088
+ const { width, height, highlights = [], className, noInteraction = false, loadNodeImage, translate, onTransform } = props;
2089
+ const { simulation, data, rootId, rootImageSources, theme, hideThumbnails, setHoveredNode, setSelectedNode, lockedNodeId } = useOpenFormGraph();
2090
+ const canvasRef = useRef(null);
2091
+ useEffect(() => {
2092
+ if (!canvasRef.current) return;
2093
+ simulation.current = new OpenGraphSimulation({
2094
+ width,
2095
+ height,
2096
+ canvas: canvasRef.current,
2097
+ rootImageSources,
2098
+ loadNodeImage,
2099
+ theme,
2100
+ translate,
2101
+ lockedNodeId
2102
+ });
2103
+ simulation.current.emitter.on("selected-node-changed", setSelectedNode);
2104
+ simulation.current.emitter.on("hovered-node-changed", setHoveredNode);
2105
+ return () => {
2106
+ simulation.current?.emitter.off("selected-node-changed", setSelectedNode);
2107
+ simulation.current?.emitter.off("hovered-node-changed", setHoveredNode);
2108
+ simulation.current?.destroy();
2109
+ };
2110
+ }, []);
2111
+ useEffect(() => {
2112
+ if (!simulation.current) return;
2113
+ if (!onTransform) return;
2114
+ simulation.current.emitter.on("transform-changed", onTransform);
2115
+ return () => {
2116
+ simulation.current?.emitter.off("transform-changed", onTransform);
2117
+ };
2118
+ }, [onTransform]);
2119
+ useEffect(() => {
2120
+ if (!simulation.current) return;
2121
+ simulation.current.resize(width, height);
2122
+ }, [width, height]);
2123
+ useEffect(() => {
2124
+ if (!simulation.current || !theme) return;
2125
+ simulation.current.setTheme(theme);
2126
+ }, [theme]);
2127
+ useEffect(() => {
2128
+ if (!simulation.current) return;
2129
+ simulation.current.setHideThumbnails(hideThumbnails);
2130
+ }, [hideThumbnails]);
2131
+ useEffect(() => {
2132
+ if (!simulation.current) return;
2133
+ simulation.current.setHighlights(highlights);
2134
+ }, [highlights]);
2135
+ useEffect(() => {
2136
+ if (!simulation.current) return;
2137
+ simulation.current.setNoInteraction(noInteraction);
2138
+ }, [noInteraction]);
2139
+ useEffect(() => {
2140
+ if (!simulation.current) return;
2141
+ simulation.current.initialize(data, rootId);
2142
+ }, [data]);
2143
+ useEffect(() => {
2144
+ if (!simulation.current) return;
2145
+ if (!translate) return;
2146
+ simulation.current.setTranslate(translate);
2147
+ }, [translate?.y, translate?.x]);
2148
+ useEffect(() => {
2149
+ if (!simulation.current) return;
2150
+ if (simulation.current.lockedNodeId === lockedNodeId) return;
2151
+ simulation.current.setLockedNodeId(lockedNodeId);
2152
+ }, [translate?.y, translate?.x]);
2153
+ const dpi = devicePixelRatio || 1;
2154
+ return /* @__PURE__ */ jsx("canvas", {
2155
+ onMouseEnter: props.onMouseEnter,
2156
+ onMouseLeave: props.onMouseLeave,
2157
+ ref: canvasRef,
2158
+ className,
2159
+ width: `${width * dpi}px`,
2160
+ height: `${height * dpi}px`,
2161
+ style: {
2162
+ width: `${width}px`,
2163
+ height: `${height}px`
2164
+ }
2165
+ });
2166
+ }
2167
+
2168
+ //#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