@hive-p2p/server 1.0.19 → 1.0.20
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/core/arbiter.mjs +125 -0
- package/core/config.mjs +226 -0
- package/core/crypto-codex.mjs +257 -0
- package/core/gossip.mjs +159 -0
- package/core/ice-offer-manager.mjs +181 -0
- package/core/node-services.mjs +129 -0
- package/core/node.mjs +177 -0
- package/core/peer-store.mjs +252 -0
- package/core/route-builder.mjs +176 -0
- package/core/topologist.mjs +254 -0
- package/core/unicast.mjs +155 -0
- package/libs/xxhash32.mjs +65 -0
- package/package.json +5 -5
- package/rendering/NetworkRenderer.mjs +734 -0
- package/rendering/renderer-options.mjs +85 -0
- package/rendering/renderer-stores.mjs +234 -0
- package/rendering/visualizer.css +138 -0
- package/rendering/visualizer.html +60 -0
- package/rendering/visualizer.mjs +254 -0
- package/services/clock-v2.mjs +196 -0
- package/services/clock.mjs +144 -0
- package/services/converter.mjs +64 -0
- package/services/cryptos.mjs +83 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
import { NetworkRendererElements, NetworkRendererOptions } from './renderer-options.mjs';
|
|
2
|
+
import { Node, NodesStore, ConnectionsStore } from './renderer-stores.mjs';
|
|
3
|
+
import { NODE, DISCOVERY } from '../core/config.mjs';
|
|
4
|
+
|
|
5
|
+
export class NetworkRenderer {
|
|
6
|
+
initCameraZ = 1400;
|
|
7
|
+
avoidAutoZoomUntil = 0;
|
|
8
|
+
lastAutoZoomDistance = 0;
|
|
9
|
+
fpsCountElement = document.getElementById('fpsCount');
|
|
10
|
+
maxVisibleConnections = 500; // to avoid performance issues
|
|
11
|
+
autoRotateEnabled = true;
|
|
12
|
+
autoRotateSpeed = .0005; // .001
|
|
13
|
+
autoRotateDelay = 3000; // delay before activating auto-rotation after mouse event
|
|
14
|
+
elements;
|
|
15
|
+
options;
|
|
16
|
+
onNodeLeftClick = null;
|
|
17
|
+
onNodeRightClick = null;
|
|
18
|
+
colors = {
|
|
19
|
+
background: 0x1a1a1a,
|
|
20
|
+
currentPeer: 0xFFD700,
|
|
21
|
+
hoveredPeer: 0xFF4500,
|
|
22
|
+
connectedPeerNeighbor: 0x4CAF50,
|
|
23
|
+
connectingPeerNeighbor: 0x03b5fc,
|
|
24
|
+
knownPeer: 0x7d7d7d,
|
|
25
|
+
publicNode: 0xffffff,
|
|
26
|
+
publicNodeBorder: 0xffffff,
|
|
27
|
+
twitchUser: 0xf216e4,
|
|
28
|
+
// CONNECTIONS
|
|
29
|
+
connection: 0x666666, // gray
|
|
30
|
+
currentPeerConnection: 0x4CAF50, // green
|
|
31
|
+
// DIRECT MESSAGES
|
|
32
|
+
traveledConnection: [
|
|
33
|
+
0x2803fc, // blue
|
|
34
|
+
0x0328fc, // light blue
|
|
35
|
+
0x035afc // lighter blue
|
|
36
|
+
],
|
|
37
|
+
toTravelConnection: 0x03b5fc, // even lighter blue for the remaining distance
|
|
38
|
+
// GOSSIP MESSAGES
|
|
39
|
+
gossipIncomingColor: 0xf542f5, // fuchsia
|
|
40
|
+
gossipOutgoingColor: 0xc24cc2, // dark fuchsia
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Internal state
|
|
44
|
+
scene = null;
|
|
45
|
+
camera = null;
|
|
46
|
+
renderer = null;
|
|
47
|
+
raycaster = new THREE.Raycaster();
|
|
48
|
+
mouse = new THREE.Vector2();
|
|
49
|
+
|
|
50
|
+
// Data structures
|
|
51
|
+
instancedMesh = null;
|
|
52
|
+
/** @type {NodesStore} */ nodesStore;
|
|
53
|
+
/** @type {ConnectionsStore} */ connectionsStore;
|
|
54
|
+
|
|
55
|
+
updateBatchMax = 500; // auto-adjusted based on fps
|
|
56
|
+
initUpdateBatchMax = 500;
|
|
57
|
+
|
|
58
|
+
// State
|
|
59
|
+
currentPeerId = null;
|
|
60
|
+
hoveredNodeId = null;
|
|
61
|
+
hoveredNodeRepaintInterval = null;
|
|
62
|
+
isAnimating = false;
|
|
63
|
+
isPhysicPaused = false;
|
|
64
|
+
|
|
65
|
+
/** This class is responsible for rendering the network visualization.
|
|
66
|
+
* @param {string} containerId
|
|
67
|
+
* @param {NetworkRendererOptions} options
|
|
68
|
+
* @param {NetworkRendererElements} rendererElements */
|
|
69
|
+
constructor(containerId, options, rendererElements) {
|
|
70
|
+
this.containerId = containerId;
|
|
71
|
+
this.elements = new NetworkRendererElements();
|
|
72
|
+
for (const key in rendererElements) if (key in this.elements) this.elements[key] = rendererElements[key];
|
|
73
|
+
|
|
74
|
+
this.options = new NetworkRendererOptions();
|
|
75
|
+
for (const key in options) if (key in this.options) this.options[key] = options[key];
|
|
76
|
+
|
|
77
|
+
this.init();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
init() {
|
|
81
|
+
// Scene
|
|
82
|
+
this.scene = new THREE.Scene();
|
|
83
|
+
this.scene.background = new THREE.Color(this.colors.background);
|
|
84
|
+
|
|
85
|
+
// Stores (nodes + connections)
|
|
86
|
+
this.nodesStore = new NodesStore();
|
|
87
|
+
this.connectionsStore = new ConnectionsStore(this.nodesStore, this.scene);
|
|
88
|
+
|
|
89
|
+
// Camera
|
|
90
|
+
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
|
|
91
|
+
this.camera.position.set(0, 0, this.initCameraZ);
|
|
92
|
+
|
|
93
|
+
// Renderer
|
|
94
|
+
const { antialias, precision } = this.options;
|
|
95
|
+
this.renderer = new THREE.WebGLRenderer({ antialias, precision });
|
|
96
|
+
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|
97
|
+
const container = document.getElementById(this.containerId);
|
|
98
|
+
if (container) container.appendChild(this.renderer.domElement);
|
|
99
|
+
else document.body.appendChild(this.renderer.domElement);
|
|
100
|
+
|
|
101
|
+
this.#setupControls();
|
|
102
|
+
|
|
103
|
+
this.elements.modeSwitchBtn.textContent = this.options.mode === '2d' ? '2D' : '3D';
|
|
104
|
+
window.addEventListener('resize', () => {
|
|
105
|
+
this.camera.aspect = window.innerWidth / window.innerHeight;
|
|
106
|
+
this.camera.updateProjectionMatrix();
|
|
107
|
+
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|
108
|
+
});
|
|
109
|
+
this.renderer.domElement.addEventListener('mouseleave', () => {
|
|
110
|
+
if (this.hoveredNodeId) this.hoveredNodeId = null;
|
|
111
|
+
this.renderer.domElement.style.cursor = 'default';
|
|
112
|
+
this.#hideTooltip();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// PREPARE MESH INSTANCE
|
|
116
|
+
this.nodeCount = 0;
|
|
117
|
+
this.nodeIndexMap = {}; // id → instanceIndex
|
|
118
|
+
this.indexNodeMap = {}; // instanceIndex → id
|
|
119
|
+
this.nodeBorders = {}; // id → borderMesh
|
|
120
|
+
|
|
121
|
+
const geometry = new THREE.SphereGeometry(this.options.nodeRadius, 6, 3);
|
|
122
|
+
const material = new THREE.MeshBasicMaterial();
|
|
123
|
+
this.instancedMesh = new THREE.InstancedMesh(geometry, material, 50000);
|
|
124
|
+
this.instancedMesh.instanceColor = new THREE.InstancedBufferAttribute(new Float32Array(50000 * 3), 3);
|
|
125
|
+
this.instancedMesh.count = 0;
|
|
126
|
+
this.scene.add(this.instancedMesh);
|
|
127
|
+
|
|
128
|
+
if (this.isAnimating) return;
|
|
129
|
+
this.isAnimating = true;
|
|
130
|
+
this.#animate();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Public API methods
|
|
134
|
+
#createMeshBorder = (nodeMesh, color = this.colors.publicNodeBorder) => {
|
|
135
|
+
const marginBetween = this.options.nodeBorderRadius * 2;
|
|
136
|
+
const borderGeometry = new THREE.RingGeometry(
|
|
137
|
+
this.options.nodeRadius + marginBetween,
|
|
138
|
+
this.options.nodeRadius + marginBetween + this.options.nodeBorderRadius,
|
|
139
|
+
16
|
|
140
|
+
);
|
|
141
|
+
const borderMaterial = new THREE.MeshBasicMaterial({
|
|
142
|
+
color,
|
|
143
|
+
side: THREE.DoubleSide,
|
|
144
|
+
transparent: true,
|
|
145
|
+
opacity: .33
|
|
146
|
+
});
|
|
147
|
+
const borderMesh = new THREE.Mesh(borderGeometry, borderMaterial);
|
|
148
|
+
borderMesh.position.copy(nodeMesh.position);
|
|
149
|
+
borderMesh.lookAt(this.camera.position);
|
|
150
|
+
this.scene.add(borderMesh);
|
|
151
|
+
//nodeMesh.userData.border = borderMesh;
|
|
152
|
+
return borderMesh;
|
|
153
|
+
}
|
|
154
|
+
addOrUpdateNode(id, status = 'known', isPublic = false, neighbors = []) {
|
|
155
|
+
const existingNode = this.nodesStore.get(id);
|
|
156
|
+
if (!existingNode) { // Create new node
|
|
157
|
+
const newNode = new Node(id, status, isPublic, neighbors);
|
|
158
|
+
this.nodesStore.add(newNode);
|
|
159
|
+
|
|
160
|
+
// Get next available index for this node
|
|
161
|
+
const instanceIndex = this.nodeCount++; // Tu auras besoin d'un compteur this.nodeCount = 0
|
|
162
|
+
this.instancedMesh.count = this.nodeCount;
|
|
163
|
+
this.nodeIndexMap[id] = instanceIndex; // Map node id → instance index
|
|
164
|
+
this.indexNodeMap[instanceIndex] = id; // Map instance index → node id
|
|
165
|
+
|
|
166
|
+
// Set position in instanced mesh
|
|
167
|
+
const pos = newNode.position;
|
|
168
|
+
const matrix = new THREE.Matrix4();
|
|
169
|
+
matrix.setPosition(pos.x, pos.y, pos.z);
|
|
170
|
+
this.instancedMesh.setMatrixAt(instanceIndex, matrix);
|
|
171
|
+
|
|
172
|
+
// Set color
|
|
173
|
+
const color = new THREE.Color(this.#getNodeColor(id));
|
|
174
|
+
this.instancedMesh.setColorAt(instanceIndex, color);
|
|
175
|
+
|
|
176
|
+
// Handle borders (séparément, comme avant)
|
|
177
|
+
if (isPublic) this.nodeBorders[id] = this.#createMeshBorder({ position: pos }, this.colors.publicNodeBorder);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Update existing node
|
|
182
|
+
const instanceIndex = this.nodeIndexMap[id];
|
|
183
|
+
const newColor = new THREE.Color(this.#getNodeColor(id));
|
|
184
|
+
this.instancedMesh.setColorAt(instanceIndex, newColor);
|
|
185
|
+
this.instancedMesh.instanceColor.needsUpdate = true;
|
|
186
|
+
|
|
187
|
+
let needBorderUpdate = existingNode.isPublic !== isPublic;
|
|
188
|
+
existingNode.status = status;
|
|
189
|
+
existingNode.isPublic = isPublic;
|
|
190
|
+
existingNode.neighbors = neighbors;
|
|
191
|
+
this.instancedMesh.instanceMatrix.needsUpdate = true;
|
|
192
|
+
if (!needBorderUpdate) return;
|
|
193
|
+
|
|
194
|
+
// Handle border updates
|
|
195
|
+
const existingBorder = this.nodeBorders[id];
|
|
196
|
+
if (isPublic) {
|
|
197
|
+
if (existingBorder) this.scene.remove(existingBorder);
|
|
198
|
+
this.nodeBorders[id] = this.#createMeshBorder({ position: existingNode.position }, this.colors.publicNodeBorder);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!existingBorder) return;
|
|
203
|
+
this.scene.remove(existingBorder);
|
|
204
|
+
delete this.nodeBorders[id];
|
|
205
|
+
}
|
|
206
|
+
removeNode(id) {
|
|
207
|
+
if (!this.nodesStore.has(id)) return; // Node doesn't exist
|
|
208
|
+
|
|
209
|
+
const instanceIndex = this.nodeIndexMap[id];
|
|
210
|
+
if (instanceIndex !== undefined) {
|
|
211
|
+
const lastIndex = this.nodeCount - 1;
|
|
212
|
+
|
|
213
|
+
if (instanceIndex !== lastIndex) {
|
|
214
|
+
// Récupérer l'ID du dernier nœud
|
|
215
|
+
const lastNodeId = this.indexNodeMap[lastIndex];
|
|
216
|
+
if (lastNodeId) {
|
|
217
|
+
// Copier les données du dernier nœud vers l'index à supprimer
|
|
218
|
+
const lastMatrix = new THREE.Matrix4();
|
|
219
|
+
const lastColor = new THREE.Color();
|
|
220
|
+
|
|
221
|
+
this.instancedMesh.getMatrixAt(lastIndex, lastMatrix);
|
|
222
|
+
this.instancedMesh.getColorAt(lastIndex, lastColor);
|
|
223
|
+
|
|
224
|
+
this.instancedMesh.setMatrixAt(instanceIndex, lastMatrix);
|
|
225
|
+
this.instancedMesh.setColorAt(instanceIndex, lastColor);
|
|
226
|
+
this.instancedMesh.instanceMatrix.needsUpdate = true;
|
|
227
|
+
this.instancedMesh.instanceColor.needsUpdate = true;
|
|
228
|
+
|
|
229
|
+
// Mettre à jour les mappings pour le nœud déplacé
|
|
230
|
+
this.nodeIndexMap[lastNodeId] = instanceIndex;
|
|
231
|
+
this.indexNodeMap[instanceIndex] = lastNodeId;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Nettoyer les mappings pour le nœud supprimé
|
|
236
|
+
delete this.nodeIndexMap[id];
|
|
237
|
+
delete this.indexNodeMap[lastIndex];
|
|
238
|
+
this.nodeCount--;
|
|
239
|
+
this.instancedMesh.count = this.nodeCount;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Gérer les borders
|
|
243
|
+
const border = this.nodeBorders[id];
|
|
244
|
+
if (border) {
|
|
245
|
+
this.scene.remove(border);
|
|
246
|
+
border.geometry.dispose();
|
|
247
|
+
border.material.dispose();
|
|
248
|
+
delete this.nodeBorders[id];
|
|
249
|
+
}
|
|
250
|
+
this.nodesStore.remove(id);
|
|
251
|
+
}
|
|
252
|
+
digestConnectionsArray(conns = [], displayNeighborsDegree = 1) {
|
|
253
|
+
const existingConnsKeys = {};
|
|
254
|
+
const drawLinesKeys = {};
|
|
255
|
+
const currentPeerNode = this.nodesStore.get(this.currentPeerId);
|
|
256
|
+
const cNeighbors = currentPeerNode?.neighbors || [];
|
|
257
|
+
for (const [fromId, toId] of conns) { // add new physicConnections
|
|
258
|
+
const { success, key } = this.connectionsStore.set(fromId, toId);
|
|
259
|
+
if (existingConnsKeys[key]) continue; // already processed
|
|
260
|
+
existingConnsKeys[key] = true; // store for control
|
|
261
|
+
|
|
262
|
+
const isOneOfThePeer = fromId === this.currentPeerId || toId === this.currentPeerId;
|
|
263
|
+
if (displayNeighborsDegree === 0 && !isOneOfThePeer) continue;
|
|
264
|
+
|
|
265
|
+
const [fromNode, toNode] = [this.nodesStore.get(fromId), this.nodesStore.get(toId)];
|
|
266
|
+
const [fNeighbors, tNeighbors] = [fromNode?.neighbors || [], toNode?.neighbors || []];
|
|
267
|
+
let isFirstDegree = cNeighbors.includes(fromId) || cNeighbors.includes(toId);
|
|
268
|
+
isFirstDegree = isFirstDegree || fNeighbors.includes(this.currentPeerId)
|
|
269
|
+
isFirstDegree = isFirstDegree || tNeighbors.includes(this.currentPeerId);
|
|
270
|
+
if (displayNeighborsDegree === 1 && !isFirstDegree) continue;
|
|
271
|
+
this.connectionsStore.updateOrAssignLineColor(fromId, toId);
|
|
272
|
+
drawLinesKeys[key] = true; // store for control
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// remove physicConnections that are not in the array
|
|
276
|
+
// remove visual lines that are not in the array
|
|
277
|
+
for (const connStr in this.connectionsStore.store) {
|
|
278
|
+
const peerConn = this.connectionsStore.store[connStr];
|
|
279
|
+
if (peerConn.isHovered) continue; // still hovered
|
|
280
|
+
if (peerConn.repaintIgnored) continue; // still ignored
|
|
281
|
+
if (!existingConnsKeys[connStr]) this.connectionsStore.unset(...connStr.split(':'));
|
|
282
|
+
else if (!drawLinesKeys[connStr] && peerConn.line) this.connectionsStore.unassignLine(...connStr.split(':'));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
displayDirectMessageRoute(relayerId, route = [], frameToIgnore = 30) {
|
|
286
|
+
const fto = Math.round(frameToIgnore * (this.frameCount / 60));
|
|
287
|
+
const maxTraveledColorIndex = this.colors.traveledConnection.length - 1;
|
|
288
|
+
let traveledIndex = 0;
|
|
289
|
+
let isRelayerIdPassed = false;
|
|
290
|
+
for (let i = 1; i < route.length; i++) {
|
|
291
|
+
const color = isRelayerIdPassed ? this.colors.toTravelConnection : this.colors.traveledConnection[traveledIndex];
|
|
292
|
+
this.connectionsStore.updateOrAssignLineColor(route[i - 1], route[i], color, .5, fto, true);
|
|
293
|
+
traveledIndex = Math.min(traveledIndex + 1, maxTraveledColorIndex);
|
|
294
|
+
if (route[i - 1] === relayerId) isRelayerIdPassed = true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
displayGossipMessageRoute(relayerId, senderId, topic = 'peer_connected', data, frameToIgnore = 25) {
|
|
298
|
+
const fto = Math.round(frameToIgnore * (this.frameCount / 60));
|
|
299
|
+
const fto2 = Math.round((frameToIgnore + 5) * (this.frameCount / 60));
|
|
300
|
+
this.connectionsStore.updateOrAssignLineColor(senderId, relayerId, this.colors.gossipOutgoingColor, .4, fto, true);
|
|
301
|
+
this.connectionsStore.updateOrAssignLineColor(relayerId, this.currentPeerId, this.colors.gossipIncomingColor, .8, fto2, true);
|
|
302
|
+
}
|
|
303
|
+
setCurrentPeer(peerId, clearNetworkOneChange = true) {
|
|
304
|
+
if (clearNetworkOneChange && peerId !== this.currentPeerId) {
|
|
305
|
+
this.clearNetwork();
|
|
306
|
+
this.avoidAutoZoomUntil = 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Reset previous current peer
|
|
310
|
+
if (this.currentPeerId && this.nodesStore.has(this.currentPeerId)) this.nodesStore.get(this.currentPeerId).status = 'known';
|
|
311
|
+
if (peerId && this.nodesStore.has(peerId)) this.nodesStore.get(peerId).status = 'current';
|
|
312
|
+
this.currentPeerId = peerId;
|
|
313
|
+
}
|
|
314
|
+
updateStats() {
|
|
315
|
+
const info = this.nodesStore.getInfo();
|
|
316
|
+
this.maxDistance = info.maxDistance;
|
|
317
|
+
this.elements.nodeCountElement.textContent = info.total;
|
|
318
|
+
const { connsCount, linesCount } = this.connectionsStore.getConnectionsCount();
|
|
319
|
+
this.elements.connectionsCountElement.textContent = connsCount;
|
|
320
|
+
this.elements.linesCountElement.textContent = linesCount;
|
|
321
|
+
this.elements.connectingCountElement.textContent = info.connecting;
|
|
322
|
+
const nonPublicNeighbors = info.connected - info.connectedPublic;
|
|
323
|
+
const isTargetReached = nonPublicNeighbors >= DISCOVERY.TARGET_NEIGHBORS_COUNT;
|
|
324
|
+
this.elements.neighborCountElement.textContent = `${nonPublicNeighbors} | +${info.connectedPublic} Public ${isTargetReached ? '🟢' : ''}`;
|
|
325
|
+
this.elements.publicNeighborCountElement.textContent = info.connectedPublic;
|
|
326
|
+
}
|
|
327
|
+
switchMode() {
|
|
328
|
+
this.options.mode = this.options.mode === '2d' ? '3d' : '2d';
|
|
329
|
+
// reset camera angle
|
|
330
|
+
this.camera.position.set(0, 0, 500);
|
|
331
|
+
this.camera.lookAt(0, 0, 0);
|
|
332
|
+
this.elements.modeSwitchBtn.textContent = this.options.mode === '2d' ? '2D' : '3D';
|
|
333
|
+
}
|
|
334
|
+
clearNetwork() {
|
|
335
|
+
// Clear data
|
|
336
|
+
this.nodesStore = new NodesStore(this.nodes);
|
|
337
|
+
this.connectionsStore.destroy();
|
|
338
|
+
this.connectionsStore = new ConnectionsStore(this.nodesStore, this.scene);
|
|
339
|
+
this.currentPeerId = null;
|
|
340
|
+
this.hoveredNodeId = null;
|
|
341
|
+
|
|
342
|
+
// Clear InstancedMesh nodes - Reset count to 0
|
|
343
|
+
this.nodeCount = 0;
|
|
344
|
+
this.instancedMesh.count = 0;
|
|
345
|
+
this.nodeIndexMap = {};
|
|
346
|
+
this.indexNodeMap = {};
|
|
347
|
+
|
|
348
|
+
// Clear borders
|
|
349
|
+
for (const id in this.nodeBorders) {
|
|
350
|
+
const border = this.nodeBorders[id];
|
|
351
|
+
this.scene.remove(border);
|
|
352
|
+
border.geometry.dispose();
|
|
353
|
+
border.material.dispose();
|
|
354
|
+
}
|
|
355
|
+
this.nodeBorders = {};
|
|
356
|
+
|
|
357
|
+
// reset camera
|
|
358
|
+
this.camera.position.set(0, 0, this.initCameraZ);
|
|
359
|
+
this.camera.lookAt(0, 0, 0);
|
|
360
|
+
this.updateBatchMax = this.initUpdateBatchMax; // reset rendering batch size
|
|
361
|
+
}
|
|
362
|
+
destroy() {
|
|
363
|
+
this.isAnimating = false;
|
|
364
|
+
this.scene.clear();
|
|
365
|
+
this.renderer.dispose();
|
|
366
|
+
if (this.renderer.domElement.parentNode) this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Internal methods
|
|
370
|
+
/** @param {string} axis @param {'3d'|'2d'|null} restrictToMode */
|
|
371
|
+
#autoRotate(axis = 'z', restrictToMode = null) {
|
|
372
|
+
if (!this.autoRotateEnabled || !this.isAnimating) return;
|
|
373
|
+
try { if (!restrictToMode || this.options.mode === restrictToMode) this.scene.rotation[axis] -= this.autoRotateSpeed;
|
|
374
|
+
} catch (error) { console.error('Error during auto-rotation:', error); }
|
|
375
|
+
}
|
|
376
|
+
#autoZoom(margin = 1.2) {
|
|
377
|
+
if (!this.isAnimating || this.options.mode !== '3d' || this.isPhysicPaused) return;
|
|
378
|
+
if (this.avoidAutoZoomUntil > Date.now()) return;
|
|
379
|
+
|
|
380
|
+
const maxDist = this.maxDistance * margin;
|
|
381
|
+
const fov = this.camera.fov * (Math.PI / 180);
|
|
382
|
+
const height = 2 * maxDist;
|
|
383
|
+
const distance = height / (2 * Math.tan(fov / 2));
|
|
384
|
+
const targetDistance = Math.max(distance * 1.2, this.initCameraZ);
|
|
385
|
+
const currentDistance = this.camera.position.length();
|
|
386
|
+
const zoomSpeed = .1;
|
|
387
|
+
const mode = currentDistance < targetDistance ? true : false; // true = zoom out | false = zoom in
|
|
388
|
+
const delta = Math.min(20, Math.abs(targetDistance - currentDistance) * zoomSpeed);
|
|
389
|
+
const newDistance = mode ? currentDistance + delta : currentDistance - delta;
|
|
390
|
+
|
|
391
|
+
if (Math.abs(newDistance - this.lastAutoZoomDistance) < 1) return;
|
|
392
|
+
this.lastAutoZoomDistance = newDistance;
|
|
393
|
+
|
|
394
|
+
this.camera.position.normalize().multiplyScalar(newDistance);
|
|
395
|
+
this.camera.lookAt(0, 0, 0);
|
|
396
|
+
}
|
|
397
|
+
#setupControls() {
|
|
398
|
+
let setupAutoRotateTimeout;
|
|
399
|
+
const initZoomSpeed2D = .1;
|
|
400
|
+
const maxZoomSpeed2D = 5;
|
|
401
|
+
let zoomSpeed2D = .1;
|
|
402
|
+
let zoomSpeedIncrement = .02;
|
|
403
|
+
let zoomSpeedIncrementFactor = .01;
|
|
404
|
+
let lastZoomDirection = null;
|
|
405
|
+
let isMouseDown = false;
|
|
406
|
+
let mouseDownGrabCursorTimeout = null;
|
|
407
|
+
let mouseButton = null;
|
|
408
|
+
let previousMousePosition = { x: 0, y: 0 };
|
|
409
|
+
|
|
410
|
+
const domElement = this.renderer.domElement;
|
|
411
|
+
domElement.addEventListener('mousedown', (e) => {
|
|
412
|
+
if (setupAutoRotateTimeout) clearTimeout(setupAutoRotateTimeout);
|
|
413
|
+
this.autoRotateEnabled = false;
|
|
414
|
+
setupAutoRotateTimeout = setTimeout(() => this.autoRotateEnabled = true, this.autoRotateDelay);
|
|
415
|
+
|
|
416
|
+
isMouseDown = true;
|
|
417
|
+
mouseButton = e.button;
|
|
418
|
+
previousMousePosition.x = e.clientX;
|
|
419
|
+
previousMousePosition.y = e.clientY;
|
|
420
|
+
if (mouseDownGrabCursorTimeout) clearTimeout(mouseDownGrabCursorTimeout);
|
|
421
|
+
mouseDownGrabCursorTimeout = setTimeout(() => domElement.style.cursor = 'grabbing', 200);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
domElement.addEventListener('mouseup', () => {
|
|
425
|
+
isMouseDown = false;
|
|
426
|
+
mouseButton = null;
|
|
427
|
+
zoomSpeed2D = initZoomSpeed2D;
|
|
428
|
+
lastZoomDirection = null;
|
|
429
|
+
if (mouseDownGrabCursorTimeout) clearTimeout(mouseDownGrabCursorTimeout);
|
|
430
|
+
setTimeout(() => domElement.style.cursor = 'default', 20);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
domElement.addEventListener('mousemove', (e) => {
|
|
434
|
+
if (isMouseDown) {
|
|
435
|
+
const deltaX = e.clientX - previousMousePosition.x;
|
|
436
|
+
const deltaY = e.clientY - previousMousePosition.y;
|
|
437
|
+
const mouseDirection = deltaY > 0 ? 'down' : 'up';
|
|
438
|
+
//console.log(`Mouse moved: ${mouseDirection} (${deltaX}, ${deltaY})`);
|
|
439
|
+
|
|
440
|
+
if (mouseButton === 2 && this.options.mode === '3d') { // Right mouse 3D - rotate
|
|
441
|
+
const rotationSpeed = 0.005;
|
|
442
|
+
const spherical = new THREE.Spherical();
|
|
443
|
+
spherical.setFromVector3(this.camera.position);
|
|
444
|
+
spherical.theta -= deltaX * rotationSpeed;
|
|
445
|
+
spherical.phi += deltaY * rotationSpeed;
|
|
446
|
+
spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi));
|
|
447
|
+
this.camera.position.setFromSpherical(spherical);
|
|
448
|
+
this.camera.lookAt(0, 0, 0);
|
|
449
|
+
} else if (mouseDirection && mouseButton === 2 && this.options.mode === '2d') { // Right mouse 2D - Zoom
|
|
450
|
+
// log increase zoom speed on same direction, until max
|
|
451
|
+
const oppositeDirection = !lastZoomDirection
|
|
452
|
+
|| lastZoomDirection === 'out' && mouseDirection === 'down'
|
|
453
|
+
|| lastZoomDirection === 'in' && mouseDirection === 'up';
|
|
454
|
+
|
|
455
|
+
if (oppositeDirection && zoomSpeed2D === initZoomSpeed2D)
|
|
456
|
+
lastZoomDirection = mouseDirection === 'up' ? 'out' : 'in'; // handle direction switch
|
|
457
|
+
|
|
458
|
+
const zf = zoomSpeed2D * zoomSpeedIncrementFactor;
|
|
459
|
+
const upperSpeed = zoomSpeed2D + zf + zoomSpeedIncrement;
|
|
460
|
+
const lowerSpeed = zoomSpeed2D - zf - zoomSpeedIncrement;
|
|
461
|
+
if (lastZoomDirection === 'out')
|
|
462
|
+
if (mouseDirection === 'down') zoomSpeed2D = Math.max(0.1, lowerSpeed);
|
|
463
|
+
else zoomSpeed2D = Math.min(maxZoomSpeed2D, upperSpeed);
|
|
464
|
+
else if (lastZoomDirection === 'in')
|
|
465
|
+
if (mouseDirection === 'up') zoomSpeed2D = Math.max(0.1, lowerSpeed);
|
|
466
|
+
else zoomSpeed2D = Math.min(maxZoomSpeed2D, upperSpeed);
|
|
467
|
+
|
|
468
|
+
//console.log(`Zoom speed: ${zoomSpeed2D.toFixed(2)}`);
|
|
469
|
+
|
|
470
|
+
const forward = new THREE.Vector3();
|
|
471
|
+
this.camera.getWorldDirection(forward);
|
|
472
|
+
this.camera.position.add(forward.multiplyScalar(deltaY * zoomSpeed2D));
|
|
473
|
+
} else if (mouseButton === 0) { // Left mouse - pan
|
|
474
|
+
const panSpeed = 1;
|
|
475
|
+
const right = new THREE.Vector3();
|
|
476
|
+
const up = new THREE.Vector3();
|
|
477
|
+
this.camera.getWorldDirection(right);
|
|
478
|
+
right.cross(this.camera.up).normalize();
|
|
479
|
+
up.copy(this.camera.up);
|
|
480
|
+
|
|
481
|
+
const panVector = right.multiplyScalar(-deltaX * panSpeed)
|
|
482
|
+
.add(up.multiplyScalar(deltaY * panSpeed));
|
|
483
|
+
this.camera.position.add(panVector);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
previousMousePosition.x = e.clientX;
|
|
487
|
+
previousMousePosition.y = e.clientY;
|
|
488
|
+
this.avoidAutoZoomUntil = Date.now() + 5000;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
this.#handleMouseMove(e);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
domElement.addEventListener('wheel', (e) => {
|
|
495
|
+
e.preventDefault();
|
|
496
|
+
const zoomSpeed = .4;
|
|
497
|
+
const forward = new THREE.Vector3();
|
|
498
|
+
this.camera.getWorldDirection(forward);
|
|
499
|
+
this.camera.position.add(forward.multiplyScalar(e.deltaY * -zoomSpeed));
|
|
500
|
+
this.avoidAutoZoomUntil = Date.now() + 5000;
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
this.elements.modeSwitchBtn.addEventListener('click', () => this.switchMode());
|
|
504
|
+
domElement.addEventListener('click', () => this.onNodeLeftClick?.(this.hoveredNodeId));
|
|
505
|
+
domElement.addEventListener('contextmenu', (e) => {
|
|
506
|
+
e.preventDefault();
|
|
507
|
+
if (domElement.style.cursor === 'grabbing') return;
|
|
508
|
+
this.onNodeRightClick?.(this.hoveredNodeId)
|
|
509
|
+
});
|
|
510
|
+
window.addEventListener('keydown', (e) => {
|
|
511
|
+
if (e.code === 'Space') this.isPhysicPaused = !this.isPhysicPaused;
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
#handleMouseMove(event) {
|
|
515
|
+
if (this.renderer.domElement.style.cursor === 'grabbing') return;
|
|
516
|
+
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
|
517
|
+
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
|
518
|
+
|
|
519
|
+
this.raycaster.setFromCamera(this.mouse, this.camera);
|
|
520
|
+
const intersects = this.raycaster.intersectObjects(this.scene.children);
|
|
521
|
+
|
|
522
|
+
let foundNode = null;
|
|
523
|
+
for (const intersect of intersects) {
|
|
524
|
+
if (intersect.object !== this.instancedMesh || intersect.instanceId === undefined) continue;
|
|
525
|
+
foundNode = this.indexNodeMap[intersect.instanceId];
|
|
526
|
+
if (foundNode) break;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (foundNode === this.hoveredNodeId) return; // No change in hovered node
|
|
530
|
+
|
|
531
|
+
// Reset previous hovered node
|
|
532
|
+
if (this.hoveredNodeId) {
|
|
533
|
+
clearInterval(this.hoveredNodeRepaintInterval);
|
|
534
|
+
this.hoveredNodeRepaintInterval = null;
|
|
535
|
+
const prevInstanceIndex = this.nodeIndexMap[this.hoveredNodeId];
|
|
536
|
+
if (prevInstanceIndex !== undefined) {
|
|
537
|
+
const originalColor = new THREE.Color(this.#getNodeColor(this.hoveredNodeId));
|
|
538
|
+
this.instancedMesh.setColorAt(prevInstanceIndex, originalColor);
|
|
539
|
+
this.instancedMesh.instanceColor.needsUpdate = true;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
this.hoveredNodeId = foundNode;
|
|
544
|
+
this.#updateHoveredNodeInfo(event.clientX, event.clientY);
|
|
545
|
+
}
|
|
546
|
+
#updateHoveredNodeInfo(clientX, clientY) {
|
|
547
|
+
if (!this.hoveredNodeId) {
|
|
548
|
+
this.renderer.domElement.style.cursor = 'default';
|
|
549
|
+
this.#hideTooltip();
|
|
550
|
+
this.connectionsStore.resetHovered();
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
this.#showTooltip(clientX, clientY, this.hoveredNodeId);
|
|
555
|
+
if (this.hoveredNodeId === this.currentPeerId) return;
|
|
556
|
+
|
|
557
|
+
// Set hover color
|
|
558
|
+
const instanceIndex = this.nodeIndexMap[this.hoveredNodeId];
|
|
559
|
+
if (instanceIndex === undefined) return;
|
|
560
|
+
|
|
561
|
+
const hoverColor = new THREE.Color(this.colors.hoveredPeer);
|
|
562
|
+
this.instancedMesh.setColorAt(instanceIndex, hoverColor);
|
|
563
|
+
this.instancedMesh.instanceColor.needsUpdate = true;
|
|
564
|
+
this.renderer.domElement.style.cursor = 'pointer';
|
|
565
|
+
|
|
566
|
+
// Set hovered connections flag
|
|
567
|
+
this.hoveredNodeRepaintInterval = setInterval(() => {
|
|
568
|
+
const hoveredNode = this.nodesStore.get(this.hoveredNodeId);
|
|
569
|
+
console.log(`Repainting hovered node ${this.hoveredNodeId} and its neighbors`);
|
|
570
|
+
const neighbors = hoveredNode ? hoveredNode.neighbors : [];
|
|
571
|
+
for (const toId of neighbors) this.connectionsStore.setHovered(toId, this.hoveredNodeId);
|
|
572
|
+
}, 60);
|
|
573
|
+
}
|
|
574
|
+
#showTooltip(x, y, nodeId, element = document.getElementById('tooltip')) {
|
|
575
|
+
const node = this.nodesStore.get(nodeId);
|
|
576
|
+
if (!node) return;
|
|
577
|
+
|
|
578
|
+
const json = {
|
|
579
|
+
Peer: nodeId,
|
|
580
|
+
Type: node.status,
|
|
581
|
+
NeighborsCount: node.neighbors.length,
|
|
582
|
+
Neighbors: node.neighbors.length > 0 ? node.neighbors : 'None',
|
|
583
|
+
IsPublic: node.isPublic
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
element.innerHTML = `<pre>${JSON.stringify(json, null, 2)}</pre>`;
|
|
587
|
+
element.style.left = x + 10 + 'px';
|
|
588
|
+
element.style.top = y + 10 + 'px';
|
|
589
|
+
element.style.display = 'block';
|
|
590
|
+
}
|
|
591
|
+
#hideTooltip(element = document.getElementById('tooltip')) {
|
|
592
|
+
element.style.display = 'none';
|
|
593
|
+
}
|
|
594
|
+
#getNodeColor(peerId) {
|
|
595
|
+
const { status, isPublic } = this.nodesStore.get(peerId);
|
|
596
|
+
const isTwitchUser = peerId.startsWith('F_');
|
|
597
|
+
if (status !== 'current' && isTwitchUser) return this.colors.twitchUser;
|
|
598
|
+
switch (status) {
|
|
599
|
+
case 'current': return this.colors.currentPeer;
|
|
600
|
+
case 'connected': return this.colors.connectedPeerNeighbor;
|
|
601
|
+
case 'connecting': return this.colors.connectingPeerNeighbor;
|
|
602
|
+
default: return isPublic ? this.colors.publicNode : this.colors.knownPeer;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
#getReducedBatch = (nodeIds) => {
|
|
606
|
+
const nodeIdsCount = nodeIds.length;
|
|
607
|
+
const batchSize = Math.floor(Math.min(nodeIdsCount, this.updateBatchMax));
|
|
608
|
+
if (batchSize >= nodeIdsCount) return { batchIds: nodeIds, forceMultiplier: 1 };
|
|
609
|
+
|
|
610
|
+
const result = [];
|
|
611
|
+
for (let i = 0; i < batchSize; i++)
|
|
612
|
+
result.push(nodeIds[i + Math.floor(Math.random() * (nodeIdsCount - i))]);
|
|
613
|
+
|
|
614
|
+
const batchIds = result.slice(0, batchSize);
|
|
615
|
+
const forceMultiplier = Math.round(Math.max(1, nodeIds.length / batchSize));
|
|
616
|
+
return { batchIds, forceMultiplier };
|
|
617
|
+
}
|
|
618
|
+
#updateNodesPositions(nodeIds = [], lockCurrentNodePosition = true) {
|
|
619
|
+
const { batchIds, forceMultiplier } = this.#getReducedBatch(nodeIds);
|
|
620
|
+
for (const id of batchIds) {
|
|
621
|
+
const [pos, vel] = [this.nodesStore.get(id)?.position, this.nodesStore.get(id)?.velocity];
|
|
622
|
+
const node = this.nodesStore.get(id);
|
|
623
|
+
const instanceIndex = this.nodeIndexMap[id];
|
|
624
|
+
if (!pos || !vel || !node || instanceIndex === undefined) continue;
|
|
625
|
+
|
|
626
|
+
let fx = 0, fy = 0, fz = 0;
|
|
627
|
+
|
|
628
|
+
// Repulsion between nodes
|
|
629
|
+
for (const otherId of [...batchIds, ...node.neighbors]) {
|
|
630
|
+
if (id === otherId) continue;
|
|
631
|
+
|
|
632
|
+
const otherNode = this.nodesStore.get(otherId);
|
|
633
|
+
const otherPos = otherNode?.position;
|
|
634
|
+
if (!otherPos || !otherNode) continue;
|
|
635
|
+
|
|
636
|
+
const dx = pos.x - otherPos.x;
|
|
637
|
+
const dy = pos.y - otherPos.y;
|
|
638
|
+
const dz = pos.z - otherPos.z;
|
|
639
|
+
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
640
|
+
if (distance > this.options.repulsionOpts.maxDistance) continue;
|
|
641
|
+
|
|
642
|
+
const force = this.options.repulsion * (distance * distance + 1); // +1 to avoid division by zero
|
|
643
|
+
fx += (dx / distance) * force;
|
|
644
|
+
fy += (dy / distance) * force;
|
|
645
|
+
fz += (dz / distance) * force;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Attraction along physicConnections
|
|
649
|
+
for (const neighborId of node.neighbors) {
|
|
650
|
+
const neighborPos = this.nodesStore.get(neighborId)?.position;
|
|
651
|
+
if (!neighborPos) continue;
|
|
652
|
+
|
|
653
|
+
const dx = neighborPos.x - pos.x;
|
|
654
|
+
const dy = neighborPos.y - pos.y;
|
|
655
|
+
const dz = neighborPos.z - pos.z;
|
|
656
|
+
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
657
|
+
if (distance < this.options.attractionOpts.minDistance) continue;
|
|
658
|
+
|
|
659
|
+
const force = distance * this.options.attraction;
|
|
660
|
+
fx += (dx / distance) * force;
|
|
661
|
+
fy += (dy / distance) * force;
|
|
662
|
+
fz += (dz / distance) * force;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Center force
|
|
666
|
+
fx += -pos.x * this.options.centerForce;
|
|
667
|
+
fy += -pos.y * this.options.centerForce;
|
|
668
|
+
fz += -pos.z * this.options.centerForce;
|
|
669
|
+
|
|
670
|
+
// Update velocity
|
|
671
|
+
vel.x = (vel.x + fx) * this.options.damping;
|
|
672
|
+
vel.y = (vel.y + fy) * this.options.damping;
|
|
673
|
+
vel.z = (vel.z + fz) * this.options.damping;
|
|
674
|
+
|
|
675
|
+
// Limit velocity
|
|
676
|
+
const speed = Math.sqrt(vel.x * vel.x + vel.y * vel.y + vel.z * vel.z);
|
|
677
|
+
if (speed > this.options.maxVelocity) {
|
|
678
|
+
vel.x = (vel.x / speed) * this.options.maxVelocity * forceMultiplier;
|
|
679
|
+
vel.y = (vel.y / speed) * this.options.maxVelocity * forceMultiplier;
|
|
680
|
+
vel.z = (vel.z / speed) * this.options.maxVelocity * forceMultiplier;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Update position
|
|
684
|
+
pos.x += vel.x;
|
|
685
|
+
pos.y += vel.y;
|
|
686
|
+
pos.z += vel.z;
|
|
687
|
+
if (this.currentPeerId === id && lockCurrentNodePosition) for (const key of ['x', 'y', 'z']) pos[key] = 0;
|
|
688
|
+
|
|
689
|
+
// Update visual object
|
|
690
|
+
const matrix = new THREE.Matrix4();
|
|
691
|
+
const visualZ = this.options.mode === '3d' ? pos.z : 0;
|
|
692
|
+
matrix.setPosition(pos.x, pos.y, visualZ);
|
|
693
|
+
this.instancedMesh.setMatrixAt(instanceIndex, matrix);
|
|
694
|
+
|
|
695
|
+
// Update border position
|
|
696
|
+
const border = this.nodeBorders[id];
|
|
697
|
+
if (!border) continue;
|
|
698
|
+
border.position.set(pos.x, pos.y, visualZ);
|
|
699
|
+
border.lookAt(this.camera.position);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
lastPhysicUpdate = 0;
|
|
704
|
+
frameCount = 60;
|
|
705
|
+
lastFpsUpdate = 0;
|
|
706
|
+
#animate() {
|
|
707
|
+
if (!this.isAnimating) return;
|
|
708
|
+
|
|
709
|
+
const currentTime = performance.now();
|
|
710
|
+
this.frameCount++;
|
|
711
|
+
if (currentTime - this.lastFpsUpdate >= 1000) {
|
|
712
|
+
if (this.frameCount < 60 * .98) this.updateBatchMax = Math.round(Math.max(100, this.updateBatchMax * .9));
|
|
713
|
+
this.fpsCountElement.textContent = this.frameCount;
|
|
714
|
+
this.frameCount = 0;
|
|
715
|
+
this.lastFpsUpdate = currentTime;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const shouldUpdatePhysic = currentTime - this.lastPhysicUpdate >= 1000 / 60;
|
|
719
|
+
if (shouldUpdatePhysic && !this.isPhysicPaused) {
|
|
720
|
+
this.lastPhysicUpdate = currentTime;
|
|
721
|
+
const nodeIds = this.nodesStore.getNodesIds();
|
|
722
|
+
this.#updateNodesPositions(nodeIds);
|
|
723
|
+
this.#autoRotate();
|
|
724
|
+
this.#autoZoom();
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
this.connectionsStore.updateConnections(this.currentPeerId, this.hoveredNodeId, this.colors, this.options.mode);
|
|
728
|
+
this.instancedMesh.instanceMatrix.needsUpdate = true;
|
|
729
|
+
this.instancedMesh.instanceColor.needsUpdate = true;
|
|
730
|
+
this.renderer.render(this.scene, this.camera);
|
|
731
|
+
|
|
732
|
+
requestAnimationFrame(() => this.#animate());
|
|
733
|
+
}
|
|
734
|
+
}
|