@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/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 () { zoomCanvas(0.82); });
131
- els.zoomIn.addEventListener("click", function () { zoomCanvas(1.22); });
132
- els.fitView.addEventListener("click", function () { fitCanvas(); drawCanvasGraph(); });
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
- resizeCanvas();
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
- fetchJson("./data/kage/graph.json"),
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
- renderCanvasGraph(graphChanged);
872
+ renderActiveGraph(graphChanged);
833
873
  renderLists();
834
874
  renderDetails();
835
875
  renderMetrics();
@@ -1088,9 +1128,69 @@
1088
1128
  }));
1089
1129
  }
1090
1130
 
1091
- function renderCanvasGraph(graphChanged) {
1092
- resizeCanvas();
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;