@fusefactory/fuse-three-forcegraph 1.0.5 → 1.0.6

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.
Files changed (4) hide show
  1. package/README.md +61 -43
  2. package/dist/index.d.mts +869 -860
  3. package/dist/index.mjs +725 -649
  4. package/package.json +12 -12
package/dist/index.mjs CHANGED
@@ -1,434 +1,80 @@
1
1
  import CameraControls from "camera-controls";
2
2
  import * as THREE from "three";
3
3
  import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
4
- import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
5
4
  import { OutputPass } from "three/examples/jsm/postprocessing/OutputPass.js";
6
- import { SMAAPass } from "three/examples/jsm/postprocessing/SMAAPass.js";
5
+ import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
7
6
  import { ShaderPass } from "three/examples/jsm/postprocessing/ShaderPass.js";
7
+ import { SMAAPass } from "three/examples/jsm/postprocessing/SMAAPass.js";
8
8
  import { FXAAShader } from "three/examples/jsm/shaders/FXAAShader.js";
9
9
 
10
- //#region core/EventEmitter.ts
11
- var EventEmitter = class {
12
- constructor() {
13
- this.listeners = {};
14
- }
15
- listenerCount(event) {
16
- return this.listeners[event] ? this.listeners[event].length : 0;
17
- }
18
- on(event, listener) {
19
- if (!this.listeners[event]) this.listeners[event] = [];
20
- this.listeners[event].push(listener);
21
- return () => this.off(event, listener);
22
- }
23
- off(event, listener) {
24
- if (!this.listeners[event]) return;
25
- const index = this.listeners[event].indexOf(listener);
26
- if (index === -1) return;
27
- this.listeners[event].splice(index, 1);
28
- }
29
- emit(event, ...args) {
30
- if (!this.listeners[event]) return;
31
- this.listeners[event].forEach((listener) => listener(...args));
32
- }
33
- };
34
-
35
- //#endregion
36
- //#region controls/InputProcessor.ts
10
+ //#region core/Clock.ts
37
11
  /**
38
- * Manages pointer/mouse input and emits interaction events
39
- * Uses GPU picking for efficient node detection
12
+ * Clock - Unified timing for the force graph package
40
13
  */
41
- var InputProcessor = class extends EventEmitter {
42
- constructor(pickFn, canvas, viewport) {
43
- super();
44
- this.pickFn = pickFn;
45
- this.canvas = canvas;
46
- this.viewport = viewport;
47
- this.CLICK_THRESHOLD = 200;
48
- this.HOVER_TO_POP_MS = 800;
49
- this.pointer = new THREE.Vector2();
50
- this.canvasPointer = new THREE.Vector2();
51
- this.isPointerDown = false;
52
- this.isDragging = false;
53
- this.mouseDownTime = 0;
54
- this.draggedIndex = -1;
55
- this.currentHoverIndex = -1;
56
- this.hoverStartTime = 0;
57
- this.hoverProgress = 0;
58
- this.hasPopped = false;
59
- this.isTouch = false;
60
- this.TOUCH_MOVE_THRESHOLD = 10;
61
- this.pointerDownX = 0;
62
- this.pointerDownY = 0;
63
- this.lastClientX = null;
64
- this.lastClientY = null;
65
- this.handlePointerMove = (event) => {
66
- this.updatePointer(event);
67
- this.lastClientX = event.clientX;
68
- this.lastClientY = event.clientY;
69
- const pickedIndex = this.pickFn(this.canvasPointer.x, this.canvasPointer.y);
70
- if (!this.isTouch) this.updateHover(pickedIndex);
71
- if (this.isTouch && !this.isDragging) {
72
- const dx = event.clientX - this.pointerDownX;
73
- const dy = event.clientY - this.pointerDownY;
74
- if (dx * dx + dy * dy < this.TOUCH_MOVE_THRESHOLD * this.TOUCH_MOVE_THRESHOLD) return;
75
- }
76
- if (this.isPointerDown && this.draggedIndex >= 0) {
77
- if (!this.isDragging) {
78
- this.isDragging = true;
79
- this.emit("pointer:dragstart", {
80
- index: this.draggedIndex,
81
- x: this.canvasPointer.x,
82
- y: this.canvasPointer.y,
83
- clientX: event.clientX,
84
- clientY: event.clientY
85
- });
86
- }
87
- this.emit("pointer:drag", {
88
- index: this.draggedIndex,
89
- x: this.canvasPointer.x,
90
- y: this.canvasPointer.y,
91
- clientX: event.clientX,
92
- clientY: event.clientY
93
- });
94
- }
95
- };
96
- this.handlePointerDown = (event) => {
97
- this.updatePointer(event);
98
- this.mouseDownTime = Date.now();
99
- this.isPointerDown = true;
100
- this.isTouch = event.pointerType === "touch";
101
- this.pointerDownX = event.clientX;
102
- this.pointerDownY = event.clientY;
103
- const pickedIndex = this.pickFn(this.canvasPointer.x, this.canvasPointer.y);
104
- this.draggedIndex = pickedIndex;
105
- if (pickedIndex >= 0) this.emit("pointer:down", {
106
- index: pickedIndex,
107
- x: this.canvasPointer.x,
108
- y: this.canvasPointer.y,
109
- clientX: event.clientX,
110
- clientY: event.clientY
111
- });
112
- };
113
- this.handlePointerUp = (event) => {
114
- const clickDuration = Date.now() - this.mouseDownTime;
115
- const wasClick = this.isTouch ? !this.isDragging : clickDuration < this.CLICK_THRESHOLD;
116
- if (this.isDragging) this.emit("pointer:dragend", {
117
- index: this.draggedIndex,
118
- x: this.canvasPointer.x,
119
- y: this.canvasPointer.y,
120
- clientX: event.clientX,
121
- clientY: event.clientY
122
- });
123
- else if (wasClick && this.draggedIndex >= 0) this.emit("pointer:click", {
124
- index: this.draggedIndex,
125
- x: this.canvasPointer.x,
126
- y: this.canvasPointer.y,
127
- clientX: event.clientX,
128
- clientY: event.clientY
129
- });
130
- else if (wasClick && this.draggedIndex < 0) this.emit("pointer:clickempty", {
131
- x: this.canvasPointer.x,
132
- y: this.canvasPointer.y,
133
- clientX: event.clientX,
134
- clientY: event.clientY
135
- });
136
- this.isPointerDown = false;
137
- this.isDragging = false;
138
- this.draggedIndex = -1;
139
- };
140
- this.handlePointerLeave = () => {
141
- this.lastClientX = null;
142
- this.lastClientY = null;
143
- this.updateHover(-1);
144
- };
145
- this.setupEventListeners();
146
- }
147
- setupEventListeners() {
148
- this.canvas.addEventListener("pointermove", this.handlePointerMove);
149
- this.canvas.addEventListener("pointerdown", this.handlePointerDown);
150
- this.canvas.addEventListener("pointerup", this.handlePointerUp);
151
- this.canvas.addEventListener("pointerleave", this.handlePointerLeave);
152
- }
153
- updateHover(index) {
154
- const prevIndex = this.currentHoverIndex;
155
- if (index === prevIndex) {
156
- if (index >= 0) {
157
- const elapsed = Date.now() - this.hoverStartTime;
158
- this.hoverProgress = Math.min(1, elapsed / this.HOVER_TO_POP_MS);
159
- this.emit("pointer:hover", {
160
- index,
161
- progress: this.hoverProgress,
162
- x: this.canvasPointer.x,
163
- y: this.canvasPointer.y,
164
- clientX: this.lastClientX,
165
- clientY: this.lastClientY
166
- });
167
- if (this.hoverProgress >= 1 && !this.hasPopped) {
168
- this.hasPopped = true;
169
- this.emit("pointer:pop", {
170
- index,
171
- x: this.canvasPointer.x,
172
- y: this.canvasPointer.y,
173
- clientX: this.lastClientX,
174
- clientY: this.lastClientY
175
- });
176
- }
177
- }
178
- return;
179
- }
180
- if (prevIndex >= 0) this.emit("pointer:hoverend", { index: prevIndex });
181
- if (index >= 0) {
182
- this.currentHoverIndex = index;
183
- this.hoverStartTime = Date.now();
184
- this.hoverProgress = 0;
185
- this.hasPopped = false;
186
- this.emit("pointer:hoverstart", {
187
- index,
188
- x: this.canvasPointer.x,
189
- y: this.canvasPointer.y,
190
- clientX: this.lastClientX,
191
- clientY: this.lastClientY
192
- });
193
- } else {
194
- this.currentHoverIndex = -1;
195
- this.hoverStartTime = 0;
196
- this.hoverProgress = 0;
197
- this.hasPopped = false;
198
- }
199
- }
14
+ var Clock = class {
15
+ previousTime = 0;
16
+ elapsedTime = 0;
17
+ deltaTime = 0;
18
+ isRunning = false;
19
+ maxDeltaTime = .1;
20
+ constructor() {}
200
21
  /**
201
- * Update hover state even when pointer is stationary
202
- * Called from render loop
22
+ * Start the clock
203
23
  */
204
- update() {
205
- if (this.isTouch) return;
206
- if (this.lastClientX !== null && this.lastClientY !== null) {
207
- const rect = this.canvas.getBoundingClientRect();
208
- const x = this.lastClientX - rect.left;
209
- const y = this.lastClientY - rect.top;
210
- const pickedIndex = this.pickFn(x, y);
211
- this.updateHover(pickedIndex);
212
- }
213
- }
214
- updatePointer(event) {
215
- const rect = this.canvas.getBoundingClientRect();
216
- const x = event.clientX - rect.left;
217
- const y = event.clientY - rect.top;
218
- const ndcX = x / rect.width * 2 - 1;
219
- const ndcY = -(y / rect.height) * 2 + 1;
220
- this.pointer.set(ndcX, ndcY);
221
- this.canvasPointer.set(x, y);
24
+ start() {
25
+ if (this.isRunning) return;
26
+ this.previousTime = performance.now() / 1e3;
27
+ this.isRunning = true;
222
28
  }
223
29
  /**
224
- * Update viewport dimensions on resize
30
+ * Stop the clock
225
31
  */
226
- resize(width, height) {
227
- this.viewport.width = width;
228
- this.viewport.height = height;
229
- }
230
- dispose() {
231
- this.canvas.removeEventListener("pointermove", this.handlePointerMove);
232
- this.canvas.removeEventListener("pointerdown", this.handlePointerDown);
233
- this.canvas.removeEventListener("pointerup", this.handlePointerUp);
234
- this.canvas.removeEventListener("pointerleave", this.handlePointerLeave);
235
- }
236
- };
237
-
238
- //#endregion
239
- //#region controls/handlers/HoverHandler.ts
240
- var HoverHandler = class extends EventEmitter {
241
- constructor(getNodeByIndex) {
242
- super();
243
- this.getNodeByIndex = getNodeByIndex;
244
- this.handleHoverStart = (data) => {
245
- const node = this.getNodeByIndex(data.index);
246
- if (!node) return;
247
- this.emit("node:hoverstart", {
248
- node,
249
- x: data.x,
250
- y: data.y,
251
- clientX: data.clientX,
252
- clientY: data.clientY
253
- });
254
- };
255
- this.handleHover = (data) => {
256
- const node = this.getNodeByIndex(data.index);
257
- if (!node) return;
258
- const progress = data.progress ?? 0;
259
- this.emit("node:hover", {
260
- node,
261
- x: data.x,
262
- y: data.y,
263
- progress,
264
- clientX: data.clientX,
265
- clientY: data.clientY
266
- });
267
- };
268
- this.handlePop = (data) => {
269
- const node = this.getNodeByIndex(data.index);
270
- if (!node) return;
271
- this.emit("node:pop", {
272
- node,
273
- x: data.x,
274
- y: data.y,
275
- clientX: data.clientX,
276
- clientY: data.clientY
277
- });
278
- };
279
- this.handleHoverEnd = (data) => {
280
- if (!this.getNodeByIndex(data.index)) return;
281
- this.emit("node:hoverend");
282
- };
283
- this.on("pointer:hoverstart", this.handleHoverStart);
284
- this.on("pointer:hover", this.handleHover);
285
- this.on("pointer:pop", this.handlePop);
286
- this.on("pointer:hoverend", this.handleHoverEnd);
287
- }
288
- dispose() {
289
- this.off("pointer:hoverstart", this.handleHoverStart);
290
- this.off("pointer:hover", this.handleHover);
291
- this.off("pointer:pop", this.handlePop);
292
- this.off("pointer:hoverend", this.handleHoverEnd);
293
- }
294
- };
295
-
296
- //#endregion
297
- //#region controls/handlers/ClickHandler.ts
298
- var ClickHandler = class extends EventEmitter {
299
- constructor(getNodeByIndex) {
300
- super();
301
- this.getNodeByIndex = getNodeByIndex;
302
- this.handleClick = (data) => {
303
- const node = this.getNodeByIndex(data.index);
304
- if (!node) return;
305
- this.emit("node:click", {
306
- node,
307
- x: data.x,
308
- y: data.y,
309
- clientX: data.clientX,
310
- clientY: data.clientY
311
- });
312
- };
313
- this.on("pointer:click", this.handleClick);
314
- }
315
- dispose() {
316
- this.off("pointer:click", this.handleClick);
32
+ stop() {
33
+ this.isRunning = false;
317
34
  }
318
- };
319
-
320
- //#endregion
321
- //#region types/iGraphNode.ts
322
- let NodeState = /* @__PURE__ */ function(NodeState) {
323
- NodeState[NodeState["Hidden"] = 0] = "Hidden";
324
- NodeState[NodeState["Passive"] = 1] = "Passive";
325
- NodeState[NodeState["Fixed"] = 2] = "Fixed";
326
- NodeState[NodeState["Active"] = 3] = "Active";
327
- return NodeState;
328
- }({});
329
-
330
- //#endregion
331
- //#region controls/handlers/DragHandler.ts
332
- var DragHandler = class extends EventEmitter {
333
- constructor(getNodeByIndex, cameraController, forceSimulation, viewport) {
334
- super();
335
- this.getNodeByIndex = getNodeByIndex;
336
- this.cameraController = cameraController;
337
- this.forceSimulation = forceSimulation;
338
- this.viewport = viewport;
339
- this.isDragging = false;
340
- this.raycaster = new THREE.Raycaster();
341
- this.dragPlane = new THREE.Plane();
342
- this.pointer = new THREE.Vector2();
343
- this.handleDragStart = (data) => {
344
- const node = this.getNodeByIndex(data.index);
345
- if (!node) return;
346
- let state = node.state ?? NodeState.Active;
347
- if (node.state === void 0) state = NodeState.Fixed;
348
- if (state !== NodeState.Active || node.metadata?.hidden) return;
349
- this.isDragging = true;
350
- if (this.cameraController) this.cameraController.setEnabled(false);
351
- if (this.cameraController) {
352
- this.cameraController.camera;
353
- const target = this.cameraController.getTarget();
354
- this.setupDragPlane(target);
355
- }
356
- if (this.forceSimulation) {
357
- const worldPos = this.screenToWorld(data.x, data.y);
358
- if (worldPos) this.forceSimulation.startDrag(data.index, worldPos);
359
- }
360
- this.emit("node:dragstart", { node });
361
- };
362
- this.handleDrag = (data) => {
363
- const node = this.getNodeByIndex(data.index);
364
- if (!node) return;
365
- if (this.forceSimulation) {
366
- const worldPos = this.screenToWorld(data.x, data.y);
367
- if (worldPos) this.forceSimulation.updateDrag(worldPos);
368
- }
369
- this.emit("node:drag", {
370
- node,
371
- x: data.x,
372
- y: data.y
373
- });
374
- };
375
- this.handleDragEnd = (data) => {
376
- const node = this.getNodeByIndex(data.index);
377
- if (!node) return;
378
- this.isDragging = false;
379
- if (this.cameraController) this.cameraController.setEnabled(true);
380
- if (this.forceSimulation) this.forceSimulation.endDrag();
381
- this.emit("node:dragend", { node });
382
- };
383
- this.on("pointer:dragstart", this.handleDragStart);
384
- this.on("pointer:drag", this.handleDrag);
385
- this.on("pointer:dragend", this.handleDragEnd);
35
+ /**
36
+ * Update the clock - call once per frame
37
+ * @returns delta time in seconds
38
+ */
39
+ update() {
40
+ if (!this.isRunning) this.start();
41
+ const now = performance.now() / 1e3;
42
+ const rawDelta = now - this.previousTime;
43
+ this.previousTime = now;
44
+ this.deltaTime = Math.min(rawDelta, this.maxDeltaTime);
45
+ this.elapsedTime += this.deltaTime;
46
+ return this.deltaTime;
386
47
  }
387
48
  /**
388
- * Set up drag plane perpendicular to camera, passing through a world point
49
+ * Get delta time in seconds
389
50
  */
390
- setupDragPlane(worldPoint) {
391
- if (!this.cameraController) return;
392
- const camera = this.cameraController.camera;
393
- const cameraDirection = new THREE.Vector3();
394
- camera.getWorldDirection(cameraDirection);
395
- const planeNormal = cameraDirection.clone().negate();
396
- this.dragPlane.setFromNormalAndCoplanarPoint(planeNormal, worldPoint);
51
+ getDeltaTime() {
52
+ return this.deltaTime;
397
53
  }
398
54
  /**
399
- * Convert screen coordinates to world position on drag plane
55
+ * Get total elapsed time in seconds
400
56
  */
401
- screenToWorld(clientX, clientY) {
402
- if (!this.cameraController || !this.viewport) return null;
403
- this.pointer.x = clientX / this.viewport.width * 2 - 1;
404
- this.pointer.y = -(clientY / this.viewport.height) * 2 + 1;
405
- this.raycaster.setFromCamera(this.pointer, this.cameraController.camera);
406
- const target = new THREE.Vector3();
407
- return this.raycaster.ray.intersectPlane(this.dragPlane, target);
57
+ getElapsedTime() {
58
+ return this.elapsedTime;
408
59
  }
409
60
  /**
410
- * Update viewport dimensions on resize
61
+ * Check if clock is running
411
62
  */
412
- resize(width, height) {
413
- if (this.viewport) {
414
- this.viewport.width = width;
415
- this.viewport.height = height;
416
- }
417
- }
418
- dispose() {
419
- this.off("pointer:dragstart", this.handleDragStart);
420
- this.off("pointer:drag", this.handleDrag);
421
- this.off("pointer:dragend", this.handleDragEnd);
63
+ getIsRunning() {
64
+ return this.isRunning;
422
65
  }
423
66
  };
424
67
 
425
68
  //#endregion
426
69
  //#region ui/Tooltips.ts
427
70
  var Tooltip = class {
71
+ element;
72
+ isVisible = false;
73
+ cleanup;
74
+ positionCallback;
428
75
  constructor(type, container) {
429
76
  this.type = type;
430
77
  this.container = container;
431
- this.isVisible = false;
432
78
  this.element = this.createElement();
433
79
  }
434
80
  createElement() {
@@ -493,11 +139,15 @@ var Tooltip = class {
493
139
  //#endregion
494
140
  //#region ui/TooltipManager.ts
495
141
  var TooltipManager = class {
142
+ mainTooltip;
143
+ previewTooltip;
144
+ chainTooltips = /* @__PURE__ */ new Map();
145
+ config;
146
+ closeButton = null;
147
+ onCloseCallback;
496
148
  constructor(container, canvas, config) {
497
149
  this.container = container;
498
150
  this.canvas = canvas;
499
- this.chainTooltips = /* @__PURE__ */ new Map();
500
- this.closeButton = null;
501
151
  this.config = { ...config };
502
152
  this.mainTooltip = new Tooltip("main", container);
503
153
  this.previewTooltip = new Tooltip("preview", container);
@@ -593,82 +243,501 @@ var TooltipManager = class {
593
243
  this.closeButton.addEventListener("mouseenter", () => {
594
244
  if (this.closeButton) this.closeButton.style.background = "rgba(255, 255, 255, 0.3)";
595
245
  });
596
- this.closeButton.addEventListener("mouseleave", () => {
597
- if (this.closeButton) this.closeButton.style.background = "rgba(255, 255, 255, 0.2)";
246
+ this.closeButton.addEventListener("mouseleave", () => {
247
+ if (this.closeButton) this.closeButton.style.background = "rgba(255, 255, 255, 0.2)";
248
+ });
249
+ this.closeButton.addEventListener("click", (e) => {
250
+ e.stopPropagation();
251
+ this.hideFull();
252
+ if (this.onCloseCallback) this.onCloseCallback();
253
+ });
254
+ element.appendChild(this.closeButton);
255
+ }
256
+ /**
257
+ * Remove close button from tooltip
258
+ */
259
+ removeCloseButton() {
260
+ if (this.closeButton) {
261
+ this.closeButton.remove();
262
+ this.closeButton = null;
263
+ }
264
+ const element = this.mainTooltip.getElement();
265
+ element.style.pointerEvents = "none";
266
+ }
267
+ /**
268
+ * Hide all tooltips
269
+ */
270
+ hideAll() {
271
+ this.hideGrabIcon();
272
+ this.hidePreview();
273
+ this.hideFull();
274
+ this.hideChainTooltips();
275
+ }
276
+ showMainTooltip(content, x, y) {
277
+ this.mainTooltip.open(content);
278
+ this.mainTooltip.updatePos(x, y);
279
+ console.log("Showing main tooltip");
280
+ }
281
+ hideMainTooltip() {
282
+ this.mainTooltip.close();
283
+ }
284
+ showChainTooltip(nodeId, content, x, y) {
285
+ if (!this.chainTooltips.has(nodeId)) this.chainTooltips.set(nodeId, new Tooltip("label", this.container));
286
+ const tooltip = this.chainTooltips.get(nodeId);
287
+ tooltip.open(content);
288
+ tooltip.updatePos(x, y);
289
+ }
290
+ hideChainTooltips() {
291
+ this.chainTooltips.forEach((tooltip) => tooltip.close());
292
+ }
293
+ updateMainTooltipPos(x, y) {
294
+ this.mainTooltip.updatePos(x, y);
295
+ }
296
+ /**
297
+ * Set main tooltip visibility (doesn't affect open state or content)
298
+ */
299
+ setMainTooltipVisibility(visible) {
300
+ if (visible) this.mainTooltip.show();
301
+ else this.mainTooltip.hide();
302
+ }
303
+ dispose() {
304
+ this.removeCloseButton();
305
+ this.mainTooltip.destroy();
306
+ this.previewTooltip.destroy();
307
+ this.chainTooltips.forEach((tooltip) => tooltip.destroy());
308
+ this.chainTooltips.clear();
309
+ }
310
+ };
311
+
312
+ //#endregion
313
+ //#region core/EventEmitter.ts
314
+ var EventEmitter = class {
315
+ listeners = {};
316
+ listenerCount(event) {
317
+ return this.listeners[event] ? this.listeners[event].length : 0;
318
+ }
319
+ on(event, listener) {
320
+ if (!this.listeners[event]) this.listeners[event] = [];
321
+ this.listeners[event].push(listener);
322
+ return () => this.off(event, listener);
323
+ }
324
+ off(event, listener) {
325
+ if (!this.listeners[event]) return;
326
+ const index = this.listeners[event].indexOf(listener);
327
+ if (index === -1) return;
328
+ this.listeners[event].splice(index, 1);
329
+ }
330
+ emit(event, ...args) {
331
+ if (!this.listeners[event]) return;
332
+ this.listeners[event].forEach((listener) => listener(...args));
333
+ }
334
+ };
335
+
336
+ //#endregion
337
+ //#region controls/handlers/ClickHandler.ts
338
+ var ClickHandler = class extends EventEmitter {
339
+ constructor(getNodeByIndex) {
340
+ super();
341
+ this.getNodeByIndex = getNodeByIndex;
342
+ this.on("pointer:click", this.handleClick);
343
+ }
344
+ handleClick = (data) => {
345
+ const node = this.getNodeByIndex(data.index);
346
+ if (!node) return;
347
+ this.emit("node:click", {
348
+ node,
349
+ x: data.x,
350
+ y: data.y,
351
+ clientX: data.clientX,
352
+ clientY: data.clientY
353
+ });
354
+ };
355
+ dispose() {
356
+ this.off("pointer:click", this.handleClick);
357
+ }
358
+ };
359
+
360
+ //#endregion
361
+ //#region types/iGraphNode.ts
362
+ let NodeState = /* @__PURE__ */ function(NodeState) {
363
+ NodeState[NodeState["Hidden"] = 0] = "Hidden";
364
+ NodeState[NodeState["Passive"] = 1] = "Passive";
365
+ NodeState[NodeState["Fixed"] = 2] = "Fixed";
366
+ NodeState[NodeState["Active"] = 3] = "Active";
367
+ return NodeState;
368
+ }({});
369
+
370
+ //#endregion
371
+ //#region controls/handlers/DragHandler.ts
372
+ var DragHandler = class extends EventEmitter {
373
+ isDragging = false;
374
+ raycaster = new THREE.Raycaster();
375
+ dragPlane = new THREE.Plane();
376
+ pointer = new THREE.Vector2();
377
+ constructor(getNodeByIndex, cameraController, forceSimulation, viewport) {
378
+ super();
379
+ this.getNodeByIndex = getNodeByIndex;
380
+ this.cameraController = cameraController;
381
+ this.forceSimulation = forceSimulation;
382
+ this.viewport = viewport;
383
+ this.on("pointer:dragstart", this.handleDragStart);
384
+ this.on("pointer:drag", this.handleDrag);
385
+ this.on("pointer:dragend", this.handleDragEnd);
386
+ }
387
+ /**
388
+ * Set up drag plane perpendicular to camera, passing through a world point
389
+ */
390
+ setupDragPlane(worldPoint) {
391
+ if (!this.cameraController) return;
392
+ const camera = this.cameraController.camera;
393
+ const cameraDirection = new THREE.Vector3();
394
+ camera.getWorldDirection(cameraDirection);
395
+ const planeNormal = cameraDirection.clone().negate();
396
+ this.dragPlane.setFromNormalAndCoplanarPoint(planeNormal, worldPoint);
397
+ }
398
+ handleDragStart = (data) => {
399
+ const node = this.getNodeByIndex(data.index);
400
+ if (!node) return;
401
+ let state = node.state ?? NodeState.Active;
402
+ if (node.state === void 0) state = NodeState.Fixed;
403
+ if (state !== NodeState.Active || node.metadata?.hidden) return;
404
+ this.isDragging = true;
405
+ if (this.cameraController) this.cameraController.setEnabled(false);
406
+ if (this.cameraController) {
407
+ this.cameraController.camera;
408
+ const target = this.cameraController.getTarget();
409
+ this.setupDragPlane(target);
410
+ }
411
+ if (this.forceSimulation) {
412
+ const worldPos = this.screenToWorld(data.x, data.y);
413
+ if (worldPos) this.forceSimulation.startDrag(data.index, worldPos);
414
+ }
415
+ this.emit("node:dragstart", { node });
416
+ };
417
+ handleDrag = (data) => {
418
+ const node = this.getNodeByIndex(data.index);
419
+ if (!node) return;
420
+ if (this.forceSimulation) {
421
+ const worldPos = this.screenToWorld(data.x, data.y);
422
+ if (worldPos) this.forceSimulation.updateDrag(worldPos);
423
+ }
424
+ this.emit("node:drag", {
425
+ node,
426
+ x: data.x,
427
+ y: data.y
428
+ });
429
+ };
430
+ handleDragEnd = (data) => {
431
+ const node = this.getNodeByIndex(data.index);
432
+ if (!node) return;
433
+ this.isDragging = false;
434
+ if (this.cameraController) this.cameraController.setEnabled(true);
435
+ if (this.forceSimulation) this.forceSimulation.endDrag();
436
+ this.emit("node:dragend", { node });
437
+ };
438
+ /**
439
+ * Convert screen coordinates to world position on drag plane
440
+ */
441
+ screenToWorld(clientX, clientY) {
442
+ if (!this.cameraController || !this.viewport) return null;
443
+ this.pointer.x = clientX / this.viewport.width * 2 - 1;
444
+ this.pointer.y = -(clientY / this.viewport.height) * 2 + 1;
445
+ this.raycaster.setFromCamera(this.pointer, this.cameraController.camera);
446
+ const target = new THREE.Vector3();
447
+ return this.raycaster.ray.intersectPlane(this.dragPlane, target);
448
+ }
449
+ /**
450
+ * Update viewport dimensions on resize
451
+ */
452
+ resize(width, height) {
453
+ if (this.viewport) {
454
+ this.viewport.width = width;
455
+ this.viewport.height = height;
456
+ }
457
+ }
458
+ dispose() {
459
+ this.off("pointer:dragstart", this.handleDragStart);
460
+ this.off("pointer:drag", this.handleDrag);
461
+ this.off("pointer:dragend", this.handleDragEnd);
462
+ }
463
+ };
464
+
465
+ //#endregion
466
+ //#region controls/handlers/HoverHandler.ts
467
+ var HoverHandler = class extends EventEmitter {
468
+ constructor(getNodeByIndex) {
469
+ super();
470
+ this.getNodeByIndex = getNodeByIndex;
471
+ this.on("pointer:hoverstart", this.handleHoverStart);
472
+ this.on("pointer:hover", this.handleHover);
473
+ this.on("pointer:pop", this.handlePop);
474
+ this.on("pointer:hoverend", this.handleHoverEnd);
475
+ }
476
+ handleHoverStart = (data) => {
477
+ const node = this.getNodeByIndex(data.index);
478
+ if (!node) return;
479
+ this.emit("node:hoverstart", {
480
+ node,
481
+ x: data.x,
482
+ y: data.y,
483
+ clientX: data.clientX,
484
+ clientY: data.clientY
485
+ });
486
+ };
487
+ handleHover = (data) => {
488
+ const node = this.getNodeByIndex(data.index);
489
+ if (!node) return;
490
+ const progress = data.progress ?? 0;
491
+ this.emit("node:hover", {
492
+ node,
493
+ x: data.x,
494
+ y: data.y,
495
+ progress,
496
+ clientX: data.clientX,
497
+ clientY: data.clientY
498
+ });
499
+ };
500
+ handlePop = (data) => {
501
+ const node = this.getNodeByIndex(data.index);
502
+ if (!node) return;
503
+ this.emit("node:pop", {
504
+ node,
505
+ x: data.x,
506
+ y: data.y,
507
+ clientX: data.clientX,
508
+ clientY: data.clientY
509
+ });
510
+ };
511
+ handleHoverEnd = (data) => {
512
+ if (!this.getNodeByIndex(data.index)) return;
513
+ this.emit("node:hoverend");
514
+ };
515
+ dispose() {
516
+ this.off("pointer:hoverstart", this.handleHoverStart);
517
+ this.off("pointer:hover", this.handleHover);
518
+ this.off("pointer:pop", this.handlePop);
519
+ this.off("pointer:hoverend", this.handleHoverEnd);
520
+ }
521
+ };
522
+
523
+ //#endregion
524
+ //#region controls/InputProcessor.ts
525
+ /**
526
+ * Manages pointer/mouse input and emits interaction events
527
+ * Uses GPU picking for efficient node detection
528
+ */
529
+ var InputProcessor = class extends EventEmitter {
530
+ CLICK_THRESHOLD = 200;
531
+ HOVER_TO_POP_MS = 800;
532
+ pointer = new THREE.Vector2();
533
+ canvasPointer = new THREE.Vector2();
534
+ isPointerDown = false;
535
+ isDragging = false;
536
+ mouseDownTime = 0;
537
+ draggedIndex = -1;
538
+ currentHoverIndex = -1;
539
+ hoverStartTime = 0;
540
+ hoverProgress = 0;
541
+ hasPopped = false;
542
+ isTouch = false;
543
+ TOUCH_MOVE_THRESHOLD = 10;
544
+ pointerDownX = 0;
545
+ pointerDownY = 0;
546
+ lastClientX = null;
547
+ lastClientY = null;
548
+ constructor(pickFn, canvas, viewport) {
549
+ super();
550
+ this.pickFn = pickFn;
551
+ this.canvas = canvas;
552
+ this.viewport = viewport;
553
+ this.setupEventListeners();
554
+ }
555
+ setupEventListeners() {
556
+ this.canvas.addEventListener("pointermove", this.handlePointerMove);
557
+ this.canvas.addEventListener("pointerdown", this.handlePointerDown);
558
+ this.canvas.addEventListener("pointerup", this.handlePointerUp);
559
+ this.canvas.addEventListener("pointerleave", this.handlePointerLeave);
560
+ }
561
+ handlePointerMove = (event) => {
562
+ this.updatePointer(event);
563
+ this.lastClientX = event.clientX;
564
+ this.lastClientY = event.clientY;
565
+ const pickedIndex = this.pickFn(this.canvasPointer.x, this.canvasPointer.y);
566
+ if (!this.isTouch) this.updateHover(pickedIndex);
567
+ if (this.isTouch && !this.isDragging) {
568
+ const dx = event.clientX - this.pointerDownX;
569
+ const dy = event.clientY - this.pointerDownY;
570
+ if (dx * dx + dy * dy < this.TOUCH_MOVE_THRESHOLD * this.TOUCH_MOVE_THRESHOLD) return;
571
+ }
572
+ if (this.isPointerDown && this.draggedIndex >= 0) {
573
+ if (!this.isDragging) {
574
+ this.isDragging = true;
575
+ this.emit("pointer:dragstart", {
576
+ index: this.draggedIndex,
577
+ x: this.canvasPointer.x,
578
+ y: this.canvasPointer.y,
579
+ clientX: event.clientX,
580
+ clientY: event.clientY
581
+ });
582
+ }
583
+ this.emit("pointer:drag", {
584
+ index: this.draggedIndex,
585
+ x: this.canvasPointer.x,
586
+ y: this.canvasPointer.y,
587
+ clientX: event.clientX,
588
+ clientY: event.clientY
589
+ });
590
+ }
591
+ };
592
+ handlePointerDown = (event) => {
593
+ this.updatePointer(event);
594
+ this.mouseDownTime = Date.now();
595
+ this.isPointerDown = true;
596
+ this.isTouch = event.pointerType === "touch";
597
+ this.pointerDownX = event.clientX;
598
+ this.pointerDownY = event.clientY;
599
+ const pickedIndex = this.pickFn(this.canvasPointer.x, this.canvasPointer.y);
600
+ this.draggedIndex = pickedIndex;
601
+ if (pickedIndex >= 0) this.emit("pointer:down", {
602
+ index: pickedIndex,
603
+ x: this.canvasPointer.x,
604
+ y: this.canvasPointer.y,
605
+ clientX: event.clientX,
606
+ clientY: event.clientY
607
+ });
608
+ };
609
+ handlePointerUp = (event) => {
610
+ const clickDuration = Date.now() - this.mouseDownTime;
611
+ const wasClick = this.isTouch ? !this.isDragging : clickDuration < this.CLICK_THRESHOLD;
612
+ if (this.isDragging) this.emit("pointer:dragend", {
613
+ index: this.draggedIndex,
614
+ x: this.canvasPointer.x,
615
+ y: this.canvasPointer.y,
616
+ clientX: event.clientX,
617
+ clientY: event.clientY
598
618
  });
599
- this.closeButton.addEventListener("click", (e) => {
600
- e.stopPropagation();
601
- this.hideFull();
602
- if (this.onCloseCallback) this.onCloseCallback();
619
+ else if (wasClick && this.draggedIndex >= 0) this.emit("pointer:click", {
620
+ index: this.draggedIndex,
621
+ x: this.canvasPointer.x,
622
+ y: this.canvasPointer.y,
623
+ clientX: event.clientX,
624
+ clientY: event.clientY
603
625
  });
604
- element.appendChild(this.closeButton);
605
- }
606
- /**
607
- * Remove close button from tooltip
608
- */
609
- removeCloseButton() {
610
- if (this.closeButton) {
611
- this.closeButton.remove();
612
- this.closeButton = null;
626
+ else if (wasClick && this.draggedIndex < 0) this.emit("pointer:clickempty", {
627
+ x: this.canvasPointer.x,
628
+ y: this.canvasPointer.y,
629
+ clientX: event.clientX,
630
+ clientY: event.clientY
631
+ });
632
+ this.isPointerDown = false;
633
+ this.isDragging = false;
634
+ this.draggedIndex = -1;
635
+ };
636
+ handlePointerLeave = () => {
637
+ this.lastClientX = null;
638
+ this.lastClientY = null;
639
+ this.updateHover(-1);
640
+ };
641
+ updateHover(index) {
642
+ const prevIndex = this.currentHoverIndex;
643
+ if (index === prevIndex) {
644
+ if (index >= 0) {
645
+ const elapsed = Date.now() - this.hoverStartTime;
646
+ this.hoverProgress = Math.min(1, elapsed / this.HOVER_TO_POP_MS);
647
+ this.emit("pointer:hover", {
648
+ index,
649
+ progress: this.hoverProgress,
650
+ x: this.canvasPointer.x,
651
+ y: this.canvasPointer.y,
652
+ clientX: this.lastClientX,
653
+ clientY: this.lastClientY
654
+ });
655
+ if (this.hoverProgress >= 1 && !this.hasPopped) {
656
+ this.hasPopped = true;
657
+ this.emit("pointer:pop", {
658
+ index,
659
+ x: this.canvasPointer.x,
660
+ y: this.canvasPointer.y,
661
+ clientX: this.lastClientX,
662
+ clientY: this.lastClientY
663
+ });
664
+ }
665
+ }
666
+ return;
667
+ }
668
+ if (prevIndex >= 0) this.emit("pointer:hoverend", { index: prevIndex });
669
+ if (index >= 0) {
670
+ this.currentHoverIndex = index;
671
+ this.hoverStartTime = Date.now();
672
+ this.hoverProgress = 0;
673
+ this.hasPopped = false;
674
+ this.emit("pointer:hoverstart", {
675
+ index,
676
+ x: this.canvasPointer.x,
677
+ y: this.canvasPointer.y,
678
+ clientX: this.lastClientX,
679
+ clientY: this.lastClientY
680
+ });
681
+ } else {
682
+ this.currentHoverIndex = -1;
683
+ this.hoverStartTime = 0;
684
+ this.hoverProgress = 0;
685
+ this.hasPopped = false;
613
686
  }
614
- const element = this.mainTooltip.getElement();
615
- element.style.pointerEvents = "none";
616
687
  }
617
688
  /**
618
- * Hide all tooltips
689
+ * Update hover state even when pointer is stationary
690
+ * Called from render loop
619
691
  */
620
- hideAll() {
621
- this.hideGrabIcon();
622
- this.hidePreview();
623
- this.hideFull();
624
- this.hideChainTooltips();
625
- }
626
- showMainTooltip(content, x, y) {
627
- this.mainTooltip.open(content);
628
- this.mainTooltip.updatePos(x, y);
629
- console.log("Showing main tooltip");
630
- }
631
- hideMainTooltip() {
632
- this.mainTooltip.close();
633
- }
634
- showChainTooltip(nodeId, content, x, y) {
635
- if (!this.chainTooltips.has(nodeId)) this.chainTooltips.set(nodeId, new Tooltip("label", this.container));
636
- const tooltip = this.chainTooltips.get(nodeId);
637
- tooltip.open(content);
638
- tooltip.updatePos(x, y);
639
- }
640
- hideChainTooltips() {
641
- this.chainTooltips.forEach((tooltip) => tooltip.close());
692
+ update() {
693
+ if (this.isTouch) return;
694
+ if (this.lastClientX !== null && this.lastClientY !== null) {
695
+ const rect = this.canvas.getBoundingClientRect();
696
+ const x = this.lastClientX - rect.left;
697
+ const y = this.lastClientY - rect.top;
698
+ const pickedIndex = this.pickFn(x, y);
699
+ this.updateHover(pickedIndex);
700
+ }
642
701
  }
643
- updateMainTooltipPos(x, y) {
644
- this.mainTooltip.updatePos(x, y);
702
+ updatePointer(event) {
703
+ const rect = this.canvas.getBoundingClientRect();
704
+ const x = event.clientX - rect.left;
705
+ const y = event.clientY - rect.top;
706
+ const ndcX = x / rect.width * 2 - 1;
707
+ const ndcY = -(y / rect.height) * 2 + 1;
708
+ this.pointer.set(ndcX, ndcY);
709
+ this.canvasPointer.set(x, y);
645
710
  }
646
711
  /**
647
- * Set main tooltip visibility (doesn't affect open state or content)
712
+ * Update viewport dimensions on resize
648
713
  */
649
- setMainTooltipVisibility(visible) {
650
- if (visible) this.mainTooltip.show();
651
- else this.mainTooltip.hide();
714
+ resize(width, height) {
715
+ this.viewport.width = width;
716
+ this.viewport.height = height;
652
717
  }
653
718
  dispose() {
654
- this.removeCloseButton();
655
- this.mainTooltip.destroy();
656
- this.previewTooltip.destroy();
657
- this.chainTooltips.forEach((tooltip) => tooltip.destroy());
658
- this.chainTooltips.clear();
719
+ this.canvas.removeEventListener("pointermove", this.handlePointerMove);
720
+ this.canvas.removeEventListener("pointerdown", this.handlePointerDown);
721
+ this.canvas.removeEventListener("pointerup", this.handlePointerUp);
722
+ this.canvas.removeEventListener("pointerleave", this.handlePointerLeave);
659
723
  }
660
724
  };
661
725
 
662
726
  //#endregion
663
727
  //#region controls/InteractionManager.ts
664
728
  var InteractionManager = class {
729
+ pointerInput;
730
+ hoverHandler;
731
+ clickHandler;
732
+ dragHandler;
733
+ tooltipManager;
734
+ isDragging = false;
735
+ isTooltipSticky = false;
736
+ stickyNodeId = null;
737
+ searchHighlightIds = [];
665
738
  constructor(pickFunction, canvas, viewport, getNodeByIndex, cameraController, forceSimulation, tooltipConfig, graphScene, getConnectedNodeIds) {
666
739
  this.graphScene = graphScene;
667
740
  this.getConnectedNodeIds = getConnectedNodeIds;
668
- this.isDragging = false;
669
- this.isTooltipSticky = false;
670
- this.stickyNodeId = null;
671
- this.searchHighlightIds = [];
672
741
  this.pointerInput = new InputProcessor(pickFunction, canvas, viewport);
673
742
  const tooltipContainer = document.body;
674
743
  this.tooltipManager = new TooltipManager(tooltipContainer, canvas, tooltipConfig);
@@ -855,11 +924,13 @@ let CameraMode = /* @__PURE__ */ function(CameraMode) {
855
924
  //#endregion
856
925
  //#region rendering/CameraController.ts
857
926
  var CameraController = class {
927
+ camera;
928
+ controls;
929
+ sceneBounds = null;
930
+ autorotateEnabled = false;
931
+ autorotateSpeed = .5;
932
+ userIsActive = true;
858
933
  constructor(domelement, config) {
859
- this.sceneBounds = null;
860
- this.autorotateEnabled = false;
861
- this.autorotateSpeed = .5;
862
- this.userIsActive = true;
863
934
  this.camera = new THREE.PerspectiveCamera(config?.fov ?? 50, config?.aspect ?? window.innerWidth / window.innerHeight, config?.near ?? 1e-5, config?.far ?? 1e4);
864
935
  if (config?.position) this.camera.position.copy(config.position);
865
936
  else this.camera.position.set(0, 0, 2);
@@ -890,7 +961,7 @@ var CameraController = class {
890
961
  case CameraMode.Orbit:
891
962
  default:
892
963
  this.controls.mouseButtons.left = CameraControls.ACTION.ROTATE;
893
- this.controls.mouseButtons.right = CameraControls.ACTION.ROTATE;
964
+ this.controls.mouseButtons.right = CameraControls.ACTION.TRUCK;
894
965
  this.controls.mouseButtons.wheel = CameraControls.ACTION.DOLLY;
895
966
  this.controls.touches.one = CameraControls.ACTION.TOUCH_ROTATE;
896
967
  this.controls.touches.two = CameraControls.ACTION.TOUCH_DOLLY_TRUCK;
@@ -948,8 +1019,11 @@ var CameraController = class {
948
1019
  /**
949
1020
  * Reset camera to default position
950
1021
  */
951
- async reset(enableTransition = true) {
952
- return this.controls.setLookAt(0, 0, 50, 0, 0, 0, enableTransition);
1022
+ async reset(position, target, enableTransition = true) {
1023
+ this.controls.normalizeRotations();
1024
+ const { x: px, y: py, z: pz } = position ?? new THREE.Vector3(0, 0, 50);
1025
+ const { x: tx, y: ty, z: tz } = target ?? new THREE.Vector3(0, 0, 0);
1026
+ return this.controls.setLookAt(px, py, pz, tx, ty, tz, enableTransition);
953
1027
  }
954
1028
  /**
955
1029
  * Enable/disable controls
@@ -1028,12 +1102,10 @@ const DEFAULT_LINK_STYLE = { color: new THREE.Color(8947848) };
1028
1102
  * const style = styleRegistry.getNodeStyle('person')
1029
1103
  */
1030
1104
  var StyleRegistry = class {
1031
- constructor() {
1032
- this.nodeStyles = /* @__PURE__ */ new Map();
1033
- this.linkStyles = /* @__PURE__ */ new Map();
1034
- this.defaultNodeStyle = { ...DEFAULT_NODE_STYLE };
1035
- this.defaultLinkStyle = { ...DEFAULT_LINK_STYLE };
1036
- }
1105
+ nodeStyles = /* @__PURE__ */ new Map();
1106
+ linkStyles = /* @__PURE__ */ new Map();
1107
+ defaultNodeStyle = { ...DEFAULT_NODE_STYLE };
1108
+ defaultLinkStyle = { ...DEFAULT_LINK_STYLE };
1037
1109
  /**
1038
1110
  * Convert color input to THREE.Color
1039
1111
  */
@@ -1148,20 +1220,19 @@ const styleRegistry = new StyleRegistry();
1148
1220
  /**
1149
1221
  * Manages read-only GPU textures created at initialization
1150
1222
  * These never change during simulation (except for mode changes)
1151
- *
1223
+ *
1152
1224
  * Node data: radii, colors
1153
1225
  * Link data: source/target indices, properties
1154
1226
  */
1155
1227
  var StaticAssets = class {
1156
- constructor() {
1157
- this.nodeRadiiTexture = null;
1158
- this.nodeColorsTexture = null;
1159
- this.linkIndicesTexture = null;
1160
- this.linkPropertiesTexture = null;
1161
- this.nodeLinkMapTexture = null;
1162
- this.nodeTextureSize = 0;
1163
- this.linkTextureSize = 0;
1164
- }
1228
+ nodeRadiiTexture = null;
1229
+ nodeColorsTexture = null;
1230
+ linkIndicesTexture = null;
1231
+ linkPropertiesTexture = null;
1232
+ nodeLinkMapTexture = null;
1233
+ nodeTextureSize = 0;
1234
+ linkTextureSize = 0;
1235
+ constructor() {}
1165
1236
  /**
1166
1237
  * Set/update node radii texture
1167
1238
  */
@@ -1284,9 +1355,18 @@ const staticAssets = new StaticAssets();
1284
1355
  * LinksOpacity
1285
1356
  */
1286
1357
  var LinksOpacity = class {
1358
+ renderer;
1359
+ size;
1360
+ rtA;
1361
+ rtB;
1362
+ ping = 0;
1363
+ scene;
1364
+ camera;
1365
+ quad;
1366
+ computeMaterial;
1367
+ copyMaterial;
1368
+ targetTex = null;
1287
1369
  constructor(renderer, size) {
1288
- this.ping = 0;
1289
- this.targetTex = null;
1290
1370
  this.renderer = renderer;
1291
1371
  this.size = size;
1292
1372
  const params = {
@@ -1448,23 +1528,23 @@ var LinksOpacity = class {
1448
1528
  * Manages link-specific opacity and highlighting
1449
1529
  */
1450
1530
  var LinksRenderer = class {
1531
+ lines = null;
1532
+ links = [];
1533
+ nodeIndexMap = /* @__PURE__ */ new Map();
1534
+ linkIndexMap = /* @__PURE__ */ new Map();
1535
+ interpolationSteps = 24;
1536
+ params = {
1537
+ noiseStrength: .1,
1538
+ defaultAlpha: .02,
1539
+ highlightAlpha: .8,
1540
+ dimmedAlpha: .02,
1541
+ is2D: true
1542
+ };
1543
+ linkOpacity = null;
1544
+ simulationBuffers = null;
1451
1545
  constructor(scene, renderer) {
1452
1546
  this.scene = scene;
1453
1547
  this.renderer = renderer;
1454
- this.lines = null;
1455
- this.links = [];
1456
- this.nodeIndexMap = /* @__PURE__ */ new Map();
1457
- this.linkIndexMap = /* @__PURE__ */ new Map();
1458
- this.interpolationSteps = 24;
1459
- this.params = {
1460
- noiseStrength: .1,
1461
- defaultAlpha: .02,
1462
- highlightAlpha: .8,
1463
- dimmedAlpha: .02,
1464
- is2D: true
1465
- };
1466
- this.linkOpacity = null;
1467
- this.simulationBuffers = null;
1468
1548
  }
1469
1549
  /**
1470
1550
  * Create link geometry and materials
@@ -1903,9 +1983,18 @@ var points_default = "attribute float scale;\r\nattribute float selected;\r\natt
1903
1983
  * Similar to LinkOpacity but optimized for nodes
1904
1984
  */
1905
1985
  var NodeOpacity = class {
1986
+ renderer;
1987
+ size;
1988
+ rtA;
1989
+ rtB;
1990
+ ping = 0;
1991
+ scene;
1992
+ camera;
1993
+ quad;
1994
+ computeMaterial;
1995
+ copyMaterial;
1996
+ targetTex = null;
1906
1997
  constructor(renderer, size) {
1907
- this.ping = 0;
1908
- this.targetTex = null;
1909
1998
  this.renderer = renderer;
1910
1999
  this.size = size;
1911
2000
  const params = {
@@ -2062,22 +2151,22 @@ var NodeOpacity = class {
2062
2151
  //#endregion
2063
2152
  //#region rendering/nodes/NodesRenderer.ts
2064
2153
  var NodesRenderer = class {
2154
+ points = null;
2155
+ pickMaterial = null;
2156
+ nodeIndexMap = /* @__PURE__ */ new Map();
2157
+ idArray = [];
2158
+ nodeOpacity = null;
2159
+ targets = null;
2160
+ params = {
2161
+ defaultAlpha: 1,
2162
+ highlightAlpha: 1,
2163
+ dimmedAlpha: .2
2164
+ };
2165
+ simulationBuffers = null;
2166
+ pickBuffer = null;
2065
2167
  constructor(scene, renderer) {
2066
2168
  this.scene = scene;
2067
2169
  this.renderer = renderer;
2068
- this.points = null;
2069
- this.pickMaterial = null;
2070
- this.nodeIndexMap = /* @__PURE__ */ new Map();
2071
- this.idArray = [];
2072
- this.nodeOpacity = null;
2073
- this.targets = null;
2074
- this.params = {
2075
- defaultAlpha: 1,
2076
- highlightAlpha: 1,
2077
- dimmedAlpha: .2
2078
- };
2079
- this.simulationBuffers = null;
2080
- this.pickBuffer = null;
2081
2170
  }
2082
2171
  create(nodes, simulationBuffers, pickBuffer) {
2083
2172
  this.simulationBuffers = simulationBuffers;
@@ -2201,7 +2290,10 @@ var NodesRenderer = class {
2201
2290
  const radii = new Float32Array(totalTexels);
2202
2291
  const colors = new Float32Array(totalTexels * 4);
2203
2292
  const fovRadians = 75 * Math.PI / 180;
2204
- const pixelToWorldRatio = 2 * Math.tan(fovRadians / 2) / window.innerHeight;
2293
+ const viewHeightAtDistance1 = 2 * Math.tan(fovRadians / 2);
2294
+ const REFERENCE_HEIGHT = 1080;
2295
+ const REFERENCE_DPR = 1;
2296
+ const pixelToWorldRatio = viewHeightAtDistance1 / REFERENCE_HEIGHT;
2205
2297
  nodes.forEach((node, i) => {
2206
2298
  const style = styleRegistry.getNodeStyle(node.category);
2207
2299
  colorBuffer[i * 3] = style.color.r;
@@ -2213,7 +2305,7 @@ var NodesRenderer = class {
2213
2305
  pointIndices[i * 2] = x;
2214
2306
  pointIndices[i * 2 + 1] = y;
2215
2307
  alphaIndex[i] = i;
2216
- radii[i] = style.size * window.devicePixelRatio * pixelToWorldRatio / 2;
2308
+ radii[i] = style.size * REFERENCE_DPR * pixelToWorldRatio / 2;
2217
2309
  colors[i * 4] = style.color.r;
2218
2310
  colors[i * 4 + 1] = style.color.g;
2219
2311
  colors[i * 4 + 2] = style.color.b;
@@ -2288,9 +2380,16 @@ var NodesRenderer = class {
2288
2380
  * SMAA operates in linear space (before tone mapping), FXAA in sRGB (after OutputPass).
2289
2381
  */
2290
2382
  var PostProcessing = class {
2383
+ composer;
2384
+ renderer;
2385
+ scene;
2386
+ camera;
2387
+ mode;
2388
+ renderPass;
2389
+ outputPass;
2390
+ smaaPass = null;
2391
+ fxaaPass = null;
2291
2392
  constructor(renderer, scene, camera, mode) {
2292
- this.smaaPass = null;
2293
- this.fxaaPass = null;
2294
2393
  this.renderer = renderer;
2295
2394
  this.scene = scene;
2296
2395
  this.camera = camera;
@@ -2326,7 +2425,7 @@ var PostProcessing = class {
2326
2425
  const pixelRatio = this.renderer.getPixelRatio();
2327
2426
  this.composer.setSize(width, height);
2328
2427
  this.composer.setPixelRatio(pixelRatio);
2329
- if (this.fxaaPass) this.fxaaPass.material.uniforms["resolution"].value.set(1 / (width * pixelRatio), 1 / (height * pixelRatio));
2428
+ if (this.fxaaPass) this.fxaaPass.material.uniforms.resolution.value.set(1 / (width * pixelRatio), 1 / (height * pixelRatio));
2330
2429
  if (this.smaaPass) this.smaaPass.setSize(width * pixelRatio, height * pixelRatio);
2331
2430
  }
2332
2431
  /**
@@ -2364,7 +2463,7 @@ var PostProcessing = class {
2364
2463
  this.fxaaPass = new ShaderPass(FXAAShader);
2365
2464
  const size = this.renderer.getSize(new THREE.Vector2());
2366
2465
  const pixelRatio = this.renderer.getPixelRatio();
2367
- this.fxaaPass.material.uniforms["resolution"].value.set(1 / (size.x * pixelRatio), 1 / (size.y * pixelRatio));
2466
+ this.fxaaPass.material.uniforms.resolution.value.set(1 / (size.x * pixelRatio), 1 / (size.y * pixelRatio));
2368
2467
  }
2369
2468
  this.composer.addPass(this.fxaaPass);
2370
2469
  }
@@ -2383,11 +2482,15 @@ var PostProcessing = class {
2383
2482
  * - Mode application (visual changes)
2384
2483
  */
2385
2484
  var GraphScene = class {
2485
+ scene;
2486
+ renderer;
2487
+ camera;
2488
+ nodeRenderer = null;
2489
+ clock = new THREE.Clock();
2490
+ linkRenderer = null;
2491
+ postProcessing = null;
2492
+ antialiasingMode;
2386
2493
  constructor(canvas, cameraConfig, antialiasing = "msaa") {
2387
- this.nodeRenderer = null;
2388
- this.clock = new THREE.Clock();
2389
- this.linkRenderer = null;
2390
- this.postProcessing = null;
2391
2494
  this.antialiasingMode = antialiasing;
2392
2495
  this.scene = new THREE.Scene();
2393
2496
  this.scene.background = new THREE.Color(0);
@@ -2546,10 +2649,8 @@ function resolveAttractor(attractor) {
2546
2649
  * Each pass operates on simulation buffers and can read from static assets
2547
2650
  */
2548
2651
  var BasePass = class {
2549
- constructor() {
2550
- this.material = null;
2551
- this.enabled = true;
2552
- }
2652
+ material = null;
2653
+ enabled = true;
2553
2654
  /**
2554
2655
  * Execute the pass (renders to current velocity target)
2555
2656
  */
@@ -2629,15 +2730,12 @@ const MAX_GROUPS = 4;
2629
2730
  * ])
2630
2731
  */
2631
2732
  var AttractorPass = class extends BasePass {
2632
- constructor(..._args) {
2633
- super(..._args);
2634
- this.attractors = [];
2635
- this.groupMap = /* @__PURE__ */ new Map();
2636
- this.attractorsTexture = null;
2637
- this.attractorGroupsTexture = null;
2638
- this.attractorParamsTexture = null;
2639
- this.nodeGroupsTexture = null;
2640
- }
2733
+ attractors = [];
2734
+ groupMap = /* @__PURE__ */ new Map();
2735
+ attractorsTexture = null;
2736
+ attractorGroupsTexture = null;
2737
+ attractorParamsTexture = null;
2738
+ nodeGroupsTexture = null;
2641
2739
  getName() {
2642
2740
  return "attractor";
2643
2741
  }
@@ -2857,12 +2955,9 @@ var drag_default = "precision highp float;\n\nuniform sampler2D uPositionsTextur
2857
2955
  * Reads configuration from context.config, maintains drag state internally
2858
2956
  */
2859
2957
  var DragPass = class extends BasePass {
2860
- constructor(..._args) {
2861
- super(..._args);
2862
- this.draggedIndex = null;
2863
- this.dragUV = null;
2864
- this.dragTarget = new THREE.Vector3(0, 0, 0);
2865
- }
2958
+ draggedIndex = null;
2959
+ dragUV = null;
2960
+ dragTarget = new THREE.Vector3(0, 0, 0);
2866
2961
  getName() {
2867
2962
  return "drag";
2868
2963
  }
@@ -3137,10 +3232,28 @@ var VelocityCarryPass = class extends BasePass {
3137
3232
  * - No manual syncing required
3138
3233
  */
3139
3234
  var ForceSimulation = class {
3235
+ renderer;
3236
+ simulationBuffers = null;
3237
+ computeScene;
3238
+ computeCamera;
3239
+ computeQuad;
3240
+ velocityCarryPass;
3241
+ collisionPass;
3242
+ manyBodyPass;
3243
+ gravityPass;
3244
+ linkPass;
3245
+ elasticPass;
3246
+ attractorPass;
3247
+ dragPass;
3248
+ integratePass;
3249
+ forcePasses;
3250
+ config;
3251
+ isInitialized = false;
3252
+ iterationCount = 0;
3253
+ FIXED_DT = 1 / 60;
3254
+ MAX_STEPS_PER_FRAME = 4;
3255
+ timeAccumulator = 0;
3140
3256
  constructor(renderer) {
3141
- this.simulationBuffers = null;
3142
- this.isInitialized = false;
3143
- this.iterationCount = 0;
3144
3257
  this.renderer = renderer;
3145
3258
  this.computeScene = new THREE.Scene();
3146
3259
  this.computeCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
@@ -3171,7 +3284,6 @@ var ForceSimulation = class {
3171
3284
  alphaDecay: .01,
3172
3285
  deltaTime: .016,
3173
3286
  spaceSize: 1e3,
3174
- maxVelocity: 10,
3175
3287
  is3D: true
3176
3288
  };
3177
3289
  this.velocityCarryPass = new VelocityCarryPass();
@@ -3276,13 +3388,25 @@ var ForceSimulation = class {
3276
3388
  }
3277
3389
  }
3278
3390
  /**
3279
- * Run one simulation step
3391
+ * Run simulation steps using a fixed timestep accumulator.
3392
+ * Regardless of monitor refresh rate, physics always steps at 1/60s intervals.
3280
3393
  */
3281
3394
  step(deltaTime) {
3282
3395
  if (!this.isInitialized || !this.simulationBuffers) return;
3283
- if (deltaTime !== void 0) this.config.deltaTime = deltaTime;
3396
+ this.timeAccumulator += deltaTime ?? this.config.deltaTime;
3397
+ let steps = 0;
3398
+ while (this.timeAccumulator >= this.FIXED_DT && steps < this.MAX_STEPS_PER_FRAME) {
3399
+ this.timeAccumulator -= this.FIXED_DT;
3400
+ this.config.deltaTime = this.FIXED_DT;
3401
+ this.fixedStep();
3402
+ steps++;
3403
+ }
3404
+ }
3405
+ /**
3406
+ * Execute one fixed-rate physics step (always at 1/60s)
3407
+ */
3408
+ fixedStep() {
3284
3409
  const context = this.createContext();
3285
- this.simulationBuffers.swapVelocities();
3286
3410
  const currentVelocityTarget = this.simulationBuffers.getCurrentVelocityTarget();
3287
3411
  if (currentVelocityTarget) {
3288
3412
  this.renderer.setRenderTarget(currentVelocityTarget);
@@ -3303,9 +3427,7 @@ var ForceSimulation = class {
3303
3427
  this.renderer.setRenderTarget(newPositionTarget);
3304
3428
  this.integratePass.execute(context);
3305
3429
  }
3306
- const timeScale = (this.config.deltaTime || .016) * 60;
3307
- const decayFactor = Math.pow(1 - this.config.alphaDecay, timeScale);
3308
- this.config.alpha *= decayFactor;
3430
+ this.config.alpha *= 1 - this.config.alphaDecay;
3309
3431
  this.config.alpha = Math.max(this.config.alpha, 0);
3310
3432
  this.renderer.setRenderTarget(null);
3311
3433
  this.iterationCount++;
@@ -3399,6 +3521,7 @@ var ForceSimulation = class {
3399
3521
  if (!this.isInitialized || !this.simulationBuffers) return;
3400
3522
  this.config.alpha = 1;
3401
3523
  this.iterationCount = 0;
3524
+ this.timeAccumulator = 0;
3402
3525
  this.simulationBuffers.resetPositions();
3403
3526
  this.simulationBuffers.initVelocities();
3404
3527
  }
@@ -3418,8 +3541,9 @@ var ForceSimulation = class {
3418
3541
  * Separate from simulation buffers as it has different lifecycle
3419
3542
  */
3420
3543
  var PickBuffer = class {
3544
+ renderer;
3545
+ pickTarget = null;
3421
3546
  constructor(renderer) {
3422
- this.pickTarget = null;
3423
3547
  this.renderer = renderer;
3424
3548
  }
3425
3549
  /**
@@ -3488,18 +3612,19 @@ var PickBuffer = class {
3488
3612
  * Buffers are created lazily when data is initialized
3489
3613
  */
3490
3614
  var SimulationBuffers = class {
3615
+ renderer;
3616
+ textureSize = 0;
3617
+ positionBuffers = {
3618
+ current: null,
3619
+ previous: null,
3620
+ original: null
3621
+ };
3622
+ velocityBuffers = {
3623
+ current: null,
3624
+ previous: null
3625
+ };
3626
+ isInitialized = false;
3491
3627
  constructor(renderer) {
3492
- this.textureSize = 0;
3493
- this.positionBuffers = {
3494
- current: null,
3495
- previous: null,
3496
- original: null
3497
- };
3498
- this.velocityBuffers = {
3499
- current: null,
3500
- previous: null
3501
- };
3502
- this.isInitialized = false;
3503
3628
  this.renderer = renderer;
3504
3629
  }
3505
3630
  /**
@@ -3753,66 +3878,6 @@ var SimulationBuffers = class {
3753
3878
  }
3754
3879
  };
3755
3880
 
3756
- //#endregion
3757
- //#region core/Clock.ts
3758
- /**
3759
- * Clock - Unified timing for the force graph package
3760
- */
3761
- var Clock = class {
3762
- constructor() {
3763
- this.previousTime = 0;
3764
- this.elapsedTime = 0;
3765
- this.deltaTime = 0;
3766
- this.isRunning = false;
3767
- this.maxDeltaTime = .1;
3768
- }
3769
- /**
3770
- * Start the clock
3771
- */
3772
- start() {
3773
- if (this.isRunning) return;
3774
- this.previousTime = performance.now() / 1e3;
3775
- this.isRunning = true;
3776
- }
3777
- /**
3778
- * Stop the clock
3779
- */
3780
- stop() {
3781
- this.isRunning = false;
3782
- }
3783
- /**
3784
- * Update the clock - call once per frame
3785
- * @returns delta time in seconds
3786
- */
3787
- update() {
3788
- if (!this.isRunning) this.start();
3789
- const now = performance.now() / 1e3;
3790
- let rawDelta = now - this.previousTime;
3791
- this.previousTime = now;
3792
- this.deltaTime = Math.min(rawDelta, this.maxDeltaTime);
3793
- this.elapsedTime += this.deltaTime;
3794
- return this.deltaTime;
3795
- }
3796
- /**
3797
- * Get delta time in seconds
3798
- */
3799
- getDeltaTime() {
3800
- return this.deltaTime;
3801
- }
3802
- /**
3803
- * Get total elapsed time in seconds
3804
- */
3805
- getElapsedTime() {
3806
- return this.elapsedTime;
3807
- }
3808
- /**
3809
- * Check if clock is running
3810
- */
3811
- getIsRunning() {
3812
- return this.isRunning;
3813
- }
3814
- };
3815
-
3816
3881
  //#endregion
3817
3882
  //#region core/GraphStore.ts
3818
3883
  /**
@@ -3825,15 +3890,14 @@ var Clock = class {
3825
3890
  * - Change notifications
3826
3891
  */
3827
3892
  var GraphStore = class {
3828
- constructor() {
3829
- this.nodes = /* @__PURE__ */ new Map();
3830
- this.links = /* @__PURE__ */ new Map();
3831
- this.nodeArray = [];
3832
- this.linkArray = [];
3833
- this.nodeIdToIndex = /* @__PURE__ */ new Map();
3834
- this.linkIdToIndex = /* @__PURE__ */ new Map();
3835
- this.nodeToLinks = /* @__PURE__ */ new Map();
3836
- }
3893
+ nodes = /* @__PURE__ */ new Map();
3894
+ links = /* @__PURE__ */ new Map();
3895
+ nodeArray = [];
3896
+ linkArray = [];
3897
+ nodeIdToIndex = /* @__PURE__ */ new Map();
3898
+ linkIdToIndex = /* @__PURE__ */ new Map();
3899
+ nodeToLinks = /* @__PURE__ */ new Map();
3900
+ constructor() {}
3837
3901
  /**
3838
3902
  * Set graph data (replaces all)
3839
3903
  */
@@ -4038,25 +4102,20 @@ CameraControls.install({ THREE });
4038
4102
  * - Handles user interaction
4039
4103
  */
4040
4104
  var Engine = class {
4105
+ graphStore;
4106
+ graphScene;
4107
+ forceSimulation;
4108
+ interactionManager;
4109
+ clock;
4110
+ simulationBuffers;
4111
+ pickBuffer;
4112
+ animationFrameId = null;
4113
+ isRunning = false;
4114
+ boundResizeHandler = null;
4115
+ groupOrder;
4116
+ smoothedTooltipPos = null;
4041
4117
  constructor(canvas, options = {}) {
4042
4118
  this.canvas = canvas;
4043
- this.animationFrameId = null;
4044
- this.isRunning = false;
4045
- this.boundResizeHandler = null;
4046
- this.smoothedTooltipPos = null;
4047
- this.animate = () => {
4048
- if (!this.isRunning) return;
4049
- const deltaTime = this.clock.update();
4050
- const elapsedTime = this.clock.getElapsedTime();
4051
- this.forceSimulation.step(deltaTime);
4052
- const positionTexture = this.simulationBuffers.getCurrentPositionTexture();
4053
- this.graphScene.updatePositions(positionTexture);
4054
- this.graphScene.update(elapsedTime);
4055
- this.interactionManager.getPointerInput().update();
4056
- this.graphScene.render();
4057
- this.updateStickyTooltipPosition();
4058
- this.animationFrameId = requestAnimationFrame(this.animate);
4059
- };
4060
4119
  console.log("Initializing Engine with options:", options);
4061
4120
  this.groupOrder = options.groupOrder;
4062
4121
  const width = options.width ?? window.innerWidth;
@@ -4094,7 +4153,7 @@ var Engine = class {
4094
4153
  const startPositions = new Float32Array(nodes.length * 4);
4095
4154
  const originalPositions = new Float32Array(nodes.length * 4);
4096
4155
  nodes.forEach((node, i) => {
4097
- let state = node.state ?? 3;
4156
+ const state = node.state ?? 3;
4098
4157
  startPositions[i * 4] = node.x ?? node.fx ?? 0;
4099
4158
  startPositions[i * 4 + 1] = node.y ?? node.fy ?? 0;
4100
4159
  startPositions[i * 4 + 2] = node.z ?? node.fz ?? 0;
@@ -4179,7 +4238,8 @@ var Engine = class {
4179
4238
  getAlpha() {
4180
4239
  return this.forceSimulation.getAlpha();
4181
4240
  }
4182
- /**Apply a preset to the graph
4241
+ /**
4242
+ Apply a preset to the graph
4183
4243
  */
4184
4244
  applyPreset(preset) {
4185
4245
  if (preset.force) {
@@ -4248,6 +4308,22 @@ var Engine = class {
4248
4308
  }
4249
4309
  }
4250
4310
  /**
4311
+ * Render loop
4312
+ */
4313
+ animate = () => {
4314
+ if (!this.isRunning) return;
4315
+ const deltaTime = this.clock.update();
4316
+ const elapsedTime = this.clock.getElapsedTime();
4317
+ this.forceSimulation.step(deltaTime);
4318
+ const positionTexture = this.simulationBuffers.getCurrentPositionTexture();
4319
+ this.graphScene.updatePositions(positionTexture);
4320
+ this.graphScene.update(elapsedTime);
4321
+ this.interactionManager.getPointerInput().update();
4322
+ this.graphScene.render();
4323
+ this.updateStickyTooltipPosition();
4324
+ this.animationFrameId = requestAnimationFrame(this.animate);
4325
+ };
4326
+ /**
4251
4327
  * GPU pick node at canvas coordinates
4252
4328
  */
4253
4329
  pickNode(x, y) {
@@ -4415,7 +4491,7 @@ var Engine = class {
4415
4491
  await this.graphScene.camera.setLookAt(posVec, targetVec, true);
4416
4492
  }
4417
4493
  /**
4418
- * Focus camera on a specific target node
4494
+ * Focus camera on a specific target node
4419
4495
  */
4420
4496
  async setCameraFocus(target, distance, transitionDuration) {
4421
4497
  const camPos = this.graphScene.camera.camera.position;