@fxhash/open-form-graph 0.0.1 → 0.0.2

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,1277 @@
1
+ import { DEFAULT_GRAPH_CONFIG, VOID_ROOT_ID, useOpenFormGraph } from "./provider-CTDz6ZQd.js";
2
+ import { useEffect, useRef } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ import { forceCenter, forceLink, forceManyBody, forceSimulation } from "d3-force";
5
+ import { scaleLinear, scaleLog } from "d3-scale";
6
+
7
+ //#region src/util/types.ts
8
+ function isSimNode(node) {
9
+ return typeof node === "object" && "id" in node;
10
+ }
11
+ function isSimLink(link) {
12
+ return typeof link === "object" && "source" in link && typeof link.source !== "string";
13
+ }
14
+
15
+ //#endregion
16
+ //#region src/util/graph.ts
17
+ function getNodeId(n) {
18
+ return typeof n === "object" && n !== null && "id" in n ? n.id : n;
19
+ }
20
+ function getParents(id, links) {
21
+ return links.filter((l) => {
22
+ const targetId = isSimNode(l.target) ? l.target.id : l.target;
23
+ return targetId === id;
24
+ }).map((link) => isSimNode(link.source) ? link.source.id : link.source.toString());
25
+ }
26
+ function getChildren(id, links) {
27
+ return links.filter((l) => {
28
+ const sourceId = isSimNode(l.source) ? l.source.id : l.source;
29
+ return sourceId === id;
30
+ }).map((link) => link.target.toString());
31
+ }
32
+ function getClusterSize(id, links) {
33
+ const children = getChildren(id, links);
34
+ return children.reduce((acc, childId) => {
35
+ return acc + getClusterSize(childId, links);
36
+ }, children.length || 0);
37
+ }
38
+ function hasOnlyLeafs(id, links) {
39
+ const children = getChildren(id, links);
40
+ return children.every((childId) => getChildren(childId, links).length === 0);
41
+ }
42
+ function getNodeSubgraph(nodeId, nodes, links, rootId) {
43
+ const nodesById = Object.fromEntries(nodes.map((n) => [n.id, n]));
44
+ const parentSet = new Set();
45
+ const childSet = new Set();
46
+ const subLinks = new Set();
47
+ let currentId = nodeId;
48
+ while (currentId !== rootId) {
49
+ const parentLink = links.find((l) => {
50
+ const targetId = isSimNode(l.target) ? l.target.id : l.target;
51
+ return targetId === currentId;
52
+ });
53
+ if (!parentLink) break;
54
+ const parentId = isSimNode(parentLink.source) ? parentLink.source.id : parentLink.source;
55
+ if (parentSet.has(parentId.toString())) break;
56
+ parentSet.add(parentId.toString());
57
+ subLinks.add(parentLink);
58
+ currentId = parentId.toString();
59
+ }
60
+ function collectChildren(id) {
61
+ for (const link of links) {
62
+ const sourceId = isSimNode(link.source) ? link.source.id : link.source;
63
+ const targetId = isSimNode(link.target) ? link.target.id : link.target;
64
+ if (sourceId === id && !childSet.has(targetId.toString())) {
65
+ childSet.add(targetId.toString());
66
+ subLinks.add(link);
67
+ collectChildren(targetId.toString());
68
+ }
69
+ }
70
+ }
71
+ collectChildren(nodeId);
72
+ const validIds = new Set([
73
+ ...parentSet,
74
+ nodeId,
75
+ ...childSet
76
+ ]);
77
+ const filteredLinks = Array.from(subLinks).filter((link) => {
78
+ const sourceId = isSimNode(link.source) ? link.source.id : link.source;
79
+ const targetId = isSimNode(link.target) ? link.target.id : link.target;
80
+ return validIds.has(sourceId.toString()) && validIds.has(targetId.toString());
81
+ });
82
+ const allNodeIds = Array.from(validIds);
83
+ const subNodes = allNodeIds.map((id) => nodesById[id]).filter(Boolean);
84
+ return {
85
+ nodes: subNodes,
86
+ links: filteredLinks
87
+ };
88
+ }
89
+
90
+ //#endregion
91
+ //#region src/util/img.ts
92
+ function loadImage(src) {
93
+ return new Promise((resolve, reject) => {
94
+ const img$1 = new Image();
95
+ img$1.onload = () => resolve(img$1);
96
+ img$1.onerror = reject;
97
+ img$1.src = src;
98
+ });
99
+ }
100
+
101
+ //#endregion
102
+ //#region src/sim/TransformCanvas.ts
103
+ const MIN_ZOOM = .1;
104
+ const MAX_ZOOM = 10;
105
+ const CLICK_THRESHOLD = 5;
106
+ var TransformCanvas = class {
107
+ canvas;
108
+ transform = {
109
+ x: 0,
110
+ y: 0,
111
+ scale: 1
112
+ };
113
+ targetTransform = {
114
+ x: 0,
115
+ y: 0,
116
+ scale: 1
117
+ };
118
+ isAnimating = false;
119
+ animationFrame = null;
120
+ isDragging = false;
121
+ dragStart = null;
122
+ moved = false;
123
+ lastMousePos = {
124
+ x: 0,
125
+ y: 0
126
+ };
127
+ lastMoveMousePos = {
128
+ x: 0,
129
+ y: 0
130
+ };
131
+ zoomFocus = {
132
+ x: 0,
133
+ y: 0
134
+ };
135
+ onUpdate;
136
+ onClick;
137
+ onMove;
138
+ noInteraction = false;
139
+ touchStart = null;
140
+ lastTouchPos = {
141
+ x: 0,
142
+ y: 0
143
+ };
144
+ pinchStartDist = null;
145
+ pinchStartScale = 1;
146
+ focus = null;
147
+ constructor(canvas, options) {
148
+ this.canvas = canvas;
149
+ this.onUpdate = options?.onUpdate;
150
+ this.onClick = options?.onClick;
151
+ this.onMove = options?.onMove;
152
+ this.handleWheel = this.handleWheel.bind(this);
153
+ this.handleMouseDown = this.handleMouseDown.bind(this);
154
+ this.handleMouseMove = this.handleMouseMove.bind(this);
155
+ this.handleMouseUp = this.handleMouseUp.bind(this);
156
+ this.handleMouseUpCanvas = this.handleMouseUpCanvas.bind(this);
157
+ canvas.addEventListener("wheel", this.handleWheel, { passive: false });
158
+ canvas.addEventListener("mousedown", this.handleMouseDown);
159
+ window.addEventListener("mousemove", this.handleMouseMove);
160
+ window.addEventListener("mouseup", this.handleMouseUp);
161
+ canvas.addEventListener("mouseup", this.handleMouseUpCanvas);
162
+ this.handleTouchStart = this.handleTouchStart.bind(this);
163
+ this.handleTouchMove = this.handleTouchMove.bind(this);
164
+ this.handleTouchEnd = this.handleTouchEnd.bind(this);
165
+ canvas.addEventListener("touchstart", this.handleTouchStart, { passive: false });
166
+ canvas.addEventListener("touchmove", this.handleTouchMove, { passive: false });
167
+ canvas.addEventListener("touchend", this.handleTouchEnd, { passive: false });
168
+ canvas.addEventListener("touchcancel", this.handleTouchEnd, { passive: false });
169
+ }
170
+ lerp(a, b, t) {
171
+ return a + (b - a) * t;
172
+ }
173
+ animateTransform = () => {
174
+ const speed = .05;
175
+ const prev = this.transform;
176
+ const target = this.focus?.() || this.targetTransform;
177
+ const next = {
178
+ x: this.lerp(prev.x, target.x, speed),
179
+ y: this.lerp(prev.y, target.y, speed),
180
+ scale: this.lerp(prev.scale, target.scale, speed)
181
+ };
182
+ const done = Math.abs(next.x - target.x) < .5 && Math.abs(next.y - target.y) < .5 && Math.abs(next.scale - target.scale) < .001;
183
+ if (done) {
184
+ this.transform = { ...target };
185
+ this.isAnimating = false;
186
+ this.focus = null;
187
+ } else {
188
+ this.transform = next;
189
+ this.isAnimating = true;
190
+ }
191
+ this.onUpdate?.(this.transform);
192
+ if (this.isAnimating) this.animationFrame = requestAnimationFrame(this.animateTransform);
193
+ else if (this.animationFrame) {
194
+ cancelAnimationFrame(this.animationFrame);
195
+ this.animationFrame = null;
196
+ }
197
+ };
198
+ handleWheel(e) {
199
+ if (this.noInteraction) return;
200
+ if (this.isAnimating) {
201
+ this.isAnimating = false;
202
+ this.focus = null;
203
+ if (this.animationFrame) {
204
+ cancelAnimationFrame(this.animationFrame);
205
+ this.animationFrame = null;
206
+ }
207
+ }
208
+ e.preventDefault();
209
+ const rect$1 = this.canvas.getBoundingClientRect();
210
+ const mouseX = e.clientX - rect$1.left;
211
+ const mouseY = e.clientY - rect$1.top;
212
+ this.zoomFocus = {
213
+ x: mouseX,
214
+ y: mouseY
215
+ };
216
+ const scaleFactor = e.deltaY > 0 ? .95 : 1.05;
217
+ const newScale = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, this.targetTransform.scale * scaleFactor));
218
+ const { x, y, scale } = this.transform;
219
+ const dx = mouseX - x;
220
+ const dy = mouseY - y;
221
+ const scaleChange = newScale / scale;
222
+ const newX = mouseX - dx * scaleChange;
223
+ const newY = mouseY - dy * scaleChange;
224
+ this.targetTransform = {
225
+ x: newX,
226
+ y: newY,
227
+ scale: newScale
228
+ };
229
+ if (!this.isAnimating) {
230
+ this.isAnimating = true;
231
+ this.animationFrame = requestAnimationFrame(this.animateTransform);
232
+ }
233
+ }
234
+ handleMouseDown(e) {
235
+ if (this.noInteraction) return;
236
+ if (this.isAnimating) {
237
+ this.isAnimating = false;
238
+ this.focus = null;
239
+ if (this.animationFrame) {
240
+ cancelAnimationFrame(this.animationFrame);
241
+ this.animationFrame = null;
242
+ }
243
+ }
244
+ this.isDragging = true;
245
+ this.moved = false;
246
+ this.dragStart = {
247
+ x: e.clientX,
248
+ y: e.clientY
249
+ };
250
+ this.lastMousePos = {
251
+ x: e.clientX,
252
+ y: e.clientY
253
+ };
254
+ }
255
+ handleMouseMove(e) {
256
+ if (this.noInteraction) return;
257
+ const rect$1 = this.canvas.getBoundingClientRect();
258
+ const x = e.clientX - rect$1.left;
259
+ const y = e.clientY - rect$1.top;
260
+ this.lastMoveMousePos = {
261
+ x: e.clientX,
262
+ y: e.clientY
263
+ };
264
+ this.onMove?.(x, y);
265
+ if (!this.isDragging || !this.dragStart) return;
266
+ const dx = e.clientX - this.dragStart.x;
267
+ const dy = e.clientY - this.dragStart.y;
268
+ if (Math.abs(dx) > CLICK_THRESHOLD || Math.abs(dy) > CLICK_THRESHOLD) {
269
+ this.moved = true;
270
+ const deltaX = e.clientX - this.lastMousePos.x;
271
+ const deltaY = e.clientY - this.lastMousePos.y;
272
+ this.transform.x += deltaX;
273
+ this.transform.y += deltaY;
274
+ this.targetTransform.x += deltaX;
275
+ this.targetTransform.y += deltaY;
276
+ this.lastMousePos = {
277
+ x: e.clientX,
278
+ y: e.clientY
279
+ };
280
+ this.onUpdate?.(this.transform);
281
+ }
282
+ }
283
+ handleMouseUpCanvas(e) {
284
+ if (this.noInteraction) return;
285
+ if (!this.moved && this.onClick) {
286
+ const rect$1 = this.canvas.getBoundingClientRect();
287
+ const x = e.clientX - rect$1.left;
288
+ const y = e.clientY - rect$1.top;
289
+ this.onClick(x, y);
290
+ }
291
+ }
292
+ handleMouseUp(e) {
293
+ this.isDragging = false;
294
+ this.dragStart = null;
295
+ this.moved = false;
296
+ }
297
+ handleTouchStart(e) {
298
+ if (this.noInteraction) return;
299
+ if (this.isAnimating) {
300
+ this.isAnimating = false;
301
+ this.focus = null;
302
+ if (this.animationFrame) {
303
+ cancelAnimationFrame(this.animationFrame);
304
+ this.animationFrame = null;
305
+ }
306
+ }
307
+ if (e.touches.length === 1) {
308
+ const rect$1 = this.canvas.getBoundingClientRect();
309
+ const touch = e.touches[0];
310
+ this.isDragging = true;
311
+ this.moved = false;
312
+ this.touchStart = {
313
+ x: touch.clientX,
314
+ y: touch.clientY
315
+ };
316
+ this.lastTouchPos = {
317
+ x: touch.clientX,
318
+ y: touch.clientY
319
+ };
320
+ this.lastMoveMousePos = {
321
+ x: touch.clientX,
322
+ y: touch.clientY
323
+ };
324
+ } else if (e.touches.length === 2) {
325
+ this.isDragging = false;
326
+ const [t1, t2] = [e.touches[0], e.touches[1]];
327
+ this.pinchStartDist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
328
+ this.pinchStartScale = this.targetTransform.scale;
329
+ const rect$1 = this.canvas.getBoundingClientRect();
330
+ this.zoomFocus = {
331
+ x: (t1.clientX + t2.clientX) / 2 - rect$1.left,
332
+ y: (t1.clientY + t2.clientY) / 2 - rect$1.top
333
+ };
334
+ }
335
+ }
336
+ handleTouchMove(e) {
337
+ if (this.noInteraction) return;
338
+ e.preventDefault();
339
+ const rect$1 = this.canvas.getBoundingClientRect();
340
+ if (e.touches.length === 1 && this.touchStart) {
341
+ const touch = e.touches[0];
342
+ const dx = touch.clientX - this.touchStart.x;
343
+ const dy = touch.clientY - this.touchStart.y;
344
+ if (Math.abs(dx) > CLICK_THRESHOLD || Math.abs(dy) > CLICK_THRESHOLD) {
345
+ this.moved = true;
346
+ const deltaX = touch.clientX - this.lastTouchPos.x;
347
+ const deltaY = touch.clientY - this.lastTouchPos.y;
348
+ this.transform.x += deltaX;
349
+ this.transform.y += deltaY;
350
+ this.targetTransform.x += deltaX;
351
+ this.targetTransform.y += deltaY;
352
+ this.lastTouchPos = {
353
+ x: touch.clientX,
354
+ y: touch.clientY
355
+ };
356
+ this.lastMoveMousePos = {
357
+ x: touch.clientX,
358
+ y: touch.clientY
359
+ };
360
+ this.onUpdate?.(this.transform);
361
+ this.onMove?.(touch.clientX - rect$1.left, touch.clientY - rect$1.top);
362
+ }
363
+ } else if (e.touches.length === 2 && this.pinchStartDist != null) {
364
+ const [t1, t2] = [e.touches[0], e.touches[1]];
365
+ const dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
366
+ let scale = dist / this.pinchStartDist * this.pinchStartScale;
367
+ scale = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, scale));
368
+ const { x, y, scale: prevScale } = this.transform;
369
+ const dx = this.zoomFocus.x - x;
370
+ const dy = this.zoomFocus.y - y;
371
+ const scaleChange = scale / prevScale;
372
+ const newX = this.zoomFocus.x - dx * scaleChange;
373
+ const newY = this.zoomFocus.y - dy * scaleChange;
374
+ this.targetTransform = {
375
+ x: newX,
376
+ y: newY,
377
+ scale
378
+ };
379
+ if (!this.isAnimating) {
380
+ this.isAnimating = true;
381
+ this.animationFrame = requestAnimationFrame(this.animateTransform);
382
+ }
383
+ }
384
+ }
385
+ handleTouchEnd(e) {
386
+ if (this.noInteraction) return;
387
+ if (e.touches.length === 0) {
388
+ if (this.isDragging && !this.moved && this.onClick && this.touchStart) {
389
+ const rect$1 = this.canvas.getBoundingClientRect();
390
+ this.onClick(this.touchStart.x - rect$1.left, this.touchStart.y - rect$1.top);
391
+ }
392
+ this.isDragging = false;
393
+ this.touchStart = null;
394
+ this.moved = false;
395
+ this.pinchStartDist = null;
396
+ } else if (e.touches.length === 1) {
397
+ const touch = e.touches[0];
398
+ this.isDragging = true;
399
+ this.touchStart = {
400
+ x: touch.clientX,
401
+ y: touch.clientY
402
+ };
403
+ this.lastTouchPos = {
404
+ x: touch.clientX,
405
+ y: touch.clientY
406
+ };
407
+ this.pinchStartDist = null;
408
+ }
409
+ }
410
+ resetZoom() {
411
+ this.transform = {
412
+ x: 0,
413
+ y: 0,
414
+ scale: 1
415
+ };
416
+ this.targetTransform = {
417
+ x: 0,
418
+ y: 0,
419
+ scale: 1
420
+ };
421
+ this.onUpdate?.(this.transform);
422
+ }
423
+ transformTo(update) {
424
+ this.targetTransform = {
425
+ ...this.transform,
426
+ ...update
427
+ };
428
+ if (!this.isAnimating) {
429
+ this.isAnimating = true;
430
+ this.animationFrame = requestAnimationFrame(this.animateTransform);
431
+ }
432
+ }
433
+ trackCursor() {
434
+ const rect$1 = this.canvas.getBoundingClientRect();
435
+ const x = this.lastMoveMousePos.x - rect$1.left;
436
+ const y = this.lastMoveMousePos.y - rect$1.top;
437
+ this.onMove?.(x, y);
438
+ }
439
+ destroy() {
440
+ this.canvas.removeEventListener("wheel", this.handleWheel);
441
+ this.canvas.removeEventListener("mousedown", this.handleMouseDown);
442
+ this.canvas.removeEventListener("touchstart", this.handleTouchStart);
443
+ this.canvas.removeEventListener("touchmove", this.handleTouchMove);
444
+ this.canvas.removeEventListener("touchend", this.handleTouchEnd);
445
+ this.canvas.removeEventListener("touchcancel", this.handleTouchEnd);
446
+ this.canvas.removeEventListener("mouseup", this.handleMouseUpCanvas);
447
+ window.removeEventListener("mousemove", this.handleMouseMove);
448
+ window.removeEventListener("mouseup", this.handleMouseUp);
449
+ this.isAnimating = false;
450
+ this.focus = null;
451
+ if (this.animationFrame) {
452
+ cancelAnimationFrame(this.animationFrame);
453
+ this.animationFrame = null;
454
+ }
455
+ }
456
+ setNoInteraction = (noInteraction) => {
457
+ this.noInteraction = noInteraction;
458
+ };
459
+ focusOn = (focus) => {
460
+ this.focus = focus;
461
+ };
462
+ };
463
+
464
+ //#endregion
465
+ //#region src/util/canvas.ts
466
+ /**
467
+ * draws a circle on the canvas
468
+ * @param ctx - The canvas rendering context
469
+ * @param x - The x-coordinate of the circle's center
470
+ * @param y - The y-coordinate of the circle's center
471
+ * @param radius - The radius of the circle (default is 5)
472
+ * @param options - Optional parameters for styling the circle
473
+ * @param options.fill - Whether to fill the circle (default is true)
474
+ * @param options.fillStyle - The fill color of the circle
475
+ * @param options.stroke - Whether to stroke the circle (default is false)
476
+ * @param options.strokeStyle - The stroke color of the circle
477
+ * @param options.lineWidth - The width of the stroke (default is 0.2)
478
+ * @returns void
479
+ */
480
+ function circle(ctx, x, y, radius = 5, options) {
481
+ const { fill = true, fillStyle, stroke = false, strokeStyle, lineWidth = .2 } = options || {};
482
+ ctx.save();
483
+ if (fillStyle !== void 0) ctx.fillStyle = fillStyle;
484
+ if (strokeStyle !== void 0) ctx.strokeStyle = strokeStyle;
485
+ if (lineWidth !== void 0) ctx.lineWidth = lineWidth;
486
+ ctx.beginPath();
487
+ ctx.arc(x, y, radius, 0, 2 * Math.PI);
488
+ ctx.closePath();
489
+ if (fill) ctx.fill();
490
+ if (stroke) ctx.stroke();
491
+ ctx.restore();
492
+ }
493
+ /**
494
+ * draws a rectangle on the canvas
495
+ * @param ctx - The canvas rendering context
496
+ * @param x - The x-coordinate of the rectangle's top-left corner
497
+ * @param y - The y-coordinate of the rectangle's top-left corner
498
+ * @param width - The width of the rectangle
499
+ * @param height - The height of the rectangle
500
+ * @param options - Optional parameters for styling the rectangle
501
+ * @param options.fill - Whether to fill the rectangle (default is true)
502
+ * @param options.fillStyle - The fill color of the rectangle
503
+ * @param options.stroke - Whether to stroke the rectangle (default is false)
504
+ * @param options.strokeStyle - The stroke color of the rectangle
505
+ * @param options.lineWidth - The width of the stroke (default is 0.2)
506
+ * @param options.borderRadius - The radius of the corners (default is 0)
507
+ * @returns void
508
+ */
509
+ function rect(ctx, x, y, width, height, options) {
510
+ const { fill = true, fillStyle, stroke = false, strokeStyle, lineWidth = .2, borderRadius = 0 } = options || {};
511
+ ctx.save();
512
+ if (fillStyle !== void 0) ctx.fillStyle = fillStyle;
513
+ if (strokeStyle !== void 0) ctx.strokeStyle = strokeStyle;
514
+ if (lineWidth !== void 0) ctx.lineWidth = lineWidth;
515
+ const r = Math.min(borderRadius, width / 2, height / 2);
516
+ ctx.beginPath();
517
+ if (r > 0) {
518
+ ctx.moveTo(x + r, y);
519
+ ctx.lineTo(x + width - r, y);
520
+ ctx.quadraticCurveTo(x + width, y, x + width, y + r);
521
+ ctx.lineTo(x + width, y + height - r);
522
+ ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
523
+ ctx.lineTo(x + r, y + height);
524
+ ctx.quadraticCurveTo(x, y + height, x, y + height - r);
525
+ ctx.lineTo(x, y + r);
526
+ ctx.quadraticCurveTo(x, y, x + r, y);
527
+ } else ctx.rect(x, y, width, height);
528
+ ctx.closePath();
529
+ if (fill) ctx.fill();
530
+ if (stroke) ctx.stroke();
531
+ ctx.restore();
532
+ }
533
+ /**
534
+ * draws an image on the canvas with optional border radius and opacity
535
+ * @param ctx - The canvas rendering context
536
+ * @param image - The HTMLImageElement to draw
537
+ * @param x - The x-coordinate of the image's top-left corner
538
+ * @param y - The y-coordinate of the image's top-left corner
539
+ * @param width - The width of the image
540
+ * @param height - The height of the image
541
+ * @param borderRadius - The radius of the corners (default is 0)
542
+ * @param opacity - The opacity of the image (default is 1.0)
543
+ * @param bgColor - Optional background color to fill the clipped area
544
+ * @returns void
545
+ */
546
+ function img(ctx, image, x, y, width, height, borderRadius = 0, opacity = 1, bgColor) {
547
+ ctx.save();
548
+ ctx.beginPath();
549
+ if (borderRadius > 0) {
550
+ const r = Math.min(borderRadius, width / 2, height / 2);
551
+ ctx.moveTo(x + r, y);
552
+ ctx.lineTo(x + width - r, y);
553
+ ctx.quadraticCurveTo(x + width, y, x + width, y + r);
554
+ ctx.lineTo(x + width, y + height - r);
555
+ ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
556
+ ctx.lineTo(x + r, y + height);
557
+ ctx.quadraticCurveTo(x, y + height, x, y + height - r);
558
+ ctx.lineTo(x, y + r);
559
+ ctx.quadraticCurveTo(x, y, x + r, y);
560
+ } else ctx.rect(x, y, width, height);
561
+ ctx.closePath();
562
+ if (bgColor) {
563
+ ctx.save();
564
+ ctx.fillStyle = bgColor;
565
+ ctx.globalAlpha = 1;
566
+ ctx.fill();
567
+ ctx.restore();
568
+ }
569
+ ctx.clip();
570
+ ctx.globalAlpha = opacity;
571
+ ctx.drawImage(image, x, y, width, height);
572
+ ctx.restore();
573
+ }
574
+ function hexagon(ctx, x, y, radius, options) {
575
+ const { fill = true, fillStyle, stroke = false, strokeStyle, lineWidth = .2, rotation = 0, borderRadius = 0 } = options || {};
576
+ ctx.save();
577
+ if (fillStyle !== void 0) ctx.fillStyle = fillStyle;
578
+ if (strokeStyle !== void 0) ctx.strokeStyle = strokeStyle;
579
+ if (lineWidth !== void 0) ctx.lineWidth = lineWidth;
580
+ const sides = 6;
581
+ const angleStep = Math.PI * 2 / sides;
582
+ ctx.beginPath();
583
+ const points = [];
584
+ for (let i = 0; i < sides; i++) {
585
+ const angle = rotation + i * angleStep;
586
+ points.push({
587
+ x: x + radius * Math.cos(angle),
588
+ y: y + radius * Math.sin(angle)
589
+ });
590
+ }
591
+ if (borderRadius > 0) {
592
+ const maxBorderRadius = Math.min(borderRadius, radius / 3);
593
+ for (let i = 0; i < sides; i++) {
594
+ const current = points[i];
595
+ const next = points[(i + 1) % sides];
596
+ const prev = points[(i - 1 + sides) % sides];
597
+ const toPrev = {
598
+ x: prev.x - current.x,
599
+ y: prev.y - current.y
600
+ };
601
+ const toNext = {
602
+ x: next.x - current.x,
603
+ y: next.y - current.y
604
+ };
605
+ const lenPrev = Math.sqrt(toPrev.x * toPrev.x + toPrev.y * toPrev.y);
606
+ const lenNext = Math.sqrt(toNext.x * toNext.x + toNext.y * toNext.y);
607
+ const normPrev = {
608
+ x: toPrev.x / lenPrev,
609
+ y: toPrev.y / lenPrev
610
+ };
611
+ const normNext = {
612
+ x: toNext.x / lenNext,
613
+ y: toNext.y / lenNext
614
+ };
615
+ const cpPrev = {
616
+ x: current.x + normPrev.x * maxBorderRadius,
617
+ y: current.y + normPrev.y * maxBorderRadius
618
+ };
619
+ const cpNext = {
620
+ x: current.x + normNext.x * maxBorderRadius,
621
+ y: current.y + normNext.y * maxBorderRadius
622
+ };
623
+ if (i === 0) ctx.moveTo(cpPrev.x, cpPrev.y);
624
+ else ctx.lineTo(cpPrev.x, cpPrev.y);
625
+ ctx.quadraticCurveTo(current.x, current.y, cpNext.x, cpNext.y);
626
+ }
627
+ } else {
628
+ ctx.moveTo(points[0].x, points[0].y);
629
+ for (let i = 1; i < sides; i++) ctx.lineTo(points[i].x, points[i].y);
630
+ }
631
+ ctx.closePath();
632
+ if (fill) ctx.fill();
633
+ if (stroke) ctx.stroke();
634
+ ctx.restore();
635
+ }
636
+
637
+ //#endregion
638
+ //#region src/util/color.ts
639
+ /**
640
+ * Some utility functions to handle colors
641
+ */
642
+ function color(rgb) {
643
+ const colorHandler = function(arg) {
644
+ if (typeof arg === "number") return `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${arg})`;
645
+ if (typeof arg === "function") {
646
+ const transformedRGB = arg(rgb);
647
+ return color(transformedRGB);
648
+ }
649
+ return `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
650
+ };
651
+ colorHandler.rgb = rgb;
652
+ return colorHandler;
653
+ }
654
+ /**
655
+ * Dims a color to white or black
656
+ * @param color An array of 3 numbers representing RGB values (0-255)
657
+ * @param dimFactor A value between 0 and 1 where 0 is completely black and 1 is the original color
658
+ * @returns A new array with the dimmed RGB values
659
+ */
660
+ function dim(factor = .6, white = true) {
661
+ const base = white ? 255 : 0;
662
+ return function(rgb) {
663
+ return [
664
+ Math.round(rgb[0] + (base - rgb[0]) * (1 - factor)),
665
+ Math.round(rgb[1] + (base - rgb[1]) * (1 - factor)),
666
+ Math.round(rgb[2] + (base - rgb[2]) * (1 - factor))
667
+ ];
668
+ };
669
+ }
670
+
671
+ //#endregion
672
+ //#region src/util/data.ts
673
+ const images = [];
674
+ function generateTree(maxNodes, maxChildren) {
675
+ const nodes = [];
676
+ const links = [];
677
+ let index = 0;
678
+ function createNode(label) {
679
+ const isRoot = label === VOID_ROOT_ID;
680
+ const idx = isRoot ? VOID_ROOT_ID : index++;
681
+ return {
682
+ id: idx.toString(),
683
+ label,
684
+ imgSrc: isRoot ? void 0 : images[index % images.length]
685
+ };
686
+ }
687
+ const root = createNode(VOID_ROOT_ID);
688
+ nodes.push(root);
689
+ const queue = [root];
690
+ while (queue.length > 0 && nodes.length < maxNodes) {
691
+ const parent = queue.shift();
692
+ const rand = Math.random();
693
+ const biased = Math.floor(Math.pow(rand, 2) * (maxChildren + 1));
694
+ const childrenCount = Math.min(biased, maxChildren);
695
+ for (let i = 0; i < childrenCount; i++) {
696
+ if (nodes.length >= maxNodes) break;
697
+ const child = createNode(`Node ${nodes.length}`);
698
+ nodes.push(child);
699
+ links.push({
700
+ source: parent.id,
701
+ target: child.id
702
+ });
703
+ queue.push(child);
704
+ }
705
+ }
706
+ return {
707
+ nodes,
708
+ links
709
+ };
710
+ }
711
+ function getPrunedData(startId, nodes, links) {
712
+ const nodesById = Object.fromEntries(nodes.map((node) => [node.id, node]));
713
+ const visibleNodes = [];
714
+ const visibleLinks = [];
715
+ const visited = new Set();
716
+ (function traverseTree(node = nodesById[startId]) {
717
+ if (!node || visited.has(node.id)) return;
718
+ visited.add(node.id);
719
+ visibleNodes.push(node);
720
+ if (node?.state?.collapsed) return;
721
+ const childLinks = links.filter((l) => (isSimNode(l.source) ? l.source.id : l.source) === node.id);
722
+ visibleLinks.push(...childLinks);
723
+ childLinks.map((link) => link.target).forEach((n) => traverseTree(isSimNode(n) ? n : nodesById[n.toString()]));
724
+ })();
725
+ return {
726
+ nodes: visibleNodes,
727
+ links: visibleLinks
728
+ };
729
+ }
730
+ /**
731
+ * Automatically identifies root nodes and builds a nested structure
732
+ * @param nodes Array of raw nodes
733
+ * @param links Array of links between nodes
734
+ * @returns Array of nested nodes starting from identified roots
735
+ */
736
+ function buildTreeFromGraphData(nodes, links) {
737
+ const nodeMap = new Map();
738
+ nodes.forEach((node) => nodeMap.set(node.id, node));
739
+ const childrenMap = new Map();
740
+ const parentMap = new Map();
741
+ nodes.forEach((node) => {
742
+ childrenMap.set(node.id, []);
743
+ parentMap.set(node.id, []);
744
+ });
745
+ links.forEach((link) => {
746
+ if (nodeMap.has(link.source) && nodeMap.has(link.target)) {
747
+ const children = childrenMap.get(link.source) || [];
748
+ if (!children.includes(link.target)) {
749
+ children.push(link.target);
750
+ childrenMap.set(link.source, children);
751
+ }
752
+ const parents = parentMap.get(link.target) || [];
753
+ if (!parents.includes(link.source)) {
754
+ parents.push(link.source);
755
+ parentMap.set(link.target, parents);
756
+ }
757
+ }
758
+ });
759
+ const rootHashes = [];
760
+ nodeMap.forEach((_, hash) => {
761
+ const parents = parentMap.get(hash) || [];
762
+ if (parents.length === 0) rootHashes.push(hash);
763
+ });
764
+ const buildNode = (hash, visited = new Set()) => {
765
+ if (visited.has(hash)) return null;
766
+ visited.add(hash);
767
+ const node = nodeMap.get(hash);
768
+ if (!node) return null;
769
+ const childHashes = childrenMap.get(hash) || [];
770
+ const nestedChildren = [];
771
+ childHashes.forEach((childHash) => {
772
+ const childNode = buildNode(childHash, new Set([...visited]));
773
+ if (childNode) nestedChildren.push(childNode);
774
+ });
775
+ return {
776
+ ...node,
777
+ children: nestedChildren
778
+ };
779
+ };
780
+ const result = [];
781
+ rootHashes.forEach((rootHash) => {
782
+ const nestedRoot = buildNode(rootHash);
783
+ if (nestedRoot) result.push(nestedRoot);
784
+ });
785
+ const processedNodes = new Set();
786
+ const markProcessed = (node) => {
787
+ processedNodes.add(node.id);
788
+ node.children.forEach(markProcessed);
789
+ };
790
+ result.forEach(markProcessed);
791
+ nodeMap.forEach((_, hash) => {
792
+ if (!processedNodes.has(hash)) {
793
+ const orphanedRoot = buildNode(hash);
794
+ if (orphanedRoot) result.push(orphanedRoot);
795
+ }
796
+ });
797
+ return result;
798
+ }
799
+ /**
800
+ * Recursively retrieves all parents of a node from a graph data structure
801
+ * @param {string} nodeHash - The hash of the node to find parents for
802
+ * @param {RawNode[]} nodes - Array of nodes in the graph
803
+ * @param {RawLink[]} links - Array of links connecting the nodes
804
+ * @returns {RawNode[]} - Array of parent nodes
805
+ */
806
+ function searchParents(nodeHash, nodes, links) {
807
+ const visited = new Set();
808
+ function findParents(hash) {
809
+ if (visited.has(hash)) return [];
810
+ visited.add(hash);
811
+ const immediateParents = links.filter((link) => link.target === hash).map((link) => link.source);
812
+ const parentNodes = nodes.filter((node) => immediateParents.includes(node.id));
813
+ const ancestorNodes = immediateParents.flatMap((parentHash) => findParents(parentHash));
814
+ return [...parentNodes, ...ancestorNodes];
815
+ }
816
+ return findParents(nodeHash);
817
+ }
818
+
819
+ //#endregion
820
+ //#region src/sim/OpenGraphSimulation.ts
821
+ var OpenGraphSimulation = class {
822
+ width;
823
+ height;
824
+ config;
825
+ rootImageSources;
826
+ canvas;
827
+ transformCanvas;
828
+ theme;
829
+ data = {
830
+ nodes: [],
831
+ links: []
832
+ };
833
+ prunedData = {
834
+ nodes: [],
835
+ links: []
836
+ };
837
+ subGraph = {
838
+ nodes: [],
839
+ links: []
840
+ };
841
+ rootId = "";
842
+ simulation = null;
843
+ clusterSizeRange = [0, 1];
844
+ imageCache = new Map();
845
+ rootImages = [];
846
+ hideThumbnails = false;
847
+ noInteraction = false;
848
+ selectedNode = null;
849
+ onSelectedNodeChange;
850
+ hoveredNode = null;
851
+ onHoveredNodeChange;
852
+ highlights = [];
853
+ constructor(props) {
854
+ this.theme = props.theme || "light";
855
+ this.width = props.width;
856
+ this.height = props.height;
857
+ this.config = props.config || DEFAULT_GRAPH_CONFIG;
858
+ this.rootImageSources = props.rootImageSources || [];
859
+ this.canvas = props.canvas;
860
+ this.onHoveredNodeChange = props.onHoveredNodeChange;
861
+ this.onSelectedNodeChange = props.onSelectedNodeChange;
862
+ this.transformCanvas = new TransformCanvas(this.canvas, {
863
+ onUpdate: this.onDraw,
864
+ onClick: this.handleClick,
865
+ onMove: this.handleMove
866
+ });
867
+ this.rootImageSources.forEach((src, idx) => {
868
+ if (src && !this.imageCache.get(src)) loadImage(src).then((img$1) => {
869
+ this.imageCache.set(src, img$1);
870
+ this.rootImages[idx] = img$1;
871
+ });
872
+ });
873
+ }
874
+ getNodeAtPosition = (cx, cy) => {
875
+ const transform = this.transformCanvas.transform;
876
+ const { x: tx, y: ty, scale } = transform;
877
+ const x = (cx - tx) / scale;
878
+ const y = (cy - ty) / scale;
879
+ for (let node of this.data.nodes) {
880
+ const r = this.getNodeSize(node.id) / 2;
881
+ if (node.x == null || node.y == null) continue;
882
+ const dx = node.x - x;
883
+ const dy = node.y - y;
884
+ if (dx * dx + dy * dy < r * r) return node;
885
+ }
886
+ return null;
887
+ };
888
+ getNodeScreenPosition = (node) => {
889
+ const _x = node.x || 0;
890
+ const _y = node.y || 0;
891
+ const transform = this.transformCanvas.transform;
892
+ const x = this.width / 2 - _x * transform.scale;
893
+ const y = this.height / 2 - _y * transform.scale;
894
+ return {
895
+ x,
896
+ y
897
+ };
898
+ };
899
+ handleClick = (x, y) => {
900
+ const node = this.getNodeAtPosition(x, y);
901
+ this.handleClickNode(node);
902
+ };
903
+ handleClickNode = (node) => {
904
+ if (node) {
905
+ if (node.id === this.rootId) {
906
+ this.selectedNode = null;
907
+ this.onSelectedNodeChange?.(null);
908
+ this.subGraph = {
909
+ nodes: [],
910
+ links: []
911
+ };
912
+ return;
913
+ }
914
+ if (node.state) {
915
+ const children = getChildren(node.id, this.data.links);
916
+ if (children.length > 0) {
917
+ if (this.selectedNode?.id !== node.id) node.state.collapsed = false;
918
+ else node.state.collapsed = !node.state.collapsed;
919
+ if (!node.state.collapsed) children.forEach((childId) => {
920
+ const childNode = this.data.nodes.find((n) => n.id === childId);
921
+ if (childNode && isSimNode(childNode)) {
922
+ if (!childNode.x || childNode.x === 0) childNode.x = (node.x || this.width / 2) + Math.random() * 50 - 5;
923
+ if (!childNode.y || childNode.y === 0) childNode.y = (node.y || this.height / 2) + Math.random() * 50 - 5;
924
+ }
925
+ });
926
+ }
927
+ }
928
+ if (!node.state?.collapsed) {
929
+ const parents = getParents(node.id, this.data.links);
930
+ parents.forEach((parentId) => {
931
+ const parentNode = this.data.nodes.find((n) => n.id === parentId);
932
+ if (parentNode && isSimNode(parentNode) && parentNode.state) parentNode.state.collapsed = false;
933
+ });
934
+ }
935
+ this.restart();
936
+ this.subGraph = getNodeSubgraph(node.id, this.data.nodes, this.data.links, this.rootId);
937
+ const nodePos = this.getNodeScreenPosition(node);
938
+ this.transformCanvas.transformTo({
939
+ x: nodePos.x,
940
+ y: nodePos.y
941
+ });
942
+ this.transformCanvas.focusOn(() => {
943
+ const nodePos$1 = this.getNodeScreenPosition(node);
944
+ return {
945
+ x: nodePos$1.x,
946
+ y: nodePos$1.y,
947
+ scale: this.transformCanvas.transform.scale
948
+ };
949
+ });
950
+ }
951
+ if (this.selectedNode?.id !== node?.id) {
952
+ this.selectedNode = node;
953
+ this.onSelectedNodeChange?.(node);
954
+ }
955
+ };
956
+ handleMove = (x, y) => {
957
+ const node = this.getNodeAtPosition(x, y);
958
+ if (this.hoveredNode === node) return;
959
+ this.hoveredNode = node;
960
+ this.onHoveredNodeChange?.(node);
961
+ this.canvas.style.cursor = node ? "pointer" : "default";
962
+ this.onDraw();
963
+ };
964
+ initialize = (data, rootId) => {
965
+ this.rootId = rootId;
966
+ const _links = data.links.map((l) => ({ ...l }));
967
+ const _nodes = data.nodes.map((n) => {
968
+ const existingData = this.data.nodes.find((x) => x.id === n.id);
969
+ return {
970
+ ...n,
971
+ state: { collapsed: hasOnlyLeafs(n.id, _links) && getChildren(n.id, _links).length > 1 },
972
+ clusterSize: getClusterSize(n.id, _links),
973
+ x: existingData?.x || this.width / 2 + Math.random() * 200 - 5,
974
+ y: existingData?.y || this.height / 2 + Math.random() * 200 - 5
975
+ };
976
+ });
977
+ if (!data.nodes.find((n) => n.id === rootId)) {
978
+ _nodes.push({
979
+ id: this.rootId,
980
+ state: {
981
+ collapsed: false,
982
+ image: void 0
983
+ },
984
+ clusterSize: 1,
985
+ x: this.width / 2,
986
+ y: this.height / 2
987
+ });
988
+ const targetIds = new Set(_links.map((link) => link.target));
989
+ const rootNodes = _nodes.filter((node) => !targetIds.has(node.id));
990
+ for (const node of rootNodes) _links.push({
991
+ source: rootId,
992
+ target: node.id
993
+ });
994
+ }
995
+ this.data = {
996
+ nodes: _nodes,
997
+ links: _links
998
+ };
999
+ this.loadNodeImages();
1000
+ this.restart();
1001
+ };
1002
+ restart = () => {
1003
+ this.setSelectedNode(null);
1004
+ this.prunedData = getPrunedData(this.rootId, this.data.nodes, this.data.links);
1005
+ 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]);
1006
+ this.simulation = forceSimulation(this.prunedData.nodes).force("link", forceLink(this.prunedData.links).id((d) => d.id).distance((l) => {
1007
+ if (isSimNode(l.target) && !l.target?.state?.collapsed) return this.config.nodeSize;
1008
+ return this.config.nodeSize * 3;
1009
+ }).strength((l) => {
1010
+ return .6;
1011
+ })).force("charge", forceManyBody().strength(() => {
1012
+ return -130;
1013
+ })).force("center", forceCenter(this.width / 2, this.height / 2).strength(.01));
1014
+ this.simulation.on("tick", this.onDraw);
1015
+ this.simulation.on("end", this.onEnd);
1016
+ };
1017
+ get visiblityScale() {
1018
+ return scaleLog().domain(this.clusterSizeRange).range([3, 1.5]).clamp(true);
1019
+ }
1020
+ get color() {
1021
+ return color(this.theme === "light" ? this.config.theme.dark : this.config.theme.light);
1022
+ }
1023
+ get colorContrast() {
1024
+ return color(this.theme === "light" ? this.config.theme.light : this.config.theme.dark);
1025
+ }
1026
+ getNodeSize = (nodeId) => {
1027
+ const { nodeSize } = this.config;
1028
+ if (nodeId === this.rootId) return nodeSize * 2;
1029
+ const node = this.data.nodes.find((n) => n.id === nodeId);
1030
+ const isCollapsed = !!node?.state?.collapsed;
1031
+ if (isCollapsed) {
1032
+ const scale = scaleLinear().domain(this.clusterSizeRange).range([nodeSize, nodeSize * 3]);
1033
+ return scale(node.clusterSize || 1);
1034
+ }
1035
+ const isSelected = this.selectedNode?.id === nodeId;
1036
+ return isSelected ? nodeSize * 2 : nodeSize;
1037
+ };
1038
+ onDraw = () => {
1039
+ const context = this.canvas?.getContext("2d");
1040
+ const transform = this.transformCanvas.transform;
1041
+ if (!context) return;
1042
+ const dpi = devicePixelRatio || 1;
1043
+ context.save();
1044
+ context.scale(dpi, dpi);
1045
+ context.clearRect(0, 0, this.width, this.height);
1046
+ context.translate(transform.x, transform.y);
1047
+ context.scale(transform.scale, transform.scale);
1048
+ context.save();
1049
+ const isLight = this.theme === "light";
1050
+ this.prunedData.links.forEach((l) => {
1051
+ const _dim = !!this.selectedNode && !this.subGraph.links.find((sl) => getNodeId(sl.source) === getNodeId(l.source) && getNodeId(sl.target) === getNodeId(l.target));
1052
+ const stroke = _dim ? this.color(dim(.09, isLight))() : this.color(dim(.18, isLight))();
1053
+ context.globalAlpha = .5;
1054
+ context.strokeStyle = stroke;
1055
+ context.lineWidth = _dim ? .3 : .8;
1056
+ context.beginPath();
1057
+ const sx = isSimNode(l.source) && l.source.x || 0;
1058
+ const sy = isSimNode(l.source) && l.source.y || 0;
1059
+ context.moveTo(sx, sy);
1060
+ const tx = isSimNode(l.target) && l.target.x || 0;
1061
+ const ty = isSimNode(l.target) && l.target.y || 0;
1062
+ context.lineTo(tx, ty);
1063
+ context.stroke();
1064
+ context.closePath();
1065
+ });
1066
+ context.globalAlpha = 1;
1067
+ this.prunedData.nodes.forEach((node) => {
1068
+ const x = node.x || 0;
1069
+ const y = node.y || 0;
1070
+ const isSelected = this.selectedNode?.id === node.id;
1071
+ const isHovered = this.hoveredNode?.id === node.id;
1072
+ const isCollapsed = !!node.state?.collapsed;
1073
+ const _dim = !!this.selectedNode && !this.subGraph?.nodes.some((n) => n.id === node.id);
1074
+ const fill = _dim ? this.color(dim(.075, isLight))() : isCollapsed ? this.color(dim(.18, isLight))() : isHovered ? this.color(dim(.4, isLight))() : this.color();
1075
+ const stroke = this.colorContrast();
1076
+ const blue = [
1077
+ 94,
1078
+ 112,
1079
+ 235
1080
+ ];
1081
+ const red = [
1082
+ 238,
1083
+ 125,
1084
+ 121
1085
+ ];
1086
+ const highlightedStroke = _dim ? color(red)(dim(.4, isLight))() : color(red)();
1087
+ const size = this.getNodeSize(node.id);
1088
+ const highlighted = this.highlights.includes(node.id);
1089
+ if (node.id === this.rootId) {
1090
+ circle(context, x, y, size / 2, {
1091
+ stroke: false,
1092
+ strokeStyle: stroke,
1093
+ lineWidth: isSelected ? 1 : .2,
1094
+ fill: true,
1095
+ fillStyle: this.color(dim(.18, isLight))()
1096
+ });
1097
+ if (this.rootImages) {
1098
+ const _idx = Math.min(isLight ? 0 : 1, this.rootImages.length - 1);
1099
+ const _img = this.rootImages[_idx];
1100
+ const _imgSize = size * .55;
1101
+ if (_img) img(context, _img, x - _imgSize / 2, y - _imgSize / 2, _imgSize, _imgSize, 0, _dim ? .1 : 1);
1102
+ }
1103
+ } else if (isCollapsed) {
1104
+ if (highlighted) {
1105
+ const _size = size + 4;
1106
+ circle(context, x, y, _size / 2, {
1107
+ stroke: true,
1108
+ strokeStyle: highlightedStroke,
1109
+ lineWidth: 1,
1110
+ fill: false,
1111
+ fillStyle: fill
1112
+ });
1113
+ }
1114
+ circle(context, x, y, size / 2, {
1115
+ stroke: true,
1116
+ strokeStyle: stroke,
1117
+ lineWidth: isSelected ? 1 : .2,
1118
+ fill: true,
1119
+ fillStyle: fill
1120
+ });
1121
+ const showLabel = transform.scale >= this.visiblityScale(node.clusterSize || 1) ? 1 : 0;
1122
+ if (showLabel) {
1123
+ context.font = `${14 / transform.scale}px Sans-Serif`;
1124
+ context.textAlign = "center";
1125
+ context.textBaseline = "middle";
1126
+ context.fillStyle = this.color(dim(_dim ? .2 : .5, isLight))();
1127
+ context.fillText((node.clusterSize || 1).toString(), x, y);
1128
+ }
1129
+ } else {
1130
+ if (highlighted) {
1131
+ const _size = size + 4;
1132
+ rect(context, x - _size / 2, y - _size / 2, _size, _size, {
1133
+ stroke: true,
1134
+ strokeStyle: highlightedStroke,
1135
+ lineWidth: 1,
1136
+ fill: false,
1137
+ fillStyle: fill,
1138
+ borderRadius: 1
1139
+ });
1140
+ }
1141
+ rect(context, x - size / 2, y - size / 2, size, size, {
1142
+ stroke: true,
1143
+ strokeStyle: stroke,
1144
+ lineWidth: isSelected ? .3 : .2,
1145
+ fill: true,
1146
+ fillStyle: fill,
1147
+ borderRadius: 1
1148
+ });
1149
+ if (node.state?.image && !this.hideThumbnails) {
1150
+ const _size = size - 1;
1151
+ img(context, node.state?.image, x - _size / 2, y - _size / 2, _size, _size, 1, _dim ? .1 : 1);
1152
+ }
1153
+ }
1154
+ });
1155
+ context.restore();
1156
+ context.restore();
1157
+ this.transformCanvas.trackCursor();
1158
+ };
1159
+ onEnd = () => {};
1160
+ loadNodeImages = () => {
1161
+ this.data.nodes.forEach((node) => {
1162
+ if (node.imgSrc && !this.imageCache.get(node.imgSrc)) loadImage(node.imgSrc).then((img$1) => {
1163
+ this.imageCache.set(node.imgSrc, img$1);
1164
+ node.state = node.state || {};
1165
+ node.state.image = img$1;
1166
+ });
1167
+ });
1168
+ };
1169
+ destroy = () => {
1170
+ this.simulation?.stop();
1171
+ this.simulation?.on("tick", null);
1172
+ this.simulation?.on("end", null);
1173
+ this.transformCanvas.destroy();
1174
+ };
1175
+ resize = (width, height) => {
1176
+ this.width = width;
1177
+ this.height = height;
1178
+ this.onDraw();
1179
+ };
1180
+ setTheme = (theme) => {
1181
+ this.theme = theme;
1182
+ this.onDraw();
1183
+ };
1184
+ setHideThumbnails = (hide) => {
1185
+ this.hideThumbnails = hide;
1186
+ this.onDraw();
1187
+ };
1188
+ setSelectedNode = (node) => {
1189
+ this.selectedNode = node;
1190
+ this.prunedData.nodes.sort((a, b) => {
1191
+ if (a.id === this.selectedNode?.id) return 1;
1192
+ if (b.id === this.selectedNode?.id) return -1;
1193
+ return 0;
1194
+ });
1195
+ this.onDraw();
1196
+ };
1197
+ setHighlights = (highlights) => {
1198
+ this.highlights = highlights;
1199
+ this.onDraw();
1200
+ };
1201
+ setNoInteraction = (noInteraction) => {
1202
+ this.noInteraction = noInteraction;
1203
+ this.transformCanvas.setNoInteraction(this.noInteraction);
1204
+ this.onDraw();
1205
+ };
1206
+ getNodeById = (nodeId) => {
1207
+ return this.data.nodes.find((n) => n.id === nodeId) || null;
1208
+ };
1209
+ };
1210
+
1211
+ //#endregion
1212
+ //#region src/components/OpenFormGraph.tsx
1213
+ function OpenFormGraph(props) {
1214
+ const { width, height, highlights = [], className, noInteraction = false } = props;
1215
+ const { simulation, data, rootId, rootImageSources, theme, hideThumbnails, setHoveredNode, setSelectedNode } = useOpenFormGraph();
1216
+ const canvasRef = useRef(null);
1217
+ useEffect(() => {
1218
+ if (!canvasRef.current) return;
1219
+ simulation.current = new OpenGraphSimulation({
1220
+ width,
1221
+ height,
1222
+ canvas: canvasRef.current,
1223
+ rootImageSources,
1224
+ theme,
1225
+ onHoveredNodeChange: (n) => {
1226
+ setHoveredNode(n);
1227
+ },
1228
+ onSelectedNodeChange: (n) => {
1229
+ setSelectedNode(n);
1230
+ }
1231
+ });
1232
+ return () => {
1233
+ simulation.current?.destroy();
1234
+ };
1235
+ }, []);
1236
+ useEffect(() => {
1237
+ if (!simulation.current) return;
1238
+ simulation.current.resize(width, height);
1239
+ }, [width, height]);
1240
+ useEffect(() => {
1241
+ if (!simulation.current || !theme) return;
1242
+ simulation.current.setTheme(theme);
1243
+ }, [theme]);
1244
+ useEffect(() => {
1245
+ if (!simulation.current) return;
1246
+ simulation.current.setHideThumbnails(hideThumbnails);
1247
+ }, [hideThumbnails]);
1248
+ useEffect(() => {
1249
+ if (!simulation.current) return;
1250
+ simulation.current.setHighlights(highlights);
1251
+ }, [highlights]);
1252
+ useEffect(() => {
1253
+ if (!simulation.current) return;
1254
+ simulation.current.setNoInteraction(noInteraction);
1255
+ }, [noInteraction]);
1256
+ useEffect(() => {
1257
+ if (!simulation.current) return;
1258
+ simulation.current.initialize(data, rootId);
1259
+ }, [data]);
1260
+ const dpi = devicePixelRatio || 1;
1261
+ return /* @__PURE__ */ jsx("canvas", {
1262
+ onMouseEnter: props.onMouseEnter,
1263
+ onMouseLeave: props.onMouseLeave,
1264
+ ref: canvasRef,
1265
+ className,
1266
+ width: `${width * dpi}px`,
1267
+ height: `${height * dpi}px`,
1268
+ style: {
1269
+ width: `${width}px`,
1270
+ height: `${height}px`
1271
+ }
1272
+ });
1273
+ }
1274
+
1275
+ //#endregion
1276
+ export { OpenFormGraph, buildTreeFromGraphData, circle, color, dim, generateTree, getChildren, getClusterSize, getNodeId, getNodeSubgraph, getParents, getPrunedData, hasOnlyLeafs, hexagon, img, isSimLink, isSimNode, loadImage, rect, searchParents };
1277
+ //# sourceMappingURL=OpenFormGraph-wo_Y20C6.js.map