@kage-core/kage-graph-mcp 1.1.23 → 1.1.25
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/dist/daemon.js +5 -1
- package/dist/kernel.js +83 -13
- package/package.json +2 -1
- package/viewer/app.js +691 -9
- package/viewer/index.html +18 -4
- package/viewer/styles.css +81 -6
package/dist/daemon.js
CHANGED
|
@@ -232,6 +232,7 @@ async function startViewer(projectDir, options = {}) {
|
|
|
232
232
|
const host = options.host ?? DEFAULT_HOST;
|
|
233
233
|
const port = options.port ?? DEFAULT_VIEWER_PORT;
|
|
234
234
|
const viewerDir = (0, node_path_1.resolve)(__dirname, "..", "viewer");
|
|
235
|
+
const threeDir = (0, node_path_1.resolve)(__dirname, "..", "node_modules", "three");
|
|
235
236
|
const projectRoot = (0, node_path_1.resolve)(projectDir);
|
|
236
237
|
const graphPath = (0, node_path_1.join)(projectRoot, ".agent_memory", "graph", "graph.json");
|
|
237
238
|
const codePath = (0, node_path_1.join)(projectRoot, ".agent_memory", "code_graph", "graph.json");
|
|
@@ -262,13 +263,16 @@ async function startViewer(projectDir, options = {}) {
|
|
|
262
263
|
else if (requestUrl.pathname.startsWith("/viewer/")) {
|
|
263
264
|
filePath = (0, node_path_1.join)(viewerDir, (0, node_path_1.normalize)(requestUrl.pathname.replace(/^\/viewer\//, "")));
|
|
264
265
|
}
|
|
266
|
+
else if (requestUrl.pathname.startsWith("/vendor/three/")) {
|
|
267
|
+
filePath = (0, node_path_1.join)(threeDir, (0, node_path_1.normalize)(requestUrl.pathname.replace(/^\/vendor\/three\//, "")));
|
|
268
|
+
}
|
|
265
269
|
else {
|
|
266
270
|
const decoded = decodeURIComponent(requestUrl.pathname);
|
|
267
271
|
filePath = (0, node_path_1.resolve)(decoded);
|
|
268
272
|
if (!isInside(projectRoot, filePath))
|
|
269
273
|
filePath = null;
|
|
270
274
|
}
|
|
271
|
-
if (!filePath || (!isInside(viewerDir, filePath) && !isInside(projectRoot, filePath)) || !(0, node_fs_1.existsSync)(filePath)) {
|
|
275
|
+
if (!filePath || (!isInside(viewerDir, filePath) && !isInside(projectRoot, filePath) && !isInside(threeDir, filePath)) || !(0, node_fs_1.existsSync)(filePath)) {
|
|
272
276
|
json(res, 404, { ok: false, error: "not_found" });
|
|
273
277
|
return;
|
|
274
278
|
}
|
package/dist/kernel.js
CHANGED
|
@@ -460,6 +460,12 @@ function estimateTokens(text) {
|
|
|
460
460
|
function packetText(packet) {
|
|
461
461
|
return `${packet.title}\n${packet.summary}\n${packet.body}\n${packet.type}\n${packet.tags.join(" ")}\n${packet.paths.join(" ")}`;
|
|
462
462
|
}
|
|
463
|
+
function isGeneratedChangeMemory(packet) {
|
|
464
|
+
return packet.type === "workflow"
|
|
465
|
+
&& packet.tags.includes("change-memory")
|
|
466
|
+
&& packet.tags.includes("diff-proposal")
|
|
467
|
+
&& packet.source_refs.some((ref) => ref.kind === "git_diff");
|
|
468
|
+
}
|
|
463
469
|
function tokenSet(text) {
|
|
464
470
|
return new Set(tokenize(text).filter((term) => term.length > 2));
|
|
465
471
|
}
|
|
@@ -476,6 +482,7 @@ function duplicateCandidates(projectDir, packet, threshold = 0.58) {
|
|
|
476
482
|
const current = tokenSet(packetText(packet));
|
|
477
483
|
return [...loadApprovedPackets(projectDir), ...loadPendingPackets(projectDir)]
|
|
478
484
|
.filter((candidate) => candidate.id !== packet.id)
|
|
485
|
+
.filter((candidate) => !(isGeneratedChangeMemory(packet) && isGeneratedChangeMemory(candidate)))
|
|
479
486
|
.map((candidate) => ({ packet: candidate, score: jaccard(current, tokenSet(packetText(candidate))) }))
|
|
480
487
|
.filter((entry) => entry.score >= threshold)
|
|
481
488
|
.sort((a, b) => b.score - a.score || a.packet.title.localeCompare(b.packet.title))
|
|
@@ -888,6 +895,30 @@ function readGit(projectDir, args) {
|
|
|
888
895
|
return null;
|
|
889
896
|
}
|
|
890
897
|
}
|
|
898
|
+
function safeStat(path) {
|
|
899
|
+
try {
|
|
900
|
+
return (0, node_fs_1.statSync)(path);
|
|
901
|
+
}
|
|
902
|
+
catch {
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
function safeLstat(path) {
|
|
907
|
+
try {
|
|
908
|
+
return (0, node_fs_1.lstatSync)(path);
|
|
909
|
+
}
|
|
910
|
+
catch {
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
function safeReadText(path) {
|
|
915
|
+
try {
|
|
916
|
+
return (0, node_fs_1.readFileSync)(path, "utf8");
|
|
917
|
+
}
|
|
918
|
+
catch {
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
891
922
|
function gitBranch(projectDir) {
|
|
892
923
|
return readGit(projectDir, ["branch", "--show-current"]) || readGit(projectDir, ["rev-parse", "--short", "HEAD"]);
|
|
893
924
|
}
|
|
@@ -901,6 +932,12 @@ function gitMergeBase(projectDir) {
|
|
|
901
932
|
return readGit(projectDir, ["merge-base", "HEAD", "origin/main"])
|
|
902
933
|
|| readGit(projectDir, ["merge-base", "HEAD", "origin/master"]);
|
|
903
934
|
}
|
|
935
|
+
function gitProjectPrefix(projectDir) {
|
|
936
|
+
const prefix = readGit(projectDir, ["rev-parse", "--show-prefix"]);
|
|
937
|
+
if (prefix === null)
|
|
938
|
+
return null;
|
|
939
|
+
return prefix.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
940
|
+
}
|
|
904
941
|
// Directories that are never meaningful in change-memory packets.
|
|
905
942
|
// These are typically generated, vendored, or ephemeral — any project can
|
|
906
943
|
// accumulate thousands of files here that bury real signal.
|
|
@@ -934,12 +971,26 @@ function isNoisePath(filePath) {
|
|
|
934
971
|
return false;
|
|
935
972
|
return NOISE_PATH_PREFIXES.some((prefix) => filePath.startsWith(prefix));
|
|
936
973
|
}
|
|
937
|
-
function
|
|
974
|
+
function gitPathToProjectRelative(projectDir, path) {
|
|
975
|
+
const normalized = path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
976
|
+
const projectPrefix = gitProjectPrefix(projectDir);
|
|
977
|
+
if (projectPrefix === null || projectPrefix === "")
|
|
978
|
+
return normalized;
|
|
979
|
+
if (normalized === projectPrefix)
|
|
980
|
+
return "";
|
|
981
|
+
const prefix = `${projectPrefix}/`;
|
|
982
|
+
if (!normalized.startsWith(prefix)) {
|
|
983
|
+
return (0, node_fs_1.existsSync)((0, node_path_1.join)(projectDir, normalized)) ? normalized : null;
|
|
984
|
+
}
|
|
985
|
+
return normalized.slice(prefix.length);
|
|
986
|
+
}
|
|
987
|
+
function parsePorcelainStatus(projectDir, status) {
|
|
938
988
|
return unique(status
|
|
939
989
|
.split(/\r?\n/)
|
|
940
990
|
.map(parsePorcelainPath)
|
|
941
991
|
.map((path) => path.replace(/^.* -> /, ""))
|
|
942
|
-
.
|
|
992
|
+
.map((path) => gitPathToProjectRelative(projectDir, path))
|
|
993
|
+
.filter((path) => Boolean(path))
|
|
943
994
|
.filter((path) => !shouldSkipRepoMemoryPath(path))).sort();
|
|
944
995
|
}
|
|
945
996
|
function parsePorcelainPath(line) {
|
|
@@ -948,13 +999,14 @@ function parsePorcelainPath(line) {
|
|
|
948
999
|
}
|
|
949
1000
|
function branchDiffStat(projectDir, changedFiles) {
|
|
950
1001
|
const diffStats = [
|
|
951
|
-
readGit(projectDir, ["diff", "--stat"]),
|
|
952
|
-
readGit(projectDir, ["diff", "--cached", "--stat"]),
|
|
1002
|
+
readGit(projectDir, ["diff", "--stat", "--relative"]),
|
|
1003
|
+
readGit(projectDir, ["diff", "--cached", "--stat", "--relative"]),
|
|
953
1004
|
].filter(Boolean).join("\n").trim();
|
|
954
1005
|
const untracked = new Set((readGit(projectDir, ["ls-files", "--others", "--exclude-standard"]) ?? "")
|
|
955
1006
|
.split(/\r?\n/)
|
|
956
1007
|
.map((path) => path.trim())
|
|
957
|
-
.
|
|
1008
|
+
.map((path) => gitPathToProjectRelative(projectDir, path))
|
|
1009
|
+
.filter((path) => Boolean(path))
|
|
958
1010
|
.filter((path) => changedFiles.includes(path)));
|
|
959
1011
|
const untrackedStats = [...untracked]
|
|
960
1012
|
.filter((file) => !diffStats.includes(file))
|
|
@@ -1016,8 +1068,9 @@ function createRepoOverviewPacket(projectDir) {
|
|
|
1016
1068
|
if (deps.stripe)
|
|
1017
1069
|
tags.push("stripe");
|
|
1018
1070
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1071
|
+
const readmeText = (0, node_fs_1.existsSync)(readmePath) ? safeReadText(readmePath) : null;
|
|
1072
|
+
if (readmeText) {
|
|
1073
|
+
const readme = readmeText.slice(0, 1000);
|
|
1021
1074
|
bodyParts.push(`README excerpt:\n${readme}`);
|
|
1022
1075
|
}
|
|
1023
1076
|
const createdAt = nowIso();
|
|
@@ -1038,7 +1091,7 @@ function createRepoOverviewPacket(projectDir) {
|
|
|
1038
1091
|
stack,
|
|
1039
1092
|
source_refs: [
|
|
1040
1093
|
...((0, node_fs_1.existsSync)(packagePath) ? [{ kind: "file", path: "package.json" }] : []),
|
|
1041
|
-
...(
|
|
1094
|
+
...(readmeText ? [{ kind: "file", path: "README.md" }] : []),
|
|
1042
1095
|
],
|
|
1043
1096
|
context: {
|
|
1044
1097
|
fact: "Generated repo overview summarizes package metadata and the README as a navigation aid for agent startup.",
|
|
@@ -1748,7 +1801,20 @@ function scanStructuralFiles(projectDir) {
|
|
|
1748
1801
|
ignore("kageignore");
|
|
1749
1802
|
continue;
|
|
1750
1803
|
}
|
|
1751
|
-
const
|
|
1804
|
+
const linkStats = safeLstat(absolutePath);
|
|
1805
|
+
if (!linkStats) {
|
|
1806
|
+
ignore("unreadable_path");
|
|
1807
|
+
continue;
|
|
1808
|
+
}
|
|
1809
|
+
if (linkStats.isSymbolicLink()) {
|
|
1810
|
+
ignore("symlink");
|
|
1811
|
+
continue;
|
|
1812
|
+
}
|
|
1813
|
+
const stats = safeStat(absolutePath);
|
|
1814
|
+
if (!stats) {
|
|
1815
|
+
ignore("unreadable_path");
|
|
1816
|
+
continue;
|
|
1817
|
+
}
|
|
1752
1818
|
if (stats.isDirectory()) {
|
|
1753
1819
|
visit(absolutePath);
|
|
1754
1820
|
continue;
|
|
@@ -5282,10 +5348,14 @@ function baselineDiscoveryFiles(projectDir, task) {
|
|
|
5282
5348
|
const absolute = (0, node_path_1.join)(projectDir, path);
|
|
5283
5349
|
if (!(0, node_fs_1.existsSync)(absolute))
|
|
5284
5350
|
return null;
|
|
5285
|
-
const stats = (
|
|
5351
|
+
const stats = safeStat(absolute);
|
|
5352
|
+
if (!stats)
|
|
5353
|
+
return null;
|
|
5286
5354
|
if (!stats.isFile() || stats.size > 240_000)
|
|
5287
5355
|
return null;
|
|
5288
|
-
const text = (
|
|
5356
|
+
const text = safeReadText(absolute);
|
|
5357
|
+
if (text === null)
|
|
5358
|
+
return null;
|
|
5289
5359
|
const score = scoreText(terms, `${path}\n${text.slice(0, 8000)}`, [path]);
|
|
5290
5360
|
const alwaysUseful = ["README.md", "AGENTS.md", "CLAUDE.md", "package.json"].includes(path);
|
|
5291
5361
|
if (score <= 0 && !alwaysUseful)
|
|
@@ -6421,7 +6491,7 @@ function proposeFromDiff(projectDir) {
|
|
|
6421
6491
|
const status = readGit(projectDir, ["status", "--porcelain", "-uall"]);
|
|
6422
6492
|
if (status === null)
|
|
6423
6493
|
return { ok: false, changedFiles: [], errors: ["Not a git repository or git is unavailable."] };
|
|
6424
|
-
const changedFiles = parsePorcelainStatus(status);
|
|
6494
|
+
const changedFiles = parsePorcelainStatus(projectDir, status);
|
|
6425
6495
|
if (changedFiles.length === 0)
|
|
6426
6496
|
return { ok: false, changedFiles: [], errors: ["No changed files found."] };
|
|
6427
6497
|
const stat = branchDiffStat(projectDir, changedFiles);
|
|
@@ -6470,7 +6540,7 @@ function buildBranchOverlay(projectDir) {
|
|
|
6470
6540
|
branch: gitBranch(projectDir),
|
|
6471
6541
|
head: gitHead(projectDir),
|
|
6472
6542
|
merge_base: gitMergeBase(projectDir),
|
|
6473
|
-
changed_files: parsePorcelainStatus(status),
|
|
6543
|
+
changed_files: parsePorcelainStatus(projectDir, status),
|
|
6474
6544
|
pending_packet_ids: loadPendingPackets(projectDir).map((packet) => packet.id).sort(),
|
|
6475
6545
|
generated_at: nowIso(),
|
|
6476
6546
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kage-core/kage-graph-mcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.25",
|
|
4
4
|
"description": "Local-first repo memory, code graph, and recall MCP server for coding agents",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"license": "GPL-3.0-only",
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.10.2",
|
|
40
|
+
"three": "^0.184.0",
|
|
40
41
|
"typescript": "^5.0.0"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
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);
|
|
@@ -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;
|
package/viewer/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Kage Memory Terminal</title>
|
|
7
|
-
<link rel="stylesheet" href="./styles.css?v=
|
|
7
|
+
<link rel="stylesheet" href="./styles.css?v=19">
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<header class="app-header">
|
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
<h1>Memory Terminal</h1>
|
|
14
14
|
<p id="graphSummary">Load memory, code, and metrics graphs to inspect what agents know and why.</p>
|
|
15
15
|
</div>
|
|
16
|
+
<nav class="site-links" aria-label="Kage site links">
|
|
17
|
+
<a href="../">Home</a>
|
|
18
|
+
<a href="../guide.html">Docs</a>
|
|
19
|
+
<a href="../releases.html">Releases</a>
|
|
20
|
+
<a href="https://github.com/kage-core/Kage">GitHub</a>
|
|
21
|
+
</nav>
|
|
16
22
|
<div id="statusStrip" class="status-strip" aria-label="Graph health"></div>
|
|
17
23
|
<div id="autoLoadStatus" class="autoload-status">auto-load: waiting</div>
|
|
18
24
|
<label class="file-picker">
|
|
@@ -29,14 +35,15 @@
|
|
|
29
35
|
<p id="graphSubhead">Combined repo memory and source graph.</p>
|
|
30
36
|
</div>
|
|
31
37
|
<div class="graph-actions" aria-label="Graph view controls">
|
|
32
|
-
<span class="interaction-hint">drag canvas / wheel zoom / click node</span>
|
|
38
|
+
<span id="interactionHint" class="interaction-hint">drag canvas / wheel zoom / click node</span>
|
|
33
39
|
<button id="zoomOut" type="button">-</button>
|
|
34
40
|
<button id="fitView" type="button">Fit</button>
|
|
35
41
|
<button id="zoomIn" type="button">+</button>
|
|
36
42
|
</div>
|
|
37
43
|
</div>
|
|
38
|
-
<div class="graph-canvas-wrap">
|
|
44
|
+
<div id="graphCanvasWrap" class="graph-canvas-wrap">
|
|
39
45
|
<canvas id="graphCanvas" aria-label="Interactive Kage memory and code graph"></canvas>
|
|
46
|
+
<div id="threeGraph" class="three-graph" aria-label="Interactive 3D Kage memory and code graph"></div>
|
|
40
47
|
<div id="graphTooltip" class="graph-tooltip" role="status"></div>
|
|
41
48
|
</div>
|
|
42
49
|
<svg id="graphSvg" class="fallback-graph" viewBox="0 0 1000 660" role="img" aria-labelledby="graphTitle graphDescription">
|
|
@@ -74,6 +81,13 @@
|
|
|
74
81
|
<option value="code">Code</option>
|
|
75
82
|
</select>
|
|
76
83
|
</label>
|
|
84
|
+
<label>
|
|
85
|
+
<span>Graph Mode</span>
|
|
86
|
+
<select id="renderMode">
|
|
87
|
+
<option value="2d" selected>2D Canvas</option>
|
|
88
|
+
<option value="3d">3D Space</option>
|
|
89
|
+
</select>
|
|
90
|
+
</label>
|
|
77
91
|
<label>
|
|
78
92
|
<span>Node Type</span>
|
|
79
93
|
<select id="typeFilter">
|
|
@@ -149,6 +163,6 @@
|
|
|
149
163
|
</section>
|
|
150
164
|
</main>
|
|
151
165
|
|
|
152
|
-
<script src="./app.js?v=
|
|
166
|
+
<script src="./app.js?v=19"></script>
|
|
153
167
|
</body>
|
|
154
168
|
</html>
|
package/viewer/styles.css
CHANGED
|
@@ -40,7 +40,10 @@ h2 { color: var(--terminal); font-size: 13px; letter-spacing: 0.04em; text-trans
|
|
|
40
40
|
|
|
41
41
|
.app-header {
|
|
42
42
|
display: grid;
|
|
43
|
-
grid-template-columns: minmax(
|
|
43
|
+
grid-template-columns: minmax(360px, 1fr) auto auto;
|
|
44
|
+
grid-template-areas:
|
|
45
|
+
"brand links picker"
|
|
46
|
+
"status status autoload";
|
|
44
47
|
gap: 16px;
|
|
45
48
|
align-items: center;
|
|
46
49
|
padding: 14px 18px;
|
|
@@ -52,7 +55,11 @@ h2 { color: var(--terminal); font-size: 13px; letter-spacing: 0.04em; text-trans
|
|
|
52
55
|
z-index: 10;
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
.brand-block {
|
|
58
|
+
.brand-block {
|
|
59
|
+
grid-area: brand;
|
|
60
|
+
min-width: 280px;
|
|
61
|
+
max-width: 620px;
|
|
62
|
+
}
|
|
56
63
|
.eyebrow {
|
|
57
64
|
display: inline-flex;
|
|
58
65
|
margin-bottom: 5px;
|
|
@@ -72,14 +79,42 @@ h2 { color: var(--terminal); font-size: 13px; letter-spacing: 0.04em; text-trans
|
|
|
72
79
|
overflow-wrap: anywhere;
|
|
73
80
|
}
|
|
74
81
|
|
|
82
|
+
.site-links {
|
|
83
|
+
grid-area: links;
|
|
84
|
+
display: flex;
|
|
85
|
+
flex-wrap: wrap;
|
|
86
|
+
gap: 10px;
|
|
87
|
+
justify-content: flex-end;
|
|
88
|
+
}
|
|
89
|
+
.site-links a {
|
|
90
|
+
display: inline-flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
min-height: 28px;
|
|
93
|
+
padding: 4px 9px;
|
|
94
|
+
border: 1px solid var(--line);
|
|
95
|
+
border-radius: 4px;
|
|
96
|
+
background: rgba(13, 25, 19, 0.86);
|
|
97
|
+
color: var(--terminal-dim);
|
|
98
|
+
font-size: 11px;
|
|
99
|
+
font-weight: 780;
|
|
100
|
+
text-decoration: none;
|
|
101
|
+
white-space: nowrap;
|
|
102
|
+
}
|
|
103
|
+
.site-links a:hover {
|
|
104
|
+
border-color: var(--line-strong);
|
|
105
|
+
color: var(--terminal-strong);
|
|
106
|
+
}
|
|
107
|
+
|
|
75
108
|
.status-strip {
|
|
109
|
+
grid-area: status;
|
|
76
110
|
display: flex;
|
|
77
111
|
flex-wrap: wrap;
|
|
78
112
|
gap: 8px;
|
|
79
|
-
justify-content: flex-
|
|
113
|
+
justify-content: flex-start;
|
|
80
114
|
}
|
|
81
115
|
|
|
82
116
|
.autoload-status {
|
|
117
|
+
grid-area: autoload;
|
|
83
118
|
display: inline-flex;
|
|
84
119
|
align-items: center;
|
|
85
120
|
min-height: 28px;
|
|
@@ -131,6 +166,7 @@ h2 { color: var(--terminal); font-size: 13px; letter-spacing: 0.04em; text-trans
|
|
|
131
166
|
font-weight: 780;
|
|
132
167
|
box-shadow: inset 0 0 0 1px rgba(65, 255, 143, 0.10);
|
|
133
168
|
}
|
|
169
|
+
.file-picker { grid-area: picker; }
|
|
134
170
|
.file-picker:hover, button:hover { background: #0d1f15; }
|
|
135
171
|
.file-picker input {
|
|
136
172
|
position: absolute;
|
|
@@ -337,16 +373,49 @@ input:focus, select:focus, button:focus, .file-picker:focus-within {
|
|
|
337
373
|
#020503;
|
|
338
374
|
}
|
|
339
375
|
|
|
340
|
-
#graphCanvas {
|
|
376
|
+
#graphCanvas, .three-graph {
|
|
341
377
|
width: 100%;
|
|
342
378
|
height: 100%;
|
|
343
379
|
min-height: 600px;
|
|
344
380
|
display: block;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
#graphCanvas {
|
|
345
384
|
cursor: grab;
|
|
346
385
|
}
|
|
347
386
|
|
|
348
387
|
#graphCanvas:active { cursor: grabbing; }
|
|
349
388
|
|
|
389
|
+
.three-graph {
|
|
390
|
+
position: absolute;
|
|
391
|
+
inset: 0;
|
|
392
|
+
display: none;
|
|
393
|
+
cursor: grab;
|
|
394
|
+
background:
|
|
395
|
+
linear-gradient(rgba(65, 255, 143, 0.035) 1px, transparent 1px),
|
|
396
|
+
linear-gradient(90deg, rgba(65, 255, 143, 0.035) 1px, transparent 1px),
|
|
397
|
+
radial-gradient(circle at 52% 46%, rgba(106, 215, 255, 0.10), transparent 46%),
|
|
398
|
+
#020503;
|
|
399
|
+
background-size: 34px 34px, 34px 34px, 100% 100%, 100% 100%;
|
|
400
|
+
}
|
|
401
|
+
.three-graph canvas {
|
|
402
|
+
display: block;
|
|
403
|
+
width: 100% !important;
|
|
404
|
+
height: 100% !important;
|
|
405
|
+
}
|
|
406
|
+
.three-graph:active { cursor: grabbing; }
|
|
407
|
+
.three-status {
|
|
408
|
+
position: absolute;
|
|
409
|
+
inset: 0;
|
|
410
|
+
display: grid;
|
|
411
|
+
place-items: center;
|
|
412
|
+
color: var(--terminal-dim);
|
|
413
|
+
font-weight: 780;
|
|
414
|
+
}
|
|
415
|
+
.graph-canvas-wrap.mode-3d #graphCanvas { display: none; }
|
|
416
|
+
.graph-canvas-wrap.mode-3d .three-graph { display: block; }
|
|
417
|
+
.graph-canvas-wrap.mode-2d .three-graph { display: none; }
|
|
418
|
+
|
|
350
419
|
#graphSvg {
|
|
351
420
|
width: 100%;
|
|
352
421
|
height: 100%;
|
|
@@ -669,6 +738,12 @@ input:focus, select:focus, button:focus, .file-picker:focus-within {
|
|
|
669
738
|
@media (max-width: 1120px) {
|
|
670
739
|
.app-header {
|
|
671
740
|
grid-template-columns: 1fr;
|
|
741
|
+
grid-template-areas:
|
|
742
|
+
"brand"
|
|
743
|
+
"links"
|
|
744
|
+
"status"
|
|
745
|
+
"autoload"
|
|
746
|
+
"picker";
|
|
672
747
|
align-items: stretch;
|
|
673
748
|
padding: 12px 14px;
|
|
674
749
|
}
|
|
@@ -693,7 +768,7 @@ input:focus, select:focus, button:focus, .file-picker:focus-within {
|
|
|
693
768
|
.control-panel .panel-heading, .metrics-grid, .legend { grid-column: 1 / -1; }
|
|
694
769
|
.control-panel label, .control-panel button { margin-top: 0; }
|
|
695
770
|
.graph-panel { min-height: 620px; }
|
|
696
|
-
#graphCanvas, .graph-canvas-wrap, #graphSvg { min-height: 560px; }
|
|
771
|
+
#graphCanvas, .three-graph, .graph-canvas-wrap, #graphSvg { min-height: 560px; }
|
|
697
772
|
.details-panel {
|
|
698
773
|
height: auto;
|
|
699
774
|
max-height: 70vh;
|
|
@@ -710,5 +785,5 @@ input:focus, select:focus, button:focus, .file-picker:focus-within {
|
|
|
710
785
|
.graph-toolbar { align-items: flex-start; flex-direction: column; }
|
|
711
786
|
.graph-actions { width: 100%; }
|
|
712
787
|
.interaction-hint { flex: 1; white-space: normal; }
|
|
713
|
-
#graphCanvas, .graph-canvas-wrap, #graphSvg { min-height: 450px; }
|
|
788
|
+
#graphCanvas, .three-graph, .graph-canvas-wrap, #graphSvg { min-height: 450px; }
|
|
714
789
|
}
|