@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.
- package/README.md +61 -43
- package/dist/index.d.mts +869 -860
- package/dist/index.mjs +725 -649
- 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 {
|
|
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/
|
|
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
|
-
*
|
|
39
|
-
* Uses GPU picking for efficient node detection
|
|
12
|
+
* Clock - Unified timing for the force graph package
|
|
40
13
|
*/
|
|
41
|
-
var
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
*
|
|
202
|
-
* Called from render loop
|
|
22
|
+
* Start the clock
|
|
203
23
|
*/
|
|
204
|
-
|
|
205
|
-
if (this.
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
*
|
|
30
|
+
* Stop the clock
|
|
225
31
|
*/
|
|
226
|
-
|
|
227
|
-
this.
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
*
|
|
49
|
+
* Get delta time in seconds
|
|
389
50
|
*/
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
*
|
|
55
|
+
* Get total elapsed time in seconds
|
|
400
56
|
*/
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
*
|
|
61
|
+
* Check if clock is running
|
|
411
62
|
*/
|
|
412
|
-
|
|
413
|
-
|
|
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.
|
|
600
|
-
|
|
601
|
-
this.
|
|
602
|
-
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
*
|
|
689
|
+
* Update hover state even when pointer is stationary
|
|
690
|
+
* Called from render loop
|
|
619
691
|
*/
|
|
620
|
-
|
|
621
|
-
this.
|
|
622
|
-
this.
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
-
|
|
644
|
-
this.
|
|
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
|
-
*
|
|
712
|
+
* Update viewport dimensions on resize
|
|
648
713
|
*/
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
714
|
+
resize(width, height) {
|
|
715
|
+
this.viewport.width = width;
|
|
716
|
+
this.viewport.height = height;
|
|
652
717
|
}
|
|
653
718
|
dispose() {
|
|
654
|
-
this.
|
|
655
|
-
this.
|
|
656
|
-
this.
|
|
657
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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
|
|
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 *
|
|
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
|
|
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
|
|
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
|
-
|
|
2550
|
-
|
|
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
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
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
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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;
|