@fusefactory/fuse-three-forcegraph 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,4213 @@
1
+ import CameraControls from "camera-controls";
2
+ import * as THREE from "three";
3
+
4
+ //#region core/EventEmitter.ts
5
+ var EventEmitter = class {
6
+ listeners = {};
7
+ listenerCount(event) {
8
+ return this.listeners[event] ? this.listeners[event].length : 0;
9
+ }
10
+ on(event, listener) {
11
+ if (!this.listeners[event]) this.listeners[event] = [];
12
+ this.listeners[event].push(listener);
13
+ return () => this.off(event, listener);
14
+ }
15
+ off(event, listener) {
16
+ if (!this.listeners[event]) return;
17
+ const index = this.listeners[event].indexOf(listener);
18
+ if (index === -1) return;
19
+ this.listeners[event].splice(index, 1);
20
+ }
21
+ emit(event, ...args) {
22
+ if (!this.listeners[event]) return;
23
+ this.listeners[event].forEach((listener) => listener(...args));
24
+ }
25
+ };
26
+
27
+ //#endregion
28
+ //#region controls/InputProcessor.ts
29
+ /**
30
+ * Manages pointer/mouse input and emits interaction events
31
+ * Uses GPU picking for efficient node detection
32
+ */
33
+ var InputProcessor = class extends EventEmitter {
34
+ CLICK_THRESHOLD = 200;
35
+ HOVER_TO_POP_MS = 800;
36
+ pointer = new THREE.Vector2();
37
+ canvasPointer = new THREE.Vector2();
38
+ isPointerDown = false;
39
+ isDragging = false;
40
+ mouseDownTime = 0;
41
+ draggedIndex = -1;
42
+ currentHoverIndex = -1;
43
+ hoverStartTime = 0;
44
+ hoverProgress = 0;
45
+ hasPopped = false;
46
+ lastClientX = null;
47
+ lastClientY = null;
48
+ constructor(pickFn, canvas, viewport) {
49
+ super();
50
+ this.pickFn = pickFn;
51
+ this.canvas = canvas;
52
+ this.viewport = viewport;
53
+ this.setupEventListeners();
54
+ }
55
+ setupEventListeners() {
56
+ this.canvas.addEventListener("pointermove", this.handlePointerMove);
57
+ this.canvas.addEventListener("pointerdown", this.handlePointerDown);
58
+ this.canvas.addEventListener("pointerup", this.handlePointerUp);
59
+ this.canvas.addEventListener("pointerleave", this.handlePointerLeave);
60
+ }
61
+ handlePointerMove = (event) => {
62
+ this.updatePointer(event);
63
+ this.lastClientX = event.clientX;
64
+ this.lastClientY = event.clientY;
65
+ const pickedIndex = this.pickFn(this.canvasPointer.x, this.canvasPointer.y);
66
+ this.updateHover(pickedIndex);
67
+ if (this.isPointerDown && this.draggedIndex >= 0) {
68
+ if (!this.isDragging) {
69
+ this.isDragging = true;
70
+ this.emit("pointer:dragstart", {
71
+ index: this.draggedIndex,
72
+ x: this.canvasPointer.x,
73
+ y: this.canvasPointer.y,
74
+ clientX: event.clientX,
75
+ clientY: event.clientY
76
+ });
77
+ }
78
+ this.emit("pointer:drag", {
79
+ index: this.draggedIndex,
80
+ x: this.canvasPointer.x,
81
+ y: this.canvasPointer.y,
82
+ clientX: event.clientX,
83
+ clientY: event.clientY
84
+ });
85
+ }
86
+ };
87
+ handlePointerDown = (event) => {
88
+ this.updatePointer(event);
89
+ this.mouseDownTime = Date.now();
90
+ this.isPointerDown = true;
91
+ const pickedIndex = this.pickFn(this.canvasPointer.x, this.canvasPointer.y);
92
+ this.draggedIndex = pickedIndex;
93
+ if (pickedIndex >= 0) this.emit("pointer:down", {
94
+ index: pickedIndex,
95
+ x: this.canvasPointer.x,
96
+ y: this.canvasPointer.y,
97
+ clientX: event.clientX,
98
+ clientY: event.clientY
99
+ });
100
+ };
101
+ handlePointerUp = (event) => {
102
+ const wasClick = Date.now() - this.mouseDownTime < this.CLICK_THRESHOLD;
103
+ if (this.isDragging) this.emit("pointer:dragend", {
104
+ index: this.draggedIndex,
105
+ x: this.canvasPointer.x,
106
+ y: this.canvasPointer.y,
107
+ clientX: event.clientX,
108
+ clientY: event.clientY
109
+ });
110
+ else if (wasClick && this.draggedIndex >= 0) this.emit("pointer:click", {
111
+ index: this.draggedIndex,
112
+ x: this.canvasPointer.x,
113
+ y: this.canvasPointer.y,
114
+ clientX: event.clientX,
115
+ clientY: event.clientY
116
+ });
117
+ else if (wasClick && this.draggedIndex < 0) this.emit("pointer:clickempty", {
118
+ x: this.canvasPointer.x,
119
+ y: this.canvasPointer.y,
120
+ clientX: event.clientX,
121
+ clientY: event.clientY
122
+ });
123
+ this.isPointerDown = false;
124
+ this.isDragging = false;
125
+ this.draggedIndex = -1;
126
+ };
127
+ handlePointerLeave = () => {
128
+ this.lastClientX = null;
129
+ this.lastClientY = null;
130
+ this.updateHover(-1);
131
+ };
132
+ updateHover(index) {
133
+ const prevIndex = this.currentHoverIndex;
134
+ if (index === prevIndex) {
135
+ if (index >= 0) {
136
+ const elapsed = Date.now() - this.hoverStartTime;
137
+ this.hoverProgress = Math.min(1, elapsed / this.HOVER_TO_POP_MS);
138
+ this.emit("pointer:hover", {
139
+ index,
140
+ progress: this.hoverProgress,
141
+ x: this.canvasPointer.x,
142
+ y: this.canvasPointer.y,
143
+ clientX: this.lastClientX,
144
+ clientY: this.lastClientY
145
+ });
146
+ if (this.hoverProgress >= 1 && !this.hasPopped) {
147
+ this.hasPopped = true;
148
+ this.emit("pointer:pop", {
149
+ index,
150
+ x: this.canvasPointer.x,
151
+ y: this.canvasPointer.y,
152
+ clientX: this.lastClientX,
153
+ clientY: this.lastClientY
154
+ });
155
+ }
156
+ }
157
+ return;
158
+ }
159
+ if (prevIndex >= 0) this.emit("pointer:hoverend", { index: prevIndex });
160
+ if (index >= 0) {
161
+ this.currentHoverIndex = index;
162
+ this.hoverStartTime = Date.now();
163
+ this.hoverProgress = 0;
164
+ this.hasPopped = false;
165
+ this.emit("pointer:hoverstart", {
166
+ index,
167
+ x: this.canvasPointer.x,
168
+ y: this.canvasPointer.y,
169
+ clientX: this.lastClientX,
170
+ clientY: this.lastClientY
171
+ });
172
+ } else {
173
+ this.currentHoverIndex = -1;
174
+ this.hoverStartTime = 0;
175
+ this.hoverProgress = 0;
176
+ this.hasPopped = false;
177
+ }
178
+ }
179
+ /**
180
+ * Update hover state even when pointer is stationary
181
+ * Called from render loop
182
+ */
183
+ update() {
184
+ if (this.lastClientX !== null && this.lastClientY !== null) {
185
+ const rect = this.canvas.getBoundingClientRect();
186
+ const x = this.lastClientX - rect.left;
187
+ const y = this.lastClientY - rect.top;
188
+ const pickedIndex = this.pickFn(x, y);
189
+ this.updateHover(pickedIndex);
190
+ }
191
+ }
192
+ updatePointer(event) {
193
+ const rect = this.canvas.getBoundingClientRect();
194
+ const x = event.clientX - rect.left;
195
+ const y = event.clientY - rect.top;
196
+ const ndcX = x / rect.width * 2 - 1;
197
+ const ndcY = -(y / rect.height) * 2 + 1;
198
+ this.pointer.set(ndcX, ndcY);
199
+ this.canvasPointer.set(x, y);
200
+ }
201
+ /**
202
+ * Update viewport dimensions on resize
203
+ */
204
+ resize(width, height) {
205
+ this.viewport.width = width;
206
+ this.viewport.height = height;
207
+ }
208
+ dispose() {
209
+ this.canvas.removeEventListener("pointermove", this.handlePointerMove);
210
+ this.canvas.removeEventListener("pointerdown", this.handlePointerDown);
211
+ this.canvas.removeEventListener("pointerup", this.handlePointerUp);
212
+ this.canvas.removeEventListener("pointerleave", this.handlePointerLeave);
213
+ }
214
+ };
215
+
216
+ //#endregion
217
+ //#region controls/handlers/HoverHandler.ts
218
+ var HoverHandler = class extends EventEmitter {
219
+ constructor(getNodeByIndex) {
220
+ super();
221
+ this.getNodeByIndex = getNodeByIndex;
222
+ this.on("pointer:hoverstart", this.handleHoverStart);
223
+ this.on("pointer:hover", this.handleHover);
224
+ this.on("pointer:pop", this.handlePop);
225
+ this.on("pointer:hoverend", this.handleHoverEnd);
226
+ }
227
+ handleHoverStart = (data) => {
228
+ const node = this.getNodeByIndex(data.index);
229
+ if (!node) return;
230
+ this.emit("node:hoverstart", {
231
+ node,
232
+ x: data.x,
233
+ y: data.y,
234
+ clientX: data.clientX,
235
+ clientY: data.clientY
236
+ });
237
+ };
238
+ handleHover = (data) => {
239
+ const node = this.getNodeByIndex(data.index);
240
+ if (!node) return;
241
+ const progress = data.progress ?? 0;
242
+ this.emit("node:hover", {
243
+ node,
244
+ x: data.x,
245
+ y: data.y,
246
+ progress,
247
+ clientX: data.clientX,
248
+ clientY: data.clientY
249
+ });
250
+ };
251
+ handlePop = (data) => {
252
+ const node = this.getNodeByIndex(data.index);
253
+ if (!node) return;
254
+ this.emit("node:pop", {
255
+ node,
256
+ x: data.x,
257
+ y: data.y,
258
+ clientX: data.clientX,
259
+ clientY: data.clientY
260
+ });
261
+ };
262
+ handleHoverEnd = (data) => {
263
+ if (!this.getNodeByIndex(data.index)) return;
264
+ this.emit("node:hoverend");
265
+ };
266
+ dispose() {
267
+ this.off("pointer:hoverstart", this.handleHoverStart);
268
+ this.off("pointer:hover", this.handleHover);
269
+ this.off("pointer:pop", this.handlePop);
270
+ this.off("pointer:hoverend", this.handleHoverEnd);
271
+ }
272
+ };
273
+
274
+ //#endregion
275
+ //#region controls/handlers/ClickHandler.ts
276
+ var ClickHandler = class extends EventEmitter {
277
+ constructor(getNodeByIndex) {
278
+ super();
279
+ this.getNodeByIndex = getNodeByIndex;
280
+ this.on("pointer:click", this.handleClick);
281
+ }
282
+ handleClick = (data) => {
283
+ const node = this.getNodeByIndex(data.index);
284
+ if (!node) return;
285
+ this.emit("node:click", {
286
+ node,
287
+ x: data.x,
288
+ y: data.y,
289
+ clientX: data.clientX,
290
+ clientY: data.clientY
291
+ });
292
+ };
293
+ dispose() {
294
+ this.off("pointer:click", this.handleClick);
295
+ }
296
+ };
297
+
298
+ //#endregion
299
+ //#region types/iGraphNode.ts
300
+ let NodeState = /* @__PURE__ */ function(NodeState) {
301
+ NodeState[NodeState["Hidden"] = 0] = "Hidden";
302
+ NodeState[NodeState["Passive"] = 1] = "Passive";
303
+ NodeState[NodeState["Fixed"] = 2] = "Fixed";
304
+ NodeState[NodeState["Active"] = 3] = "Active";
305
+ return NodeState;
306
+ }({});
307
+
308
+ //#endregion
309
+ //#region controls/handlers/DragHandler.ts
310
+ var DragHandler = class extends EventEmitter {
311
+ isDragging = false;
312
+ raycaster = new THREE.Raycaster();
313
+ dragPlane = new THREE.Plane();
314
+ pointer = new THREE.Vector2();
315
+ constructor(getNodeByIndex, cameraController, forceSimulation, viewport) {
316
+ super();
317
+ this.getNodeByIndex = getNodeByIndex;
318
+ this.cameraController = cameraController;
319
+ this.forceSimulation = forceSimulation;
320
+ this.viewport = viewport;
321
+ this.on("pointer:dragstart", this.handleDragStart);
322
+ this.on("pointer:drag", this.handleDrag);
323
+ this.on("pointer:dragend", this.handleDragEnd);
324
+ }
325
+ /**
326
+ * Set up drag plane perpendicular to camera, passing through a world point
327
+ */
328
+ setupDragPlane(worldPoint) {
329
+ if (!this.cameraController) return;
330
+ const camera = this.cameraController.camera;
331
+ const cameraDirection = new THREE.Vector3();
332
+ camera.getWorldDirection(cameraDirection);
333
+ const planeNormal = cameraDirection.clone().negate();
334
+ this.dragPlane.setFromNormalAndCoplanarPoint(planeNormal, worldPoint);
335
+ }
336
+ handleDragStart = (data) => {
337
+ const node = this.getNodeByIndex(data.index);
338
+ if (!node) return;
339
+ let state = node.state ?? NodeState.Active;
340
+ if (node.state === void 0) state = NodeState.Fixed;
341
+ if (state !== NodeState.Active || node.metadata?.hidden) return;
342
+ this.isDragging = true;
343
+ if (this.cameraController) this.cameraController.setEnabled(false);
344
+ if (this.cameraController) {
345
+ this.cameraController.camera;
346
+ const target = this.cameraController.getTarget();
347
+ this.setupDragPlane(target);
348
+ }
349
+ if (this.forceSimulation) {
350
+ const worldPos = this.screenToWorld(data.x, data.y);
351
+ if (worldPos) this.forceSimulation.startDrag(data.index, worldPos);
352
+ }
353
+ this.emit("node:dragstart", { node });
354
+ };
355
+ handleDrag = (data) => {
356
+ const node = this.getNodeByIndex(data.index);
357
+ if (!node) return;
358
+ if (this.forceSimulation) {
359
+ const worldPos = this.screenToWorld(data.x, data.y);
360
+ if (worldPos) this.forceSimulation.updateDrag(worldPos);
361
+ }
362
+ this.emit("node:drag", {
363
+ node,
364
+ x: data.x,
365
+ y: data.y
366
+ });
367
+ };
368
+ handleDragEnd = (data) => {
369
+ const node = this.getNodeByIndex(data.index);
370
+ if (!node) return;
371
+ this.isDragging = false;
372
+ if (this.cameraController) this.cameraController.setEnabled(true);
373
+ if (this.forceSimulation) this.forceSimulation.endDrag();
374
+ this.emit("node:dragend", { node });
375
+ };
376
+ /**
377
+ * Convert screen coordinates to world position on drag plane
378
+ */
379
+ screenToWorld(clientX, clientY) {
380
+ if (!this.cameraController || !this.viewport) return null;
381
+ this.pointer.x = clientX / this.viewport.width * 2 - 1;
382
+ this.pointer.y = -(clientY / this.viewport.height) * 2 + 1;
383
+ this.raycaster.setFromCamera(this.pointer, this.cameraController.camera);
384
+ const target = new THREE.Vector3();
385
+ return this.raycaster.ray.intersectPlane(this.dragPlane, target);
386
+ }
387
+ /**
388
+ * Update viewport dimensions on resize
389
+ */
390
+ resize(width, height) {
391
+ if (this.viewport) {
392
+ this.viewport.width = width;
393
+ this.viewport.height = height;
394
+ }
395
+ }
396
+ dispose() {
397
+ this.off("pointer:dragstart", this.handleDragStart);
398
+ this.off("pointer:drag", this.handleDrag);
399
+ this.off("pointer:dragend", this.handleDragEnd);
400
+ }
401
+ };
402
+
403
+ //#endregion
404
+ //#region ui/Tooltips.ts
405
+ var Tooltip = class {
406
+ element;
407
+ isVisible = false;
408
+ cleanup;
409
+ constructor(type, container) {
410
+ this.type = type;
411
+ this.container = container;
412
+ this.element = this.createElement();
413
+ }
414
+ createElement() {
415
+ const tooltip = document.createElement("div");
416
+ tooltip.className = `tooltip tooltip-${this.type}`;
417
+ tooltip.style.position = "absolute";
418
+ tooltip.style.visibility = "hidden";
419
+ tooltip.style.pointerEvents = "none";
420
+ tooltip.style.zIndex = "1000";
421
+ this.container.appendChild(tooltip);
422
+ return tooltip;
423
+ }
424
+ open(content) {
425
+ this.element.innerHTML = content;
426
+ this.element.style.visibility = "visible";
427
+ this.isVisible = true;
428
+ }
429
+ openWithRenderer(renderFn) {
430
+ const result = renderFn();
431
+ if (typeof result === "function") this.cleanup = result;
432
+ this.element.style.visibility = "visible";
433
+ this.isVisible = true;
434
+ }
435
+ close() {
436
+ this.element.style.visibility = "hidden";
437
+ this.isVisible = false;
438
+ if (this.cleanup) {
439
+ this.cleanup();
440
+ this.cleanup = void 0;
441
+ }
442
+ }
443
+ updatePos(x, y) {
444
+ if (this.isVisible) {
445
+ this.element.style.left = `${x}px`;
446
+ this.element.style.top = `${y}px`;
447
+ }
448
+ }
449
+ getElement() {
450
+ return this.element;
451
+ }
452
+ destroy() {
453
+ if (this.cleanup) this.cleanup();
454
+ this.element.remove();
455
+ }
456
+ };
457
+
458
+ //#endregion
459
+ //#region ui/TooltipManager.ts
460
+ var TooltipManager = class {
461
+ mainTooltip;
462
+ previewTooltip;
463
+ chainTooltips = /* @__PURE__ */ new Map();
464
+ config;
465
+ closeButton = null;
466
+ onCloseCallback;
467
+ constructor(container, canvas, config) {
468
+ this.container = container;
469
+ this.canvas = canvas;
470
+ this.config = {
471
+ useDefaultStyles: true,
472
+ ...config
473
+ };
474
+ this.mainTooltip = new Tooltip("main", container, this.config.useDefaultStyles);
475
+ this.previewTooltip = new Tooltip("preview", container, this.config.useDefaultStyles);
476
+ }
477
+ /**
478
+ * Show grab cursor when hovering over a node
479
+ */
480
+ showGrabIcon(x, y, progress) {
481
+ if (this.config.cursorHandler) {
482
+ const state = progress > 0 ? "grab" : "pointer";
483
+ this.config.cursorHandler(this.canvas, state);
484
+ } else this.canvas.style.cursor = progress > 0 ? "grab" : "pointer";
485
+ }
486
+ hideGrabIcon() {
487
+ if (this.config.cursorHandler) this.config.cursorHandler(this.canvas, "default");
488
+ else this.canvas.style.cursor = "default";
489
+ }
490
+ /**
491
+ * Show preview tooltip (triggered by pop event when hover reaches 1.0)
492
+ */
493
+ showPreview(node, x, y) {
494
+ if (this.config.renderer) this.previewTooltip.openWithRenderer(() => this.config.renderer(this.previewTooltip.getElement(), node, "preview", x, y));
495
+ else {
496
+ const content = this.generatePreviewContent(node);
497
+ this.previewTooltip.open(content);
498
+ }
499
+ this.previewTooltip.updatePos(x + 20, y + 20);
500
+ }
501
+ hidePreview() {
502
+ this.previewTooltip.close();
503
+ }
504
+ /**
505
+ * Show full tooltip (triggered by click)
506
+ */
507
+ showFull(node, x, y) {
508
+ if (this.config.renderer) this.mainTooltip.openWithRenderer(() => this.config.renderer(this.mainTooltip.getElement(), node, "full", x, y));
509
+ else {
510
+ const content = this.generateFullContent(node);
511
+ this.mainTooltip.open(content);
512
+ }
513
+ if (!this.config.renderer) this.addCloseButton();
514
+ this.mainTooltip.updatePos(x + 20, y + 20);
515
+ }
516
+ hideFull() {
517
+ this.mainTooltip.close();
518
+ this.removeCloseButton();
519
+ }
520
+ /**
521
+ * Set callback for when tooltip is closed
522
+ */
523
+ setOnCloseCallback(callback) {
524
+ this.onCloseCallback = callback;
525
+ }
526
+ /**
527
+ * Add close button to tooltip
528
+ */
529
+ addCloseButton() {
530
+ this.removeCloseButton();
531
+ const element = this.mainTooltip.getElement();
532
+ element.style.pointerEvents = "auto";
533
+ this.closeButton = document.createElement("button");
534
+ this.closeButton.innerHTML = "×";
535
+ this.closeButton.className = "tooltip-close";
536
+ this.closeButton.style.cssText = `
537
+ position: absolute;
538
+ top: 8px;
539
+ right: 8px;
540
+ background: rgba(255, 255, 255, 0.2);
541
+ border: none;
542
+ color: white;
543
+ width: 24px;
544
+ height: 24px;
545
+ border-radius: 50%;
546
+ cursor: pointer;
547
+ font-size: 20px;
548
+ line-height: 1;
549
+ display: flex;
550
+ align-items: center;
551
+ justify-content: center;
552
+ transition: background 0.2s;
553
+ padding: 0;
554
+ `;
555
+ this.closeButton.addEventListener("mouseenter", () => {
556
+ if (this.closeButton) this.closeButton.style.background = "rgba(255, 255, 255, 0.3)";
557
+ });
558
+ this.closeButton.addEventListener("mouseleave", () => {
559
+ if (this.closeButton) this.closeButton.style.background = "rgba(255, 255, 255, 0.2)";
560
+ });
561
+ this.closeButton.addEventListener("click", (e) => {
562
+ e.stopPropagation();
563
+ this.hideFull();
564
+ if (this.onCloseCallback) this.onCloseCallback();
565
+ });
566
+ element.appendChild(this.closeButton);
567
+ }
568
+ /**
569
+ * Remove close button from tooltip
570
+ */
571
+ removeCloseButton() {
572
+ if (this.closeButton) {
573
+ this.closeButton.remove();
574
+ this.closeButton = null;
575
+ }
576
+ const element = this.mainTooltip.getElement();
577
+ element.style.pointerEvents = "none";
578
+ }
579
+ /**
580
+ * Hide all tooltips
581
+ */
582
+ hideAll() {
583
+ this.hideGrabIcon();
584
+ this.hidePreview();
585
+ this.hideFull();
586
+ this.hideChainTooltips();
587
+ }
588
+ /**
589
+ * Generate preview content (subset of fields)
590
+ */
591
+ generatePreviewContent(node) {
592
+ return `
593
+ <div class="tooltip-preview" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
594
+ <h3 style="margin: 0 0 8px 0; font-size: 16px; font-weight: 600;">${this.escapeHtml(node.title || node.id)}</h3>
595
+ <p style="margin: 0; font-size: 13px; opacity: 0.8;">${this.escapeHtml(node.group || "unknown")}</p>
596
+ </div>
597
+ `;
598
+ }
599
+ /**
600
+ * Generate full tooltip content (all fields)
601
+ */
602
+ generateFullContent(node) {
603
+ return `
604
+ <div class="tooltip-full" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding-right: 24px;">
605
+ <h2 style="margin: 0 0 12px 0; font-size: 18px; font-weight: 600;">${this.escapeHtml(node.title || node.id)}</h2>
606
+ ${node.thumbnailUrl ? `<img src="${this.escapeHtml(node.thumbnailUrl)}" alt="thumbnail" style="max-width: 100%; height: auto; border-radius: 4px; margin: 0 0 12px 0;" />` : ""}
607
+ <p style="margin: 0 0 8px 0; font-size: 13px;"><strong>Group:</strong> ${this.escapeHtml(node.group || "unknown")}</p>
608
+ ${node.description ? `<p style="margin: 0 0 8px 0; font-size: 13px; line-height: 1.5;">${this.escapeHtml(node.description)}</p>` : ""}
609
+ <p style="margin: 0; font-size: 12px; opacity: 0.6;"><strong>ID:</strong> ${this.escapeHtml(node.id)}</p>
610
+ </div>
611
+ `;
612
+ }
613
+ escapeHtml(text) {
614
+ const div = document.createElement("div");
615
+ div.textContent = text;
616
+ return div.innerHTML;
617
+ }
618
+ showMainTooltip(content, x, y) {
619
+ this.mainTooltip.open(content);
620
+ this.mainTooltip.updatePos(x, y);
621
+ }
622
+ hideMainTooltip() {
623
+ this.mainTooltip.close();
624
+ }
625
+ showChainTooltip(nodeId, content, x, y) {
626
+ if (!this.chainTooltips.has(nodeId)) this.chainTooltips.set(nodeId, new Tooltip("label", this.container, this.config.useDefaultStyles));
627
+ const tooltip = this.chainTooltips.get(nodeId);
628
+ tooltip.open(content);
629
+ tooltip.updatePos(x, y);
630
+ }
631
+ hideChainTooltips() {
632
+ this.chainTooltips.forEach((tooltip) => tooltip.close());
633
+ }
634
+ updateMainTooltipPos(x, y) {
635
+ this.mainTooltip.updatePos(x, y);
636
+ }
637
+ dispose() {
638
+ this.removeCloseButton();
639
+ this.mainTooltip.destroy();
640
+ this.previewTooltip.destroy();
641
+ this.chainTooltips.forEach((tooltip) => tooltip.destroy());
642
+ this.chainTooltips.clear();
643
+ }
644
+ };
645
+
646
+ //#endregion
647
+ //#region controls/InteractionManager.ts
648
+ var InteractionManager = class {
649
+ pointerInput;
650
+ hoverHandler;
651
+ clickHandler;
652
+ dragHandler;
653
+ tooltipManager;
654
+ isDragging = false;
655
+ isTooltipSticky = false;
656
+ stickyNodeId = null;
657
+ searchHighlightIds = [];
658
+ constructor(pickFunction, canvas, viewport, getNodeByIndex, cameraController, forceSimulation, tooltipConfig, graphScene, getConnectedNodeIds) {
659
+ this.graphScene = graphScene;
660
+ this.getConnectedNodeIds = getConnectedNodeIds;
661
+ this.pointerInput = new InputProcessor(pickFunction, canvas, viewport);
662
+ const tooltipContainer = document.body;
663
+ this.tooltipManager = new TooltipManager(tooltipContainer, canvas, tooltipConfig);
664
+ this.tooltipManager.setOnCloseCallback(() => {
665
+ this.isTooltipSticky = false;
666
+ });
667
+ this.hoverHandler = new HoverHandler(getNodeByIndex);
668
+ this.clickHandler = new ClickHandler(getNodeByIndex);
669
+ this.dragHandler = new DragHandler(getNodeByIndex, cameraController, forceSimulation, viewport);
670
+ this.wireEvents();
671
+ this.wireTooltipEvents();
672
+ this.wireVisualFeedbackEvents();
673
+ }
674
+ wireEvents() {
675
+ this.pointerInput.on("pointer:hoverstart", (data) => {
676
+ this.hoverHandler.emit("pointer:hoverstart", data);
677
+ });
678
+ this.pointerInput.on("pointer:hover", (data) => {
679
+ this.hoverHandler.emit("pointer:hover", data);
680
+ });
681
+ this.pointerInput.on("pointer:hoverend", (data) => {
682
+ this.hoverHandler.emit("pointer:hoverend", data);
683
+ });
684
+ this.pointerInput.on("pointer:pop", (data) => {
685
+ this.hoverHandler.emit("pointer:pop", data);
686
+ });
687
+ this.pointerInput.on("pointer:click", (data) => {
688
+ this.clickHandler.emit("pointer:click", data);
689
+ });
690
+ this.pointerInput.on("pointer:clickempty", () => {
691
+ if (this.isTooltipSticky) {
692
+ this.tooltipManager.hideFull();
693
+ this.isTooltipSticky = false;
694
+ this.stickyNodeId = null;
695
+ const linkRenderer = this.graphScene.getLinkRenderer();
696
+ const nodeRenderer = this.graphScene.getNodeRenderer();
697
+ if (this.searchHighlightIds.length > 0) {
698
+ linkRenderer?.clearHighlights(.05);
699
+ nodeRenderer?.highlight(this.searchHighlightIds);
700
+ } else {
701
+ linkRenderer?.clearHighlights(.05);
702
+ nodeRenderer?.clearHighlights(.05);
703
+ }
704
+ }
705
+ });
706
+ this.pointerInput.on("pointer:dragstart", (data) => {
707
+ this.dragHandler.emit("pointer:dragstart", data);
708
+ this.isDragging = true;
709
+ });
710
+ this.pointerInput.on("pointer:drag", (data) => {
711
+ this.dragHandler.emit("pointer:drag", data);
712
+ });
713
+ this.pointerInput.on("pointer:dragend", (data) => {
714
+ this.dragHandler.emit("pointer:dragend", data);
715
+ this.isDragging = false;
716
+ });
717
+ }
718
+ wireTooltipEvents() {
719
+ this.hoverHandler.on("node:hover", ({ x, y, progress }) => {
720
+ this.tooltipManager.showGrabIcon(x, y, progress);
721
+ });
722
+ this.hoverHandler.on("node:pop", ({ node, x, y, clientX, clientY }) => {
723
+ if (!this.isTooltipSticky) {
724
+ this.tooltipManager.showPreview(node, clientX ?? x, clientY ?? y);
725
+ window.dispatchEvent(new CustomEvent("audio:hover"));
726
+ }
727
+ });
728
+ this.hoverHandler.on("node:hoverend", () => {
729
+ this.tooltipManager.hideGrabIcon();
730
+ if (!this.isTooltipSticky) this.tooltipManager.hidePreview();
731
+ });
732
+ this.clickHandler.on("node:click", ({ node, x, y, clientX, clientY }) => {
733
+ if (this.isTooltipSticky && this.stickyNodeId === node.id) {
734
+ this.tooltipManager.hideFull();
735
+ this.isTooltipSticky = false;
736
+ this.stickyNodeId = null;
737
+ const linkRenderer = this.graphScene.getLinkRenderer();
738
+ const nodeRenderer = this.graphScene.getNodeRenderer();
739
+ if (this.searchHighlightIds.length > 0) {
740
+ linkRenderer?.clearHighlights(.05);
741
+ nodeRenderer?.highlight(this.searchHighlightIds);
742
+ } else {
743
+ linkRenderer?.clearHighlights(.05);
744
+ nodeRenderer?.clearHighlights(.05);
745
+ }
746
+ } else {
747
+ this.tooltipManager.hidePreview();
748
+ this.tooltipManager.showFull(node, clientX ?? x, clientY ?? y);
749
+ this.isTooltipSticky = true;
750
+ this.stickyNodeId = node.id;
751
+ window.dispatchEvent(new CustomEvent("audio:open"));
752
+ this.graphScene.getLinkRenderer()?.highlightConnectedLinks(node.id);
753
+ const nodeRenderer = this.graphScene.getNodeRenderer();
754
+ if (this.getConnectedNodeIds) {
755
+ const connectedIds = this.getConnectedNodeIds(node.id);
756
+ connectedIds.push(node.id);
757
+ nodeRenderer?.highlight(connectedIds);
758
+ }
759
+ }
760
+ });
761
+ }
762
+ /**
763
+ * Wire visual feedback events (opacity, highlighting)
764
+ * Centralized control of visual responses to interactions
765
+ */
766
+ wireVisualFeedbackEvents() {
767
+ if (!this.graphScene) return;
768
+ this.hoverHandler.on("node:hoverstart", ({ node }) => {
769
+ this.graphScene.getLinkRenderer()?.highlightConnectedLinks(node.id);
770
+ const nodeRenderer = this.graphScene.getNodeRenderer();
771
+ if (this.getConnectedNodeIds) {
772
+ const connectedIds = this.getConnectedNodeIds(node.id);
773
+ connectedIds.push(node.id);
774
+ nodeRenderer?.highlight(connectedIds);
775
+ }
776
+ });
777
+ this.hoverHandler.on("node:hoverend", () => {
778
+ const linkRenderer = this.graphScene.getLinkRenderer();
779
+ const nodeRenderer = this.graphScene.getNodeRenderer();
780
+ if (this.isTooltipSticky && this.stickyNodeId) {
781
+ linkRenderer?.highlightConnectedLinks(this.stickyNodeId);
782
+ if (this.getConnectedNodeIds) {
783
+ const connectedIds = this.getConnectedNodeIds(this.stickyNodeId);
784
+ connectedIds.push(this.stickyNodeId);
785
+ nodeRenderer?.highlight(connectedIds);
786
+ }
787
+ } else if (this.searchHighlightIds.length > 0) {
788
+ linkRenderer?.clearHighlights(.05);
789
+ nodeRenderer?.highlight(this.searchHighlightIds);
790
+ } else {
791
+ linkRenderer?.clearHighlights(.05);
792
+ nodeRenderer?.clearHighlights(.05);
793
+ }
794
+ });
795
+ }
796
+ getPointerInput() {
797
+ return this.pointerInput;
798
+ }
799
+ getHoverHandler() {
800
+ return this.hoverHandler;
801
+ }
802
+ getClickHandler() {
803
+ return this.clickHandler;
804
+ }
805
+ getDragHandler() {
806
+ return this.dragHandler;
807
+ }
808
+ getTooltipManager() {
809
+ return this.tooltipManager;
810
+ }
811
+ isTooltipStickyOpen() {
812
+ return this.isTooltipSticky;
813
+ }
814
+ /**
815
+ * Update viewport dimensions on resize
816
+ */
817
+ resize(width, height) {
818
+ this.pointerInput.resize(width, height);
819
+ this.dragHandler.resize(width, height);
820
+ }
821
+ cleanup() {
822
+ this.tooltipManager.hideAll();
823
+ this.isTooltipSticky = false;
824
+ this.isDragging = false;
825
+ }
826
+ dispose() {
827
+ this.cleanup();
828
+ this.pointerInput.dispose();
829
+ this.hoverHandler.dispose();
830
+ this.clickHandler.dispose();
831
+ this.dragHandler.dispose();
832
+ this.tooltipManager.dispose();
833
+ }
834
+ };
835
+
836
+ //#endregion
837
+ //#region types/iCameraMode.ts
838
+ let CameraMode = /* @__PURE__ */ function(CameraMode) {
839
+ CameraMode["Orbit"] = "orbit";
840
+ CameraMode["Map"] = "map";
841
+ return CameraMode;
842
+ }({});
843
+
844
+ //#endregion
845
+ //#region rendering/CameraController.ts
846
+ var CameraController = class {
847
+ camera;
848
+ controls;
849
+ sceneBounds = null;
850
+ autorotateEnabled = false;
851
+ autorotateSpeed = .5;
852
+ userIsActive = true;
853
+ constructor(domelement, config) {
854
+ this.camera = new THREE.PerspectiveCamera(config?.fov ?? 50, config?.aspect ?? window.innerWidth / window.innerHeight, config?.near ?? 1e-5, config?.far ?? 1e4);
855
+ if (config?.position) this.camera.position.copy(config.position);
856
+ else this.camera.position.set(0, 0, 2);
857
+ this.controls = new CameraControls(this.camera, domelement);
858
+ console.log(this.controls, domelement);
859
+ this.setupDefaultControls(config);
860
+ }
861
+ setupDefaultControls(config) {
862
+ this.controls.smoothTime = .8;
863
+ this.controls.dollyToCursor = true;
864
+ this.controls.minDistance = config?.minDistance ?? 100;
865
+ this.controls.maxDistance = config?.maxDistance ?? 4e3;
866
+ this.setMode(CameraMode.Orbit);
867
+ }
868
+ /**
869
+ * Set camera control mode
870
+ */
871
+ setMode(mode) {
872
+ switch (mode) {
873
+ case CameraMode.Map:
874
+ this.controls.mouseButtons.left = CameraControls.ACTION.TRUCK;
875
+ this.controls.mouseButtons.right = CameraControls.ACTION.TRUCK;
876
+ this.controls.mouseButtons.wheel = CameraControls.ACTION.DOLLY;
877
+ this.controls.touches.one = CameraControls.ACTION.TOUCH_TRUCK;
878
+ this.controls.touches.two = CameraControls.ACTION.TOUCH_DOLLY_TRUCK;
879
+ this.controls.touches.three = CameraControls.ACTION.TOUCH_TRUCK;
880
+ this.controls.maxPolarAngle = Math.PI / 2 - .1;
881
+ break;
882
+ case CameraMode.Orbit:
883
+ default:
884
+ this.controls.mouseButtons.left = CameraControls.ACTION.ROTATE;
885
+ this.controls.mouseButtons.right = CameraControls.ACTION.TRUCK;
886
+ this.controls.mouseButtons.wheel = CameraControls.ACTION.DOLLY;
887
+ this.controls.touches.one = CameraControls.ACTION.TOUCH_ROTATE;
888
+ this.controls.touches.two = CameraControls.ACTION.TOUCH_DOLLY_TRUCK;
889
+ this.controls.touches.three = CameraControls.ACTION.TOUCH_TRUCK;
890
+ this.controls.maxPolarAngle = Math.PI;
891
+ break;
892
+ }
893
+ }
894
+ /**
895
+ * Set camera boundary
896
+ */
897
+ setBoundary(box) {
898
+ this.controls.setBoundary(box);
899
+ }
900
+ /**
901
+ * Update camera controls (should be called in render loop)
902
+ * Only autorotate if enabled and user is inactive
903
+ */
904
+ update(delta) {
905
+ if (this.autorotateEnabled && !this.userIsActive) this.controls.azimuthAngle += this.autorotateSpeed * delta;
906
+ return this.controls.update(delta);
907
+ }
908
+ /**
909
+ * Set autorotate enabled/disabled
910
+ */
911
+ setAutorotate(enabled, speed) {
912
+ this.autorotateEnabled = enabled;
913
+ this.autorotateSpeed = speed ? speed * THREE.MathUtils.DEG2RAD : 0;
914
+ }
915
+ /**
916
+ * Set user activity state
917
+ */
918
+ setUserIsActive(isActive) {
919
+ this.userIsActive = isActive;
920
+ }
921
+ /**
922
+ * Configure camera controls
923
+ */
924
+ configureControls(config) {
925
+ Object.assign(this.controls, config);
926
+ }
927
+ /**
928
+ * Resize camera when window resizes
929
+ */
930
+ resize(width, height) {
931
+ this.camera.aspect = width / height;
932
+ this.camera.updateProjectionMatrix();
933
+ }
934
+ /**
935
+ * Animate camera to look at a specific target from a specific position
936
+ */
937
+ async setLookAt(position, target, enableTransition = true) {
938
+ await this.controls.setLookAt(position.x, position.y, position.z, target.x, target.y, target.z, enableTransition);
939
+ }
940
+ /**
941
+ * Reset camera to default position
942
+ */
943
+ async reset(enableTransition = true) {
944
+ return this.controls.setLookAt(0, 0, 50, 0, 0, 0, enableTransition);
945
+ }
946
+ /**
947
+ * Enable/disable controls
948
+ */
949
+ setEnabled(enabled) {
950
+ this.controls.enabled = enabled;
951
+ }
952
+ /**
953
+ * Get current camera target (look-at point)
954
+ */
955
+ getTarget(out) {
956
+ const target = out ?? new THREE.Vector3();
957
+ this.controls.getTarget(target);
958
+ return target;
959
+ }
960
+ /**
961
+ * Dispose camera manager and clean up
962
+ */
963
+ dispose() {
964
+ this.controls.dispose();
965
+ }
966
+ /**
967
+ * Clear camera bounds (remove boundary restrictions)
968
+ */
969
+ clearBounds() {
970
+ this.sceneBounds = null;
971
+ }
972
+ /**
973
+ * Get current scene bounds
974
+ */
975
+ getSceneBounds() {
976
+ return this.sceneBounds;
977
+ }
978
+ /**
979
+ * Update scene bounds used by the camera manager.
980
+ * Stores the bounds for future use (e.g. constraining controls).
981
+ */
982
+ updateBounds(box) {
983
+ this.sceneBounds = box.clone();
984
+ this.controls.setBoundary(box);
985
+ }
986
+ /**
987
+ * Unified configuration method
988
+ */
989
+ setOptions(options) {
990
+ if (options.controls) this.configureControls(options.controls);
991
+ if (options.autoRotate !== void 0) this.setAutorotate(options.autoRotate, options.autoRotateSpeed);
992
+ }
993
+ };
994
+
995
+ //#endregion
996
+ //#region assets/glsl/lines/lines.frag
997
+ var lines_default$1 = "precision mediump float;\n\nvarying vec3 vColor;\r\nflat varying float vAlphaIndex;\n\nuniform sampler2D uAlphaTexture;\r\nuniform vec2 uAlphaTextureSize; \n\nvec2 alphaUvFromIndex(float idx) {\r\n float width = uAlphaTextureSize.x;\r\n float texX = mod(idx, width);\r\n float texY = floor(idx / width);\r\n return (vec2(texX + 0.5, texY + 0.5) / uAlphaTextureSize);\r\n}\n\nvoid main() {\r\n vec2 uv = alphaUvFromIndex(vAlphaIndex);\r\n float a = texture2D(uAlphaTexture, uv).r;\n\n gl_FragColor = vec4(vColor, a);\r\n #include <colorspace_fragment>\r\n}";
998
+
999
+ //#endregion
1000
+ //#region assets/glsl/lines/lines.vert
1001
+ var lines_default = "attribute float t;\n\nattribute vec2 instanceLinkA;\r\nattribute vec2 instanceLinkB;\r\nattribute vec3 instanceColorA;\r\nattribute vec3 instanceColorB;\r\nattribute float instanceAlphaIndex;\n\nuniform sampler2D uPositionsTexture;\r\nuniform vec2 uPositionsTextureSize;\r\nuniform float uNoiseStrength;\r\nuniform float uTime;\n\nvarying vec3 vColor;\r\nflat varying float vAlphaIndex;\n\nvec3 organicNoise(vec3 p, float seed) {\r\n float frequency = 1.5;\r\n float branchiness = 5.;\r\n \r\n \n vec3 n1 = sin(p * frequency + seed) * 0.5;\r\n vec3 n2 = sin(p * frequency * 2.3 + seed * 1.5) * 0.25 * (1.0 + branchiness);\r\n vec3 n3 = sin(p * frequency * 4.7 + seed * 2.3) * 0.125 * (1.0 + branchiness);\r\n \r\n return n1 + n2 + n3;\r\n}\n\nfloat smoothCurve(float t) {\r\n return t * t * (3.0 - 2.0 * t);\r\n}\n\nvoid main() {\r\n \n vColor = mix(instanceColorA, instanceColorB, t);\r\n vAlphaIndex = instanceAlphaIndex;\n\n \n vec2 uvA = (instanceLinkA + vec2(0.5)) / uPositionsTextureSize;\r\n vec2 uvB = (instanceLinkB + vec2(0.5)) / uPositionsTextureSize;\n\n vec4 posA = texture2D(uPositionsTexture, uvA);\r\n vec4 posB = texture2D(uPositionsTexture, uvB);\r\n if (posA.w < 0.5 || posB.w < 0.5) {\r\n \n gl_Position = vec4(2.0, 2.0, 2.0, 1.0); \r\n return;\r\n }\r\n vec3 linear = mix(posA.xyz, posB.xyz, t);\r\n \r\n \n vec3 dir = normalize(posB.xyz - posA.xyz);\r\n float dist = length(posB.xyz - posA.xyz);\r\n vec3 up = abs(dir.y) < 0.99 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);\r\n vec3 perp = normalize(cross(dir, up));\r\n vec3 upVec = normalize(cross(perp, dir));\n\n \r\n \n float sinPhase = instanceAlphaIndex * 0.1 + uTime * 0.3;\r\n float sinWave = sin(t * 3.14159265 * 2.0 + sinPhase);\r\n \r\n \n float tSmooth = smoothCurve(t);\r\n float curve = sin(t * 3.14159265) * tSmooth;\r\n \r\n \n float curvature = 0.1;\r\n curve = pow(curve, 1.0 - curvature * 0.5);\r\n \r\n float intensity = curve * uNoiseStrength;\n\n vec3 pos = linear;\n\n if (intensity > 0.001) {\r\n \n vec3 samplePos = linear * 0.5 + vec3(instanceAlphaIndex, 0.0, instanceAlphaIndex * 0.7);\r\n vec3 noise = organicNoise(samplePos, instanceAlphaIndex);\r\n \r\n \n float sinModulation = 0.5 + 0.5 * sinWave;\r\n pos += perp * noise.x * intensity * sinModulation;\r\n pos += upVec * noise.y * intensity * 0.7;\r\n pos += dir * noise.z * intensity * 0.3;\r\n }\n\n vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);\r\n gl_Position = projectionMatrix * mvPosition;\r\n}";
1002
+
1003
+ //#endregion
1004
+ //#region core/StyleRegistry.ts
1005
+ /**
1006
+ * Default styles
1007
+ */
1008
+ const DEFAULT_NODE_STYLE = {
1009
+ color: new THREE.Color(214748364),
1010
+ size: 6
1011
+ };
1012
+ const DEFAULT_LINK_STYLE = { color: new THREE.Color(8947848) };
1013
+ /**
1014
+ * StyleRegistry - Manages visual styles for node and link categories
1015
+ *
1016
+ * Usage:
1017
+ * styleRegistry.setNodeStyle('person', { color: 0xff0000, size: 20 })
1018
+ * styleRegistry.setLinkStyle('friendship', { color: '#00ff00', width: 2 })
1019
+ *
1020
+ * const style = styleRegistry.getNodeStyle('person')
1021
+ */
1022
+ var StyleRegistry = class {
1023
+ nodeStyles = /* @__PURE__ */ new Map();
1024
+ linkStyles = /* @__PURE__ */ new Map();
1025
+ defaultNodeStyle = { ...DEFAULT_NODE_STYLE };
1026
+ defaultLinkStyle = { ...DEFAULT_LINK_STYLE };
1027
+ /**
1028
+ * Convert color input to THREE.Color
1029
+ */
1030
+ toColor(input) {
1031
+ if (input instanceof THREE.Color) return input.clone();
1032
+ return new THREE.Color(input);
1033
+ }
1034
+ /**
1035
+ * Set the default style for nodes without a category
1036
+ */
1037
+ setDefaultNodeStyle(style) {
1038
+ if (style.color !== void 0) this.defaultNodeStyle.color = this.toColor(style.color);
1039
+ if (style.size !== void 0) this.defaultNodeStyle.size = style.size;
1040
+ }
1041
+ /**
1042
+ * Set the default style for links without a category
1043
+ */
1044
+ setDefaultLinkStyle(style) {
1045
+ if (style.color !== void 0) this.defaultLinkStyle.color = this.toColor(style.color);
1046
+ }
1047
+ /**
1048
+ * Register a style for a node category
1049
+ */
1050
+ setNodeStyle(category, style) {
1051
+ this.nodeStyles.set(category, {
1052
+ color: this.toColor(style.color),
1053
+ size: style.size
1054
+ });
1055
+ }
1056
+ /**
1057
+ * Register multiple node styles at once
1058
+ */
1059
+ setNodeStyles(styles) {
1060
+ for (const [category, style] of Object.entries(styles)) this.setNodeStyle(category, style);
1061
+ }
1062
+ /**
1063
+ * Register a style for a link category
1064
+ */
1065
+ setLinkStyle(category, style) {
1066
+ this.linkStyles.set(category, { color: this.toColor(style.color) });
1067
+ }
1068
+ /**
1069
+ * Register multiple link styles at once
1070
+ */
1071
+ setLinkStyles(styles) {
1072
+ for (const [category, style] of Object.entries(styles)) this.setLinkStyle(category, style);
1073
+ }
1074
+ /**
1075
+ * Get the resolved style for a node category
1076
+ */
1077
+ getNodeStyle(category) {
1078
+ if (!category) return this.defaultNodeStyle;
1079
+ return this.nodeStyles.get(category) || this.defaultNodeStyle;
1080
+ }
1081
+ /**
1082
+ * Get the resolved style for a link category
1083
+ */
1084
+ getLinkStyle(category) {
1085
+ if (!category) return this.defaultLinkStyle;
1086
+ return this.linkStyles.get(category) || this.defaultLinkStyle;
1087
+ }
1088
+ /**
1089
+ * Check if a node category exists
1090
+ */
1091
+ hasNodeStyle(category) {
1092
+ return this.nodeStyles.has(category);
1093
+ }
1094
+ /**
1095
+ * Check if a link category exists
1096
+ */
1097
+ hasLinkStyle(category) {
1098
+ return this.linkStyles.has(category);
1099
+ }
1100
+ /**
1101
+ * Remove a node style
1102
+ */
1103
+ removeNodeStyle(category) {
1104
+ this.nodeStyles.delete(category);
1105
+ }
1106
+ /**
1107
+ * Remove a link style
1108
+ */
1109
+ removeLinkStyle(category) {
1110
+ this.linkStyles.delete(category);
1111
+ }
1112
+ /**
1113
+ * Clear all styles
1114
+ */
1115
+ clear() {
1116
+ this.nodeStyles.clear();
1117
+ this.linkStyles.clear();
1118
+ this.defaultNodeStyle = { ...DEFAULT_NODE_STYLE };
1119
+ this.defaultLinkStyle = { ...DEFAULT_LINK_STYLE };
1120
+ }
1121
+ /**
1122
+ * Get all registered node categories
1123
+ */
1124
+ getNodeCategories() {
1125
+ return Array.from(this.nodeStyles.keys());
1126
+ }
1127
+ /**
1128
+ * Get all registered link categories
1129
+ */
1130
+ getLinkCategories() {
1131
+ return Array.from(this.linkStyles.keys());
1132
+ }
1133
+ };
1134
+ const styleRegistry = new StyleRegistry();
1135
+
1136
+ //#endregion
1137
+ //#region textures/StaticAssets.ts
1138
+ /**
1139
+ * Manages read-only GPU textures created at initialization
1140
+ * These never change during simulation (except for mode changes)
1141
+ *
1142
+ * Node data: radii, colors
1143
+ * Link data: source/target indices, properties
1144
+ */
1145
+ var StaticAssets = class {
1146
+ nodeRadiiTexture = null;
1147
+ nodeColorsTexture = null;
1148
+ linkIndicesTexture = null;
1149
+ linkPropertiesTexture = null;
1150
+ nodeLinkMapTexture = null;
1151
+ nodeTextureSize = 0;
1152
+ linkTextureSize = 0;
1153
+ constructor() {}
1154
+ /**
1155
+ * Set/update node radii texture
1156
+ */
1157
+ setNodeRadii(radii, textureSize) {
1158
+ const expectedSize = textureSize * textureSize;
1159
+ if (radii.length !== expectedSize) {
1160
+ console.warn(`[StaticAssets] Radii array size mismatch! Got ${radii.length}, expected ${expectedSize}`);
1161
+ const correctedRadii = new Float32Array(expectedSize);
1162
+ correctedRadii.set(radii.slice(0, Math.min(radii.length, expectedSize)));
1163
+ radii = correctedRadii;
1164
+ }
1165
+ if (this.nodeRadiiTexture && this.nodeTextureSize === textureSize) {
1166
+ this.nodeRadiiTexture.image.data = radii;
1167
+ this.nodeRadiiTexture.needsUpdate = true;
1168
+ } else {
1169
+ this.nodeRadiiTexture?.dispose();
1170
+ this.nodeRadiiTexture = this.createTexture(radii, textureSize, THREE.RedFormat);
1171
+ this.nodeTextureSize = textureSize;
1172
+ }
1173
+ }
1174
+ /**
1175
+ * Set/update node colors texture
1176
+ */
1177
+ setNodeColors(colors, textureSize) {
1178
+ if (this.nodeColorsTexture && this.nodeTextureSize === textureSize) {
1179
+ this.nodeColorsTexture.image.data = colors;
1180
+ this.nodeColorsTexture.needsUpdate = true;
1181
+ } else {
1182
+ this.nodeColorsTexture?.dispose();
1183
+ this.nodeColorsTexture = this.createTexture(colors, textureSize, THREE.RGBAFormat);
1184
+ this.nodeTextureSize = textureSize;
1185
+ }
1186
+ }
1187
+ /**
1188
+ * Set link indices (source/target pairs)
1189
+ * Format: [sourceX, sourceY, targetX, targetY] per link
1190
+ */
1191
+ setLinkIndices(linkIndices, textureSize) {
1192
+ this.linkIndicesTexture?.dispose();
1193
+ this.linkIndicesTexture = this.createTexture(linkIndices, textureSize, THREE.RGBAFormat);
1194
+ this.linkTextureSize = textureSize;
1195
+ }
1196
+ /**
1197
+ * Set link properties (strength, distance, etc.)
1198
+ */
1199
+ setLinkProperties(linkProperties, textureSize) {
1200
+ this.linkPropertiesTexture?.dispose();
1201
+ this.linkPropertiesTexture = this.createTexture(linkProperties, textureSize, THREE.RGBAFormat);
1202
+ this.linkTextureSize = textureSize;
1203
+ }
1204
+ setNodeLinkMap(linkMap, textureSize) {
1205
+ this.nodeLinkMapTexture?.dispose();
1206
+ this.nodeLinkMapTexture = this.createTexture(linkMap, textureSize, THREE.RGBAFormat);
1207
+ }
1208
+ /**
1209
+ * Create a data texture with proper settings for GPU compute
1210
+ */
1211
+ createTexture(data, size, format) {
1212
+ const texture = new THREE.DataTexture(data, size, size, format, THREE.FloatType);
1213
+ texture.needsUpdate = true;
1214
+ texture.minFilter = THREE.NearestFilter;
1215
+ texture.magFilter = THREE.NearestFilter;
1216
+ texture.wrapS = THREE.ClampToEdgeWrapping;
1217
+ texture.wrapT = THREE.ClampToEdgeWrapping;
1218
+ texture.generateMipmaps = false;
1219
+ return texture;
1220
+ }
1221
+ getNodeRadiiTexture() {
1222
+ return this.nodeRadiiTexture;
1223
+ }
1224
+ getNodeColorsTexture() {
1225
+ return this.nodeColorsTexture;
1226
+ }
1227
+ getLinkIndicesTexture() {
1228
+ return this.linkIndicesTexture;
1229
+ }
1230
+ getLinkPropertiesTexture() {
1231
+ return this.linkPropertiesTexture;
1232
+ }
1233
+ getLinkMapTexture() {
1234
+ return this.nodeLinkMapTexture;
1235
+ }
1236
+ getNodeTextureSize() {
1237
+ return this.nodeTextureSize;
1238
+ }
1239
+ getLinkTextureSize() {
1240
+ return this.linkTextureSize;
1241
+ }
1242
+ /**
1243
+ * Check if assets are ready
1244
+ */
1245
+ hasNodeAssets() {
1246
+ return this.nodeRadiiTexture !== null;
1247
+ }
1248
+ hasLinkAssets() {
1249
+ return this.linkIndicesTexture !== null;
1250
+ }
1251
+ /**
1252
+ * Cleanup
1253
+ */
1254
+ dispose() {
1255
+ this.nodeRadiiTexture?.dispose();
1256
+ this.nodeColorsTexture?.dispose();
1257
+ this.linkIndicesTexture?.dispose();
1258
+ this.linkPropertiesTexture?.dispose();
1259
+ this.nodeLinkMapTexture?.dispose();
1260
+ this.nodeRadiiTexture = null;
1261
+ this.nodeColorsTexture = null;
1262
+ this.linkIndicesTexture = null;
1263
+ this.linkPropertiesTexture = null;
1264
+ this.nodeTextureSize = 0;
1265
+ this.linkTextureSize = 0;
1266
+ }
1267
+ };
1268
+ const staticAssets = new StaticAssets();
1269
+
1270
+ //#endregion
1271
+ //#region rendering/links/LinksOpacity.ts
1272
+ /**
1273
+ * LinksOpacity
1274
+ */
1275
+ var LinksOpacity = class {
1276
+ renderer;
1277
+ size;
1278
+ rtA;
1279
+ rtB;
1280
+ ping = 0;
1281
+ scene;
1282
+ camera;
1283
+ quad;
1284
+ computeMaterial;
1285
+ copyMaterial;
1286
+ targetTex = null;
1287
+ constructor(renderer, size) {
1288
+ this.renderer = renderer;
1289
+ this.size = size;
1290
+ const params = {
1291
+ minFilter: THREE.NearestFilter,
1292
+ magFilter: THREE.NearestFilter,
1293
+ wrapS: THREE.ClampToEdgeWrapping,
1294
+ wrapT: THREE.ClampToEdgeWrapping,
1295
+ format: THREE.RGBAFormat,
1296
+ type: THREE.FloatType,
1297
+ depthBuffer: false,
1298
+ stencilBuffer: false
1299
+ };
1300
+ this.rtA = new THREE.WebGLRenderTarget(size, size, params);
1301
+ this.rtB = new THREE.WebGLRenderTarget(size, size, params);
1302
+ this.scene = new THREE.Scene();
1303
+ this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
1304
+ const geo = new THREE.PlaneGeometry(2, 2);
1305
+ this.computeMaterial = new THREE.ShaderMaterial({
1306
+ vertexShader: `
1307
+ void main() {
1308
+ gl_Position = vec4(position, 1.0);
1309
+ }
1310
+ `,
1311
+ fragmentShader: `
1312
+ precision highp float;
1313
+
1314
+ uniform sampler2D uPrev;
1315
+ uniform sampler2D uTarget;
1316
+ uniform float uStep;
1317
+ uniform vec2 uResolution;
1318
+
1319
+ void main() {
1320
+ // compute stable texcoord from pixel coords
1321
+ vec2 uv = gl_FragCoord.xy / uResolution;
1322
+ float prev = texture2D(uPrev, uv).r;
1323
+ float target = texture2D(uTarget, uv).r;
1324
+
1325
+ // Lerp towards target
1326
+ float nextVal = mix(prev, target, uStep);
1327
+
1328
+ // Clamp to valid range
1329
+ nextVal = clamp(nextVal, 0.0, 1.0);
1330
+
1331
+ gl_FragColor = vec4(nextVal, 0.0, 0.0, 1.0);
1332
+ }
1333
+
1334
+ `,
1335
+ uniforms: {
1336
+ uPrev: { value: null },
1337
+ uTarget: { value: null },
1338
+ uStep: { value: .01 },
1339
+ uResolution: { value: new THREE.Vector2(size, size) }
1340
+ },
1341
+ depthTest: false,
1342
+ depthWrite: false
1343
+ });
1344
+ this.copyMaterial = new THREE.ShaderMaterial({
1345
+ vertexShader: this.computeMaterial.vertexShader,
1346
+ fragmentShader: `
1347
+ precision highp float;
1348
+ uniform sampler2D uSrc;
1349
+ uniform vec2 uResolution;
1350
+ void main() {
1351
+ vec2 uv = gl_FragCoord.xy / uResolution;
1352
+ vec4 s = texture2D(uSrc, uv);
1353
+ gl_FragColor = vec4(s.r, 0.0, 0.0, 1.0);
1354
+ }
1355
+ `,
1356
+ uniforms: {
1357
+ uSrc: { value: null },
1358
+ uResolution: { value: new THREE.Vector2(size, size) }
1359
+ },
1360
+ depthTest: false,
1361
+ depthWrite: false
1362
+ });
1363
+ this.quad = new THREE.Mesh(geo, this.computeMaterial);
1364
+ this.scene.add(this.quad);
1365
+ this.targetTex = new THREE.DataTexture(new Float32Array(size * size * 4).fill(0), size, size, THREE.RGBAFormat, THREE.FloatType);
1366
+ this.targetTex.minFilter = THREE.NearestFilter;
1367
+ this.targetTex.magFilter = THREE.NearestFilter;
1368
+ this.targetTex.wrapS = THREE.ClampToEdgeWrapping;
1369
+ this.targetTex.wrapT = THREE.ClampToEdgeWrapping;
1370
+ this.targetTex.flipY = false;
1371
+ this.targetTex.needsUpdate = true;
1372
+ }
1373
+ init(initialAlphaTexture) {
1374
+ this.copyMaterial.uniforms.uSrc.value = initialAlphaTexture;
1375
+ this.copyMaterial.uniforms.uResolution.value.set(this.size, this.size);
1376
+ this.quad.material = this.copyMaterial;
1377
+ const prevRT = this.renderer.getRenderTarget();
1378
+ this.renderer.setRenderTarget(this.rtA);
1379
+ this.renderer.render(this.scene, this.camera);
1380
+ this.renderer.setRenderTarget(this.rtB);
1381
+ this.renderer.render(this.scene, this.camera);
1382
+ this.renderer.setRenderTarget(prevRT);
1383
+ this.ping = 0;
1384
+ this.quad.material = this.computeMaterial;
1385
+ }
1386
+ setTargets(targets) {
1387
+ const total = this.size * this.size;
1388
+ const data = this.targetTex && this.targetTex.image && this.targetTex.image.data.length === total * 4 ? this.targetTex.image.data : new Float32Array(total * 4);
1389
+ for (let i = 0; i < total; i++) {
1390
+ const v = targets[i] ?? 0;
1391
+ const base = i * 4;
1392
+ data[base] = v;
1393
+ data[base + 1] = 0;
1394
+ data[base + 2] = 0;
1395
+ data[base + 3] = 1;
1396
+ }
1397
+ if (this.targetTex) {
1398
+ this.targetTex.image.data = data;
1399
+ this.targetTex.needsUpdate = true;
1400
+ }
1401
+ }
1402
+ runComputePass() {
1403
+ const prevRT = this.getPrevRT();
1404
+ this.computeMaterial.uniforms.uPrev.value = prevRT.texture;
1405
+ if (this.targetTex) this.computeMaterial.uniforms.uTarget.value = this.targetTex;
1406
+ const targetRT = this.getNextRT();
1407
+ this.renderer.setRenderTarget(targetRT);
1408
+ this.renderer.render(this.scene, this.camera);
1409
+ this.renderer.setRenderTarget(null);
1410
+ this.ping = 1 - this.ping;
1411
+ }
1412
+ update() {
1413
+ this.runComputePass();
1414
+ }
1415
+ getCurrentAlphaTexture() {
1416
+ return this.getPrevRT().texture;
1417
+ }
1418
+ /**
1419
+ * Set the step size for alpha transitions
1420
+ * @param step - Step size (default: 0.01)
1421
+ */
1422
+ setStep(step) {
1423
+ this.computeMaterial.uniforms.uStep.value = step;
1424
+ }
1425
+ getPrevRT() {
1426
+ return this.ping === 0 ? this.rtA : this.rtB;
1427
+ }
1428
+ getNextRT() {
1429
+ return this.ping === 0 ? this.rtB : this.rtA;
1430
+ }
1431
+ dispose() {
1432
+ this.rtA.dispose();
1433
+ this.rtB.dispose();
1434
+ this.copyMaterial.dispose();
1435
+ this.computeMaterial.dispose();
1436
+ this.quad.geometry.dispose();
1437
+ if (this.targetTex) this.targetTex.dispose();
1438
+ }
1439
+ };
1440
+
1441
+ //#endregion
1442
+ //#region rendering/links/LinksRenderer.ts
1443
+ /**
1444
+ * LinksRenderer - Renders links as line segments
1445
+ * Uses shared SimulationBuffers for node positions
1446
+ * Manages link-specific opacity and highlighting
1447
+ */
1448
+ var LinksRenderer = class {
1449
+ lines = null;
1450
+ links = [];
1451
+ nodeIndexMap = /* @__PURE__ */ new Map();
1452
+ linkIndexMap = /* @__PURE__ */ new Map();
1453
+ interpolationSteps = 24;
1454
+ params = {
1455
+ noiseStrength: .2,
1456
+ defaultAlpha: .02,
1457
+ highlightAlpha: .8,
1458
+ dimmedAlpha: .02,
1459
+ is2D: true
1460
+ };
1461
+ linkOpacity = null;
1462
+ simulationBuffers = null;
1463
+ constructor(scene, renderer) {
1464
+ this.scene = scene;
1465
+ this.renderer = renderer;
1466
+ }
1467
+ /**
1468
+ * Create link geometry and materials
1469
+ * Receives shared buffers as dependencies
1470
+ */
1471
+ create(links, nodes, simulationBuffers) {
1472
+ this.links = links;
1473
+ this.simulationBuffers = simulationBuffers;
1474
+ const textureSize = simulationBuffers.getTextureSize();
1475
+ const linkCount = links.length;
1476
+ this.buildNodeMapping(nodes);
1477
+ const { geometry, linkIndexMap } = this.createLinkGeometry(links, nodes, textureSize);
1478
+ this.linkIndexMap = linkIndexMap;
1479
+ const { linkIndicesData, linkPropertiesData, nodeLinkMapData, linksTextureSize } = this.buildLinkTextures(links, nodes, textureSize);
1480
+ staticAssets.setLinkIndices(linkIndicesData, linksTextureSize);
1481
+ staticAssets.setLinkProperties(linkPropertiesData, linksTextureSize);
1482
+ staticAssets.setNodeLinkMap(nodeLinkMapData, textureSize);
1483
+ const alphaTextureSize = Math.ceil(Math.sqrt(linkCount));
1484
+ this.linkOpacity = new LinksOpacity(this.renderer, alphaTextureSize);
1485
+ const total = alphaTextureSize * alphaTextureSize;
1486
+ const initialAlphaData = new Float32Array(total * 4);
1487
+ for (let i = 0; i < total; i++) {
1488
+ initialAlphaData[i * 4] = this.params.defaultAlpha;
1489
+ initialAlphaData[i * 4 + 1] = 0;
1490
+ initialAlphaData[i * 4 + 2] = 0;
1491
+ initialAlphaData[i * 4 + 3] = 1;
1492
+ }
1493
+ const initialAlphaTexture = new THREE.DataTexture(initialAlphaData, alphaTextureSize, alphaTextureSize, THREE.RGBAFormat, THREE.FloatType);
1494
+ initialAlphaTexture.needsUpdate = true;
1495
+ this.linkOpacity.init(initialAlphaTexture);
1496
+ initialAlphaTexture.dispose();
1497
+ const material = this.createRenderMaterial(textureSize, alphaTextureSize);
1498
+ this.lines = new THREE.LineSegments(geometry, material);
1499
+ this.lines.frustumCulled = false;
1500
+ this.lines.renderOrder = 998;
1501
+ this.scene.add(this.lines);
1502
+ this.clearHighlights();
1503
+ }
1504
+ /**
1505
+ * Update position texture from simulation
1506
+ * This is the SHARED texture used by both nodes and links
1507
+ */
1508
+ setPositionTexture(positionTexture) {
1509
+ if (!this.lines) return;
1510
+ const material = this.lines.material;
1511
+ if (material.uniforms.uPositionsTexture) material.uniforms.uPositionsTexture.value = positionTexture;
1512
+ }
1513
+ /**
1514
+ * Update shader uniforms (called each frame)
1515
+ */
1516
+ update(time) {
1517
+ if (!this.lines) return;
1518
+ const material = this.lines.material;
1519
+ if (material.uniforms.uTime) material.uniforms.uTime.value = time;
1520
+ }
1521
+ /**
1522
+ * Update link opacity (called every frame)
1523
+ */
1524
+ updateOpacity() {
1525
+ if (!this.linkOpacity) return;
1526
+ this.linkOpacity.update();
1527
+ if (this.lines && this.lines.material) {
1528
+ const material = this.lines.material;
1529
+ if (material.uniforms.uAlphaTexture) material.uniforms.uAlphaTexture.value = this.linkOpacity.getCurrentAlphaTexture();
1530
+ }
1531
+ }
1532
+ /**
1533
+ * Highlight links connected to a specific node
1534
+ */
1535
+ highlightConnectedLinks(nodeId, step = .01) {
1536
+ if (!this.lines || !this.linkOpacity) return;
1537
+ const linkCount = this.lines.geometry instanceof THREE.InstancedBufferGeometry ? this.lines.geometry.instanceCount : 0;
1538
+ if (linkCount === 0 || linkCount === void 0) return;
1539
+ const targets = new Array(linkCount).fill(this.params.dimmedAlpha);
1540
+ let hasConnections = false;
1541
+ for (const [linkId, instanceIndex] of this.linkIndexMap.entries()) {
1542
+ const [sourceId, targetId] = linkId.split("-");
1543
+ if (sourceId === nodeId || targetId === nodeId) {
1544
+ targets[instanceIndex] = this.params.highlightAlpha;
1545
+ hasConnections = true;
1546
+ }
1547
+ }
1548
+ if (!hasConnections) return;
1549
+ this.linkOpacity.setStep(step);
1550
+ this.linkOpacity.setTargets(targets);
1551
+ }
1552
+ /**
1553
+ * Clear all highlights (return to defaultAlpha)
1554
+ */
1555
+ clearHighlights(step = .1) {
1556
+ if (!this.linkOpacity) return;
1557
+ const linkCount = this.lines?.geometry instanceof THREE.InstancedBufferGeometry ? this.lines.geometry.instanceCount : 0;
1558
+ if (linkCount === 0 || linkCount === void 0) return;
1559
+ const targets = new Array(linkCount).fill(this.params.defaultAlpha);
1560
+ this.linkOpacity.setStep(step);
1561
+ this.linkOpacity.setTargets(targets);
1562
+ }
1563
+ /**
1564
+ * Update noise strength parameter
1565
+ */
1566
+ setNoiseStrength(strength) {
1567
+ this.params.noiseStrength = strength;
1568
+ this.refreshUniforms();
1569
+ }
1570
+ /**
1571
+ * Update alpha parameters
1572
+ */
1573
+ setAlphaParams(params) {
1574
+ if (params.defaultAlpha !== void 0) this.params.defaultAlpha = params.defaultAlpha;
1575
+ if (params.highlightAlpha !== void 0) this.params.highlightAlpha = params.highlightAlpha;
1576
+ if (params.dimmedAlpha !== void 0) this.params.dimmedAlpha = params.dimmedAlpha;
1577
+ }
1578
+ /**
1579
+ * Unified configuration method
1580
+ */
1581
+ setOptions(options) {
1582
+ if (options.visible !== void 0) this.setVisible(options.visible);
1583
+ if (options.noiseStrength !== void 0) this.setNoiseStrength(options.noiseStrength);
1584
+ if (options.alpha) {
1585
+ this.setAlphaParams({
1586
+ defaultAlpha: options.alpha.default,
1587
+ highlightAlpha: options.alpha.highlight,
1588
+ dimmedAlpha: options.alpha.dimmed
1589
+ });
1590
+ this.clearHighlights();
1591
+ }
1592
+ }
1593
+ /**
1594
+ * Refresh shader uniforms when params change
1595
+ */
1596
+ refreshUniforms() {
1597
+ if (!this.lines) return;
1598
+ const material = this.lines.material;
1599
+ if (material.uniforms.uNoiseStrength) material.uniforms.uNoiseStrength.value = this.params.noiseStrength;
1600
+ }
1601
+ /**
1602
+ * Handle window resize
1603
+ */
1604
+ resize(width, height) {}
1605
+ /**
1606
+ * Set visibility of links
1607
+ */
1608
+ setVisible(visible) {
1609
+ if (this.lines) this.lines.visible = visible;
1610
+ }
1611
+ /**
1612
+ * Check if links are visible
1613
+ */
1614
+ isVisible() {
1615
+ return this.lines?.visible ?? false;
1616
+ }
1617
+ /**
1618
+ * Cleanup
1619
+ */
1620
+ dispose() {
1621
+ if (this.lines) {
1622
+ this.scene.remove(this.lines);
1623
+ this.lines.geometry.dispose();
1624
+ this.lines.material.dispose();
1625
+ this.lines = null;
1626
+ }
1627
+ this.linkOpacity?.dispose();
1628
+ this.linkIndexMap.clear();
1629
+ this.nodeIndexMap.clear();
1630
+ this.simulationBuffers = null;
1631
+ }
1632
+ /**
1633
+ * Build node index mapping
1634
+ */
1635
+ buildNodeMapping(nodes) {
1636
+ this.nodeIndexMap.clear();
1637
+ nodes.forEach((node, i) => {
1638
+ this.nodeIndexMap.set(node.id, i);
1639
+ });
1640
+ }
1641
+ /**
1642
+ * Create link geometry with interpolated segments
1643
+ */
1644
+ createLinkGeometry(links, nodes, nodeTextureSize) {
1645
+ const linkIndexMap = /* @__PURE__ */ new Map();
1646
+ const nodeMap = /* @__PURE__ */ new Map();
1647
+ nodes.forEach((node) => nodeMap.set(node.id, node));
1648
+ const baseGeometry = new THREE.BufferGeometry();
1649
+ const baseVertices = this.interpolationSteps * 2;
1650
+ const tValues = new Float32Array(baseVertices);
1651
+ const positions = new Float32Array(baseVertices * 3);
1652
+ let vertexIndex = 0;
1653
+ for (let i = 0; i < this.interpolationSteps; i++) {
1654
+ const t0 = i / this.interpolationSteps;
1655
+ const t1 = (i + 1) / this.interpolationSteps;
1656
+ tValues[vertexIndex] = t0;
1657
+ vertexIndex++;
1658
+ tValues[vertexIndex] = t1;
1659
+ vertexIndex++;
1660
+ }
1661
+ baseGeometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
1662
+ baseGeometry.setAttribute("t", new THREE.BufferAttribute(tValues, 1));
1663
+ const geometry = new THREE.InstancedBufferGeometry();
1664
+ geometry.index = baseGeometry.index;
1665
+ geometry.attributes = baseGeometry.attributes;
1666
+ geometry.instanceCount = links.length;
1667
+ const instanceLinkA = new Float32Array(links.length * 2);
1668
+ const instanceLinkB = new Float32Array(links.length * 2);
1669
+ const instanceColorA = new Float32Array(links.length * 3);
1670
+ const instanceColorB = new Float32Array(links.length * 3);
1671
+ const instanceAlphaIndex = new Float32Array(links.length);
1672
+ links.forEach((link, i) => {
1673
+ const sourceId = link.source && typeof link.source === "object" ? link.source.id : link.source;
1674
+ const targetId = link.target && typeof link.target === "object" ? link.target.id : link.target;
1675
+ if (!sourceId || !targetId) {
1676
+ console.warn("Link missing source or target:", link);
1677
+ return;
1678
+ }
1679
+ const sourceIndex = this.nodeIndexMap.get(sourceId);
1680
+ const targetIndex = this.nodeIndexMap.get(targetId);
1681
+ if (sourceIndex === void 0 || targetIndex === void 0) {
1682
+ console.warn("Node not found for link:", link);
1683
+ return;
1684
+ }
1685
+ const sourceX = sourceIndex % nodeTextureSize;
1686
+ const sourceY = Math.floor(sourceIndex / nodeTextureSize);
1687
+ const targetX = targetIndex % nodeTextureSize;
1688
+ const targetY = Math.floor(targetIndex / nodeTextureSize);
1689
+ const sourceNode = nodeMap.get(sourceId);
1690
+ const targetNode = nodeMap.get(targetId);
1691
+ let srcColor;
1692
+ let tgtColor;
1693
+ srcColor = styleRegistry.getNodeStyle(sourceNode?.category).color;
1694
+ tgtColor = styleRegistry.getNodeStyle(targetNode?.category).color;
1695
+ instanceLinkA[i * 2] = sourceX;
1696
+ instanceLinkA[i * 2 + 1] = sourceY;
1697
+ instanceLinkB[i * 2] = targetX;
1698
+ instanceLinkB[i * 2 + 1] = targetY;
1699
+ instanceColorA[i * 3] = srcColor.r;
1700
+ instanceColorA[i * 3 + 1] = srcColor.g;
1701
+ instanceColorA[i * 3 + 2] = srcColor.b;
1702
+ instanceColorB[i * 3] = tgtColor.r;
1703
+ instanceColorB[i * 3 + 1] = tgtColor.g;
1704
+ instanceColorB[i * 3 + 2] = tgtColor.b;
1705
+ instanceAlphaIndex[i] = i;
1706
+ const linkId = `${sourceId}-${targetId}`;
1707
+ linkIndexMap.set(linkId, i);
1708
+ });
1709
+ geometry.setAttribute("instanceLinkA", new THREE.InstancedBufferAttribute(instanceLinkA, 2));
1710
+ geometry.setAttribute("instanceLinkB", new THREE.InstancedBufferAttribute(instanceLinkB, 2));
1711
+ geometry.setAttribute("instanceColorA", new THREE.InstancedBufferAttribute(instanceColorA, 3));
1712
+ geometry.setAttribute("instanceColorB", new THREE.InstancedBufferAttribute(instanceColorB, 3));
1713
+ geometry.setAttribute("instanceAlphaIndex", new THREE.InstancedBufferAttribute(instanceAlphaIndex, 1));
1714
+ return {
1715
+ geometry,
1716
+ linkIndexMap
1717
+ };
1718
+ }
1719
+ /**
1720
+ * Build all link-related textures for simulation
1721
+ * - linkIndicesData: parent node INDEX per link entry (organized by child)
1722
+ * - linkPropertiesData: strength/distance per link entry
1723
+ * - nodeLinkMapData: per-node metadata (startX, startY, count, hasLinks)
1724
+ */
1725
+ buildLinkTextures(links, nodes, pointsTexSize, groupOrder = [
1726
+ "root",
1727
+ "series",
1728
+ "artwork",
1729
+ "exhibition",
1730
+ "media"
1731
+ ]) {
1732
+ const getId = (v) => typeof v === "string" ? v : v?.id ?? "";
1733
+ const counts = new Array(this.nodeIndexMap.size).fill(0);
1734
+ for (const link of links) {
1735
+ if (!link?.source || !link?.target) continue;
1736
+ const tId = getId(link.target);
1737
+ const tIdx = this.nodeIndexMap.get(tId);
1738
+ if (tIdx !== void 0 && tIdx >= 0 && tIdx < counts.length) counts[tIdx] = (counts[tIdx] ?? 0) + 1;
1739
+ }
1740
+ const totalTexels = counts.reduce((a, b) => a + b, 0);
1741
+ const minSize = Math.ceil(Math.sqrt(Math.max(1, totalTexels)));
1742
+ const linksTexSize = Math.max(1, 2 ** Math.ceil(Math.log2(minSize)));
1743
+ const totalLinkTexels = linksTexSize * linksTexSize;
1744
+ const linkIndicesData = new Float32Array(totalLinkTexels * 4).fill(-1);
1745
+ const linkPropertiesData = new Float32Array(totalLinkTexels * 4).fill(0);
1746
+ const infoTotal = pointsTexSize * pointsTexSize;
1747
+ const nodeLinkMapData = new Float32Array(infoTotal * 4).fill(0);
1748
+ let writeIndex = 0;
1749
+ for (let nodeIdx = 0; nodeIdx < counts.length; nodeIdx++) {
1750
+ const count = counts[nodeIdx] || 0;
1751
+ const startX = writeIndex % linksTexSize;
1752
+ const startY = Math.floor(writeIndex / linksTexSize);
1753
+ const infoIdx = nodeIdx * 4;
1754
+ nodeLinkMapData[infoIdx] = startX;
1755
+ nodeLinkMapData[infoIdx + 1] = startY;
1756
+ nodeLinkMapData[infoIdx + 2] = count;
1757
+ nodeLinkMapData[infoIdx + 3] = count > 0 ? 1 : 0;
1758
+ writeIndex += count;
1759
+ }
1760
+ const nodeCategories = Array.from({ length: counts.length }, () => "");
1761
+ for (let i = 0; i < nodes.length; i++) {
1762
+ const node = nodes[i];
1763
+ if (!node) continue;
1764
+ const nodeIdx = this.nodeIndexMap.get(node.id);
1765
+ if (nodeIdx !== void 0 && nodeIdx >= 0) nodeCategories[nodeIdx] = node.category ?? "";
1766
+ }
1767
+ const categoryRankMap = /* @__PURE__ */ new Map();
1768
+ groupOrder.forEach((g, i) => categoryRankMap.set(g, i));
1769
+ let nextRank = groupOrder.length;
1770
+ const nodeCursor = Array.from({ length: counts.length }).fill(0);
1771
+ for (const link of links) {
1772
+ if (!link?.source || !link?.target) continue;
1773
+ const sId = getId(link.source);
1774
+ const tId = getId(link.target);
1775
+ const sIdx = this.nodeIndexMap.get(sId);
1776
+ const tIdx = this.nodeIndexMap.get(tId);
1777
+ if (sIdx === void 0 || tIdx === void 0 || sIdx < 0 || tIdx < 0) continue;
1778
+ const rawStartX = nodeLinkMapData[tIdx * 4] ?? 0;
1779
+ const baseStartTexel = (nodeLinkMapData[tIdx * 4 + 1] ?? 0) * linksTexSize + rawStartX;
1780
+ const offset = nodeCursor[tIdx] || 0;
1781
+ const texelIndex = baseStartTexel + offset;
1782
+ if (texelIndex < totalLinkTexels) {
1783
+ const di = texelIndex * 4;
1784
+ linkIndicesData[di] = sIdx;
1785
+ const category = nodeCategories[tIdx] || "";
1786
+ if (category && !categoryRankMap.has(category)) categoryRankMap.set(category, nextRank++);
1787
+ const rank = categoryRankMap.get(category) ?? 0;
1788
+ const strength = 1 / (rank + 1);
1789
+ const distance = .3 ** rank;
1790
+ linkPropertiesData[di] = strength;
1791
+ linkPropertiesData[di + 1] = distance;
1792
+ }
1793
+ nodeCursor[tIdx] = offset + 1;
1794
+ }
1795
+ return {
1796
+ linkIndicesData,
1797
+ linkPropertiesData,
1798
+ nodeLinkMapData,
1799
+ linksTextureSize: linksTexSize
1800
+ };
1801
+ }
1802
+ /**
1803
+ * Create render material with all uniforms
1804
+ */
1805
+ createRenderMaterial(nodeTextureSize, alphaTextureSize) {
1806
+ return new THREE.ShaderMaterial({
1807
+ vertexShader: lines_default,
1808
+ fragmentShader: lines_default$1,
1809
+ transparent: true,
1810
+ depthTest: false,
1811
+ depthWrite: false,
1812
+ uniforms: {
1813
+ uPositionsTexture: { value: this.simulationBuffers.getCurrentPositionTexture() },
1814
+ uPositionsTextureSize: { value: new THREE.Vector2(nodeTextureSize, nodeTextureSize) },
1815
+ uAlphaTexture: { value: this.linkOpacity.getCurrentAlphaTexture() },
1816
+ uAlphaTextureSize: { value: new THREE.Vector2(alphaTextureSize, alphaTextureSize) },
1817
+ uNoiseStrength: { value: this.params.noiseStrength },
1818
+ uTime: { value: 0 }
1819
+ }
1820
+ });
1821
+ }
1822
+ };
1823
+
1824
+ //#endregion
1825
+ //#region assets/glsl/points/pickPoint.frag
1826
+ var pickPoint_default$1 = "precision mediump float; \r\nvarying float vIndex; \n\nvoid main(){\n\n vec2 center = vec2(0.5, 0.5);\r\n vec2 uv = gl_PointCoord - center;\r\n float dist = length(uv);\n\n \n if (dist > 0.4) {\r\n discard;\r\n }\n\n float id = vIndex + 1.0;\r\n float r = mod(id, 256.0);\r\n float g = mod(floor(id / 256.0), 256.0);\r\n float b = mod(floor(id / 65536.0), 256.0);\r\n gl_FragColor = vec4(r/255.0, g/255.0, b/255.0, 1.0);\r\n}";
1827
+
1828
+ //#endregion
1829
+ //#region assets/glsl/points/pickPoint.vert
1830
+ var pickPoint_default = "attribute vec2 pointIndices;\r\nattribute float scale;\n\nattribute float alphaIndex;\r\nuniform sampler2D uPositionsTexture;\r\nuniform vec2 uPositionsTextureSize;\r\nuniform vec2 uResolution;\r\nuniform float uAspectRatio;\r\nuniform float uPixelRatio;\n\nvarying float vIndex;\n\nvoid main() {\r\n vIndex = alphaIndex;\n\n vec2 posUV = (pointIndices + vec2(0.5)) / uPositionsTextureSize;\n\n vec4 posTex = texture2D(uPositionsTexture, posUV);\r\n vec3 gpuPos = vec3(posTex.r, posTex.g, posTex.b);\r\n if(posTex.w < 0.5) {\r\n \n gl_PointSize = 0.0;\r\n gl_Position = vec4(2.0, 2.0, 2.0, 1.0); \r\n return;\r\n }\r\n vec4 mvPosition = modelViewMatrix * vec4(gpuPos, 1.0);\n\n float distance = 1.0 / max(1e-6, length(mvPosition.xyz));\r\n gl_PointSize = scale * uPixelRatio * distance;\r\n gl_Position = projectionMatrix * mvPosition;\r\n \r\n }";
1831
+
1832
+ //#endregion
1833
+ //#region assets/glsl/points/points.frag
1834
+ var points_default$1 = "uniform sampler2D pointTexture;\r\nuniform sampler2D uAlphaTexture;\r\nuniform vec2 uTextureSize;\r\nvarying vec3 vColor;\r\nvarying float vAlphaIndex;\r\nvarying float vScale;\r\nvarying vec2 vPointIndices;\n\nvarying float vSelected;\n\nvoid main() {\r\n \n vec2 center = vec2(0.5, 0.5);\r\n vec2 uv = gl_PointCoord - center;\r\n float dist = length(uv);\n\n \n vec2 alphaUV = (vPointIndices + 0.5) / uTextureSize;\r\n float alpha = texture2D(uAlphaTexture, alphaUV).r;\n\n \n if (dist > 0.5) {\r\n discard;\r\n }\r\n vec3 color = vColor ;\n\n gl_FragColor = vec4(color, alpha);\r\n #include <colorspace_fragment>\r\n}";
1835
+
1836
+ //#endregion
1837
+ //#region assets/glsl/points/points.vert
1838
+ var points_default = "attribute float scale;\r\nattribute float selected;\r\nattribute float alphaIndex;\r\nattribute vec2 pointIndices;\n\nuniform vec2 uResolution;\r\nuniform float uAspectRatio;\r\nuniform float uPixelRatio;\r\nuniform sampler2D uPositionsTexture;\r\nuniform vec2 uPositionsTextureSize;\n\nvarying float vSelected;\r\nvarying vec3 vColor;\r\nvarying float vAlphaIndex;\r\nvarying float vScale;\r\nvarying vec2 vPointIndices;\n\nvoid main() {\r\n vColor = color;\r\n vSelected = selected;\r\n vScale = scale;\r\n vAlphaIndex = alphaIndex;\r\n vPointIndices = pointIndices;\n\n vec2 posUV = (pointIndices + vec2(0.5)) / uPositionsTextureSize;\n\n vec4 posTex = texture2D(uPositionsTexture, posUV);\r\n vec3 gpuPos = vec3(posTex.r, posTex.g, posTex.b);\r\n bool isActive = posTex.w >= 0.5;\n\n vec4 mvPosition = modelViewMatrix * vec4(gpuPos, 1.0);\n\n float distance = 1.0 / length(mvPosition.xyz + 1e-7);\r\n \r\n if (!isActive) {\r\n \n gl_PointSize = 0.0;\r\n \n gl_Position = vec4(2.0, 2.0, 2.0, 1.0); \r\n } else {\r\n gl_PointSize = scale * uPixelRatio * distance;\r\n gl_Position = projectionMatrix * mvPosition;\r\n }\r\n}";
1839
+
1840
+ //#endregion
1841
+ //#region rendering/nodes/NodeOpacity.ts
1842
+ /**
1843
+ * NodeOpacity - Manages per-node opacity using GPU compute
1844
+ * Similar to LinkOpacity but optimized for nodes
1845
+ */
1846
+ var NodeOpacity = class {
1847
+ renderer;
1848
+ size;
1849
+ rtA;
1850
+ rtB;
1851
+ ping = 0;
1852
+ scene;
1853
+ camera;
1854
+ quad;
1855
+ computeMaterial;
1856
+ copyMaterial;
1857
+ targetTex = null;
1858
+ constructor(renderer, size) {
1859
+ this.renderer = renderer;
1860
+ this.size = size;
1861
+ const params = {
1862
+ minFilter: THREE.NearestFilter,
1863
+ magFilter: THREE.NearestFilter,
1864
+ wrapS: THREE.ClampToEdgeWrapping,
1865
+ wrapT: THREE.ClampToEdgeWrapping,
1866
+ format: THREE.RGBAFormat,
1867
+ type: THREE.FloatType,
1868
+ depthBuffer: false,
1869
+ stencilBuffer: false
1870
+ };
1871
+ this.rtA = new THREE.WebGLRenderTarget(size, size, params);
1872
+ this.rtB = new THREE.WebGLRenderTarget(size, size, params);
1873
+ this.scene = new THREE.Scene();
1874
+ this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
1875
+ const geo = new THREE.PlaneGeometry(2, 2);
1876
+ this.computeMaterial = new THREE.ShaderMaterial({
1877
+ vertexShader: `
1878
+ void main() {
1879
+ gl_Position = vec4(position, 1.0);
1880
+ }
1881
+ `,
1882
+ fragmentShader: `
1883
+ precision highp float;
1884
+
1885
+ uniform sampler2D uPrev;
1886
+ uniform sampler2D uTarget;
1887
+ uniform float uStep;
1888
+ uniform vec2 uResolution;
1889
+
1890
+ void main() {
1891
+ // compute stable texcoord from pixel coords
1892
+ vec2 uv = gl_FragCoord.xy / uResolution;
1893
+ float prev = texture2D(uPrev, uv).r;
1894
+ float target = texture2D(uTarget, uv).r;
1895
+
1896
+ // Lerp towards target
1897
+ float nextVal = mix(prev, target, uStep);
1898
+
1899
+ // Clamp to valid range
1900
+ nextVal = clamp(nextVal, 0.0, 1.0);
1901
+
1902
+ gl_FragColor = vec4(nextVal, 0.0, 0.0, 1.0);
1903
+ }
1904
+
1905
+ `,
1906
+ uniforms: {
1907
+ uPrev: { value: null },
1908
+ uTarget: { value: null },
1909
+ uStep: { value: .01 },
1910
+ uResolution: { value: new THREE.Vector2(size, size) }
1911
+ },
1912
+ depthTest: false,
1913
+ depthWrite: false
1914
+ });
1915
+ this.copyMaterial = new THREE.ShaderMaterial({
1916
+ vertexShader: this.computeMaterial.vertexShader,
1917
+ fragmentShader: `
1918
+ precision highp float;
1919
+ uniform sampler2D uSrc;
1920
+ uniform vec2 uResolution;
1921
+ void main() {
1922
+ vec2 uv = gl_FragCoord.xy / uResolution;
1923
+ vec4 s = texture2D(uSrc, uv);
1924
+ gl_FragColor = vec4(s.r, 0.0, 0.0, 1.0);
1925
+ }
1926
+ `,
1927
+ uniforms: {
1928
+ uSrc: { value: null },
1929
+ uResolution: { value: new THREE.Vector2(size, size) }
1930
+ },
1931
+ depthTest: false,
1932
+ depthWrite: false
1933
+ });
1934
+ this.quad = new THREE.Mesh(geo, this.computeMaterial);
1935
+ this.scene.add(this.quad);
1936
+ this.targetTex = new THREE.DataTexture(new Float32Array(size * size * 4).fill(0), size, size, THREE.RGBAFormat, THREE.FloatType);
1937
+ this.targetTex.minFilter = THREE.NearestFilter;
1938
+ this.targetTex.magFilter = THREE.NearestFilter;
1939
+ this.targetTex.wrapS = THREE.ClampToEdgeWrapping;
1940
+ this.targetTex.wrapT = THREE.ClampToEdgeWrapping;
1941
+ this.targetTex.flipY = false;
1942
+ this.targetTex.needsUpdate = true;
1943
+ }
1944
+ init(initialAlphaTexture) {
1945
+ this.copyMaterial.uniforms.uSrc.value = initialAlphaTexture;
1946
+ this.copyMaterial.uniforms.uResolution.value.set(this.size, this.size);
1947
+ this.quad.material = this.copyMaterial;
1948
+ const prevRT = this.renderer.getRenderTarget();
1949
+ this.renderer.setRenderTarget(this.rtA);
1950
+ this.renderer.render(this.scene, this.camera);
1951
+ this.renderer.setRenderTarget(this.rtB);
1952
+ this.renderer.render(this.scene, this.camera);
1953
+ this.renderer.setRenderTarget(prevRT);
1954
+ this.ping = 0;
1955
+ this.quad.material = this.computeMaterial;
1956
+ }
1957
+ setTargets(targets) {
1958
+ const total = this.size * this.size;
1959
+ const data = this.targetTex && this.targetTex.image && this.targetTex.image.data.length === total * 4 ? this.targetTex.image.data : new Float32Array(total * 4);
1960
+ for (let i = 0; i < total; i++) {
1961
+ const v = targets[i] ?? 0;
1962
+ const base = i * 4;
1963
+ data[base] = v;
1964
+ data[base + 1] = 0;
1965
+ data[base + 2] = 0;
1966
+ data[base + 3] = 1;
1967
+ }
1968
+ if (this.targetTex) {
1969
+ this.targetTex.image.data = data;
1970
+ this.targetTex.needsUpdate = true;
1971
+ }
1972
+ }
1973
+ runComputePass() {
1974
+ const prevRT = this.getPrevRT();
1975
+ this.computeMaterial.uniforms.uPrev.value = prevRT.texture;
1976
+ if (this.targetTex) this.computeMaterial.uniforms.uTarget.value = this.targetTex;
1977
+ const targetRT = this.getNextRT();
1978
+ this.renderer.setRenderTarget(targetRT);
1979
+ this.renderer.render(this.scene, this.camera);
1980
+ this.renderer.setRenderTarget(null);
1981
+ this.ping = 1 - this.ping;
1982
+ }
1983
+ update() {
1984
+ this.runComputePass();
1985
+ }
1986
+ getCurrentAlphaTexture() {
1987
+ return this.getPrevRT().texture;
1988
+ }
1989
+ /**
1990
+ * Set the step size for alpha transitions
1991
+ * @param step - Step size (default: 0.01)
1992
+ */
1993
+ setStep(step) {
1994
+ this.computeMaterial.uniforms.uStep.value = step;
1995
+ }
1996
+ getPrevRT() {
1997
+ return this.ping === 0 ? this.rtA : this.rtB;
1998
+ }
1999
+ getNextRT() {
2000
+ return this.ping === 0 ? this.rtB : this.rtA;
2001
+ }
2002
+ dispose() {
2003
+ this.rtA.dispose();
2004
+ this.rtB.dispose();
2005
+ this.copyMaterial.dispose();
2006
+ this.computeMaterial.dispose();
2007
+ this.quad.geometry.dispose();
2008
+ if (this.targetTex) this.targetTex.dispose();
2009
+ }
2010
+ };
2011
+
2012
+ //#endregion
2013
+ //#region rendering/nodes/NodesRenderer.ts
2014
+ var NodesRenderer = class {
2015
+ points = null;
2016
+ pickMaterial = null;
2017
+ nodeIndexMap = /* @__PURE__ */ new Map();
2018
+ idArray = [];
2019
+ nodeOpacity = null;
2020
+ targets = null;
2021
+ params = {
2022
+ defaultAlpha: 1,
2023
+ highlightAlpha: 1,
2024
+ dimmedAlpha: .2
2025
+ };
2026
+ simulationBuffers = null;
2027
+ pickBuffer = null;
2028
+ constructor(scene, renderer) {
2029
+ this.scene = scene;
2030
+ this.renderer = renderer;
2031
+ }
2032
+ create(nodes, simulationBuffers, pickBuffer) {
2033
+ this.simulationBuffers = simulationBuffers;
2034
+ this.pickBuffer = pickBuffer;
2035
+ const textureSize = simulationBuffers.getTextureSize();
2036
+ const nodeCount = nodes.length;
2037
+ this.buildNodeMapping(nodes);
2038
+ this.nodeOpacity = new NodeOpacity(this.renderer, textureSize);
2039
+ const total = textureSize * textureSize;
2040
+ this.targets = new Float32Array(total).fill(this.params.defaultAlpha);
2041
+ const initialAlphaData = new Float32Array(total * 4);
2042
+ for (let i = 0; i < total; i++) {
2043
+ initialAlphaData[i * 4] = this.params.defaultAlpha;
2044
+ initialAlphaData[i * 4 + 1] = 0;
2045
+ initialAlphaData[i * 4 + 2] = 0;
2046
+ initialAlphaData[i * 4 + 3] = 1;
2047
+ }
2048
+ const initialAlphaTexture = new THREE.DataTexture(initialAlphaData, textureSize, textureSize, THREE.RGBAFormat, THREE.FloatType);
2049
+ initialAlphaTexture.needsUpdate = true;
2050
+ this.nodeOpacity.init(initialAlphaTexture);
2051
+ initialAlphaTexture.dispose();
2052
+ const nodeData = this.createNodeData(nodes, textureSize);
2053
+ staticAssets.setNodeRadii(nodeData.radii, textureSize);
2054
+ staticAssets.setNodeColors(nodeData.colors, textureSize);
2055
+ const geometry = this.createGeometry(nodeData, nodeCount);
2056
+ const material = this.createRenderMaterial(textureSize);
2057
+ this.pickMaterial = this.createPickMaterial(textureSize);
2058
+ this.points = new THREE.Points(geometry, material);
2059
+ this.points.frustumCulled = false;
2060
+ this.points.renderOrder = 999;
2061
+ this.scene.add(this.points);
2062
+ this.clearHighlights();
2063
+ }
2064
+ setPositionTexture(positionTexture) {
2065
+ if (!this.points) return;
2066
+ const material = this.points.material;
2067
+ if (material.uniforms.uPositionsTexture) material.uniforms.uPositionsTexture.value = positionTexture;
2068
+ if (this.pickMaterial?.uniforms.uPositionsTexture) this.pickMaterial.uniforms.uPositionsTexture.value = positionTexture;
2069
+ }
2070
+ pick(pixelX, pixelY, camera) {
2071
+ if (!this.points || !this.pickBuffer || !this.pickMaterial) return -1;
2072
+ const originalMaterial = this.points.material;
2073
+ this.points.material = this.pickMaterial;
2074
+ this.pickBuffer.render(this.scene, camera);
2075
+ this.points.material = originalMaterial;
2076
+ return this.pickBuffer.readIdAt(pixelX, pixelY);
2077
+ }
2078
+ highlight(nodeIds, step = .1) {
2079
+ if (!this.nodeOpacity || !this.simulationBuffers || !this.targets) return;
2080
+ const indices = nodeIds.map((id) => this.nodeIndexMap.get(String(id))).filter((idx) => idx !== void 0);
2081
+ if (indices.length === 0) return;
2082
+ this.targets.fill(this.params.dimmedAlpha);
2083
+ indices.forEach((idx) => {
2084
+ if (idx < this.targets.length) this.targets[idx] = this.params.highlightAlpha;
2085
+ });
2086
+ this.nodeOpacity.setStep(step);
2087
+ this.nodeOpacity.setTargets(this.targets);
2088
+ }
2089
+ clearHighlights(step = .1) {
2090
+ if (!this.nodeOpacity || !this.simulationBuffers || !this.targets) return;
2091
+ this.targets.fill(this.params.defaultAlpha);
2092
+ this.nodeOpacity.setStep(step);
2093
+ this.nodeOpacity.setTargets(this.targets);
2094
+ }
2095
+ /**
2096
+ * Update node opacity (called every frame)
2097
+ */
2098
+ updateOpacity() {
2099
+ if (!this.nodeOpacity) return;
2100
+ this.nodeOpacity.update();
2101
+ if (this.points && this.points.material) {
2102
+ const material = this.points.material;
2103
+ if (material.uniforms.uAlphaTexture) material.uniforms.uAlphaTexture.value = this.nodeOpacity.getCurrentAlphaTexture();
2104
+ }
2105
+ }
2106
+ resize(width, height) {
2107
+ if (!this.points) return;
2108
+ const material = this.points.material;
2109
+ if (material.uniforms.uResolution) material.uniforms.uResolution.value.set(width, height);
2110
+ if (material.uniforms.uAspectRatio) material.uniforms.uAspectRatio.value = width / height;
2111
+ if (material.uniforms.uPixelRatio) material.uniforms.uPixelRatio.value = window.devicePixelRatio;
2112
+ if (this.pickMaterial) {
2113
+ if (this.pickMaterial.uniforms.uResolution) this.pickMaterial.uniforms.uResolution.value.set(width, height);
2114
+ if (this.pickMaterial.uniforms.uAspectRatio) this.pickMaterial.uniforms.uAspectRatio.value = width / height;
2115
+ if (this.pickMaterial.uniforms.uPixelRatio) this.pickMaterial.uniforms.uPixelRatio.value = window.devicePixelRatio;
2116
+ }
2117
+ }
2118
+ dispose() {
2119
+ if (this.points) {
2120
+ this.scene.remove(this.points);
2121
+ this.points.geometry.dispose();
2122
+ this.points.material.dispose();
2123
+ this.points = null;
2124
+ }
2125
+ this.pickMaterial?.dispose();
2126
+ this.nodeOpacity?.dispose();
2127
+ this.nodeIndexMap.clear();
2128
+ this.idArray = [];
2129
+ this.simulationBuffers = null;
2130
+ this.pickBuffer = null;
2131
+ }
2132
+ buildNodeMapping(nodes) {
2133
+ this.nodeIndexMap.clear();
2134
+ this.idArray = [];
2135
+ nodes.forEach((node, i) => {
2136
+ this.nodeIndexMap.set(String(node.id), i);
2137
+ this.idArray[i] = node.id;
2138
+ });
2139
+ }
2140
+ /**
2141
+ * Create all node data in one pass
2142
+ * Returns: colors, sizes, radii, nodeIds, pointIndices, alphaIndex
2143
+ */
2144
+ createNodeData(nodes, textureSize) {
2145
+ const count = nodes.length;
2146
+ const totalTexels = textureSize * textureSize;
2147
+ const colorBuffer = new Float32Array(count * 3);
2148
+ const sizeBuffer = new Float32Array(count);
2149
+ const pointIndices = new Float32Array(count * 2);
2150
+ const alphaIndex = new Float32Array(count);
2151
+ const radii = new Float32Array(totalTexels);
2152
+ const colors = new Float32Array(totalTexels * 4);
2153
+ const fovRadians = 75 * Math.PI / 180;
2154
+ const pixelToWorldRatio = 2 * Math.tan(fovRadians / 2) / window.innerHeight;
2155
+ nodes.forEach((node, i) => {
2156
+ const style = styleRegistry.getNodeStyle(node.category);
2157
+ colorBuffer[i * 3] = style.color.r;
2158
+ colorBuffer[i * 3 + 1] = style.color.g;
2159
+ colorBuffer[i * 3 + 2] = style.color.b;
2160
+ sizeBuffer[i] = style.size;
2161
+ const x = i % textureSize;
2162
+ const y = Math.floor(i / textureSize);
2163
+ pointIndices[i * 2] = x;
2164
+ pointIndices[i * 2 + 1] = y;
2165
+ alphaIndex[i] = i;
2166
+ radii[i] = style.size * window.devicePixelRatio * pixelToWorldRatio / 2;
2167
+ colors[i * 4] = style.color.r;
2168
+ colors[i * 4 + 1] = style.color.g;
2169
+ colors[i * 4 + 2] = style.color.b;
2170
+ colors[i * 4 + 3] = 1;
2171
+ });
2172
+ return {
2173
+ colorBuffer,
2174
+ sizeBuffer,
2175
+ pointIndices,
2176
+ alphaIndex,
2177
+ radii,
2178
+ colors
2179
+ };
2180
+ }
2181
+ createGeometry(nodeData, count) {
2182
+ const geometry = new THREE.BufferGeometry();
2183
+ geometry.setAttribute("color", new THREE.BufferAttribute(nodeData.colorBuffer, 3));
2184
+ geometry.setAttribute("scale", new THREE.BufferAttribute(nodeData.sizeBuffer, 1));
2185
+ geometry.setAttribute("pointIndices", new THREE.BufferAttribute(nodeData.pointIndices, 2));
2186
+ geometry.setAttribute("alphaIndex", new THREE.BufferAttribute(nodeData.alphaIndex, 1));
2187
+ geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(count * 3), 3));
2188
+ return geometry;
2189
+ }
2190
+ createRenderMaterial(textureSize) {
2191
+ return new THREE.ShaderMaterial({
2192
+ vertexShader: points_default,
2193
+ fragmentShader: points_default$1,
2194
+ vertexColors: true,
2195
+ transparent: true,
2196
+ depthTest: true,
2197
+ depthWrite: true,
2198
+ side: THREE.DoubleSide,
2199
+ uniforms: {
2200
+ uResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
2201
+ uAspectRatio: { value: window.innerWidth / window.innerHeight },
2202
+ uPixelRatio: { value: window.devicePixelRatio },
2203
+ pointTexture: { value: this.texture },
2204
+ uAlphaTexture: { value: this.nodeOpacity.getCurrentAlphaTexture() },
2205
+ uTextureSize: { value: new THREE.Vector2(textureSize, textureSize) },
2206
+ uPositionsTexture: { value: this.simulationBuffers.getCurrentPositionTexture() },
2207
+ uPositionsTextureSize: { value: new THREE.Vector2(textureSize, textureSize) }
2208
+ }
2209
+ });
2210
+ }
2211
+ createPickMaterial(textureSize) {
2212
+ return new THREE.ShaderMaterial({
2213
+ vertexShader: pickPoint_default,
2214
+ fragmentShader: pickPoint_default$1,
2215
+ uniforms: {
2216
+ uPositionsTexture: { value: this.simulationBuffers.getCurrentPositionTexture() },
2217
+ uPositionsTextureSize: { value: new THREE.Vector2(textureSize, textureSize) },
2218
+ uResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
2219
+ uAspectRatio: { value: window.innerWidth / window.innerHeight },
2220
+ uPixelRatio: { value: window.devicePixelRatio }
2221
+ },
2222
+ blending: THREE.NoBlending,
2223
+ depthTest: true,
2224
+ depthWrite: true,
2225
+ transparent: true
2226
+ });
2227
+ }
2228
+ };
2229
+
2230
+ //#endregion
2231
+ //#region rendering/GraphScene.ts
2232
+ /**
2233
+ * GraphScene - Manages the 3D scene, camera, and renderers
2234
+ * Responsibilities:
2235
+ * - Scene setup and lifecycle
2236
+ * - Node and link rendering
2237
+ * - Camera control
2238
+ * - Tooltip management
2239
+ * - Mode application (visual changes)
2240
+ */
2241
+ var GraphScene = class {
2242
+ scene;
2243
+ renderer;
2244
+ camera;
2245
+ nodeRenderer = null;
2246
+ clock = new THREE.Clock();
2247
+ linkRenderer = null;
2248
+ constructor(canvas, cameraConfig) {
2249
+ this.scene = new THREE.Scene();
2250
+ this.scene.background = new THREE.Color(0);
2251
+ this.renderer = new THREE.WebGLRenderer({
2252
+ canvas,
2253
+ antialias: true,
2254
+ alpha: true,
2255
+ depth: true
2256
+ });
2257
+ this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
2258
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
2259
+ this.camera = new CameraController(this.renderer.domElement, cameraConfig);
2260
+ }
2261
+ /**
2262
+ * Initialize scene with nodes and links
2263
+ */
2264
+ create(nodes, links, simulationBuffers, pickBuffer, groupOrder) {
2265
+ this.nodeRenderer = new NodesRenderer(this.scene, this.renderer);
2266
+ this.nodeRenderer.create(nodes, simulationBuffers, pickBuffer);
2267
+ this.linkRenderer = new LinksRenderer(this.scene, this.renderer);
2268
+ this.linkRenderer.create(links, nodes, simulationBuffers);
2269
+ }
2270
+ /**
2271
+ * Apply visual mode (colors, sizes, camera position)
2272
+ */
2273
+ applyMode(mode, options = {}) {
2274
+ const duration = options.transitionDuration ?? 1;
2275
+ options.cameraTransitionDuration;
2276
+ switch (mode) {
2277
+ case "default":
2278
+ this.applyDefaultMode(duration);
2279
+ break;
2280
+ default: console.warn(`Unknown mode: ${mode}`);
2281
+ }
2282
+ }
2283
+ /**
2284
+ * Update position textures from simulation
2285
+ */
2286
+ updatePositions(positionTexture) {
2287
+ this.nodeRenderer?.setPositionTexture(positionTexture);
2288
+ this.linkRenderer?.setPositionTexture(positionTexture);
2289
+ }
2290
+ /**
2291
+ * Update time-based uniforms (called each frame with elapsed time)
2292
+ */
2293
+ update(elapsedTime) {
2294
+ this.linkRenderer?.update(elapsedTime);
2295
+ }
2296
+ /**
2297
+ * GPU picking at canvas coordinates
2298
+ */
2299
+ pick(pixelX, pixelY) {
2300
+ if (!this.nodeRenderer) return -1;
2301
+ return this.nodeRenderer.pick(pixelX, pixelY, this.camera.camera);
2302
+ }
2303
+ /**
2304
+ * Render the scene
2305
+ */
2306
+ render() {
2307
+ const delta = this.clock.getDelta();
2308
+ this.camera.update(delta);
2309
+ this.nodeRenderer?.updateOpacity();
2310
+ this.linkRenderer?.updateOpacity();
2311
+ this.renderer.render(this.scene, this.camera.camera);
2312
+ }
2313
+ /**
2314
+ * Handle window resize
2315
+ */
2316
+ resize(width, height) {
2317
+ this.camera.resize(width, height);
2318
+ this.nodeRenderer?.resize(width, height);
2319
+ this.linkRenderer?.resize(width, height);
2320
+ this.renderer.setSize(width, height);
2321
+ }
2322
+ /**
2323
+ * Cleanup all resources
2324
+ */
2325
+ dispose() {
2326
+ this.nodeRenderer?.dispose();
2327
+ this.nodeRenderer = null;
2328
+ this.camera.dispose();
2329
+ this.scene.clear();
2330
+ }
2331
+ applyDefaultMode(duration) {}
2332
+ getCamera() {
2333
+ return this.camera;
2334
+ }
2335
+ getNodeRenderer() {
2336
+ return this.nodeRenderer;
2337
+ }
2338
+ getLinkRenderer() {
2339
+ return this.linkRenderer;
2340
+ }
2341
+ };
2342
+
2343
+ //#endregion
2344
+ //#region assets/glsl/force-sim/attractor.frag
2345
+ var attractor_default = "#ifdef GL_ES\r\nprecision highp float;\r\n#endif\n\nuniform sampler2D uPositionsTexture;\r\nuniform sampler2D uVelocityTexture;\r\nuniform sampler2D uNodeCategoriesTexture; \nuniform sampler2D uAttractorsTexture; \nuniform sampler2D uAttractorCategoriesTexture; \nuniform sampler2D uAttractorParamsTexture; \nuniform float uTextureSize;\r\nuniform float uAttractorCount;\r\nuniform float uAttractorStrength; \nuniform float uAlpha;\r\nuniform bool uIs3D;\n\nvarying vec2 vUv;\n\nconst float ATTRACT_ALL = -1.0;\n\nvoid main() {\r\n vec4 currentPosition = texture2D(uPositionsTexture, vUv);\r\n vec3 velocity = texture2D(uVelocityTexture, vUv).xyz;\r\n vec3 pos = currentPosition.xyz;\r\n \r\n \n vec4 nodeCategories = texture2D(uNodeCategoriesTexture, vUv);\r\n \r\n vec3 totalForce = vec3(0.0);\r\n \r\n \n for (float i = 0.0; i < 32.0; i++) {\r\n if (i >= uAttractorCount) break;\r\n \r\n \n float u = (i + 0.5) / 32.0;\r\n vec4 attractorData = texture2D(uAttractorsTexture, vec2(u, 0.5));\r\n vec4 attractorCategories = texture2D(uAttractorCategoriesTexture, vec2(u, 0.5));\r\n vec4 attractorParams = texture2D(uAttractorParamsTexture, vec2(u, 0.5));\r\n \r\n vec3 attractorPos = attractorData.xyz;\r\n float attractorStrength = attractorParams.r;\r\n \n \r\n \n \n bool shouldAttract = false;\r\n \r\n if (attractorCategories.r < -1.5) {\r\n \n shouldAttract = true;\r\n } else {\r\n \n for (int n = 0; n < 4; n++) {\r\n float nc = (n == 0) ? nodeCategories.r : (n == 1) ? nodeCategories.g : (n == 2) ? nodeCategories.b : nodeCategories.a;\r\n if (nc < -0.5) continue; \n\n if (attractorCategories.r >= 0.0 && abs(attractorCategories.r - nc) < 0.5) { shouldAttract = true; break; }\r\n if (attractorCategories.g >= 0.0 && abs(attractorCategories.g - nc) < 0.5) { shouldAttract = true; break; }\r\n if (attractorCategories.b >= 0.0 && abs(attractorCategories.b - nc) < 0.5) { shouldAttract = true; break; }\r\n if (attractorCategories.a >= 0.0 && abs(attractorCategories.a - nc) < 0.5) { shouldAttract = true; break; }\r\n }\r\n }\r\n \r\n if (!shouldAttract) continue;\r\n \r\n \n vec3 toAttractor = attractorPos - pos;\r\n if (!uIs3D) {\r\n toAttractor.z = 0.0;\r\n }\r\n \r\n float dist = length(toAttractor);\r\n vec3 direction = dist > 0.001 ? toAttractor / dist : vec3(0.0);\r\n \r\n \n \n \n float effectiveDist = min(dist, 10.0);\r\n \r\n \n float forceMagnitude = attractorStrength * uAttractorStrength * effectiveDist;\n\n totalForce += direction * forceMagnitude;\r\n }\r\n \r\n \n velocity += totalForce * uAlpha;\r\n \r\n gl_FragColor = vec4(velocity, 0.0);\r\n}";
2346
+
2347
+ //#endregion
2348
+ //#region assets/glsl/force-sim/compute.vert
2349
+ var compute_default = "varying vec2 vUv;\n\nvoid main() {\r\n vUv = uv;\r\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\r\n}";
2350
+
2351
+ //#endregion
2352
+ //#region types/iAttractor.ts
2353
+ /**
2354
+ * Apply defaults to an attractor
2355
+ */
2356
+ function resolveAttractor(attractor) {
2357
+ return {
2358
+ id: attractor.id,
2359
+ position: {
2360
+ x: attractor.position.x,
2361
+ y: attractor.position.y,
2362
+ z: attractor.position.z
2363
+ },
2364
+ groups: [...attractor.groups || []],
2365
+ strength: attractor.strength ?? 1,
2366
+ radius: attractor.radius ?? 0
2367
+ };
2368
+ }
2369
+
2370
+ //#endregion
2371
+ //#region simulation/BasePass.ts
2372
+ /**
2373
+ * Base class for GPU force simulation passes
2374
+ * Each pass operates on simulation buffers and can read from static assets
2375
+ */
2376
+ var BasePass = class {
2377
+ material = null;
2378
+ enabled = true;
2379
+ /**
2380
+ * Execute the pass (renders to current velocity target)
2381
+ */
2382
+ execute(context) {
2383
+ if (!this.enabled || !this.material) return;
2384
+ this.updateUniforms(context);
2385
+ this.render(context);
2386
+ }
2387
+ /**
2388
+ * Render the compute shader
2389
+ */
2390
+ render(context) {
2391
+ if (!this.material) return;
2392
+ const mesh = new THREE.Mesh(context.quad, this.material);
2393
+ context.scene.clear();
2394
+ context.scene.add(mesh);
2395
+ context.renderer.render(context.scene, context.camera);
2396
+ context.scene.remove(mesh);
2397
+ }
2398
+ /**
2399
+ * Create a shader material helper
2400
+ */
2401
+ createMaterial(vertexShader, fragmentShader, uniforms) {
2402
+ return new THREE.ShaderMaterial({
2403
+ uniforms,
2404
+ vertexShader,
2405
+ fragmentShader,
2406
+ transparent: false,
2407
+ depthTest: false,
2408
+ depthWrite: false
2409
+ });
2410
+ }
2411
+ /**
2412
+ * Safe uniform setter - avoids TypeScript strict null check issues
2413
+ */
2414
+ setUniform(name, value) {
2415
+ if (this.material?.uniforms[name]) this.material.uniforms[name].value = value;
2416
+ }
2417
+ /**
2418
+ * Enable or disable this pass
2419
+ */
2420
+ setEnabled(enabled) {
2421
+ this.enabled = enabled;
2422
+ }
2423
+ /**
2424
+ * Check if pass is enabled
2425
+ */
2426
+ isEnabled() {
2427
+ return this.enabled;
2428
+ }
2429
+ /**
2430
+ * Get the material for external access
2431
+ */
2432
+ getMaterial() {
2433
+ return this.material;
2434
+ }
2435
+ /**
2436
+ * Cleanup resources
2437
+ */
2438
+ dispose() {
2439
+ this.material?.dispose();
2440
+ this.material = null;
2441
+ }
2442
+ };
2443
+
2444
+ //#endregion
2445
+ //#region simulation/passes/AttractorPass.ts
2446
+ const MAX_ATTRACTORS = 32;
2447
+ const MAX_GROUPS = 4;
2448
+ /**
2449
+ * Attractor force pass - attracts nodes to attractor points based on group membership
2450
+ *
2451
+ * Usage:
2452
+ * attractorPass.setAttractors([
2453
+ * { id: 'center', position: { x: 0, y: 0, z: 0 }, categories: ['root'] }, // categories acts as groups
2454
+ * { id: 'left', position: { x: -100, y: 0, z: 0 }, categories: ['artwork', 'series'] }
2455
+ * ])
2456
+ */
2457
+ var AttractorPass = class extends BasePass {
2458
+ attractors = [];
2459
+ groupMap = /* @__PURE__ */ new Map();
2460
+ attractorsTexture = null;
2461
+ attractorGroupsTexture = null;
2462
+ attractorParamsTexture = null;
2463
+ nodeGroupsTexture = null;
2464
+ getName() {
2465
+ return "attractor";
2466
+ }
2467
+ initMaterial(context) {
2468
+ this.createAttractorTextures();
2469
+ this.material = this.createMaterial(compute_default, attractor_default, {
2470
+ uPositionsTexture: { value: null },
2471
+ uVelocityTexture: { value: null },
2472
+ uNodeCategoriesTexture: { value: this.nodeGroupsTexture },
2473
+ uAttractorsTexture: { value: this.attractorsTexture },
2474
+ uAttractorCategoriesTexture: { value: this.attractorGroupsTexture },
2475
+ uAttractorParamsTexture: { value: this.attractorParamsTexture },
2476
+ uTextureSize: { value: context.textureSize },
2477
+ uAttractorCount: { value: 0 },
2478
+ uAttractorStrength: { value: 1 },
2479
+ uAlpha: { value: 0 },
2480
+ uIs3D: { value: false }
2481
+ });
2482
+ }
2483
+ updateUniforms(context) {
2484
+ const c = context.config;
2485
+ this.setUniform("uPositionsTexture", context.simBuffers.getCurrentPositionTexture());
2486
+ this.setUniform("uVelocityTexture", context.simBuffers.getCurrentVelocityTexture());
2487
+ this.setUniform("uNodeCategoriesTexture", this.nodeGroupsTexture);
2488
+ this.setUniform("uAttractorsTexture", this.attractorsTexture);
2489
+ this.setUniform("uAttractorCategoriesTexture", this.attractorGroupsTexture);
2490
+ this.setUniform("uAttractorParamsTexture", this.attractorParamsTexture);
2491
+ this.setUniform("uAttractorCount", this.attractors.length);
2492
+ this.setUniform("uAttractorStrength", c.attractorStrength ?? 1);
2493
+ this.setUniform("uAlpha", c.alpha);
2494
+ this.setUniform("uIs3D", c.is3D);
2495
+ }
2496
+ /**
2497
+ * Set attractors for the simulation
2498
+ */
2499
+ setAttractors(attractors) {
2500
+ this.attractors = attractors.slice(0, MAX_ATTRACTORS).map(resolveAttractor);
2501
+ this.updateAttractorTextures();
2502
+ }
2503
+ /**
2504
+ * Add a single attractor
2505
+ */
2506
+ addAttractor(attractor) {
2507
+ if (this.attractors.length >= MAX_ATTRACTORS) {
2508
+ console.warn(`[AttractorPass] Maximum ${MAX_ATTRACTORS} attractors supported`);
2509
+ return;
2510
+ }
2511
+ this.attractors.push(resolveAttractor(attractor));
2512
+ this.updateAttractorTextures();
2513
+ }
2514
+ /**
2515
+ * Remove an attractor by ID
2516
+ */
2517
+ removeAttractor(id) {
2518
+ this.attractors = this.attractors.filter((a) => a.id !== id);
2519
+ this.updateAttractorTextures();
2520
+ }
2521
+ /**
2522
+ * Update an existing attractor's position
2523
+ */
2524
+ updateAttractorPosition(id, position) {
2525
+ const attractor = this.attractors.find((a) => a.id === id);
2526
+ if (attractor) {
2527
+ attractor.position.x = position.x;
2528
+ attractor.position.y = position.y;
2529
+ attractor.position.z = position.z;
2530
+ this.updateAttractorTextures();
2531
+ }
2532
+ }
2533
+ /**
2534
+ * Get current attractors
2535
+ */
2536
+ getAttractors() {
2537
+ return this.attractors;
2538
+ }
2539
+ /**
2540
+ * Set node groups from graph data
2541
+ * Call this when graph data changes or grouping criteria changes
2542
+ */
2543
+ setNodeGroups(groups, textureSize) {
2544
+ this.groupMap.clear();
2545
+ let nextIndex = 0;
2546
+ for (const nodeGroups of groups) for (const group of nodeGroups) if (group && !this.groupMap.has(group)) this.groupMap.set(group, nextIndex++);
2547
+ const totalTexels = textureSize * textureSize;
2548
+ const data = new Float32Array(totalTexels * 4);
2549
+ data.fill(-1);
2550
+ for (let i = 0; i < groups.length && i < totalTexels; i++) {
2551
+ const nodeGroups = groups[i];
2552
+ const baseIdx = i * 4;
2553
+ for (let j = 0; j < 4; j++) if (j < nodeGroups.length) {
2554
+ const group = nodeGroups[j];
2555
+ data[baseIdx + j] = group ? this.groupMap.get(group) ?? -1 : -1;
2556
+ }
2557
+ }
2558
+ this.nodeGroupsTexture?.dispose();
2559
+ this.nodeGroupsTexture = new THREE.DataTexture(data, textureSize, textureSize, THREE.RGBAFormat, THREE.FloatType);
2560
+ this.nodeGroupsTexture.needsUpdate = true;
2561
+ this.nodeGroupsTexture.minFilter = THREE.NearestFilter;
2562
+ this.nodeGroupsTexture.magFilter = THREE.NearestFilter;
2563
+ this.updateAttractorTextures();
2564
+ }
2565
+ /**
2566
+ * Get the group map (group name -> index)
2567
+ */
2568
+ getGroupMap() {
2569
+ return new Map(this.groupMap);
2570
+ }
2571
+ createAttractorTextures() {
2572
+ const attractorData = new Float32Array(MAX_ATTRACTORS * 4);
2573
+ this.attractorsTexture = new THREE.DataTexture(attractorData, MAX_ATTRACTORS, 1, THREE.RGBAFormat, THREE.FloatType);
2574
+ this.attractorsTexture.needsUpdate = true;
2575
+ this.attractorsTexture.minFilter = THREE.NearestFilter;
2576
+ this.attractorsTexture.magFilter = THREE.NearestFilter;
2577
+ const groupData = new Float32Array(MAX_ATTRACTORS * 4).fill(-1);
2578
+ this.attractorGroupsTexture = new THREE.DataTexture(groupData, MAX_ATTRACTORS, 1, THREE.RGBAFormat, THREE.FloatType);
2579
+ this.attractorGroupsTexture.needsUpdate = true;
2580
+ this.attractorGroupsTexture.minFilter = THREE.NearestFilter;
2581
+ this.attractorGroupsTexture.magFilter = THREE.NearestFilter;
2582
+ const paramsData = new Float32Array(MAX_ATTRACTORS * 4).fill(0);
2583
+ this.attractorParamsTexture = new THREE.DataTexture(paramsData, MAX_ATTRACTORS, 1, THREE.RGBAFormat, THREE.FloatType);
2584
+ this.attractorParamsTexture.needsUpdate = true;
2585
+ this.attractorParamsTexture.minFilter = THREE.NearestFilter;
2586
+ this.attractorParamsTexture.magFilter = THREE.NearestFilter;
2587
+ this.nodeGroupsTexture = null;
2588
+ }
2589
+ updateAttractorTextures() {
2590
+ if (!this.attractorsTexture || !this.attractorGroupsTexture || !this.attractorParamsTexture) return;
2591
+ const attractorData = this.attractorsTexture.image.data;
2592
+ const groupData = this.attractorGroupsTexture.image.data;
2593
+ const paramsData = this.attractorParamsTexture.image.data;
2594
+ attractorData.fill(0);
2595
+ groupData.fill(-1);
2596
+ paramsData.fill(0);
2597
+ for (let i = 0; i < this.attractors.length; i++) {
2598
+ const attractor = this.attractors[i];
2599
+ if (!attractor) continue;
2600
+ const baseIdx = i * 4;
2601
+ attractorData[baseIdx] = attractor.position.x;
2602
+ attractorData[baseIdx + 1] = attractor.position.y;
2603
+ attractorData[baseIdx + 2] = attractor.position.z;
2604
+ attractorData[baseIdx + 3] = 0;
2605
+ paramsData[baseIdx] = attractor.strength;
2606
+ paramsData[baseIdx + 1] = attractor.radius;
2607
+ if (attractor.groups.length === 0) groupData[baseIdx] = -2;
2608
+ else for (let j = 0; j < Math.min(attractor.groups.length, MAX_GROUPS); j++) {
2609
+ const groupName = attractor.groups[j];
2610
+ if (groupName) {
2611
+ const groupIndex = this.groupMap.get(groupName);
2612
+ groupData[baseIdx + j] = groupIndex !== void 0 ? groupIndex : -1;
2613
+ }
2614
+ }
2615
+ }
2616
+ this.attractorsTexture.needsUpdate = true;
2617
+ this.attractorGroupsTexture.needsUpdate = true;
2618
+ this.attractorParamsTexture.needsUpdate = true;
2619
+ }
2620
+ dispose() {
2621
+ super.dispose();
2622
+ this.attractorsTexture?.dispose();
2623
+ this.attractorGroupsTexture?.dispose();
2624
+ this.attractorParamsTexture?.dispose();
2625
+ this.nodeGroupsTexture?.dispose();
2626
+ this.attractorsTexture = null;
2627
+ this.attractorGroupsTexture = null;
2628
+ this.attractorParamsTexture = null;
2629
+ this.nodeGroupsTexture = null;
2630
+ }
2631
+ };
2632
+
2633
+ //#endregion
2634
+ //#region assets/glsl/force-sim/collision.frag
2635
+ var collision_default = "#ifdef GL_ES\r\nprecision highp float;\r\n#endif\n\nuniform sampler2D uPositionsTexture;\r\nuniform sampler2D uVelocityTexture;\r\nuniform float uTextureSize;\r\nuniform float uAlpha; \nuniform float uMaxDistance; \nuniform float uCollisionStrength;\r\nuniform sampler2D uRadiiTexture;\n\nvarying vec2 vUv;\n\nfloat rand(vec2 co){\r\n return fract(sin(dot(co, vec2(12.9898,78.233))) * 43758.5453);\r\n}\n\nvoid main() {\r\n vec4 currentPosition = texture2D(uPositionsTexture, vUv);\r\n if(currentPosition.w < 0.5) discard;\r\n vec2 pos = currentPosition.xy;\r\n vec2 velocity = texture2D(uVelocityTexture, vUv).xy;\r\n float particleIndex = floor(vUv.y * uTextureSize) * uTextureSize + floor(vUv.x * uTextureSize);\n\n float texStep = 1.0 / uTextureSize;\r\n float halfTex = 0.5 * texStep;\r\n float eps = 1e-6;\r\n float steepness = 1.4;\n\n float r1 = texture2D(uRadiiTexture, vUv).r;\r\n vec2 push = vec2(0.0);\r\n int count = 0;\n\n for (float y = 0.0; y < 1.0; y += texStep) {\r\n for (float x = 0.0; x < 1.0; x += texStep) {\r\n vec2 otherUV = vec2(x + halfTex, y + halfTex);\r\n float otherIndex = floor(y * uTextureSize) * uTextureSize + floor(x * uTextureSize);\n\n vec4 otherPosition = texture2D(uPositionsTexture, otherUV);\r\n \r\n \n if (otherPosition.w < 1.5) continue;\n\n vec2 otherPos = otherPosition.xy;\n\n vec2 delta = pos - otherPos;\r\n float dist = length(delta);\r\n\n float r2 = texture2D(uRadiiTexture, otherUV).r;\r\n float rsum = r1 + r2;\r\n \r\n if (dist < rsum && dist > eps) {\r\n float normalizedDist = dist / rsum; \r\n float exponentialFalloff = exp(-normalizedDist * steepness); \r\n vec2 direction = delta / dist;\r\n push += direction * exponentialFalloff * uCollisionStrength;\r\n count++;\r\n }\r\n }\r\n }\n\n if (count > 0) {\r\n push /= float(count);\r\n float pushMagnitude = length(push);\r\n float maxPush = 0.05; \r\n push = normalize(push) * maxPush;\r\n velocity += push; \n }\r\n \r\n gl_FragColor = vec4(velocity.xy, 0.0, 1.0);\r\n}";
2636
+
2637
+ //#endregion
2638
+ //#region simulation/passes/CollisionPass.ts
2639
+ /**
2640
+ * Collision force pass - prevents nodes from overlapping
2641
+ * Reads configuration directly from context.config
2642
+ */
2643
+ var CollisionPass = class extends BasePass {
2644
+ getName() {
2645
+ return "collision";
2646
+ }
2647
+ initMaterial(context) {
2648
+ this.material = this.createMaterial(compute_default, collision_default, {
2649
+ uPositionsTexture: { value: null },
2650
+ uVelocityTexture: { value: null },
2651
+ uTextureSize: { value: context.textureSize },
2652
+ uRadiiTexture: { value: context.assets.getNodeRadiiTexture() },
2653
+ uCollisionStrength: { value: 0 },
2654
+ uCollisionRadius: { value: 0 },
2655
+ uAlpha: { value: 0 },
2656
+ uMaxDistance: { value: 0 },
2657
+ uIs3D: { value: true }
2658
+ });
2659
+ }
2660
+ updateUniforms(context) {
2661
+ const c = context.config;
2662
+ this.setUniform("uPositionsTexture", context.simBuffers.getCurrentPositionTexture());
2663
+ this.setUniform("uVelocityTexture", context.simBuffers.getCurrentVelocityTexture());
2664
+ this.setUniform("uCollisionStrength", c.collisionStrength);
2665
+ this.setUniform("uCollisionRadius", c.collisionRadius);
2666
+ this.setUniform("uAlpha", c.alpha);
2667
+ this.setUniform("uMaxDistance", c.collisionMaxDistance);
2668
+ this.setUniform("uIs3D", c.is3D);
2669
+ }
2670
+ };
2671
+
2672
+ //#endregion
2673
+ //#region assets/glsl/force-sim/drag.frag
2674
+ var drag_default = "precision highp float;\n\nuniform sampler2D uPositionsTexture;\r\nuniform sampler2D uVelocityTexture;\r\nuniform vec2 uDraggedUV; \r\nuniform vec3 uDragTarget;\r\nuniform float uDragStrength;\r\nuniform float uDeltaTime;\r\nvarying vec2 vUv;\n\nvoid main() {\r\n vec3 currentVel = texture2D(uVelocityTexture, vUv).xyz;\r\n vec3 outVel = currentVel;\n\n float texSize = float(textureSize(uPositionsTexture, 0).x);\r\n float eps = 0.5 / texSize;\r\n float distUV = distance(vUv, uDraggedUV);\n\n \n if (distUV < eps) {\r\n vec3 pos = texture2D(uPositionsTexture, vUv).xyz;\r\n vec3 diff = uDragTarget - pos;\r\n \r\n \n \n vec3 desiredVelocity = diff / max(uDeltaTime, 0.001);\r\n \r\n \n \n outVel = desiredVelocity * uDragStrength ;\r\n }\n\n gl_FragColor = vec4(outVel, 1.0);\r\n}";
2675
+
2676
+ //#endregion
2677
+ //#region simulation/passes/DragPass.ts
2678
+ /**
2679
+ * Drag force pass - user interaction dragging
2680
+ * Reads configuration from context.config, maintains drag state internally
2681
+ */
2682
+ var DragPass = class extends BasePass {
2683
+ draggedIndex = null;
2684
+ dragUV = null;
2685
+ dragTarget = new THREE.Vector3(0, 0, 0);
2686
+ getName() {
2687
+ return "drag";
2688
+ }
2689
+ initMaterial(context) {
2690
+ this.material = this.createMaterial(compute_default, drag_default, {
2691
+ uPositionsTexture: { value: null },
2692
+ uVelocityTexture: { value: null },
2693
+ uDraggedUV: { value: new THREE.Vector2(-1, -1) },
2694
+ uDragTarget: { value: new THREE.Vector3(0, 0, 0) },
2695
+ uDragStrength: { value: 0 },
2696
+ uTextureSize: { value: context.textureSize },
2697
+ uDeltaTime: { value: 0 }
2698
+ });
2699
+ }
2700
+ updateUniforms(context) {
2701
+ const c = context.config;
2702
+ this.setUniform("uPositionsTexture", context.simBuffers.getCurrentPositionTexture());
2703
+ this.setUniform("uVelocityTexture", context.simBuffers.getCurrentVelocityTexture());
2704
+ this.setUniform("uDraggedUV", this.dragUV || new THREE.Vector2(-1, -1));
2705
+ this.setUniform("uDragTarget", this.dragTarget);
2706
+ this.setUniform("uDragStrength", c.dragStrength);
2707
+ this.setUniform("uDeltaTime", c.deltaTime);
2708
+ }
2709
+ startDrag(index, targetWorldPos, textureSize) {
2710
+ this.draggedIndex = index;
2711
+ this.dragUV = this.indexToUV(index, textureSize);
2712
+ this.dragTarget.copy(targetWorldPos);
2713
+ this.setEnabled(true);
2714
+ }
2715
+ updateDrag(targetWorldPos) {
2716
+ if (this.draggedIndex === null) return;
2717
+ this.dragTarget.copy(targetWorldPos);
2718
+ }
2719
+ endDrag() {
2720
+ this.draggedIndex = null;
2721
+ this.dragUV = null;
2722
+ this.setEnabled(false);
2723
+ }
2724
+ isDragging() {
2725
+ return this.draggedIndex !== null;
2726
+ }
2727
+ indexToUV(index, textureSize) {
2728
+ const x = (index % textureSize + .5) / textureSize;
2729
+ const y = (Math.floor(index / textureSize) + .5) / textureSize;
2730
+ return new THREE.Vector2(x, y);
2731
+ }
2732
+ };
2733
+
2734
+ //#endregion
2735
+ //#region assets/glsl/force-sim/elastic.frag
2736
+ var elastic_default = "#ifdef GL_ES\r\nprecision highp float;\r\n#endif\n\nuniform sampler2D uPositionsTexture;\r\nuniform sampler2D uVelocityTexture;\r\nuniform sampler2D uOriginalPositionsTexture;\r\nuniform float uElasticStrength;\r\nuniform float uAlpha;\n\nvarying vec2 vUv;\n\nvoid main() {\r\n vec4 currentPosition = texture2D(uPositionsTexture, vUv);\r\n vec3 currentVelocity = texture2D(uVelocityTexture, vUv).xyz;\r\n vec4 originalPosition = texture2D(uOriginalPositionsTexture, vUv);\n\n \n vec3 displacement = originalPosition.xyz - currentPosition.xyz;\r\n float distance = length(displacement);\r\n \r\n \n vec3 force = vec3(0.0);\r\n if (distance > 0.0) {\r\n vec3 direction = displacement / distance; \n \n force = direction * distance * uElasticStrength;\r\n }\r\n \r\n \n vec3 newVelocity = currentVelocity + force * uAlpha;\r\n \r\n gl_FragColor = vec4(newVelocity, 1.0);\r\n}";
2737
+
2738
+ //#endregion
2739
+ //#region simulation/passes/ElasticPass.ts
2740
+ /**
2741
+ * Elastic force pass - spring to original position
2742
+ * Reads configuration directly from context.config
2743
+ */
2744
+ var ElasticPass = class extends BasePass {
2745
+ getName() {
2746
+ return "elastic";
2747
+ }
2748
+ initMaterial(context) {
2749
+ this.material = this.createMaterial(compute_default, elastic_default, {
2750
+ uPositionsTexture: { value: null },
2751
+ uVelocityTexture: { value: null },
2752
+ uOriginalPositionsTexture: { value: context.simBuffers.getOriginalPositionTexture() },
2753
+ uElasticStrength: { value: 0 },
2754
+ uAlpha: { value: 0 }
2755
+ });
2756
+ }
2757
+ updateUniforms(context) {
2758
+ const c = context.config;
2759
+ this.setUniform("uPositionsTexture", context.simBuffers.getCurrentPositionTexture());
2760
+ this.setUniform("uVelocityTexture", context.simBuffers.getCurrentVelocityTexture());
2761
+ this.setUniform("uOriginalPositionsTexture", context.simBuffers.getOriginalPositionTexture());
2762
+ this.setUniform("uElasticStrength", c.elasticStrength);
2763
+ this.setUniform("uAlpha", c.alpha);
2764
+ }
2765
+ };
2766
+
2767
+ //#endregion
2768
+ //#region assets/glsl/force-sim/gravity.frag
2769
+ var gravity_default = "#ifdef GL_ES\r\nprecision highp float;\r\n#endif\n\nuniform sampler2D uPositionsTexture;\r\nuniform sampler2D uVelocityTexture;\r\nuniform float uGravity;\r\nuniform float uAlpha;\r\nuniform float uSpaceSize;\r\nuniform float uTime; \n\nvarying vec2 vUv;\n\nvoid main() {\r\n vec4 currentPosition = texture2D(uPositionsTexture, vUv);\r\n vec2 velocity = texture2D(uVelocityTexture, vUv).xy;\r\n \r\n \n vec2 pos = currentPosition.xy;\r\n \r\n \n vec2 center = vec2(0.0);\r\n \r\n \n vec2 toCenter = center - pos;\r\n float distToCenter = length(toCenter);\r\n \r\n \n if (distToCenter > 0.0) {\r\n velocity += normalize(toCenter) * uAlpha * distToCenter;\r\n }\r\n \r\n gl_FragColor = vec4(velocity, 0.0, 0.0);\r\n}";
2770
+
2771
+ //#endregion
2772
+ //#region simulation/passes/GravityPass.ts
2773
+ /**
2774
+ * Gravity force pass - attracts nodes to center
2775
+ * Reads configuration directly from context.config
2776
+ */
2777
+ var GravityPass = class extends BasePass {
2778
+ getName() {
2779
+ return "gravity";
2780
+ }
2781
+ initMaterial(context) {
2782
+ this.material = this.createMaterial(compute_default, gravity_default, {
2783
+ uPositionsTexture: { value: null },
2784
+ uVelocityTexture: { value: null },
2785
+ uGravity: { value: 0 },
2786
+ uAlpha: { value: 0 },
2787
+ uSpaceSize: { value: 0 },
2788
+ uTime: { value: 0 }
2789
+ });
2790
+ }
2791
+ updateUniforms(context) {
2792
+ const c = context.config;
2793
+ this.setUniform("uPositionsTexture", context.simBuffers.getCurrentPositionTexture());
2794
+ this.setUniform("uVelocityTexture", context.simBuffers.getCurrentVelocityTexture());
2795
+ this.setUniform("uGravity", c.gravity);
2796
+ this.setUniform("uAlpha", c.alpha);
2797
+ this.setUniform("uSpaceSize", c.spaceSize);
2798
+ this.setUniform("uTime", performance.now() / 1e3);
2799
+ }
2800
+ };
2801
+
2802
+ //#endregion
2803
+ //#region assets/glsl/force-sim/integrate.frag
2804
+ var integrate_default = "#ifdef GL_ES\r\nprecision highp float;\r\n#endif\n\nuniform sampler2D uPositionsTexture;\r\nuniform sampler2D uVelocityTexture;\r\nuniform float uDeltaTime;\r\nuniform float uDamping;\r\nuniform float uSpaceSize;\n\nvarying vec2 vUv;\n\nvoid main() {\r\n vec4 currentPosition = texture2D(uPositionsTexture, vUv);\r\n vec4 velocityTex = texture2D(uVelocityTexture, vUv);\n\n \n vec3 velocity = velocityTex.rgb;\n\n \n \n \n \n \n float state = currentPosition.w;\n\n \n vec3 newPosition = currentPosition.xyz;\r\n \r\n \n if (state > 2.5) {\r\n newPosition += velocity * uDeltaTime;\r\n }\n\n gl_FragColor = vec4(newPosition, currentPosition.w);\r\n}";
2805
+
2806
+ //#endregion
2807
+ //#region simulation/passes/IntegratePass.ts
2808
+ /**
2809
+ * Integration pass - updates positions from velocities
2810
+ * Reads configuration directly from context.config
2811
+ */
2812
+ var IntegratePass = class extends BasePass {
2813
+ getName() {
2814
+ return "integrate";
2815
+ }
2816
+ initMaterial(context) {
2817
+ this.material = this.createMaterial(compute_default, integrate_default, {
2818
+ uPositionsTexture: { value: null },
2819
+ uVelocityTexture: { value: null },
2820
+ uDeltaTime: { value: 0 },
2821
+ uDamping: { value: 0 },
2822
+ uSpaceSize: { value: 0 },
2823
+ uAlpha: { value: 0 }
2824
+ });
2825
+ }
2826
+ updateUniforms(context) {
2827
+ const c = context.config;
2828
+ this.setUniform("uPositionsTexture", context.simBuffers.getPreviousPositionTexture());
2829
+ this.setUniform("uVelocityTexture", context.simBuffers.getCurrentVelocityTexture());
2830
+ this.setUniform("uDeltaTime", c.deltaTime);
2831
+ this.setUniform("uDamping", c.damping);
2832
+ this.setUniform("uSpaceSize", c.spaceSize);
2833
+ this.setUniform("uAlpha", c.alpha);
2834
+ }
2835
+ };
2836
+
2837
+ //#endregion
2838
+ //#region assets/glsl/force-sim/link.frag
2839
+ var link_default = "#ifdef GL_ES\r\nprecision highp float;\r\n#endif\n\nuniform sampler2D uPositionsTexture;\r\nuniform sampler2D uVelocityTexture;\r\nuniform sampler2D uLinksTexture; \nuniform sampler2D uLinksMapTexture; \nuniform sampler2D uLinkPropertiesTexture; \nuniform float uTextureSize;\r\nuniform float uLinksTextureSize;\r\nuniform float uSpringStrength; \nuniform float uSpringLength; \nuniform float uSpringDamping; \nuniform float uAlpha;\r\nuniform int uMaxLinks;\n\nvarying vec2 vUv;\n\nconst float JIGGLE = 1e-6;\n\nvoid main() {\r\n vec4 currentPosition = texture2D(uPositionsTexture, vUv);\r\n vec3 velocity = texture2D(uVelocityTexture, vUv).xyz;\r\n vec3 pos = currentPosition.xyz;\n\n \n vec4 mapEntry = texture2D(uLinksMapTexture, vUv);\r\n float startXf = mapEntry.r;\r\n float startYf = mapEntry.g;\r\n int count = int(mapEntry.b);\n\n if (count <= 0) {\r\n gl_FragColor = vec4(velocity, 0.0);\r\n return;\r\n }\n\n \n float maxIndexF = uLinksTextureSize - 1.0;\r\n float startXi = clamp(floor(startXf + 0.5), 0.0, maxIndexF);\r\n float startYi = clamp(floor(startYf + 0.5), 0.0, maxIndexF);\n\n \n float baseIndex = startXi + startYi * uLinksTextureSize;\r\n float totalLinkTexels = uLinksTextureSize * uLinksTextureSize;\n\n vec3 velocityDelta = vec3(0.0);\n\n \n for (int i = 0; i < count; i++) {\n\n float idx = baseIndex + float(i);\n\n \n if (idx < 0.0 || idx >= totalLinkTexels) break;\n\n float lx = mod(idx, uLinksTextureSize);\r\n float ly = floor(idx / uLinksTextureSize);\r\n vec2 linkUV = vec2((lx + 0.5) / uLinksTextureSize, (ly + 0.5) / uLinksTextureSize);\n\n \n float otherIndexF = texture2D(uLinksTexture, linkUV).r;\r\n vec3 linkProps = texture2D(uLinkPropertiesTexture, linkUV).rgb;\r\n \r\n \n float strength = uSpringStrength;\r\n if (linkProps.r > 0.0) strength *= linkProps.r;\n\n \n if (strength <= 0.0 && uSpringDamping <= 0.0) continue;\n\n float spring_dist = uSpringLength;\r\n \n\n \n float otherIndexRoundedF = floor(otherIndexF + 0.5);\n\n if (otherIndexRoundedF < 0.0) continue;\n\n \n float ox = mod(otherIndexRoundedF, uTextureSize);\r\n float oy = floor(otherIndexRoundedF / uTextureSize);\r\n vec2 otherUV = vec2((ox + 0.5) / uTextureSize, (oy + 0.5) / uTextureSize);\n\n vec4 otherPosTex = texture2D(uPositionsTexture, otherUV);\r\n vec3 otherPos = otherPosTex.xyz;\n\n \n if (otherPosTex.w < 1.5) continue;\n\n vec3 otherVel = texture2D(uVelocityTexture, otherUV).xyz;\n\n vec3 delta = otherPos - pos;\r\n vec3 force = vec3(0.0);\r\n \r\n float currentLength = length(delta);\r\n if (currentLength > 0.0) {\r\n float displacement = currentLength - spring_dist;\r\n \r\n \n vec3 direction = delta / currentLength;\r\n float springForce = displacement * strength;\r\n \r\n \n vec3 relVel = velocity - otherVel;\r\n float dampingForce = dot(relVel, direction) * uSpringDamping;\n\n \n force = direction * (springForce - dampingForce);\r\n }\n\n \n velocityDelta += force * uAlpha;\r\n }\n\n vec3 newVel = velocity + velocityDelta;\r\n gl_FragColor = vec4(newVel, 0.0);\r\n}";
2840
+
2841
+ //#endregion
2842
+ //#region simulation/passes/LinkPass.ts
2843
+ /**
2844
+ * Link force pass - spring forces between connected nodes
2845
+ * Reads configuration directly from context.config
2846
+ */
2847
+ var LinkPass = class extends BasePass {
2848
+ getName() {
2849
+ return "link";
2850
+ }
2851
+ initMaterial(context) {
2852
+ const linksTexture = context.assets.getLinkIndicesTexture();
2853
+ const linksTextureSize = linksTexture?.image?.data ? Math.ceil(Math.sqrt(linksTexture.image.data.length / 4)) : 0;
2854
+ this.material = this.createMaterial(compute_default, link_default, {
2855
+ uPositionsTexture: { value: null },
2856
+ uVelocityTexture: { value: null },
2857
+ uLinksTexture: { value: context.assets.getLinkIndicesTexture() },
2858
+ uLinksMapTexture: { value: context.assets.getLinkMapTexture() },
2859
+ uLinkPropertiesTexture: { value: context.assets.getLinkPropertiesTexture() },
2860
+ uTextureSize: { value: context.textureSize },
2861
+ uLinksTextureSize: { value: linksTextureSize },
2862
+ uSpringStrength: { value: context.config.springStrength },
2863
+ uSpringLength: { value: context.config.springLength },
2864
+ uSpringDamping: { value: context.config.springDamping },
2865
+ uAlpha: { value: context.config.alpha },
2866
+ uMaxLinks: { value: context.config.maxLinks }
2867
+ });
2868
+ }
2869
+ updateUniforms(context) {
2870
+ const c = context.config;
2871
+ this.setUniform("uPositionsTexture", context.simBuffers.getCurrentPositionTexture());
2872
+ this.setUniform("uVelocityTexture", context.simBuffers.getCurrentVelocityTexture());
2873
+ this.setUniform("uSpringStrength", c.springStrength);
2874
+ this.setUniform("uSpringLength", c.springLength);
2875
+ this.setUniform("uSpringDamping", c.springDamping);
2876
+ this.setUniform("uAlpha", c.alpha);
2877
+ this.setUniform("uMaxLinks", c.maxLinks);
2878
+ }
2879
+ };
2880
+
2881
+ //#endregion
2882
+ //#region assets/glsl/force-sim/manybody.frag
2883
+ var manybody_default = "#ifdef GL_ES\r\nprecision highp float;\r\n#endif\n\nuniform sampler2D uPositionsTexture;\r\nuniform sampler2D uVelocityTexture;\r\nuniform float uTextureSize;\r\nuniform float uManyBodyStrength; \nuniform float uMinDistance; \nuniform float uMaxDistance; \nuniform float uAlpha; \nuniform sampler2D uRadiiTexture;\r\nuniform bool uIs3D; \n\nvarying vec2 vUv;\n\nfloat rand(vec2 co){\r\n return fract(sin(dot(co, vec2(12.9898,78.233))) * 43758.5453);\r\n}\n\nvoid main() {\r\n vec4 currentPosition = texture2D(uPositionsTexture, vUv);\r\n vec3 velocity = texture2D(uVelocityTexture, vUv).xyz;\r\n float particleIndex = floor(vUv.y * uTextureSize) * uTextureSize + floor(vUv.x * uTextureSize);\n\n float texStep = 1.0 / uTextureSize;\r\n float halfTex = 0.5 * texStep;\r\n float eps = 1e-6;\n\n float r1 = texture2D(uRadiiTexture, vUv).r;\r\n vec3 force = vec3(0.0);\n\n \n for (float y = 0.0; y < 1.0; y += texStep) {\r\n for (float x = 0.0; x < 1.0; x += texStep) {\r\n vec2 otherUV = vec2(x + halfTex, y + halfTex);\r\n float otherIndex = floor(y * uTextureSize) * uTextureSize + floor(x * uTextureSize);\r\n if (abs(otherIndex - particleIndex) < 0.5) continue; \n\n vec4 otherPosition = texture2D(uPositionsTexture, otherUV);\r\n \r\n \n \n if (otherPosition.w < 1.5) continue;\n\n vec3 delta;\r\n if (uIs3D) {\r\n delta = currentPosition.xyz - otherPosition.xyz;\r\n } else {\r\n delta = vec3(currentPosition.xy - otherPosition.xy, 0.0);\r\n }\n\n float dist = length(delta);\n\n if (dist < uMinDistance || dist > uMaxDistance) continue;\n\n float r2 = texture2D(uRadiiTexture, otherUV).r;\r\n float distSq = max(dist * dist, eps); \r\n float forceMagnitude = (uManyBodyStrength * r1 * r2) / distSq;\n\n vec3 direction = length(delta) > eps ? normalize(delta) : vec3(0.0);\r\n \r\n force += direction * forceMagnitude * r1;\r\n }\r\n }\r\n \r\n \n velocity += force * uAlpha;\r\n \r\n gl_FragColor = vec4(velocity, 0.0);\r\n}";
2884
+
2885
+ //#endregion
2886
+ //#region simulation/passes/ManyBodyPass.ts
2887
+ /**
2888
+ * Many-body force pass - charge/repulsion between all nodes
2889
+ * Reads configuration directly from context.config
2890
+ */
2891
+ var ManyBodyPass = class extends BasePass {
2892
+ getName() {
2893
+ return "manybody";
2894
+ }
2895
+ initMaterial(context) {
2896
+ this.material = this.createMaterial(compute_default, manybody_default, {
2897
+ uPositionsTexture: { value: null },
2898
+ uVelocityTexture: { value: null },
2899
+ uTextureSize: { value: context.textureSize },
2900
+ uRadiiTexture: { value: context.assets.getNodeRadiiTexture() },
2901
+ uManyBodyStrength: { value: 0 },
2902
+ uMinDistance: { value: 0 },
2903
+ uMaxDistance: { value: 0 },
2904
+ uAlpha: { value: 0 },
2905
+ uIs3D: { value: true }
2906
+ });
2907
+ }
2908
+ updateUniforms(context) {
2909
+ const c = context.config;
2910
+ this.setUniform("uPositionsTexture", context.simBuffers.getCurrentPositionTexture());
2911
+ this.setUniform("uVelocityTexture", context.simBuffers.getCurrentVelocityTexture());
2912
+ this.setUniform("uManyBodyStrength", c.manyBodyStrength);
2913
+ this.setUniform("uMinDistance", c.manyBodyMinDistance);
2914
+ this.setUniform("uMaxDistance", c.manyBodyMaxDistance);
2915
+ this.setUniform("uAlpha", c.alpha);
2916
+ this.setUniform("uIs3D", c.is3D);
2917
+ }
2918
+ };
2919
+
2920
+ //#endregion
2921
+ //#region assets/glsl/force-sim/velocity_carry.frag
2922
+ var velocity_carry_default = "#ifdef GL_ES\r\nprecision highp float;\r\n#endif\n\nuniform sampler2D uPrevVelocity;\r\nuniform float uDamping;\n\nvarying vec2 vUv;\n\nvoid main() {\r\n vec3 prev = texture2D(uPrevVelocity, vUv).rgb;\r\n vec3 carried = prev * uDamping;\r\n gl_FragColor = vec4(carried, 1.0);\r\n}";
2923
+
2924
+ //#endregion
2925
+ //#region simulation/passes/VelocityCarryPass.ts
2926
+ /**
2927
+ * Velocity carry pass - applies damping to previous velocity
2928
+ * Reads configuration directly from context.config
2929
+ */
2930
+ var VelocityCarryPass = class extends BasePass {
2931
+ getName() {
2932
+ return "velocityCarry";
2933
+ }
2934
+ initMaterial(context) {
2935
+ this.material = this.createMaterial(compute_default, velocity_carry_default, {
2936
+ uPrevVelocity: { value: null },
2937
+ uDamping: { value: 0 }
2938
+ });
2939
+ }
2940
+ updateUniforms(context) {
2941
+ this.setUniform("uPrevVelocity", context.simBuffers.getPreviousVelocityTexture());
2942
+ this.setUniform("uDamping", context.config.damping);
2943
+ }
2944
+ };
2945
+
2946
+ //#endregion
2947
+ //#region simulation/ForceSimulation.ts
2948
+ /**
2949
+ * ForceSimulation - GPU-based force simulation
2950
+ *
2951
+ * Simple architecture:
2952
+ * - Single shared config object (ForceConfig)
2953
+ * - All passes read directly from config via PassContext
2954
+ * - Enable flags control which passes execute
2955
+ * - No manual syncing required
2956
+ */
2957
+ var ForceSimulation = class {
2958
+ renderer;
2959
+ simulationBuffers = null;
2960
+ computeScene;
2961
+ computeCamera;
2962
+ computeQuad;
2963
+ velocityCarryPass;
2964
+ collisionPass;
2965
+ manyBodyPass;
2966
+ gravityPass;
2967
+ linkPass;
2968
+ elasticPass;
2969
+ attractorPass;
2970
+ dragPass;
2971
+ integratePass;
2972
+ forcePasses;
2973
+ config;
2974
+ isInitialized = false;
2975
+ iterationCount = 0;
2976
+ constructor(renderer) {
2977
+ this.renderer = renderer;
2978
+ this.computeScene = new THREE.Scene();
2979
+ this.computeCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
2980
+ this.computeQuad = this.createComputeQuad();
2981
+ this.config = {
2982
+ manyBodyStrength: 3.5,
2983
+ manyBodyMinDistance: .01,
2984
+ manyBodyMaxDistance: .3,
2985
+ enableManyBody: false,
2986
+ collisionStrength: .33,
2987
+ collisionRadius: .33,
2988
+ collisionMaxDistance: .3,
2989
+ enableCollision: true,
2990
+ elasticStrength: .1,
2991
+ enableElastic: false,
2992
+ springStrength: 10,
2993
+ springLength: .1,
2994
+ springDamping: .99,
2995
+ maxLinks: 512,
2996
+ enableLinks: false,
2997
+ gravity: .9,
2998
+ enableGravity: false,
2999
+ attractorStrength: .1,
3000
+ enableAttractors: false,
3001
+ dragStrength: .2,
3002
+ damping: .75,
3003
+ alpha: 1,
3004
+ alphaDecay: .01,
3005
+ deltaTime: .016,
3006
+ spaceSize: 1e3,
3007
+ is3D: true
3008
+ };
3009
+ this.velocityCarryPass = new VelocityCarryPass();
3010
+ this.collisionPass = new CollisionPass();
3011
+ this.manyBodyPass = new ManyBodyPass();
3012
+ this.gravityPass = new GravityPass();
3013
+ this.linkPass = new LinkPass();
3014
+ this.elasticPass = new ElasticPass();
3015
+ this.attractorPass = new AttractorPass();
3016
+ this.dragPass = new DragPass();
3017
+ this.integratePass = new IntegratePass();
3018
+ this.forcePasses = [
3019
+ this.collisionPass,
3020
+ this.manyBodyPass,
3021
+ this.gravityPass,
3022
+ this.attractorPass,
3023
+ this.linkPass,
3024
+ this.elasticPass,
3025
+ this.dragPass
3026
+ ];
3027
+ }
3028
+ createComputeQuad() {
3029
+ const quad = new THREE.BufferGeometry();
3030
+ const vertices = new Float32Array([
3031
+ -1,
3032
+ -1,
3033
+ 0,
3034
+ 1,
3035
+ -1,
3036
+ 0,
3037
+ 1,
3038
+ 1,
3039
+ 0,
3040
+ -1,
3041
+ 1,
3042
+ 0
3043
+ ]);
3044
+ const uvs = new Float32Array([
3045
+ 0,
3046
+ 0,
3047
+ 1,
3048
+ 0,
3049
+ 1,
3050
+ 1,
3051
+ 0,
3052
+ 1
3053
+ ]);
3054
+ const indices = new Uint16Array([
3055
+ 0,
3056
+ 1,
3057
+ 2,
3058
+ 0,
3059
+ 2,
3060
+ 3
3061
+ ]);
3062
+ quad.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
3063
+ quad.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
3064
+ quad.setIndex(new THREE.BufferAttribute(indices, 1));
3065
+ return quad;
3066
+ }
3067
+ /**
3068
+ * Initialize simulation with buffers
3069
+ */
3070
+ initialize(simulationBuffers) {
3071
+ this.simulationBuffers = simulationBuffers;
3072
+ if (!this.simulationBuffers.isReady()) {
3073
+ console.warn("[ForceSimulation] SimulationBuffers not ready");
3074
+ return;
3075
+ }
3076
+ const context = this.createContext();
3077
+ this.velocityCarryPass.initMaterial(context);
3078
+ this.integratePass.initMaterial(context);
3079
+ for (const pass of this.forcePasses) pass.initMaterial(context);
3080
+ this.isInitialized = true;
3081
+ }
3082
+ createContext() {
3083
+ if (!this.simulationBuffers) throw new Error("[ForceSimulation] Cannot create context without buffers");
3084
+ return {
3085
+ scene: this.computeScene,
3086
+ camera: this.computeCamera,
3087
+ quad: this.computeQuad,
3088
+ simBuffers: this.simulationBuffers,
3089
+ assets: staticAssets,
3090
+ renderer: this.renderer,
3091
+ config: this.config,
3092
+ textureSize: this.simulationBuffers.getTextureSize()
3093
+ };
3094
+ }
3095
+ /**
3096
+ * Check if a pass should execute based on config
3097
+ */
3098
+ shouldExecutePass(pass) {
3099
+ switch (pass.getName()) {
3100
+ case "collision": return this.config.enableCollision;
3101
+ case "manybody": return this.config.enableManyBody;
3102
+ case "gravity": return this.config.enableGravity;
3103
+ case "attractor": return this.config.enableAttractors && this.attractorPass.getAttractors().length > 0;
3104
+ case "link": return this.config.enableLinks;
3105
+ case "elastic": return this.config.enableElastic;
3106
+ case "drag": return this.dragPass.isDragging();
3107
+ default: return true;
3108
+ }
3109
+ }
3110
+ /**
3111
+ * Run one simulation step
3112
+ */
3113
+ step(deltaTime) {
3114
+ if (!this.isInitialized || !this.simulationBuffers) return;
3115
+ if (deltaTime !== void 0) this.config.deltaTime = deltaTime;
3116
+ const context = this.createContext();
3117
+ const currentVelocityTarget = this.simulationBuffers.getCurrentVelocityTarget();
3118
+ if (currentVelocityTarget) {
3119
+ this.renderer.setRenderTarget(currentVelocityTarget);
3120
+ this.velocityCarryPass.execute(context);
3121
+ }
3122
+ for (const pass of this.forcePasses) {
3123
+ if (!this.shouldExecutePass(pass)) continue;
3124
+ const prevVelocityTarget = this.simulationBuffers.getPreviousVelocityTarget();
3125
+ if (prevVelocityTarget) {
3126
+ this.renderer.setRenderTarget(prevVelocityTarget);
3127
+ pass.execute(context);
3128
+ this.simulationBuffers.swapVelocities();
3129
+ }
3130
+ }
3131
+ this.simulationBuffers.swapPositions();
3132
+ const newPositionTarget = this.simulationBuffers.getCurrentPositionTarget();
3133
+ if (newPositionTarget) {
3134
+ this.renderer.setRenderTarget(newPositionTarget);
3135
+ this.integratePass.execute(context);
3136
+ }
3137
+ const timeScale = (this.config.deltaTime || .016) * 60;
3138
+ const decayFactor = Math.pow(1 - this.config.alphaDecay, timeScale);
3139
+ this.config.alpha *= decayFactor;
3140
+ this.config.alpha = Math.max(this.config.alpha, 0);
3141
+ this.renderer.setRenderTarget(null);
3142
+ this.iterationCount++;
3143
+ }
3144
+ /**
3145
+ * Get config for GUI binding
3146
+ * Modify this object directly - changes take effect on next step()
3147
+ */
3148
+ getConfig() {
3149
+ return this.config;
3150
+ }
3151
+ /**
3152
+ * Re-heat the simulation (set alpha to 1)
3153
+ */
3154
+ reheat(amt = 1) {
3155
+ this.config.alpha = amt;
3156
+ }
3157
+ startDrag(index, targetWorldPos) {
3158
+ if (!this.simulationBuffers) return;
3159
+ this.dragPass.startDrag(index, targetWorldPos, this.simulationBuffers.getTextureSize());
3160
+ this.reheat(.1);
3161
+ }
3162
+ updateDrag(targetWorldPos) {
3163
+ this.dragPass.updateDrag(targetWorldPos);
3164
+ this.reheat(.01);
3165
+ }
3166
+ endDrag() {
3167
+ this.dragPass.endDrag();
3168
+ }
3169
+ /**
3170
+ * Set attractors for category-based attraction
3171
+ * @param attractors Array of attractor definitions
3172
+ */
3173
+ setAttractors(attractors) {
3174
+ this.attractorPass.setAttractors(attractors);
3175
+ this.config.enableAttractors = attractors.length > 0;
3176
+ }
3177
+ /**
3178
+ * Add a single attractor
3179
+ */
3180
+ addAttractor(attractor) {
3181
+ this.attractorPass.addAttractor(attractor);
3182
+ this.config.enableAttractors = true;
3183
+ }
3184
+ /**
3185
+ * Remove an attractor by ID
3186
+ */
3187
+ removeAttractor(id) {
3188
+ this.attractorPass.removeAttractor(id);
3189
+ if (this.attractorPass.getAttractors().length === 0) this.config.enableAttractors = false;
3190
+ }
3191
+ /**
3192
+ * Update an attractor's position (useful for animated attractors)
3193
+ */
3194
+ updateAttractorPosition(id, position) {
3195
+ this.attractorPass.updateAttractorPosition(id, position);
3196
+ }
3197
+ /**
3198
+ * Get current attractors
3199
+ */
3200
+ getAttractors() {
3201
+ return this.attractorPass.getAttractors();
3202
+ }
3203
+ /**
3204
+ * Set node logic for attractor matching
3205
+ * Call this when graph data changes
3206
+ */
3207
+ setNodeGroups(groups) {
3208
+ if (!this.simulationBuffers) return;
3209
+ const textureSize = this.simulationBuffers.getTextureSize();
3210
+ const normalizedGroups = groups.map((g) => Array.isArray(g) ? g : g ? [g] : []);
3211
+ this.attractorPass.setNodeGroups(normalizedGroups, textureSize);
3212
+ }
3213
+ setNodeGroupsFromData(data, accessor) {
3214
+ const groups = data.map((item, i) => accessor(item, i) || []);
3215
+ this.setNodeGroups(groups);
3216
+ }
3217
+ /**
3218
+ * Get the attractor pass for direct access
3219
+ */
3220
+ getAttractorPass() {
3221
+ return this.attractorPass;
3222
+ }
3223
+ getAlpha() {
3224
+ return this.config.alpha;
3225
+ }
3226
+ getIterationCount() {
3227
+ return this.iterationCount;
3228
+ }
3229
+ reset() {
3230
+ if (!this.isInitialized || !this.simulationBuffers) return;
3231
+ this.config.alpha = 1;
3232
+ this.iterationCount = 0;
3233
+ this.simulationBuffers.resetPositions();
3234
+ this.simulationBuffers.initVelocities();
3235
+ }
3236
+ dispose() {
3237
+ this.velocityCarryPass.dispose();
3238
+ this.integratePass.dispose();
3239
+ for (const pass of this.forcePasses) pass.dispose();
3240
+ this.computeQuad.dispose();
3241
+ this.isInitialized = false;
3242
+ }
3243
+ };
3244
+
3245
+ //#endregion
3246
+ //#region textures/PickBuffer.ts
3247
+ /**
3248
+ * Manages GPU picking buffer for mouse interaction
3249
+ * Separate from simulation buffers as it has different lifecycle
3250
+ */
3251
+ var PickBuffer = class {
3252
+ renderer;
3253
+ pickTarget = null;
3254
+ constructor(renderer) {
3255
+ this.renderer = renderer;
3256
+ }
3257
+ /**
3258
+ * Ensure pick buffer matches viewport size
3259
+ */
3260
+ resize(width, height) {
3261
+ if (!this.pickTarget || this.pickTarget.width !== width || this.pickTarget.height !== height) {
3262
+ this.pickTarget?.dispose();
3263
+ this.pickTarget = this.createPickBuffer(width, height);
3264
+ }
3265
+ }
3266
+ createPickBuffer(width, height) {
3267
+ return new THREE.WebGLRenderTarget(width, height, {
3268
+ minFilter: THREE.NearestFilter,
3269
+ magFilter: THREE.NearestFilter,
3270
+ format: THREE.RGBAFormat,
3271
+ type: THREE.UnsignedByteType,
3272
+ stencilBuffer: false,
3273
+ depthBuffer: true,
3274
+ generateMipmaps: false
3275
+ });
3276
+ }
3277
+ /**
3278
+ * Read node ID at pixel coordinates
3279
+ */
3280
+ readIdAt(x, y) {
3281
+ if (!this.pickTarget) return -1;
3282
+ this.pickTarget.width;
3283
+ const height = this.pickTarget.height;
3284
+ const buffer = new Uint8Array(4);
3285
+ const readY = height - y - 1;
3286
+ try {
3287
+ this.renderer.readRenderTargetPixels(this.pickTarget, Math.floor(x), Math.floor(readY), 1, 1, buffer);
3288
+ } catch (e) {
3289
+ return -1;
3290
+ }
3291
+ const r = buffer[0] || 0;
3292
+ const g = buffer[1] || 0;
3293
+ const b = buffer[2] || 0;
3294
+ const id = r + (g << 8) + (b << 16) - 1;
3295
+ return id < 0 ? -1 : id;
3296
+ }
3297
+ /**
3298
+ * Render scene to pick buffer
3299
+ */
3300
+ render(scene, camera) {
3301
+ if (!this.pickTarget) return;
3302
+ const currentRenderTarget = this.renderer.getRenderTarget();
3303
+ this.renderer.setRenderTarget(this.pickTarget);
3304
+ this.renderer.render(scene, camera);
3305
+ this.renderer.setRenderTarget(currentRenderTarget);
3306
+ }
3307
+ getTarget() {
3308
+ return this.pickTarget;
3309
+ }
3310
+ dispose() {
3311
+ this.pickTarget?.dispose();
3312
+ }
3313
+ };
3314
+
3315
+ //#endregion
3316
+ //#region textures/SimulationBuffers.ts
3317
+ /**
3318
+ * Manages dynamic render targets updated by force simulation
3319
+ * Uses ping-pong technique for GPU computation
3320
+ * Buffers are created lazily when data is initialized
3321
+ */
3322
+ var SimulationBuffers = class {
3323
+ renderer;
3324
+ textureSize = 0;
3325
+ positionBuffers = {
3326
+ current: null,
3327
+ previous: null,
3328
+ original: null
3329
+ };
3330
+ velocityBuffers = {
3331
+ current: null,
3332
+ previous: null
3333
+ };
3334
+ isInitialized = false;
3335
+ constructor(renderer) {
3336
+ this.renderer = renderer;
3337
+ }
3338
+ /**
3339
+ * Check if buffers are initialized
3340
+ */
3341
+ isReady() {
3342
+ return this.isInitialized;
3343
+ }
3344
+ /**
3345
+ * Initialize buffers with point count and initial positions
3346
+ */
3347
+ init(pointsCount, initialPositions) {
3348
+ if (this.isInitialized) this.dispose();
3349
+ this.textureSize = this.calculateTextureSize(pointsCount);
3350
+ if (this.textureSize === 0 || !Number.isFinite(this.textureSize)) {
3351
+ console.error(`[SimulationBuffers] Invalid texture size calculated: ${this.textureSize}`);
3352
+ throw new Error(`Invalid texture size: ${this.textureSize}`);
3353
+ }
3354
+ this.positionBuffers = {
3355
+ current: this.createDynamicBuffer(),
3356
+ previous: this.createDynamicBuffer(),
3357
+ original: this.createDynamicBuffer()
3358
+ };
3359
+ this.velocityBuffers = {
3360
+ current: this.createDynamicBuffer(),
3361
+ previous: this.createDynamicBuffer()
3362
+ };
3363
+ this.isInitialized = true;
3364
+ if (initialPositions) this.setInitialPositions(initialPositions);
3365
+ else this.setInitialPositions(new Float32Array(pointsCount * 3));
3366
+ this.initVelocities();
3367
+ }
3368
+ /**
3369
+ * Resize buffers (re-initializes with new size)
3370
+ */
3371
+ resize(pointsCount, preserveData = false) {
3372
+ if (this.calculateTextureSize(pointsCount) === this.textureSize) return;
3373
+ let savedPositions;
3374
+ if (preserveData && this.isInitialized) savedPositions = this.readPositions();
3375
+ this.init(pointsCount, savedPositions);
3376
+ }
3377
+ calculateTextureSize(count) {
3378
+ const size = Math.ceil(Math.sqrt(count));
3379
+ return 2 ** Math.ceil(Math.log2(size));
3380
+ }
3381
+ createDynamicBuffer() {
3382
+ return new THREE.WebGLRenderTarget(this.textureSize, this.textureSize, {
3383
+ minFilter: THREE.NearestFilter,
3384
+ magFilter: THREE.NearestFilter,
3385
+ format: THREE.RGBAFormat,
3386
+ type: THREE.FloatType,
3387
+ stencilBuffer: false,
3388
+ depthBuffer: false,
3389
+ generateMipmaps: false
3390
+ });
3391
+ }
3392
+ /**
3393
+ * Initialize position buffers from initial data
3394
+ */
3395
+ setInitialPositions(positionArray) {
3396
+ if (!this.isInitialized) throw new Error("SimulationBuffers must be initialized before setting positions");
3397
+ const textureData = this.arrayToTextureData(positionArray, 4);
3398
+ const texture = this.createDataTexture(textureData);
3399
+ this.copyTextureToBuffer(texture, this.positionBuffers.current);
3400
+ this.copyTextureToBuffer(texture, this.positionBuffers.previous);
3401
+ this.copyTextureToBuffer(texture, this.positionBuffers.original);
3402
+ texture.dispose();
3403
+ }
3404
+ /**
3405
+ * Update current and previous positions, but keep original positions intact
3406
+ */
3407
+ updateCurrentPositions(positionArray) {
3408
+ if (!this.isInitialized) throw new Error("SimulationBuffers must be initialized before updating positions");
3409
+ const textureData = this.arrayToTextureData(positionArray, 4);
3410
+ const texture = this.createDataTexture(textureData);
3411
+ this.copyTextureToBuffer(texture, this.positionBuffers.current);
3412
+ this.copyTextureToBuffer(texture, this.positionBuffers.previous);
3413
+ texture.dispose();
3414
+ }
3415
+ /**
3416
+ * Set original positions (anchors) separately from current positions
3417
+ */
3418
+ setOriginalPositions(positionArray) {
3419
+ if (!this.isInitialized) throw new Error("SimulationBuffers must be initialized before setting original positions");
3420
+ const textureData = this.arrayToTextureData(positionArray, 4);
3421
+ const texture = this.createDataTexture(textureData);
3422
+ this.copyTextureToBuffer(texture, this.positionBuffers.original);
3423
+ texture.dispose();
3424
+ }
3425
+ /**
3426
+ * Initialize velocity buffers (usually to zero)
3427
+ */
3428
+ initVelocities() {
3429
+ if (!this.isInitialized) throw new Error("SimulationBuffers must be initialized before setting velocities");
3430
+ const size = this.textureSize * this.textureSize * 4;
3431
+ const textureData = new Float32Array(size).fill(0);
3432
+ const texture = this.createDataTexture(textureData);
3433
+ this.copyTextureToBuffer(texture, this.velocityBuffers.current);
3434
+ this.copyTextureToBuffer(texture, this.velocityBuffers.previous);
3435
+ texture.dispose();
3436
+ }
3437
+ /**
3438
+ * Swap position buffers (ping-pong)
3439
+ */
3440
+ swapPositions() {
3441
+ if (!this.isInitialized) return;
3442
+ const temp = this.positionBuffers.current;
3443
+ this.positionBuffers.current = this.positionBuffers.previous;
3444
+ this.positionBuffers.previous = temp;
3445
+ }
3446
+ /**
3447
+ * Swap velocity buffers (ping-pong)
3448
+ */
3449
+ swapVelocities() {
3450
+ if (!this.isInitialized) return;
3451
+ const temp = this.velocityBuffers.current;
3452
+ this.velocityBuffers.current = this.velocityBuffers.previous;
3453
+ this.velocityBuffers.previous = temp;
3454
+ }
3455
+ /**
3456
+ * Reset positions to original state
3457
+ */
3458
+ resetPositions() {
3459
+ if (!this.isInitialized) return;
3460
+ this.copyBufferToBuffer(this.positionBuffers.original, this.positionBuffers.current);
3461
+ this.copyBufferToBuffer(this.positionBuffers.original, this.positionBuffers.previous);
3462
+ }
3463
+ /**
3464
+ * Read current positions back from GPU (expensive operation)
3465
+ * Returns RGBA float array (x, y, z, state)
3466
+ */
3467
+ readPositions() {
3468
+ if (!this.isInitialized || !this.positionBuffers.current) return new Float32Array(0);
3469
+ const width = this.positionBuffers.current.width;
3470
+ const height = this.positionBuffers.current.height;
3471
+ const pixels = new Float32Array(width * height * 4);
3472
+ this.renderer.readRenderTargetPixels(this.positionBuffers.current, 0, 0, width, height, pixels);
3473
+ return pixels;
3474
+ }
3475
+ getCurrentPositionTarget() {
3476
+ return this.positionBuffers.current;
3477
+ }
3478
+ getPreviousPositionTarget() {
3479
+ return this.positionBuffers.previous;
3480
+ }
3481
+ getCurrentPositionTexture() {
3482
+ return this.positionBuffers.current.texture;
3483
+ }
3484
+ getPreviousPositionTexture() {
3485
+ return this.positionBuffers.previous?.texture ?? null;
3486
+ }
3487
+ getOriginalPositionTexture() {
3488
+ return this.positionBuffers.original?.texture ?? null;
3489
+ }
3490
+ getCurrentVelocityTarget() {
3491
+ return this.velocityBuffers.current;
3492
+ }
3493
+ getPreviousVelocityTarget() {
3494
+ return this.velocityBuffers.previous;
3495
+ }
3496
+ getCurrentVelocityTexture() {
3497
+ return this.velocityBuffers.current?.texture ?? null;
3498
+ }
3499
+ getPreviousVelocityTexture() {
3500
+ return this.velocityBuffers.previous?.texture ?? null;
3501
+ }
3502
+ getTextureSize() {
3503
+ return this.textureSize;
3504
+ }
3505
+ arrayToTextureData(array, componentsPerItem) {
3506
+ const totalTexels = this.textureSize * this.textureSize;
3507
+ const data = new Float32Array(totalTexels * 4);
3508
+ const itemCount = array.length / componentsPerItem;
3509
+ for (let i = 0; i < itemCount; i++) {
3510
+ const texIndex = i * 4;
3511
+ const arrayIndex = i * componentsPerItem;
3512
+ data[texIndex] = array[arrayIndex] || 0;
3513
+ data[texIndex + 1] = array[arrayIndex + 1] || 0;
3514
+ data[texIndex + 2] = array[arrayIndex + 2] || 0;
3515
+ data[texIndex + 3] = array[arrayIndex + 3] || 0;
3516
+ }
3517
+ return data;
3518
+ }
3519
+ createDataTexture(data) {
3520
+ const expectedSize = this.textureSize * this.textureSize * 4;
3521
+ if (data.length !== expectedSize) {
3522
+ console.error(`[SimulationBuffers] DataTexture size mismatch!`);
3523
+ console.error(` Expected: ${expectedSize} (${this.textureSize}x${this.textureSize}x4)`);
3524
+ console.error(` Got: ${data.length}`);
3525
+ console.error(` Stack trace:`, (/* @__PURE__ */ new Error()).stack);
3526
+ const correctData = new Float32Array(expectedSize);
3527
+ correctData.set(data.slice(0, Math.min(data.length, expectedSize)));
3528
+ data = correctData;
3529
+ }
3530
+ const texture = new THREE.DataTexture(data, this.textureSize, this.textureSize, THREE.RGBAFormat, THREE.FloatType);
3531
+ texture.needsUpdate = true;
3532
+ texture.minFilter = THREE.NearestFilter;
3533
+ texture.magFilter = THREE.NearestFilter;
3534
+ texture.generateMipmaps = false;
3535
+ return texture;
3536
+ }
3537
+ copyTextureToBuffer(source, target) {
3538
+ const copyMaterial = new THREE.ShaderMaterial({
3539
+ uniforms: { tSource: { value: source } },
3540
+ vertexShader: `
3541
+ varying vec2 vUv;
3542
+ void main() {
3543
+ vUv = uv;
3544
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
3545
+ }
3546
+ `,
3547
+ fragmentShader: `
3548
+ uniform sampler2D tSource;
3549
+ varying vec2 vUv;
3550
+ void main() {
3551
+ gl_FragColor = texture2D(tSource, vUv);
3552
+ }
3553
+ `
3554
+ });
3555
+ const quad = new THREE.PlaneGeometry(2, 2);
3556
+ const mesh = new THREE.Mesh(quad, copyMaterial);
3557
+ const scene = new THREE.Scene();
3558
+ scene.add(mesh);
3559
+ const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
3560
+ this.renderer.setRenderTarget(target);
3561
+ this.renderer.render(scene, camera);
3562
+ this.renderer.setRenderTarget(null);
3563
+ copyMaterial.dispose();
3564
+ quad.dispose();
3565
+ }
3566
+ copyBufferToBuffer(source, target) {
3567
+ this.copyTextureToBuffer(source.texture, target);
3568
+ }
3569
+ dispose() {
3570
+ this.positionBuffers.current?.dispose();
3571
+ this.positionBuffers.previous?.dispose();
3572
+ this.positionBuffers.original?.dispose();
3573
+ this.velocityBuffers.current?.dispose();
3574
+ this.velocityBuffers.previous?.dispose();
3575
+ this.positionBuffers = {
3576
+ current: null,
3577
+ previous: null,
3578
+ original: null
3579
+ };
3580
+ this.velocityBuffers = {
3581
+ current: null,
3582
+ previous: null
3583
+ };
3584
+ this.textureSize = 0;
3585
+ this.isInitialized = false;
3586
+ }
3587
+ };
3588
+
3589
+ //#endregion
3590
+ //#region core/Clock.ts
3591
+ /**
3592
+ * Clock - Unified timing for the force graph package
3593
+ */
3594
+ var Clock = class {
3595
+ previousTime = 0;
3596
+ elapsedTime = 0;
3597
+ deltaTime = 0;
3598
+ isRunning = false;
3599
+ maxDeltaTime = .1;
3600
+ constructor() {}
3601
+ /**
3602
+ * Start the clock
3603
+ */
3604
+ start() {
3605
+ if (this.isRunning) return;
3606
+ this.previousTime = performance.now() / 1e3;
3607
+ this.isRunning = true;
3608
+ }
3609
+ /**
3610
+ * Stop the clock
3611
+ */
3612
+ stop() {
3613
+ this.isRunning = false;
3614
+ }
3615
+ /**
3616
+ * Update the clock - call once per frame
3617
+ * @returns delta time in seconds
3618
+ */
3619
+ update() {
3620
+ if (!this.isRunning) this.start();
3621
+ const now = performance.now() / 1e3;
3622
+ let rawDelta = now - this.previousTime;
3623
+ this.previousTime = now;
3624
+ this.deltaTime = Math.min(rawDelta, this.maxDeltaTime);
3625
+ this.elapsedTime += this.deltaTime;
3626
+ return this.deltaTime;
3627
+ }
3628
+ /**
3629
+ * Get delta time in seconds
3630
+ */
3631
+ getDeltaTime() {
3632
+ return this.deltaTime;
3633
+ }
3634
+ /**
3635
+ * Get total elapsed time in seconds
3636
+ */
3637
+ getElapsedTime() {
3638
+ return this.elapsedTime;
3639
+ }
3640
+ /**
3641
+ * Check if clock is running
3642
+ */
3643
+ getIsRunning() {
3644
+ return this.isRunning;
3645
+ }
3646
+ };
3647
+
3648
+ //#endregion
3649
+ //#region core/GraphStore.ts
3650
+ /**
3651
+ * GraphStore - Central data management
3652
+ * Responsibilities:
3653
+ * - Store raw graph data (nodes, links)
3654
+ * - Node/Link CRUD operations
3655
+ * - ID mapping and lookups
3656
+ * - Data validation
3657
+ * - Change notifications
3658
+ */
3659
+ var GraphStore = class {
3660
+ nodes = /* @__PURE__ */ new Map();
3661
+ links = /* @__PURE__ */ new Map();
3662
+ nodeArray = [];
3663
+ linkArray = [];
3664
+ nodeIdToIndex = /* @__PURE__ */ new Map();
3665
+ linkIdToIndex = /* @__PURE__ */ new Map();
3666
+ nodeToLinks = /* @__PURE__ */ new Map();
3667
+ constructor() {}
3668
+ /**
3669
+ * Set graph data (replaces all)
3670
+ */
3671
+ setData(data) {
3672
+ this.clear();
3673
+ data.nodes.forEach((node, index) => {
3674
+ this.nodes.set(node.id, node);
3675
+ this.nodeIdToIndex.set(node.id, index);
3676
+ this.nodeArray.push(node);
3677
+ });
3678
+ data.links.forEach((link, index) => {
3679
+ const linkId = this.getLinkId(link);
3680
+ this.links.set(linkId, link);
3681
+ this.linkIdToIndex.set(linkId, index);
3682
+ this.linkArray.push(link);
3683
+ this.addLinkConnectivity(link.source, linkId);
3684
+ this.addLinkConnectivity(link.target, linkId);
3685
+ });
3686
+ }
3687
+ /**
3688
+ * Add nodes
3689
+ */
3690
+ addNodes(nodes) {
3691
+ nodes.forEach((node) => {
3692
+ if (!this.nodes.has(node.id)) {
3693
+ const index = this.nodeArray.length;
3694
+ this.nodes.set(node.id, node);
3695
+ this.nodeIdToIndex.set(node.id, index);
3696
+ this.nodeArray.push(node);
3697
+ }
3698
+ });
3699
+ }
3700
+ /**
3701
+ * Remove nodes (and connected links)
3702
+ */
3703
+ removeNodes(nodeIds) {
3704
+ nodeIds.forEach((id) => {
3705
+ const connectedLinks = this.nodeToLinks.get(id);
3706
+ if (connectedLinks) this.removeLinks(Array.from(connectedLinks));
3707
+ this.nodes.delete(id);
3708
+ this.nodeIdToIndex.delete(id);
3709
+ this.nodeToLinks.delete(id);
3710
+ });
3711
+ this.rebuildNodeArrays();
3712
+ }
3713
+ /**
3714
+ * Add links
3715
+ */
3716
+ addLinks(links) {
3717
+ links.forEach((link) => {
3718
+ const linkId = this.getLinkId(link);
3719
+ if (!this.links.has(linkId)) {
3720
+ const index = this.linkArray.length;
3721
+ this.links.set(linkId, link);
3722
+ this.linkIdToIndex.set(linkId, index);
3723
+ this.linkArray.push(link);
3724
+ this.addLinkConnectivity(link.source, linkId);
3725
+ this.addLinkConnectivity(link.target, linkId);
3726
+ }
3727
+ });
3728
+ }
3729
+ /**
3730
+ * Remove links
3731
+ */
3732
+ removeLinks(linkIds) {
3733
+ linkIds.forEach((id) => {
3734
+ const link = this.links.get(id);
3735
+ if (link) {
3736
+ this.removeLinkConnectivity(link.source, id);
3737
+ this.removeLinkConnectivity(link.target, id);
3738
+ this.links.delete(id);
3739
+ this.linkIdToIndex.delete(id);
3740
+ }
3741
+ });
3742
+ this.rebuildLinkArrays();
3743
+ }
3744
+ /**
3745
+ * Get all nodes as array
3746
+ */
3747
+ getNodes() {
3748
+ return this.nodeArray;
3749
+ }
3750
+ /**
3751
+ * Get all links as array
3752
+ */
3753
+ getLinks() {
3754
+ return this.linkArray;
3755
+ }
3756
+ /**
3757
+ * Get connected node IDs for a given node
3758
+ */
3759
+ getConnectedNodeIds(nodeId) {
3760
+ const connectedLinks = this.nodeToLinks.get(nodeId);
3761
+ if (!connectedLinks) return [];
3762
+ const connectedNodes = /* @__PURE__ */ new Set();
3763
+ connectedLinks.forEach((linkId) => {
3764
+ const link = this.links.get(linkId);
3765
+ if (link) {
3766
+ connectedNodes.add(link.source);
3767
+ connectedNodes.add(link.target);
3768
+ }
3769
+ });
3770
+ connectedNodes.delete(nodeId);
3771
+ return Array.from(connectedNodes);
3772
+ }
3773
+ /**
3774
+ * Get node by ID
3775
+ */
3776
+ getNodeById(id) {
3777
+ return this.nodes.get(id) ?? null;
3778
+ }
3779
+ /**
3780
+ * Get node by index
3781
+ */
3782
+ getNodeByIndex(index) {
3783
+ return this.nodeArray[index] ?? null;
3784
+ }
3785
+ /**
3786
+ * Get node index
3787
+ */
3788
+ getNodeIndex(id) {
3789
+ return this.nodeIdToIndex.get(id) ?? -1;
3790
+ }
3791
+ /**
3792
+ * Get link by ID
3793
+ */
3794
+ getLinkById(id) {
3795
+ return this.links.get(id) ?? null;
3796
+ }
3797
+ /**
3798
+ * Get links connected to node
3799
+ */
3800
+ getNodeLinks(nodeId) {
3801
+ const linkIds = this.nodeToLinks.get(nodeId);
3802
+ if (!linkIds) return [];
3803
+ return Array.from(linkIds).map((id) => this.links.get(id)).filter((link) => link !== void 0);
3804
+ }
3805
+ /**
3806
+ * Get node count
3807
+ */
3808
+ getNodeCount() {
3809
+ return this.nodeArray.length;
3810
+ }
3811
+ /**
3812
+ * Get link count
3813
+ */
3814
+ getLinkCount() {
3815
+ return this.linkArray.length;
3816
+ }
3817
+ /**
3818
+ * Clear all data
3819
+ */
3820
+ clear() {
3821
+ this.nodes.clear();
3822
+ this.links.clear();
3823
+ this.nodeArray = [];
3824
+ this.linkArray = [];
3825
+ this.nodeIdToIndex.clear();
3826
+ this.linkIdToIndex.clear();
3827
+ this.nodeToLinks.clear();
3828
+ }
3829
+ getLinkId(link) {
3830
+ return `${link.source}-${link.target}`;
3831
+ }
3832
+ addLinkConnectivity(nodeId, linkId) {
3833
+ if (!this.nodeToLinks.has(nodeId)) this.nodeToLinks.set(nodeId, /* @__PURE__ */ new Set());
3834
+ this.nodeToLinks.get(nodeId).add(linkId);
3835
+ }
3836
+ removeLinkConnectivity(nodeId, linkId) {
3837
+ const links = this.nodeToLinks.get(nodeId);
3838
+ if (links) {
3839
+ links.delete(linkId);
3840
+ if (links.size === 0) this.nodeToLinks.delete(nodeId);
3841
+ }
3842
+ }
3843
+ rebuildNodeArrays() {
3844
+ this.nodeArray = Array.from(this.nodes.values());
3845
+ this.nodeIdToIndex.clear();
3846
+ this.nodeArray.forEach((node, index) => {
3847
+ this.nodeIdToIndex.set(node.id, index);
3848
+ });
3849
+ }
3850
+ rebuildLinkArrays() {
3851
+ this.linkArray = Array.from(this.links.values());
3852
+ this.linkIdToIndex.clear();
3853
+ this.linkArray.forEach((link, index) => {
3854
+ const linkId = this.getLinkId(link);
3855
+ this.linkIdToIndex.set(linkId, index);
3856
+ });
3857
+ }
3858
+ };
3859
+
3860
+ //#endregion
3861
+ //#region core/Engine.ts
3862
+ CameraControls.install({ THREE });
3863
+ /**
3864
+ * Engine - Main orchestrator
3865
+ * Responsibilities:
3866
+ * - Owns all shared buffers (StaticAssets, SimulationBuffers, PickBuffer)
3867
+ * - Coordinates GraphStore, GraphScene, ForceSimulation
3868
+ * - Manages render loop
3869
+ * - Handles user interaction
3870
+ */
3871
+ var Engine = class {
3872
+ graphStore;
3873
+ graphScene;
3874
+ forceSimulation;
3875
+ interactionManager;
3876
+ clock;
3877
+ simulationBuffers;
3878
+ pickBuffer;
3879
+ animationFrameId = null;
3880
+ isRunning = false;
3881
+ boundResizeHandler = null;
3882
+ groupOrder;
3883
+ constructor(canvas, options = {}) {
3884
+ this.canvas = canvas;
3885
+ this.groupOrder = options.groupOrder;
3886
+ const width = options.width ?? window.innerWidth;
3887
+ const height = options.height ?? window.innerHeight;
3888
+ this.clock = new Clock();
3889
+ this.graphStore = new GraphStore();
3890
+ this.graphScene = new GraphScene(canvas, options.cameraConfig);
3891
+ this.graphScene.resize(width, height);
3892
+ this.simulationBuffers = new SimulationBuffers(this.graphScene.renderer);
3893
+ this.pickBuffer = new PickBuffer(this.graphScene.renderer);
3894
+ this.pickBuffer.resize(width, height);
3895
+ this.forceSimulation = new ForceSimulation(this.graphScene.renderer);
3896
+ this.interactionManager = new InteractionManager((x, y) => this.graphScene.pick(x, y), this.graphScene.renderer.domElement, {
3897
+ width,
3898
+ height
3899
+ }, (index) => this.graphStore.getNodeByIndex(index), this.graphScene.camera, this.forceSimulation, options.tooltipConfig, this.graphScene, (nodeId) => this.graphStore.getConnectedNodeIds(nodeId));
3900
+ this.boundResizeHandler = this.handleWindowResize.bind(this);
3901
+ window.addEventListener("resize", this.boundResizeHandler);
3902
+ }
3903
+ /**
3904
+ * Handle window resize event
3905
+ */
3906
+ handleWindowResize() {
3907
+ const width = window.innerWidth;
3908
+ const height = window.innerHeight;
3909
+ this.resize(width, height);
3910
+ }
3911
+ /**
3912
+ * Set graph data
3913
+ */
3914
+ setData(data) {
3915
+ this.graphStore.setData(data);
3916
+ const nodes = this.graphStore.getNodes();
3917
+ const links = this.graphStore.getLinks();
3918
+ const startPositions = new Float32Array(nodes.length * 4);
3919
+ const originalPositions = new Float32Array(nodes.length * 4);
3920
+ nodes.forEach((node, i) => {
3921
+ let state = node.state ?? 3;
3922
+ startPositions[i * 4] = node.x ?? node.fx ?? 0;
3923
+ startPositions[i * 4 + 1] = node.y ?? node.fy ?? 0;
3924
+ startPositions[i * 4 + 2] = node.z ?? node.fz ?? 0;
3925
+ startPositions[i * 4 + 3] = state;
3926
+ originalPositions[i * 4] = node.fx ?? node.x ?? 0;
3927
+ originalPositions[i * 4 + 1] = node.fy ?? node.y ?? 0;
3928
+ originalPositions[i * 4 + 2] = node.fz ?? node.z ?? 0;
3929
+ originalPositions[i * 4 + 3] = state;
3930
+ });
3931
+ this.simulationBuffers.init(nodes.length, startPositions);
3932
+ this.simulationBuffers.setOriginalPositions(originalPositions);
3933
+ this.graphScene.create(nodes, links, this.simulationBuffers, this.pickBuffer, this.groupOrder);
3934
+ this.forceSimulation.initialize(this.simulationBuffers);
3935
+ }
3936
+ /**
3937
+ * Update node states based on a callback
3938
+ * This allows changing visibility/behavior without resetting the simulation
3939
+ */
3940
+ updateNodeStates(callback) {
3941
+ if (!this.simulationBuffers.isReady()) return;
3942
+ const nodes = this.graphStore.getNodes();
3943
+ const currentPositions = this.simulationBuffers.readPositions();
3944
+ const newCurrentPositions = new Float32Array(nodes.length * 4);
3945
+ const newOriginalPositions = new Float32Array(nodes.length * 4);
3946
+ nodes.forEach((node, i) => {
3947
+ const newState = callback(node);
3948
+ node.state = newState;
3949
+ newCurrentPositions[i * 4] = currentPositions[i * 4];
3950
+ newCurrentPositions[i * 4 + 1] = currentPositions[i * 4 + 1];
3951
+ newCurrentPositions[i * 4 + 2] = currentPositions[i * 4 + 2];
3952
+ newCurrentPositions[i * 4 + 3] = newState;
3953
+ newOriginalPositions[i * 4] = node.fx ?? node.x ?? 0;
3954
+ newOriginalPositions[i * 4 + 1] = node.fy ?? node.y ?? 0;
3955
+ newOriginalPositions[i * 4 + 2] = node.fz ?? node.z ?? 0;
3956
+ newOriginalPositions[i * 4 + 3] = newState;
3957
+ });
3958
+ this.simulationBuffers.updateCurrentPositions(newCurrentPositions);
3959
+ this.simulationBuffers.setOriginalPositions(newOriginalPositions);
3960
+ }
3961
+ /**
3962
+ * Reheat the simulation
3963
+ */
3964
+ reheat(alpha = 1) {
3965
+ this.forceSimulation.reheat(alpha);
3966
+ }
3967
+ /**
3968
+ * Set alpha decay
3969
+ */
3970
+ setAlphaDecay(decay) {
3971
+ this.forceSimulation.config.alphaDecay = decay;
3972
+ }
3973
+ /**
3974
+ * Get current alpha
3975
+ */
3976
+ getAlpha() {
3977
+ return this.forceSimulation.getAlpha();
3978
+ }
3979
+ /**Apply a preset to the graph
3980
+ */
3981
+ applyPreset(preset) {
3982
+ if (preset.force) {
3983
+ Object.assign(this.forceSimulation.config, preset.force);
3984
+ this.forceSimulation.reheat();
3985
+ }
3986
+ if (preset.camera) {
3987
+ const { position, target, zoom, transitionDuration, mode, minDistance, maxDistance, boundary } = preset.camera;
3988
+ if (mode) this.graphScene.camera.setMode(mode);
3989
+ if (transitionDuration !== void 0) this.graphScene.camera.configureControls({ smoothTime: transitionDuration });
3990
+ if (minDistance !== void 0 || maxDistance !== void 0) this.graphScene.camera.configureControls({
3991
+ minDistance,
3992
+ maxDistance
3993
+ });
3994
+ if (boundary) {
3995
+ const box = new THREE.Box3(new THREE.Vector3(boundary.min.x, boundary.min.y, boundary.min.z), new THREE.Vector3(boundary.max.x, boundary.max.y, boundary.max.z));
3996
+ this.graphScene.camera.setBoundary(box);
3997
+ } else if (boundary === null) this.graphScene.camera.setBoundary(null);
3998
+ if (position && target) {
3999
+ const posVec = new THREE.Vector3(position.x, position.y, position.z);
4000
+ const targetVec = new THREE.Vector3(target.x, target.y, target.z);
4001
+ this.graphScene.camera.setLookAt(posVec, targetVec, true);
4002
+ } else {
4003
+ if (position) this.graphScene.camera.controls.setPosition(position.x, position.y, position.z, true);
4004
+ if (target) this.graphScene.camera.controls.setTarget(target.x, target.y, target.z, true);
4005
+ }
4006
+ if (zoom !== void 0) this.graphScene.camera.controls.zoomTo(zoom, true);
4007
+ }
4008
+ if (preset.nodes) {
4009
+ if (preset.nodes.state) {
4010
+ this.updateNodeStates(preset.nodes.state);
4011
+ this.forceSimulation.reheat();
4012
+ }
4013
+ }
4014
+ if (preset.links) {
4015
+ const linkRenderer = this.graphScene.getLinkRenderer();
4016
+ if (linkRenderer) linkRenderer.setOptions({ alpha: { default: preset.links.opacity } });
4017
+ }
4018
+ }
4019
+ /**
4020
+ *
4021
+ * Start render loop
4022
+ */
4023
+ start() {
4024
+ if (this.isRunning) return;
4025
+ this.isRunning = true;
4026
+ this.clock.start();
4027
+ this.animate();
4028
+ }
4029
+ /**
4030
+ * Stop render loop
4031
+ */
4032
+ stop() {
4033
+ this.isRunning = false;
4034
+ this.clock.stop();
4035
+ if (this.animationFrameId !== null) {
4036
+ cancelAnimationFrame(this.animationFrameId);
4037
+ this.animationFrameId = null;
4038
+ }
4039
+ }
4040
+ /**
4041
+ * Render loop
4042
+ */
4043
+ animate = () => {
4044
+ if (!this.isRunning) return;
4045
+ const deltaTime = this.clock.update();
4046
+ const elapsedTime = this.clock.getElapsedTime();
4047
+ this.forceSimulation.step(deltaTime);
4048
+ const positionTexture = this.simulationBuffers.getCurrentPositionTexture();
4049
+ this.graphScene.updatePositions(positionTexture);
4050
+ this.graphScene.update(elapsedTime);
4051
+ this.interactionManager.getPointerInput().update();
4052
+ this.graphScene.render();
4053
+ this.animationFrameId = requestAnimationFrame(this.animate);
4054
+ };
4055
+ /**
4056
+ * GPU pick node at canvas coordinates
4057
+ */
4058
+ pickNode(x, y) {
4059
+ const nodeRenderer = this.graphScene.getNodeRenderer();
4060
+ if (!nodeRenderer) return null;
4061
+ const camera = this.graphScene.getCamera();
4062
+ const nodeIndex = nodeRenderer.pick(x, y, camera.camera);
4063
+ if (nodeIndex < 0) return null;
4064
+ return this.graphStore.getNodeByIndex(nodeIndex)?.id ?? null;
4065
+ }
4066
+ /**
4067
+ * Get node by ID
4068
+ */
4069
+ getNodeById(id) {
4070
+ return this.graphStore.getNodeById(id);
4071
+ }
4072
+ /**
4073
+ * Highlight nodes
4074
+ */
4075
+ highlightNodes(nodeIds) {
4076
+ this.interactionManager.searchHighlightIds = nodeIds;
4077
+ this.graphScene.getNodeRenderer()?.highlight(nodeIds);
4078
+ }
4079
+ /**
4080
+ * Clear highlights
4081
+ */
4082
+ clearHighlights() {
4083
+ this.interactionManager.searchHighlightIds = [];
4084
+ if (this.interactionManager.isTooltipSticky && this.interactionManager.stickyNodeId) {
4085
+ const node = this.graphStore.getNodeById(this.interactionManager.stickyNodeId);
4086
+ if (node) {
4087
+ const nodeRenderer = this.graphScene.getNodeRenderer();
4088
+ const connectedIds = this.graphStore.getConnectedNodeIds(node.id);
4089
+ connectedIds.push(node.id);
4090
+ nodeRenderer?.highlight(connectedIds);
4091
+ return;
4092
+ }
4093
+ }
4094
+ this.graphScene.getNodeRenderer()?.clearHighlights();
4095
+ }
4096
+ /**
4097
+ * Get node position in 3D space
4098
+ */
4099
+ getNodePosition(nodeId) {
4100
+ if (!this.graphStore.getNodeById(nodeId)) return null;
4101
+ const index = this.graphStore.getNodes().findIndex((n) => n.id === nodeId);
4102
+ if (index === -1) return null;
4103
+ const positions = this.simulationBuffers.readPositions();
4104
+ return new THREE.Vector3(positions[index * 4], positions[index * 4 + 1], positions[index * 4 + 2]);
4105
+ }
4106
+ /**
4107
+ * Programmatically select a node (highlight + tooltip)
4108
+ */
4109
+ selectNode(nodeId) {
4110
+ const node = this.graphStore.getNodeById(nodeId);
4111
+ if (!node) return;
4112
+ this.interactionManager.isTooltipSticky = true;
4113
+ this.interactionManager.stickyNodeId = nodeId;
4114
+ this.graphScene.getLinkRenderer()?.highlightConnectedLinks(node.id);
4115
+ const nodeRenderer = this.graphScene.getNodeRenderer();
4116
+ const connectedIds = this.graphStore.getConnectedNodeIds(node.id);
4117
+ connectedIds.push(node.id);
4118
+ nodeRenderer?.highlight(connectedIds);
4119
+ const pos = this.getNodePosition(nodeId);
4120
+ if (pos) {
4121
+ const screenPos = pos.clone().project(this.graphScene.camera.camera);
4122
+ const size = new THREE.Vector2();
4123
+ this.graphScene.renderer.getSize(size);
4124
+ const rect = this.graphScene.renderer.domElement.getBoundingClientRect();
4125
+ const x = (screenPos.x + 1) / 2 * size.x + rect.left;
4126
+ const y = -(screenPos.y - 1) / 2 * size.y + rect.top;
4127
+ this.interactionManager.tooltipManager.showFull(node, x, y);
4128
+ }
4129
+ }
4130
+ /**
4131
+ * Resize canvas and all dependent components
4132
+ */
4133
+ resize(width, height) {
4134
+ this.graphScene.resize(width, height);
4135
+ this.pickBuffer.resize(width, height);
4136
+ this.interactionManager.resize(width, height);
4137
+ }
4138
+ /**
4139
+ * Get the unified clock for timing information
4140
+ */
4141
+ getClock() {
4142
+ return this.clock;
4143
+ }
4144
+ /**
4145
+ * Get all nodes
4146
+ */
4147
+ get nodes() {
4148
+ return this.graphStore.getNodes();
4149
+ }
4150
+ /**
4151
+ * Get all links
4152
+ */
4153
+ get links() {
4154
+ return this.graphStore.getLinks();
4155
+ }
4156
+ /**
4157
+ * Get force simulation for external configuration (GUI binding)
4158
+ */
4159
+ get simulation() {
4160
+ return this.forceSimulation;
4161
+ }
4162
+ /**
4163
+ * Get camera controller for external configuration
4164
+ */
4165
+ get camera() {
4166
+ return this.graphScene.camera;
4167
+ }
4168
+ /**
4169
+ * Set camera mode (Orbit, Map, Fly)
4170
+ */
4171
+ setCameraMode(mode) {
4172
+ this.graphScene.camera.setMode(mode);
4173
+ }
4174
+ /**
4175
+ * Animate camera to look at a specific target from a specific position
4176
+ */
4177
+ async setCameraLookAt(position, target, transitionDuration) {
4178
+ if (transitionDuration !== void 0) this.graphScene.camera.configureControls({ smoothTime: transitionDuration });
4179
+ const posVec = new THREE.Vector3(position.x, position.y, position.z);
4180
+ const targetVec = new THREE.Vector3(target.x, target.y, target.z);
4181
+ await this.graphScene.camera.setLookAt(posVec, targetVec, true);
4182
+ }
4183
+ /**
4184
+ * Focus camera on a specific target node
4185
+ */
4186
+ async setCameraFocus(target, distance, transitionDuration) {
4187
+ const camPos = this.graphScene.camera.camera.position;
4188
+ const targetVec = new THREE.Vector3(target.x, target.y, target.z);
4189
+ const direction = new THREE.Vector3().subVectors(camPos, targetVec).normalize();
4190
+ if (direction.lengthSq() < .001) direction.set(0, 0, 1);
4191
+ const newPos = targetVec.clone().add(direction.multiplyScalar(distance));
4192
+ await this.setCameraLookAt(newPos, targetVec, transitionDuration);
4193
+ }
4194
+ /**
4195
+ * Cleanup
4196
+ */
4197
+ dispose() {
4198
+ this.stop();
4199
+ this.graphScene.dispose();
4200
+ this.interactionManager.dispose();
4201
+ staticAssets.dispose();
4202
+ this.simulationBuffers.dispose();
4203
+ this.pickBuffer.dispose();
4204
+ this.graphStore.clear();
4205
+ }
4206
+ calculateTextureSize(nodeCount) {
4207
+ const size = Math.ceil(Math.sqrt(nodeCount));
4208
+ return 2 ** Math.ceil(Math.log2(size));
4209
+ }
4210
+ };
4211
+
4212
+ //#endregion
4213
+ export { CameraMode, Clock, Engine, ForceSimulation, GraphScene, GraphStore, NodeState, PickBuffer, SimulationBuffers, StaticAssets, staticAssets, styleRegistry };