@kage-core/kage-graph-mcp 1.1.22 → 1.1.24
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 +110 -466
- package/dist/daemon.js +5 -1
- package/package.json +2 -1
- package/viewer/app.js +692 -10
- package/viewer/index.html +12 -4
- package/viewer/styles.css +36 -3
package/viewer/app.js
CHANGED
|
@@ -37,6 +37,32 @@
|
|
|
37
37
|
idleFrames: 0,
|
|
38
38
|
adjacency: new Map()
|
|
39
39
|
},
|
|
40
|
+
three: {
|
|
41
|
+
THREE: null,
|
|
42
|
+
loading: null,
|
|
43
|
+
failed: false,
|
|
44
|
+
renderer: null,
|
|
45
|
+
scene: null,
|
|
46
|
+
camera: null,
|
|
47
|
+
root: null,
|
|
48
|
+
nodeGroup: null,
|
|
49
|
+
edgeGroup: null,
|
|
50
|
+
nodeById: new Map(),
|
|
51
|
+
edgeRefs: [],
|
|
52
|
+
nodeTextureCache: new Map(),
|
|
53
|
+
physicsTick: 0,
|
|
54
|
+
physicsIdle: 0,
|
|
55
|
+
hoverNode: null,
|
|
56
|
+
pointer: null,
|
|
57
|
+
drag: null,
|
|
58
|
+
raycaster: null,
|
|
59
|
+
raf: null,
|
|
60
|
+
distance: 850,
|
|
61
|
+
target: { x: 0, y: 0, z: 0 },
|
|
62
|
+
rotationX: -0.20,
|
|
63
|
+
rotationY: 0.24,
|
|
64
|
+
lastPointerEvent: null
|
|
65
|
+
},
|
|
40
66
|
pan: null
|
|
41
67
|
};
|
|
42
68
|
|
|
@@ -84,6 +110,7 @@
|
|
|
84
110
|
selectionStatus: document.getElementById("selectionStatus"),
|
|
85
111
|
searchInput: document.getElementById("searchInput"),
|
|
86
112
|
viewMode: document.getElementById("viewMode"),
|
|
113
|
+
renderMode: document.getElementById("renderMode"),
|
|
87
114
|
typeFilter: document.getElementById("typeFilter"),
|
|
88
115
|
relationFilter: document.getElementById("relationFilter"),
|
|
89
116
|
scopeFilter: document.getElementById("scopeFilter"),
|
|
@@ -93,7 +120,10 @@
|
|
|
93
120
|
zoomOut: document.getElementById("zoomOut"),
|
|
94
121
|
zoomIn: document.getElementById("zoomIn"),
|
|
95
122
|
fitView: document.getElementById("fitView"),
|
|
123
|
+
interactionHint: document.getElementById("interactionHint"),
|
|
124
|
+
graphWrap: document.getElementById("graphCanvasWrap"),
|
|
96
125
|
canvas: document.getElementById("graphCanvas"),
|
|
126
|
+
threeGraph: document.getElementById("threeGraph"),
|
|
97
127
|
tooltip: document.getElementById("graphTooltip"),
|
|
98
128
|
svg: document.getElementById("graphSvg"),
|
|
99
129
|
nodeLayer: document.getElementById("nodeLayer"),
|
|
@@ -122,33 +152,42 @@
|
|
|
122
152
|
els.graphFile.addEventListener("change", handleFile);
|
|
123
153
|
els.searchInput.addEventListener("input", scheduleRender);
|
|
124
154
|
els.viewMode.addEventListener("change", render);
|
|
155
|
+
els.renderMode.addEventListener("change", function () {
|
|
156
|
+
state.lastVisibleSignature = "";
|
|
157
|
+
render();
|
|
158
|
+
});
|
|
125
159
|
els.typeFilter.addEventListener("change", render);
|
|
126
160
|
els.relationFilter.addEventListener("change", render);
|
|
127
161
|
els.scopeFilter.addEventListener("change", render);
|
|
128
162
|
els.maxNodes.addEventListener("change", render);
|
|
129
163
|
els.showDependencies.addEventListener("change", render);
|
|
130
|
-
els.zoomOut.addEventListener("click", function () {
|
|
131
|
-
els.zoomIn.addEventListener("click", function () {
|
|
132
|
-
els.fitView.addEventListener("click",
|
|
164
|
+
els.zoomOut.addEventListener("click", function () { zoomGraph(0.82); });
|
|
165
|
+
els.zoomIn.addEventListener("click", function () { zoomGraph(1.22); });
|
|
166
|
+
els.fitView.addEventListener("click", fitActiveGraph);
|
|
133
167
|
els.canvas.addEventListener("mousedown", startCanvasPointer);
|
|
134
168
|
els.canvas.addEventListener("mousemove", moveCanvasPointer);
|
|
135
169
|
els.canvas.addEventListener("mouseup", endCanvasPointer);
|
|
136
170
|
els.canvas.addEventListener("mouseleave", leaveCanvasPointer);
|
|
137
171
|
els.canvas.addEventListener("wheel", handleCanvasWheel, { passive: false });
|
|
138
172
|
els.canvas.addEventListener("dblclick", handleCanvasDoubleClick);
|
|
173
|
+
els.threeGraph.addEventListener("mousedown", startThreePointer);
|
|
174
|
+
els.threeGraph.addEventListener("mousemove", moveThreePointer);
|
|
175
|
+
els.threeGraph.addEventListener("mouseup", endThreePointer);
|
|
176
|
+
els.threeGraph.addEventListener("mouseleave", leaveThreePointer);
|
|
177
|
+
els.threeGraph.addEventListener("wheel", handleThreeWheel, { passive: false });
|
|
178
|
+
els.threeGraph.addEventListener("dblclick", handleThreeDoubleClick);
|
|
139
179
|
els.svg.addEventListener("mousedown", startPan);
|
|
140
180
|
els.svg.addEventListener("click", handleSvgClick);
|
|
141
181
|
els.svg.addEventListener("wheel", handleWheelZoom, { passive: false });
|
|
142
182
|
window.addEventListener("mousemove", continuePan);
|
|
143
183
|
window.addEventListener("mouseup", endPan);
|
|
144
184
|
window.addEventListener("resize", function () {
|
|
145
|
-
|
|
146
|
-
fitCanvas();
|
|
147
|
-
drawCanvasGraph();
|
|
185
|
+
resizeActiveGraph();
|
|
148
186
|
});
|
|
149
187
|
els.resetView.addEventListener("click", function () {
|
|
150
188
|
els.searchInput.value = "";
|
|
151
189
|
els.viewMode.value = "combined";
|
|
190
|
+
els.renderMode.value = "2d";
|
|
152
191
|
els.typeFilter.value = "";
|
|
153
192
|
els.relationFilter.value = "";
|
|
154
193
|
els.scopeFilter.value = "signal";
|
|
@@ -227,6 +266,7 @@
|
|
|
227
266
|
function loadFromUrlParams() {
|
|
228
267
|
var params = new URLSearchParams(window.location.search);
|
|
229
268
|
applyRequestedView(params.get("view") || params.get("mode"));
|
|
269
|
+
applyRequestedRenderMode(params.get("render") || params.get("graphMode"));
|
|
230
270
|
var memoryGraphPaths = splitParamValues(params.getAll("graph"));
|
|
231
271
|
var codeGraphPaths = splitParamValues(params.getAll("code"));
|
|
232
272
|
var graphPaths = memoryGraphPaths.concat(codeGraphPaths);
|
|
@@ -272,7 +312,7 @@
|
|
|
272
312
|
function loadHostedDefault() {
|
|
273
313
|
setAutoLoad("loading hosted repo graph", false);
|
|
274
314
|
Promise.all([
|
|
275
|
-
|
|
315
|
+
loadGraphPath("./data/kage/graph.json"),
|
|
276
316
|
loadGraphPath("./data/kage/code_graph/graph.json"),
|
|
277
317
|
fetchJson("./data/kage/metrics.json").catch(function () { return null; }),
|
|
278
318
|
fetchJson("./data/kage/inbox.json").catch(function () { return null; })
|
|
@@ -829,7 +869,7 @@
|
|
|
829
869
|
var graphChanged = nextSignature !== state.lastVisibleSignature;
|
|
830
870
|
state.lastVisibleSignature = nextSignature;
|
|
831
871
|
|
|
832
|
-
|
|
872
|
+
renderActiveGraph(graphChanged);
|
|
833
873
|
renderLists();
|
|
834
874
|
renderDetails();
|
|
835
875
|
renderMetrics();
|
|
@@ -1088,9 +1128,69 @@
|
|
|
1088
1128
|
}));
|
|
1089
1129
|
}
|
|
1090
1130
|
|
|
1091
|
-
function
|
|
1092
|
-
|
|
1131
|
+
function activeRenderMode() {
|
|
1132
|
+
return els.renderMode && els.renderMode.value === "3d" ? "3d" : "2d";
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function renderActiveGraph(graphChanged) {
|
|
1136
|
+
var mode = activeRenderMode();
|
|
1137
|
+
updateGraphSurfaceMode(mode);
|
|
1093
1138
|
syncSimulationGraph(graphChanged);
|
|
1139
|
+
if (mode === "3d") {
|
|
1140
|
+
stopSimulation();
|
|
1141
|
+
renderThreeGraph(graphChanged);
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
stopThreeGraph();
|
|
1145
|
+
renderCanvasGraph(graphChanged, true);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function updateGraphSurfaceMode(mode) {
|
|
1149
|
+
if (els.graphWrap) {
|
|
1150
|
+
els.graphWrap.classList.toggle("mode-3d", mode === "3d");
|
|
1151
|
+
els.graphWrap.classList.toggle("mode-2d", mode !== "3d");
|
|
1152
|
+
}
|
|
1153
|
+
if (els.interactionHint) {
|
|
1154
|
+
els.interactionHint.textContent = mode === "3d"
|
|
1155
|
+
? "drag orb or node / wheel zoom / click node"
|
|
1156
|
+
: "drag canvas / wheel zoom / click node";
|
|
1157
|
+
}
|
|
1158
|
+
if (els.tooltip) els.tooltip.classList.remove("visible");
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function resizeActiveGraph() {
|
|
1162
|
+
if (activeRenderMode() === "3d") {
|
|
1163
|
+
resizeThreeGraph();
|
|
1164
|
+
renderThreeFrame();
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
resizeCanvas();
|
|
1168
|
+
fitCanvas();
|
|
1169
|
+
drawCanvasGraph();
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function fitActiveGraph() {
|
|
1173
|
+
if (activeRenderMode() === "3d") {
|
|
1174
|
+
fitThreeGraph();
|
|
1175
|
+
renderThreeFrame();
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
fitCanvas();
|
|
1179
|
+
drawCanvasGraph();
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function zoomGraph(factor) {
|
|
1183
|
+
if (activeRenderMode() === "3d") {
|
|
1184
|
+
zoomThreeGraph(factor);
|
|
1185
|
+
renderThreeFrame();
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
zoomCanvas(factor);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function renderCanvasGraph(graphChanged, alreadySynced) {
|
|
1192
|
+
resizeCanvas();
|
|
1193
|
+
if (!alreadySynced) syncSimulationGraph(graphChanged);
|
|
1094
1194
|
if (graphChanged) fitCanvas();
|
|
1095
1195
|
startSimulation();
|
|
1096
1196
|
drawCanvasGraph();
|
|
@@ -1817,6 +1917,15 @@
|
|
|
1817
1917
|
els.viewMode.value = normalized;
|
|
1818
1918
|
}
|
|
1819
1919
|
|
|
1920
|
+
function applyRequestedRenderMode(mode) {
|
|
1921
|
+
if (!mode) return;
|
|
1922
|
+
var normalized = String(mode).toLowerCase();
|
|
1923
|
+
if (normalized === "canvas") normalized = "2d";
|
|
1924
|
+
if (normalized === "space") normalized = "3d";
|
|
1925
|
+
if (["2d", "3d"].indexOf(normalized) === -1) return;
|
|
1926
|
+
els.renderMode.value = normalized;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1820
1929
|
function isMemoryCodeEdge(edge) {
|
|
1821
1930
|
return Boolean(edge && (edge.memory_code_link || isMemoryCodeRelation(edge.relation)));
|
|
1822
1931
|
}
|
|
@@ -2317,6 +2426,579 @@
|
|
|
2317
2426
|
state.sim.panY = height / 2 - ((minY + maxY) / 2) * state.sim.zoom;
|
|
2318
2427
|
}
|
|
2319
2428
|
|
|
2429
|
+
function ensureThree() {
|
|
2430
|
+
if (state.three.THREE) return Promise.resolve(state.three.THREE);
|
|
2431
|
+
if (state.three.loading) return state.three.loading;
|
|
2432
|
+
state.three.failed = false;
|
|
2433
|
+
state.three.loading = import("/vendor/three/build/three.module.min.js")
|
|
2434
|
+
.catch(function () {
|
|
2435
|
+
return import("https://unpkg.com/three@0.184.0/build/three.module.min.js");
|
|
2436
|
+
})
|
|
2437
|
+
.then(function (mod) {
|
|
2438
|
+
state.three.THREE = mod;
|
|
2439
|
+
return mod;
|
|
2440
|
+
})
|
|
2441
|
+
.catch(function (error) {
|
|
2442
|
+
state.three.failed = true;
|
|
2443
|
+
throw error;
|
|
2444
|
+
});
|
|
2445
|
+
return state.three.loading;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
function renderThreeGraph(graphChanged) {
|
|
2449
|
+
drawThreeStatus("Loading 3D graph...");
|
|
2450
|
+
ensureThree().then(function () {
|
|
2451
|
+
if (activeRenderMode() !== "3d") return;
|
|
2452
|
+
setupThreeScene();
|
|
2453
|
+
rebuildThreeScene();
|
|
2454
|
+
if (graphChanged || state.three.distance <= 0) fitThreeGraph();
|
|
2455
|
+
startThreeGraph();
|
|
2456
|
+
renderThreeFrame();
|
|
2457
|
+
}).catch(function () {
|
|
2458
|
+
drawThreeStatus("3D renderer unavailable. Falling back to 2D canvas.");
|
|
2459
|
+
if (els.renderMode) els.renderMode.value = "2d";
|
|
2460
|
+
updateGraphSurfaceMode("2d");
|
|
2461
|
+
renderCanvasGraph(true);
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
function drawThreeStatus(message) {
|
|
2466
|
+
if (!els.threeGraph || state.three.renderer) return;
|
|
2467
|
+
els.threeGraph.innerHTML = "<div class=\"three-status\"></div>";
|
|
2468
|
+
var status = els.threeGraph.querySelector(".three-status");
|
|
2469
|
+
if (status) status.textContent = message;
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
function setupThreeScene() {
|
|
2473
|
+
if (state.three.renderer) {
|
|
2474
|
+
resizeThreeGraph();
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
var THREE = state.three.THREE;
|
|
2478
|
+
els.threeGraph.textContent = "";
|
|
2479
|
+
state.three.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" });
|
|
2480
|
+
state.three.renderer.setClearColor(new THREE.Color(graphPalette.background), 1);
|
|
2481
|
+
state.three.renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));
|
|
2482
|
+
els.threeGraph.appendChild(state.three.renderer.domElement);
|
|
2483
|
+
state.three.scene = new THREE.Scene();
|
|
2484
|
+
state.three.scene.fog = new THREE.FogExp2(new THREE.Color(graphPalette.background), 0.00016);
|
|
2485
|
+
state.three.camera = new THREE.PerspectiveCamera(50, 1, 1, 5000);
|
|
2486
|
+
state.three.root = new THREE.Group();
|
|
2487
|
+
state.three.nodeGroup = new THREE.Group();
|
|
2488
|
+
state.three.edgeGroup = new THREE.Group();
|
|
2489
|
+
state.three.root.add(state.three.edgeGroup);
|
|
2490
|
+
state.three.root.add(state.three.nodeGroup);
|
|
2491
|
+
state.three.scene.add(state.three.root);
|
|
2492
|
+
state.three.scene.add(new THREE.AmbientLight(0x8fffb1, 0.54));
|
|
2493
|
+
var key = new THREE.PointLight(0x6ad7ff, 1.2, 2200);
|
|
2494
|
+
key.position.set(280, 360, 520);
|
|
2495
|
+
state.three.scene.add(key);
|
|
2496
|
+
var fill = new THREE.PointLight(0xb88cff, 0.72, 1800);
|
|
2497
|
+
fill.position.set(-420, -220, 360);
|
|
2498
|
+
state.three.scene.add(fill);
|
|
2499
|
+
state.three.raycaster = new THREE.Raycaster();
|
|
2500
|
+
state.three.pointer = new THREE.Vector2();
|
|
2501
|
+
resizeThreeGraph();
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
function rebuildThreeScene() {
|
|
2505
|
+
var THREE = state.three.THREE;
|
|
2506
|
+
clearThreeGroup(state.three.nodeGroup);
|
|
2507
|
+
clearThreeGroup(state.three.edgeGroup);
|
|
2508
|
+
state.three.nodeById = new Map();
|
|
2509
|
+
state.three.edgeRefs = [];
|
|
2510
|
+
state.three.physicsTick = 0;
|
|
2511
|
+
state.three.physicsIdle = 0;
|
|
2512
|
+
|
|
2513
|
+
state.sim.nodes.forEach(function (node, index) {
|
|
2514
|
+
var entity = node.entity;
|
|
2515
|
+
var selected = state.selected && state.selected.kind === "entity" && state.selected.id === node.id;
|
|
2516
|
+
var material = new THREE.SpriteMaterial({
|
|
2517
|
+
map: threeNodeTexture(entity, selected),
|
|
2518
|
+
transparent: true,
|
|
2519
|
+
opacity: isDependencyEntity(entity) ? 0.70 : 1,
|
|
2520
|
+
depthWrite: false
|
|
2521
|
+
});
|
|
2522
|
+
var mesh = new THREE.Sprite(material);
|
|
2523
|
+
var position = threePosition(node, index);
|
|
2524
|
+
mesh.position.set(position.x, position.y, position.z);
|
|
2525
|
+
var size = threeNodeSize(node, selected);
|
|
2526
|
+
mesh.scale.set(size, size, 1);
|
|
2527
|
+
mesh.userData.node = node;
|
|
2528
|
+
state.three.nodeGroup.add(mesh);
|
|
2529
|
+
state.three.nodeById.set(node.id, { node: node, mesh: mesh, vx: 0, vy: 0, vz: 0 });
|
|
2530
|
+
});
|
|
2531
|
+
|
|
2532
|
+
state.sim.edges.forEach(function (edge) {
|
|
2533
|
+
var from = state.three.nodeById.get(edge.from);
|
|
2534
|
+
var to = state.three.nodeById.get(edge.to);
|
|
2535
|
+
if (!from || !to) return;
|
|
2536
|
+
var fromEntity = from.node.entity;
|
|
2537
|
+
var toEntity = to.node.entity;
|
|
2538
|
+
var connected = state.selected && state.selected.kind === "entity" && (state.selected.id === edge.from || state.selected.id === edge.to);
|
|
2539
|
+
var geometry = new THREE.BufferGeometry().setFromPoints([from.mesh.position, to.mesh.position]);
|
|
2540
|
+
var material = new THREE.LineBasicMaterial({
|
|
2541
|
+
color: new THREE.Color(edgeThemeColor(edge, fromEntity, toEntity)),
|
|
2542
|
+
transparent: true,
|
|
2543
|
+
opacity: threeEdgeOpacity(edge, fromEntity, toEntity, connected),
|
|
2544
|
+
depthWrite: false,
|
|
2545
|
+
depthTest: false
|
|
2546
|
+
});
|
|
2547
|
+
var line = new THREE.Line(geometry, material);
|
|
2548
|
+
line.userData.edge = edge;
|
|
2549
|
+
line.userData.from = edge.from;
|
|
2550
|
+
line.userData.to = edge.to;
|
|
2551
|
+
state.three.edgeGroup.add(line);
|
|
2552
|
+
state.three.edgeRefs.push(line);
|
|
2553
|
+
});
|
|
2554
|
+
|
|
2555
|
+
if (state.three.root) {
|
|
2556
|
+
state.three.root.rotation.x = state.three.rotationX;
|
|
2557
|
+
state.three.root.rotation.y = state.three.rotationY;
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
function clearThreeGroup(group) {
|
|
2562
|
+
if (!group) return;
|
|
2563
|
+
while (group.children.length) {
|
|
2564
|
+
var child = group.children.pop();
|
|
2565
|
+
if (!child) continue;
|
|
2566
|
+
if (child.geometry && child.geometry.dispose) child.geometry.dispose();
|
|
2567
|
+
if (child.material) {
|
|
2568
|
+
if (Array.isArray(child.material)) child.material.forEach(function (material) { if (material.dispose) material.dispose(); });
|
|
2569
|
+
else if (child.material.dispose) child.material.dispose();
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
function threeNodeTexture(entity, selected) {
|
|
2575
|
+
var THREE = state.three.THREE;
|
|
2576
|
+
var color = nodeThemeColor(entity);
|
|
2577
|
+
var key = [color, entity.graph_kind || "", entity.type || "", selected ? "selected" : "normal", isDependencyEntity(entity) ? "dependency" : "primary"].join("|");
|
|
2578
|
+
if (state.three.nodeTextureCache.has(key)) return state.three.nodeTextureCache.get(key);
|
|
2579
|
+
var canvas = document.createElement("canvas");
|
|
2580
|
+
canvas.width = 128;
|
|
2581
|
+
canvas.height = 128;
|
|
2582
|
+
var ctx = canvas.getContext("2d");
|
|
2583
|
+
var rgb = hexToRgb(color);
|
|
2584
|
+
var fill = nodeFillColor(entity);
|
|
2585
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
2586
|
+
var halo = ctx.createRadialGradient(64, 64, 8, 64, 64, 58);
|
|
2587
|
+
halo.addColorStop(0, "rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + "," + (selected ? 0.62 : 0.38) + ")");
|
|
2588
|
+
halo.addColorStop(0.52, "rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + "," + (selected ? 0.24 : 0.13) + ")");
|
|
2589
|
+
halo.addColorStop(1, "rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + ",0)");
|
|
2590
|
+
ctx.fillStyle = halo;
|
|
2591
|
+
ctx.beginPath();
|
|
2592
|
+
ctx.arc(64, 64, 60, 0, Math.PI * 2);
|
|
2593
|
+
ctx.fill();
|
|
2594
|
+
ctx.fillStyle = fill;
|
|
2595
|
+
ctx.beginPath();
|
|
2596
|
+
ctx.arc(64, 64, selected ? 36 : 31, 0, Math.PI * 2);
|
|
2597
|
+
ctx.fill();
|
|
2598
|
+
ctx.strokeStyle = color;
|
|
2599
|
+
ctx.lineWidth = selected ? 8 : 5;
|
|
2600
|
+
ctx.beginPath();
|
|
2601
|
+
ctx.arc(64, 64, selected ? 38 : 33, 0, Math.PI * 2);
|
|
2602
|
+
ctx.stroke();
|
|
2603
|
+
if (entity.graph_kind === "memory") {
|
|
2604
|
+
ctx.fillStyle = color;
|
|
2605
|
+
ctx.globalAlpha = 0.95;
|
|
2606
|
+
ctx.beginPath();
|
|
2607
|
+
ctx.arc(64, 64, 8, 0, Math.PI * 2);
|
|
2608
|
+
ctx.fill();
|
|
2609
|
+
ctx.globalAlpha = 1;
|
|
2610
|
+
}
|
|
2611
|
+
var texture = new THREE.CanvasTexture(canvas);
|
|
2612
|
+
texture.needsUpdate = true;
|
|
2613
|
+
state.three.nodeTextureCache.set(key, texture);
|
|
2614
|
+
return texture;
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
function threeNodeSize(node, selected) {
|
|
2618
|
+
var entity = node.entity;
|
|
2619
|
+
var size = clamp(19 + degreeOf(node.id) * 1.65, 22, entity.graph_kind === "memory" ? 46 : 40);
|
|
2620
|
+
if (entity.type === "file" || entity.type === "repo") size += 5;
|
|
2621
|
+
if (selected) size *= 1.22;
|
|
2622
|
+
return size;
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
function threeEdgeOpacity(edge, fromEntity, toEntity, connected) {
|
|
2626
|
+
if (connected) return 0.48;
|
|
2627
|
+
if (edge.memory_code_link || isMemoryCodeRelation(edge.relation)) return 0.34;
|
|
2628
|
+
if (fromEntity.graph_kind === "code" && toEntity.graph_kind === "code") return 0.20;
|
|
2629
|
+
if (fromEntity.graph_kind === "memory" && toEntity.graph_kind === "memory") return 0.12;
|
|
2630
|
+
return 0.15;
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
function threePosition(node, index) {
|
|
2634
|
+
var entity = node.entity;
|
|
2635
|
+
var total = Math.max(1, state.sim.nodes.length);
|
|
2636
|
+
var rank = index + 0.5;
|
|
2637
|
+
var golden = Math.PI * (3 - Math.sqrt(5));
|
|
2638
|
+
var theta = golden * rank + (hashString(node.id || index) % 360) * 0.004;
|
|
2639
|
+
var yUnit = 1 - (2 * rank) / total;
|
|
2640
|
+
var ring = Math.sqrt(Math.max(0, 1 - yUnit * yUnit));
|
|
2641
|
+
var radius = threeOrbRadius(total);
|
|
2642
|
+
var inner = threeOrbScale(entity);
|
|
2643
|
+
var kindOffset = threeKindDepthBias(entity);
|
|
2644
|
+
return {
|
|
2645
|
+
x: Math.cos(theta) * ring * radius * inner,
|
|
2646
|
+
y: yUnit * radius * inner,
|
|
2647
|
+
z: (Math.sin(theta) * ring + kindOffset) * radius * inner
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
function threeOrbRadius(total) {
|
|
2652
|
+
return clamp(245 + Math.max(1, total) * 2.1, 280, 455);
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
function threeOrbScale(entity) {
|
|
2656
|
+
if (entity.graph_kind === "memory") return 0.88;
|
|
2657
|
+
if (entity.type === "file" || entity.type === "repo") return 0.76;
|
|
2658
|
+
if (isDependencyEntity(entity)) return 1.08;
|
|
2659
|
+
return 1;
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
function threeKindDepthBias(entity) {
|
|
2663
|
+
if (entity.graph_kind === "memory") return -0.22;
|
|
2664
|
+
if (entity.graph_kind === "code") return 0.18;
|
|
2665
|
+
return 0;
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
function resizeThreeGraph() {
|
|
2669
|
+
if (!state.three.renderer || !els.threeGraph) return;
|
|
2670
|
+
var rect = els.threeGraph.getBoundingClientRect();
|
|
2671
|
+
var width = Math.max(320, rect.width);
|
|
2672
|
+
var height = Math.max(360, rect.height);
|
|
2673
|
+
state.three.renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));
|
|
2674
|
+
state.three.renderer.setSize(width, height, false);
|
|
2675
|
+
state.three.camera.aspect = width / height;
|
|
2676
|
+
state.three.camera.updateProjectionMatrix();
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
function fitThreeGraph() {
|
|
2680
|
+
var entries = Array.from(state.three.nodeById.values());
|
|
2681
|
+
if (!entries.length) {
|
|
2682
|
+
state.three.distance = 850;
|
|
2683
|
+
return;
|
|
2684
|
+
}
|
|
2685
|
+
var maxRadius = entries.reduce(function (max, entry) {
|
|
2686
|
+
var p = entry.mesh.position;
|
|
2687
|
+
return Math.max(max, Math.sqrt(p.x * p.x + p.y * p.y + p.z * p.z));
|
|
2688
|
+
}, 260);
|
|
2689
|
+
state.three.distance = clamp(maxRadius * 2.35, 760, 2600);
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
function zoomThreeGraph(factor) {
|
|
2693
|
+
state.three.distance = clamp(state.three.distance / factor, 320, 3200);
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
function startThreeGraph() {
|
|
2697
|
+
if (state.three.raf) return;
|
|
2698
|
+
function frame() {
|
|
2699
|
+
if (activeRenderMode() !== "3d" || !state.three.renderer) {
|
|
2700
|
+
state.three.raf = null;
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2703
|
+
if (!state.three.drag && state.three.root) {
|
|
2704
|
+
state.three.rotationY += 0.0017;
|
|
2705
|
+
state.three.root.rotation.y = state.three.rotationY;
|
|
2706
|
+
}
|
|
2707
|
+
stepThreePhysics();
|
|
2708
|
+
renderThreeFrame();
|
|
2709
|
+
state.three.raf = window.requestAnimationFrame(frame);
|
|
2710
|
+
}
|
|
2711
|
+
state.three.raf = window.requestAnimationFrame(frame);
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
function stopThreeGraph() {
|
|
2715
|
+
if (state.three.raf) window.cancelAnimationFrame(state.three.raf);
|
|
2716
|
+
state.three.raf = null;
|
|
2717
|
+
if (els.tooltip) els.tooltip.classList.remove("visible");
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
function renderThreeFrame() {
|
|
2721
|
+
if (!state.three.renderer || !state.three.scene || !state.three.camera) return;
|
|
2722
|
+
resizeThreeGraph();
|
|
2723
|
+
state.three.camera.position.set(state.three.target.x, state.three.target.y, state.three.distance);
|
|
2724
|
+
state.three.camera.lookAt(state.three.target.x, state.three.target.y, state.three.target.z);
|
|
2725
|
+
updateThreeEdges();
|
|
2726
|
+
state.three.renderer.render(state.three.scene, state.three.camera);
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
function stepThreePhysics() {
|
|
2730
|
+
if (!state.three.nodeById || state.three.physicsTick > 180) return;
|
|
2731
|
+
if (state.three.drag && state.three.drag.type === "node") return;
|
|
2732
|
+
var entries = Array.from(state.three.nodeById.values());
|
|
2733
|
+
var count = entries.length;
|
|
2734
|
+
if (!count) return;
|
|
2735
|
+
var forces = new Map(entries.map(function (entry) {
|
|
2736
|
+
return [entry.node.id, { x: 0, y: 0, z: 0 }];
|
|
2737
|
+
}));
|
|
2738
|
+
var repulsion = count > 110 ? 1800 : count > 70 ? 2600 : 3400;
|
|
2739
|
+
for (var i = 0; i < entries.length; i++) {
|
|
2740
|
+
for (var j = i + 1; j < entries.length; j++) {
|
|
2741
|
+
var a = entries[i];
|
|
2742
|
+
var b = entries[j];
|
|
2743
|
+
var dx = a.mesh.position.x - b.mesh.position.x;
|
|
2744
|
+
var dy = a.mesh.position.y - b.mesh.position.y;
|
|
2745
|
+
var dz = a.mesh.position.z - b.mesh.position.z;
|
|
2746
|
+
var distSq = Math.max(1200, dx * dx + dy * dy + dz * dz);
|
|
2747
|
+
var dist = Math.sqrt(distSq);
|
|
2748
|
+
var force = repulsion / distSq;
|
|
2749
|
+
var ax = (dx / dist) * force;
|
|
2750
|
+
var ay = (dy / dist) * force;
|
|
2751
|
+
var az = (dz / dist) * force;
|
|
2752
|
+
var fa = forces.get(a.node.id);
|
|
2753
|
+
var fb = forces.get(b.node.id);
|
|
2754
|
+
fa.x += ax;
|
|
2755
|
+
fa.y += ay;
|
|
2756
|
+
fa.z += az;
|
|
2757
|
+
fb.x -= ax;
|
|
2758
|
+
fb.y -= ay;
|
|
2759
|
+
fb.z -= az;
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
state.sim.edges.forEach(function (edge) {
|
|
2764
|
+
var from = state.three.nodeById.get(edge.from);
|
|
2765
|
+
var to = state.three.nodeById.get(edge.to);
|
|
2766
|
+
if (!from || !to) return;
|
|
2767
|
+
var dx = to.mesh.position.x - from.mesh.position.x;
|
|
2768
|
+
var dy = to.mesh.position.y - from.mesh.position.y;
|
|
2769
|
+
var dz = to.mesh.position.z - from.mesh.position.z;
|
|
2770
|
+
var dist = Math.max(1, Math.sqrt(dx * dx + dy * dy + dz * dz));
|
|
2771
|
+
var memoryCode = edge.memory_code_link || isMemoryCodeRelation(edge.relation);
|
|
2772
|
+
var target = memoryCode ? 190 : (from.node.entity.graph_kind === "code" && to.node.entity.graph_kind === "code" ? 240 : 220);
|
|
2773
|
+
var strength = memoryCode ? 0.0032 : 0.0018;
|
|
2774
|
+
var force = (dist - target) * strength;
|
|
2775
|
+
var fx = (dx / dist) * force;
|
|
2776
|
+
var fy = (dy / dist) * force;
|
|
2777
|
+
var fz = (dz / dist) * force;
|
|
2778
|
+
var ff = forces.get(from.node.id);
|
|
2779
|
+
var ft = forces.get(to.node.id);
|
|
2780
|
+
ff.x += fx;
|
|
2781
|
+
ff.y += fy;
|
|
2782
|
+
ff.z += fz;
|
|
2783
|
+
ft.x -= fx;
|
|
2784
|
+
ft.y -= fy;
|
|
2785
|
+
ft.z -= fz;
|
|
2786
|
+
});
|
|
2787
|
+
|
|
2788
|
+
var baseRadius = threeOrbRadius(count);
|
|
2789
|
+
var maxVelocity = 0;
|
|
2790
|
+
entries.forEach(function (entry) {
|
|
2791
|
+
var p = entry.mesh.position;
|
|
2792
|
+
var entity = entry.node.entity;
|
|
2793
|
+
var len = Math.max(1, Math.sqrt(p.x * p.x + p.y * p.y + p.z * p.z));
|
|
2794
|
+
var desiredRadius = baseRadius * threeOrbScale(entity);
|
|
2795
|
+
var radial = (desiredRadius - len) * 0.055;
|
|
2796
|
+
var f = forces.get(entry.node.id);
|
|
2797
|
+
f.x += (p.x / len) * radial;
|
|
2798
|
+
f.y += (p.y / len) * radial;
|
|
2799
|
+
f.z += (p.z / len) * radial;
|
|
2800
|
+
var depthTarget = desiredRadius * threeKindDepthBias(entity);
|
|
2801
|
+
f.z += (depthTarget - p.z) * 0.002;
|
|
2802
|
+
entry.vx = clamp((entry.vx + f.x) * 0.80, -4.2, 4.2);
|
|
2803
|
+
entry.vy = clamp((entry.vy + f.y) * 0.80, -4.2, 4.2);
|
|
2804
|
+
entry.vz = clamp((entry.vz + f.z) * 0.80, -4.2, 4.2);
|
|
2805
|
+
p.x += entry.vx;
|
|
2806
|
+
p.y += entry.vy;
|
|
2807
|
+
p.z += entry.vz;
|
|
2808
|
+
var nextLen = Math.max(1, Math.sqrt(p.x * p.x + p.y * p.y + p.z * p.z));
|
|
2809
|
+
var correction = (desiredRadius - nextLen) * 0.08;
|
|
2810
|
+
p.x += (p.x / nextLen) * correction;
|
|
2811
|
+
p.y += (p.y / nextLen) * correction;
|
|
2812
|
+
p.z += (p.z / nextLen) * correction;
|
|
2813
|
+
maxVelocity = Math.max(maxVelocity, Math.abs(entry.vx), Math.abs(entry.vy), Math.abs(entry.vz));
|
|
2814
|
+
});
|
|
2815
|
+
state.three.physicsTick += 1;
|
|
2816
|
+
state.three.physicsIdle = maxVelocity < 0.030 ? state.three.physicsIdle + 1 : 0;
|
|
2817
|
+
if (state.three.physicsIdle > 32) state.three.physicsTick = 181;
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
function startThreePointer(event) {
|
|
2821
|
+
if (activeRenderMode() !== "3d" || event.button !== 0) return;
|
|
2822
|
+
event.preventDefault();
|
|
2823
|
+
var picked = pickThreeNode(event);
|
|
2824
|
+
if (picked) {
|
|
2825
|
+
var entry = state.three.nodeById.get(picked.id);
|
|
2826
|
+
state.three.drag = {
|
|
2827
|
+
type: "node",
|
|
2828
|
+
x: event.clientX,
|
|
2829
|
+
y: event.clientY,
|
|
2830
|
+
moved: false,
|
|
2831
|
+
picked: picked,
|
|
2832
|
+
entry: entry,
|
|
2833
|
+
plane: createThreeDragPlane(entry && entry.mesh)
|
|
2834
|
+
};
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
state.three.drag = {
|
|
2838
|
+
type: "space",
|
|
2839
|
+
x: event.clientX,
|
|
2840
|
+
y: event.clientY,
|
|
2841
|
+
rotationX: state.three.rotationX,
|
|
2842
|
+
rotationY: state.three.rotationY,
|
|
2843
|
+
moved: false,
|
|
2844
|
+
picked: null
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
function moveThreePointer(event) {
|
|
2849
|
+
if (activeRenderMode() !== "3d") return;
|
|
2850
|
+
state.three.lastPointerEvent = event;
|
|
2851
|
+
if (state.three.drag) {
|
|
2852
|
+
var dx = event.clientX - state.three.drag.x;
|
|
2853
|
+
var dy = event.clientY - state.three.drag.y;
|
|
2854
|
+
if (Math.abs(dx) > 2 || Math.abs(dy) > 2) state.three.drag.moved = true;
|
|
2855
|
+
if (state.three.drag.type === "node") {
|
|
2856
|
+
moveThreeDraggedNode(event);
|
|
2857
|
+
renderThreeFrame();
|
|
2858
|
+
return;
|
|
2859
|
+
}
|
|
2860
|
+
state.three.rotationY = state.three.drag.rotationY + dx * 0.0065;
|
|
2861
|
+
state.three.rotationX = clamp(state.three.drag.rotationX + dy * 0.0048, -1.15, 0.45);
|
|
2862
|
+
if (state.three.root) {
|
|
2863
|
+
state.three.root.rotation.x = state.three.rotationX;
|
|
2864
|
+
state.three.root.rotation.y = state.three.rotationY;
|
|
2865
|
+
}
|
|
2866
|
+
renderThreeFrame();
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
state.three.hoverNode = pickThreeNode(event);
|
|
2870
|
+
updateThreeTooltip(event);
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
function endThreePointer(event) {
|
|
2874
|
+
if (!state.three.drag) return;
|
|
2875
|
+
var picked = !state.three.drag.moved || state.three.drag.type === "node"
|
|
2876
|
+
? (pickThreeNode(event) || state.three.drag.picked)
|
|
2877
|
+
: null;
|
|
2878
|
+
state.three.drag = null;
|
|
2879
|
+
if (picked) {
|
|
2880
|
+
state.selected = { kind: "entity", id: picked.id };
|
|
2881
|
+
render();
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
function leaveThreePointer() {
|
|
2886
|
+
state.three.hoverNode = null;
|
|
2887
|
+
state.three.drag = null;
|
|
2888
|
+
if (els.tooltip) els.tooltip.classList.remove("visible");
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
function handleThreeWheel(event) {
|
|
2892
|
+
if (activeRenderMode() !== "3d") return;
|
|
2893
|
+
event.preventDefault();
|
|
2894
|
+
zoomThreeGraph(event.deltaY > 0 ? 0.88 : 1.14);
|
|
2895
|
+
renderThreeFrame();
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
function handleThreeDoubleClick(event) {
|
|
2899
|
+
var picked = pickThreeNode(event);
|
|
2900
|
+
if (!picked) return;
|
|
2901
|
+
state.selected = { kind: "entity", id: picked.id };
|
|
2902
|
+
render();
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
function pickThreeNode(event) {
|
|
2906
|
+
if (!state.three.raycaster || !state.three.camera || !state.three.pointer) return null;
|
|
2907
|
+
setThreePointerFromEvent(event);
|
|
2908
|
+
state.three.raycaster.setFromCamera(state.three.pointer, state.three.camera);
|
|
2909
|
+
var meshes = Array.from(state.three.nodeById.values()).map(function (entry) { return entry.mesh; });
|
|
2910
|
+
var hits = state.three.raycaster.intersectObjects(meshes, false);
|
|
2911
|
+
return hits.length && hits[0].object.userData ? hits[0].object.userData.node : null;
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
function createThreeDragPlane(mesh) {
|
|
2915
|
+
if (!mesh || !state.three.camera || !state.three.THREE) return null;
|
|
2916
|
+
var THREE = state.three.THREE;
|
|
2917
|
+
state.three.root.updateMatrixWorld(true);
|
|
2918
|
+
state.three.camera.updateMatrixWorld(true);
|
|
2919
|
+
var normal = new THREE.Vector3();
|
|
2920
|
+
state.three.camera.getWorldDirection(normal);
|
|
2921
|
+
var point = new THREE.Vector3();
|
|
2922
|
+
mesh.getWorldPosition(point);
|
|
2923
|
+
return new THREE.Plane().setFromNormalAndCoplanarPoint(normal, point);
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
function moveThreeDraggedNode(event) {
|
|
2927
|
+
var drag = state.three.drag;
|
|
2928
|
+
if (!drag || !drag.entry || !drag.entry.mesh || !drag.plane || !state.three.raycaster) return;
|
|
2929
|
+
var THREE = state.three.THREE;
|
|
2930
|
+
setThreePointerFromEvent(event);
|
|
2931
|
+
state.three.raycaster.setFromCamera(state.three.pointer, state.three.camera);
|
|
2932
|
+
var hit = new THREE.Vector3();
|
|
2933
|
+
if (!state.three.raycaster.ray.intersectPlane(drag.plane, hit)) return;
|
|
2934
|
+
var local = hit.clone();
|
|
2935
|
+
state.three.root.worldToLocal(local);
|
|
2936
|
+
drag.entry.mesh.position.copy(local);
|
|
2937
|
+
drag.entry.vx = 0;
|
|
2938
|
+
drag.entry.vy = 0;
|
|
2939
|
+
drag.entry.vz = 0;
|
|
2940
|
+
state.three.physicsTick = 0;
|
|
2941
|
+
state.three.physicsIdle = 0;
|
|
2942
|
+
updateThreeEdges();
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
function updateThreeEdges() {
|
|
2946
|
+
if (!state.three.edgeRefs || !state.three.edgeRefs.length) return;
|
|
2947
|
+
state.three.edgeRefs.forEach(function (line) {
|
|
2948
|
+
var from = state.three.nodeById.get(line.userData.from);
|
|
2949
|
+
var to = state.three.nodeById.get(line.userData.to);
|
|
2950
|
+
if (!from || !to || !line.geometry) return;
|
|
2951
|
+
var attr = line.geometry.getAttribute("position");
|
|
2952
|
+
if (!attr) return;
|
|
2953
|
+
attr.setXYZ(0, from.mesh.position.x, from.mesh.position.y, from.mesh.position.z);
|
|
2954
|
+
attr.setXYZ(1, to.mesh.position.x, to.mesh.position.y, to.mesh.position.z);
|
|
2955
|
+
attr.needsUpdate = true;
|
|
2956
|
+
line.geometry.computeBoundingSphere();
|
|
2957
|
+
});
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
function setThreePointerFromEvent(event) {
|
|
2961
|
+
var rect = els.threeGraph.getBoundingClientRect();
|
|
2962
|
+
state.three.pointer.x = ((event.clientX - rect.left) / Math.max(rect.width, 1)) * 2 - 1;
|
|
2963
|
+
state.three.pointer.y = -(((event.clientY - rect.top) / Math.max(rect.height, 1)) * 2 - 1);
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
function updateThreeTooltip(event) {
|
|
2967
|
+
if (!els.tooltip) return;
|
|
2968
|
+
var node = state.three.hoverNode;
|
|
2969
|
+
if (!node || state.three.drag) {
|
|
2970
|
+
els.tooltip.classList.remove("visible");
|
|
2971
|
+
return;
|
|
2972
|
+
}
|
|
2973
|
+
var entity = node.entity;
|
|
2974
|
+
var relationCount = state.sim.edges.filter(function (edge) { return edge.from === node.id || edge.to === node.id; }).length;
|
|
2975
|
+
var color = nodeThemeColor(entity);
|
|
2976
|
+
els.tooltip.innerHTML = [
|
|
2977
|
+
"<div class=\"tt-name\"></div>",
|
|
2978
|
+
"<div class=\"tt-type\"></div>",
|
|
2979
|
+
"<div class=\"tt-summary\"></div>",
|
|
2980
|
+
"<div class=\"tt-conns\"></div>"
|
|
2981
|
+
].join("");
|
|
2982
|
+
els.tooltip.querySelector(".tt-name").textContent = displayName(entity);
|
|
2983
|
+
els.tooltip.querySelector(".tt-type").textContent = nodeKindLabel(entity);
|
|
2984
|
+
els.tooltip.querySelector(".tt-type").style.color = color;
|
|
2985
|
+
els.tooltip.querySelector(".tt-summary").textContent = shortName(entity.summary || entity.id, 150);
|
|
2986
|
+
els.tooltip.querySelector(".tt-conns").textContent = relationCount + " relation" + (relationCount === 1 ? "" : "s");
|
|
2987
|
+
var rect = els.threeGraph.getBoundingClientRect();
|
|
2988
|
+
els.tooltip.style.left = (event.clientX - rect.left + 14) + "px";
|
|
2989
|
+
els.tooltip.style.top = (event.clientY - rect.top + 14) + "px";
|
|
2990
|
+
els.tooltip.classList.add("visible");
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
function hashString(value) {
|
|
2994
|
+
var text = String(value || "");
|
|
2995
|
+
var hash = 0;
|
|
2996
|
+
for (var i = 0; i < text.length; i++) {
|
|
2997
|
+
hash = ((hash << 5) - hash + text.charCodeAt(i)) | 0;
|
|
2998
|
+
}
|
|
2999
|
+
return Math.abs(hash);
|
|
3000
|
+
}
|
|
3001
|
+
|
|
2320
3002
|
function focusedCanvasNodeId() {
|
|
2321
3003
|
if (state.sim.hoverNode) return state.sim.hoverNode.id;
|
|
2322
3004
|
if (state.selected && state.selected.kind === "entity") return state.selected.id;
|