sdl-mcp 0.12.2 → 0.12.3
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 +10 -0
- package/config/sdlmcp.config.example.json +53 -8
- package/config/sdlmcp.config.schema.json +530 -106
- package/dist/.tsbuildinfo +1 -1
- package/dist/benchmark/external-manifest.d.ts +162 -0
- package/dist/benchmark/external-manifest.d.ts.map +1 -0
- package/dist/benchmark/external-manifest.js +336 -0
- package/dist/benchmark/external-manifest.js.map +1 -0
- package/dist/benchmark/external-runner.d.ts +100 -0
- package/dist/benchmark/external-runner.d.ts.map +1 -0
- package/dist/benchmark/external-runner.js +1171 -0
- package/dist/benchmark/external-runner.js.map +1 -0
- package/dist/benchmark/output-file.d.ts +3 -0
- package/dist/benchmark/output-file.d.ts.map +1 -0
- package/dist/benchmark/output-file.js +8 -0
- package/dist/benchmark/output-file.js.map +1 -0
- package/dist/cli/argParsing.d.ts.map +1 -1
- package/dist/cli/argParsing.js +6 -0
- package/dist/cli/argParsing.js.map +1 -1
- package/dist/cli/commands/benchmark.d.ts.map +1 -1
- package/dist/cli/commands/benchmark.js +3 -2
- package/dist/cli/commands/benchmark.js.map +1 -1
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +13 -7
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +5 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +73 -3
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/serve.d.ts.map +1 -1
- package/dist/cli/commands/serve.js +2 -0
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/transport/http.d.ts +1 -35
- package/dist/cli/transport/http.d.ts.map +1 -1
- package/dist/cli/transport/http.js +151 -175
- package/dist/cli/transport/http.js.map +1 -1
- package/dist/cli/types.d.ts +1 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/config/constants.d.ts +30 -0
- package/dist/config/constants.d.ts.map +1 -1
- package/dist/config/constants.js +30 -0
- package/dist/config/constants.js.map +1 -1
- package/dist/config/types.d.ts +53 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +62 -14
- package/dist/config/types.js.map +1 -1
- package/dist/db/ladybug-batching.d.ts +1 -0
- package/dist/db/ladybug-batching.d.ts.map +1 -1
- package/dist/db/ladybug-batching.js +1 -0
- package/dist/db/ladybug-batching.js.map +1 -1
- package/dist/db/ladybug-core.d.ts +3 -0
- package/dist/db/ladybug-core.d.ts.map +1 -1
- package/dist/db/ladybug-core.js +72 -2
- package/dist/db/ladybug-core.js.map +1 -1
- package/dist/db/ladybug-embeddings.d.ts +6 -1
- package/dist/db/ladybug-embeddings.d.ts.map +1 -1
- package/dist/db/ladybug-embeddings.js +6 -1
- package/dist/db/ladybug-embeddings.js.map +1 -1
- package/dist/db/ladybug-file-summaries.d.ts.map +1 -1
- package/dist/db/ladybug-file-summaries.js +23 -0
- package/dist/db/ladybug-file-summaries.js.map +1 -1
- package/dist/db/ladybug-queries.js +1 -1
- package/dist/db/ladybug-queries.js.map +1 -1
- package/dist/db/migrations/index.d.ts.map +1 -1
- package/dist/db/migrations/index.js +2 -0
- package/dist/db/migrations/index.js.map +1 -1
- package/dist/db/migrations/m007-copy-embeddings-to-symbol.d.ts +5 -9
- package/dist/db/migrations/m007-copy-embeddings-to-symbol.d.ts.map +1 -1
- package/dist/db/migrations/m007-copy-embeddings-to-symbol.js +24 -126
- package/dist/db/migrations/m007-copy-embeddings-to-symbol.js.map +1 -1
- package/dist/db/migrations/m021-remediate-symbol-embeddings.d.ts +5 -0
- package/dist/db/migrations/m021-remediate-symbol-embeddings.d.ts.map +1 -0
- package/dist/db/migrations/m021-remediate-symbol-embeddings.js +7 -0
- package/dist/db/migrations/m021-remediate-symbol-embeddings.js.map +1 -0
- package/dist/db/migrations/symbol-embedding-remediation.d.ts +57 -0
- package/dist/db/migrations/symbol-embedding-remediation.d.ts.map +1 -0
- package/dist/db/migrations/symbol-embedding-remediation.js +335 -0
- package/dist/db/migrations/symbol-embedding-remediation.js.map +1 -0
- package/dist/graph/layout/force-layout.d.ts +4 -0
- package/dist/graph/layout/force-layout.d.ts.map +1 -0
- package/dist/graph/layout/force-layout.js +127 -0
- package/dist/graph/layout/force-layout.js.map +1 -0
- package/dist/graph/layout/layout-service.d.ts +37 -0
- package/dist/graph/layout/layout-service.d.ts.map +1 -0
- package/dist/graph/layout/layout-service.js +172 -0
- package/dist/graph/layout/layout-service.js.map +1 -0
- package/dist/graph/layout/native-engine.d.ts +9 -0
- package/dist/graph/layout/native-engine.d.ts.map +1 -0
- package/dist/graph/layout/native-engine.js +59 -0
- package/dist/graph/layout/native-engine.js.map +1 -0
- package/dist/graph/layout/prng.d.ts +3 -0
- package/dist/graph/layout/prng.d.ts.map +1 -0
- package/dist/graph/layout/prng.js +19 -0
- package/dist/graph/layout/prng.js.map +1 -0
- package/dist/graph/layout/serializer.d.ts +3 -0
- package/dist/graph/layout/serializer.d.ts.map +1 -0
- package/dist/graph/layout/serializer.js +20 -0
- package/dist/graph/layout/serializer.js.map +1 -0
- package/dist/graph/layout/types.d.ts +30 -0
- package/dist/graph/layout/types.d.ts.map +1 -0
- package/dist/graph/layout/types.js +2 -0
- package/dist/graph/layout/types.js.map +1 -0
- package/dist/indexer/embeddings.d.ts +7 -0
- package/dist/indexer/embeddings.d.ts.map +1 -1
- package/dist/indexer/embeddings.js +44 -4
- package/dist/indexer/embeddings.js.map +1 -1
- package/dist/indexer/file-summary-embeddings.d.ts +12 -0
- package/dist/indexer/file-summary-embeddings.d.ts.map +1 -1
- package/dist/indexer/file-summary-embeddings.js +83 -21
- package/dist/indexer/file-summary-embeddings.js.map +1 -1
- package/dist/indexer/fileWalker.d.ts.map +1 -1
- package/dist/indexer/fileWalker.js +2 -1
- package/dist/indexer/fileWalker.js.map +1 -1
- package/dist/indexer/metrics-updater.d.ts.map +1 -1
- package/dist/indexer/metrics-updater.js +27 -4
- package/dist/indexer/metrics-updater.js.map +1 -1
- package/dist/indexer/watcher.d.ts.map +1 -1
- package/dist/indexer/watcher.js +2 -1
- package/dist/indexer/watcher.js.map +1 -1
- package/dist/mcp/context-response-projection.d.ts.map +1 -1
- package/dist/mcp/context-response-projection.js +49 -13
- package/dist/mcp/context-response-projection.js.map +1 -1
- package/dist/mcp/tools/search-edit/planner.d.ts.map +1 -1
- package/dist/mcp/tools/search-edit/planner.js +10 -3
- package/dist/mcp/tools/search-edit/planner.js.map +1 -1
- package/dist/observability/event-tap.d.ts +2 -1
- package/dist/observability/event-tap.d.ts.map +1 -1
- package/dist/observability/event-tap.js.map +1 -1
- package/dist/observability/service.d.ts +11 -4
- package/dist/observability/service.d.ts.map +1 -1
- package/dist/observability/service.js +83 -13
- package/dist/observability/service.js.map +1 -1
- package/dist/observability/types.d.ts +48 -2
- package/dist/observability/types.d.ts.map +1 -1
- package/dist/ui/.tsbuildinfo +1 -0
- package/dist/ui/vendor/fflate.js +2692 -0
- package/dist/ui/vendor/jsm/controls/ArcballControls.js +3543 -0
- package/dist/ui/vendor/jsm/controls/DragControls.js +452 -0
- package/dist/ui/vendor/jsm/controls/FirstPersonControls.js +488 -0
- package/dist/ui/vendor/jsm/controls/FlyControls.js +384 -0
- package/dist/ui/vendor/jsm/controls/MapControls.js +116 -0
- package/dist/ui/vendor/jsm/controls/OrbitControls.js +1963 -0
- package/dist/ui/vendor/jsm/controls/PointerLockControls.js +264 -0
- package/dist/ui/vendor/jsm/controls/TrackballControls.js +1001 -0
- package/dist/ui/vendor/jsm/controls/TransformControls.js +2003 -0
- package/dist/ui/vendor/jsm/loaders/3DMLoader.js +1836 -0
- package/dist/ui/vendor/jsm/loaders/3MFLoader.js +1621 -0
- package/dist/ui/vendor/jsm/loaders/AMFLoader.js +541 -0
- package/dist/ui/vendor/jsm/loaders/BVHLoader.js +484 -0
- package/dist/ui/vendor/jsm/loaders/ColladaLoader.js +153 -0
- package/dist/ui/vendor/jsm/loaders/DDSLoader.js +385 -0
- package/dist/ui/vendor/jsm/loaders/DRACOLoader.js +772 -0
- package/dist/ui/vendor/jsm/loaders/EXRLoader.js +3395 -0
- package/dist/ui/vendor/jsm/loaders/FBXLoader.js +4580 -0
- package/dist/ui/vendor/jsm/loaders/FontLoader.js +260 -0
- package/dist/ui/vendor/jsm/loaders/GCodeLoader.js +318 -0
- package/dist/ui/vendor/jsm/loaders/GLTFLoader.js +4860 -0
- package/dist/ui/vendor/jsm/loaders/HDRCubeTextureLoader.js +164 -0
- package/dist/ui/vendor/jsm/loaders/HDRLoader.js +463 -0
- package/dist/ui/vendor/jsm/loaders/IESLoader.js +379 -0
- package/dist/ui/vendor/jsm/loaders/KMZLoader.js +163 -0
- package/dist/ui/vendor/jsm/loaders/KTX2Loader.js +1276 -0
- package/dist/ui/vendor/jsm/loaders/KTXLoader.js +197 -0
- package/dist/ui/vendor/jsm/loaders/LDrawLoader.js +2510 -0
- package/dist/ui/vendor/jsm/loaders/LUT3dlLoader.js +205 -0
- package/dist/ui/vendor/jsm/loaders/LUTCubeLoader.js +190 -0
- package/dist/ui/vendor/jsm/loaders/LUTImageLoader.js +190 -0
- package/dist/ui/vendor/jsm/loaders/LWOLoader.js +1051 -0
- package/dist/ui/vendor/jsm/loaders/LottieLoader.js +130 -0
- package/dist/ui/vendor/jsm/loaders/MD2Loader.js +435 -0
- package/dist/ui/vendor/jsm/loaders/MDDLoader.js +147 -0
- package/dist/ui/vendor/jsm/loaders/MTLLoader.js +593 -0
- package/dist/ui/vendor/jsm/loaders/MaterialXLoader.js +1126 -0
- package/dist/ui/vendor/jsm/loaders/NRRDLoader.js +718 -0
- package/dist/ui/vendor/jsm/loaders/OBJLoader.js +955 -0
- package/dist/ui/vendor/jsm/loaders/PCDLoader.js +620 -0
- package/dist/ui/vendor/jsm/loaders/PDBLoader.js +272 -0
- package/dist/ui/vendor/jsm/loaders/PLYLoader.js +968 -0
- package/dist/ui/vendor/jsm/loaders/PVRLoader.js +270 -0
- package/dist/ui/vendor/jsm/loaders/RGBELoader.js +18 -0
- package/dist/ui/vendor/jsm/loaders/STLLoader.js +421 -0
- package/dist/ui/vendor/jsm/loaders/SVGLoader.js +3319 -0
- package/dist/ui/vendor/jsm/loaders/TDSLoader.js +1142 -0
- package/dist/ui/vendor/jsm/loaders/TGALoader.js +538 -0
- package/dist/ui/vendor/jsm/loaders/TIFFLoader.js +59 -0
- package/dist/ui/vendor/jsm/loaders/TTFLoader.js +261 -0
- package/dist/ui/vendor/jsm/loaders/USDLoader.js +303 -0
- package/dist/ui/vendor/jsm/loaders/USDZLoader.js +16 -0
- package/dist/ui/vendor/jsm/loaders/UltraHDRLoader.js +755 -0
- package/dist/ui/vendor/jsm/loaders/VOXLoader.js +919 -0
- package/dist/ui/vendor/jsm/loaders/VRMLLoader.js +3646 -0
- package/dist/ui/vendor/jsm/loaders/VTKLoader.js +1293 -0
- package/dist/ui/vendor/jsm/loaders/XYZLoader.js +143 -0
- package/dist/ui/vendor/jsm/loaders/collada/ColladaComposer.js +3044 -0
- package/dist/ui/vendor/jsm/loaders/collada/ColladaParser.js +1977 -0
- package/dist/ui/vendor/jsm/loaders/lwo/IFFParser.js +1217 -0
- package/dist/ui/vendor/jsm/loaders/lwo/LWO2Parser.js +414 -0
- package/dist/ui/vendor/jsm/loaders/lwo/LWO3Parser.js +373 -0
- package/dist/ui/vendor/jsm/loaders/usd/USDAParser.js +901 -0
- package/dist/ui/vendor/jsm/loaders/usd/USDCParser.js +1878 -0
- package/dist/ui/vendor/jsm/loaders/usd/USDComposer.js +4627 -0
- package/dist/ui/vendor/jsm/utils/BufferGeometryUtils.js +1501 -0
- package/dist/ui/vendor/jsm/utils/CameraUtils.js +82 -0
- package/dist/ui/vendor/jsm/utils/ColorUtils.js +76 -0
- package/dist/ui/vendor/jsm/utils/GeometryCompressionUtils.js +547 -0
- package/dist/ui/vendor/jsm/utils/GeometryUtils.js +226 -0
- package/dist/ui/vendor/jsm/utils/LDrawUtils.js +211 -0
- package/dist/ui/vendor/jsm/utils/SceneOptimizer.js +458 -0
- package/dist/ui/vendor/jsm/utils/SceneUtils.js +363 -0
- package/dist/ui/vendor/jsm/utils/ShadowMapViewer.js +244 -0
- package/dist/ui/vendor/jsm/utils/ShadowMapViewerGPU.js +233 -0
- package/dist/ui/vendor/jsm/utils/SkeletonUtils.js +496 -0
- package/dist/ui/vendor/jsm/utils/SortUtils.js +175 -0
- package/dist/ui/vendor/jsm/utils/UVsDebug.js +173 -0
- package/dist/ui/vendor/jsm/utils/WebGLTextureUtils.js +115 -0
- package/dist/ui/vendor/jsm/utils/WebGPUTextureUtils.js +81 -0
- package/dist/ui/vendor/jsm/utils/WorkerPool.js +167 -0
- package/dist/ui/vendor/three.core.min.js +6 -0
- package/dist/ui/vendor/three.module.min.js +6 -0
- package/dist/ui/viewer/ambient.js +39 -0
- package/dist/ui/viewer/api.js +93 -0
- package/dist/ui/viewer/chrome.js +36 -0
- package/dist/ui/viewer/edges.js +33 -0
- package/dist/ui/viewer/index.html +32 -0
- package/dist/ui/viewer/inspector.js +23 -0
- package/dist/ui/viewer/lenses/activity.js +24 -0
- package/dist/ui/viewer/lenses/community.js +5 -0
- package/dist/ui/viewer/lenses/edges.js +4 -0
- package/dist/ui/viewer/lenses/impact.js +6 -0
- package/dist/ui/viewer/lenses/search.js +69 -0
- package/dist/ui/viewer/lod.js +100 -0
- package/dist/ui/viewer/main.js +92 -0
- package/dist/ui/viewer/picking.js +45 -0
- package/dist/ui/viewer/scene.js +85 -0
- package/dist/ui/viewer/skins/default-skin.js +15 -0
- package/dist/ui/viewer/skins/loader.js +44 -0
- package/dist/ui/viewer/skins/manifest-schema.js +30 -0
- package/dist/ui/viewer/state.js +44 -0
- package/dist/ui/viewer/theme.js +23 -0
- package/dist/ui/viewer/universe.js +151 -0
- package/dist/ui/viewer/viewer.css +85 -0
- package/dist/util/safeRegex.d.ts.map +1 -1
- package/dist/util/safeRegex.js +221 -68
- package/dist/util/safeRegex.js.map +1 -1
- package/dist/viewer/legacy-graph.d.ts +40 -0
- package/dist/viewer/legacy-graph.d.ts.map +1 -0
- package/dist/viewer/legacy-graph.js +152 -0
- package/dist/viewer/legacy-graph.js.map +1 -0
- package/dist/viewer/routes.d.ts +10 -0
- package/dist/viewer/routes.d.ts.map +1 -0
- package/dist/viewer/routes.js +221 -0
- package/dist/viewer/routes.js.map +1 -0
- package/dist/viewer/service.d.ts +44 -0
- package/dist/viewer/service.d.ts.map +1 -0
- package/dist/viewer/service.js +160 -0
- package/dist/viewer/service.js.map +1 -0
- package/dist/viewer/types.d.ts +63 -0
- package/dist/viewer/types.d.ts.map +1 -0
- package/dist/viewer/types.js +2 -0
- package/dist/viewer/types.js.map +1 -0
- package/dist/viewer/viewer-config.d.ts +12 -0
- package/dist/viewer/viewer-config.d.ts.map +1 -0
- package/dist/viewer/viewer-config.js +36 -0
- package/dist/viewer/viewer-config.js.map +1 -0
- package/package.json +18 -9
- package/templates/SDL.md +19 -0
- package/templates/claude-code.json +8 -43
- package/dist/ui/graph.css +0 -108
- package/dist/ui/graph.d.ts +0 -2
- package/dist/ui/graph.d.ts.map +0 -1
- package/dist/ui/graph.html +0 -52
- package/dist/ui/graph.js +0 -259
- package/dist/ui/graph.js.map +0 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { setAmbient, state } from "./state.js";
|
|
3
|
+
export class AmbientController {
|
|
4
|
+
viewer;
|
|
5
|
+
universe;
|
|
6
|
+
caption;
|
|
7
|
+
lastInput = performance.now();
|
|
8
|
+
captionTimer = 0;
|
|
9
|
+
constructor(viewer, universe, caption) {
|
|
10
|
+
this.viewer = viewer;
|
|
11
|
+
this.universe = universe;
|
|
12
|
+
this.caption = caption;
|
|
13
|
+
for (const type of ["pointermove", "keydown", "wheel", "pointerdown"])
|
|
14
|
+
window.addEventListener(type, this.onInput, { passive: true });
|
|
15
|
+
viewer.onFrame((delta) => this.frame(delta));
|
|
16
|
+
}
|
|
17
|
+
setCaption(text) {
|
|
18
|
+
this.caption.textContent = text;
|
|
19
|
+
this.caption.classList.add("visible");
|
|
20
|
+
window.clearTimeout(this.captionTimer);
|
|
21
|
+
this.captionTimer = window.setTimeout(() => this.caption.classList.remove("visible"), 3500);
|
|
22
|
+
}
|
|
23
|
+
onInput = () => { this.lastInput = performance.now(); if (state.ambient)
|
|
24
|
+
setAmbient(false); };
|
|
25
|
+
frame(delta) {
|
|
26
|
+
const settings = state.settings;
|
|
27
|
+
if (!settings?.ambient.enabled)
|
|
28
|
+
return;
|
|
29
|
+
if (!state.ambient && performance.now() - this.lastInput > settings.ambient.idleSeconds * 1000)
|
|
30
|
+
setAmbient(true);
|
|
31
|
+
if (!state.ambient)
|
|
32
|
+
return;
|
|
33
|
+
this.viewer.setFpsCap(settings.ambient.fps);
|
|
34
|
+
const angle = (delta / 1000) * 0.02;
|
|
35
|
+
this.viewer.camera.position.applyAxisAngle(new THREE.Vector3(0, 1, 0), angle);
|
|
36
|
+
this.viewer.controls.target.lerp(new THREE.Vector3(0, 0, 0), 0.02);
|
|
37
|
+
void this.universe;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const TOKEN_STORAGE_KEY = "sdl-mcp-observability-token";
|
|
2
|
+
export function readAuthTokenFromPage() {
|
|
3
|
+
const params = new URLSearchParams(window.location.hash.replace(/^#/, ""));
|
|
4
|
+
const hashToken = params.get("token");
|
|
5
|
+
if (hashToken) {
|
|
6
|
+
localStorage.setItem(TOKEN_STORAGE_KEY, hashToken);
|
|
7
|
+
history.replaceState(null, "", window.location.pathname + window.location.search);
|
|
8
|
+
return hashToken;
|
|
9
|
+
}
|
|
10
|
+
return localStorage.getItem(TOKEN_STORAGE_KEY) ?? "";
|
|
11
|
+
}
|
|
12
|
+
export class ViewerApi {
|
|
13
|
+
token = readAuthTokenFromPage();
|
|
14
|
+
setToken(token) {
|
|
15
|
+
this.token = token;
|
|
16
|
+
if (token)
|
|
17
|
+
localStorage.setItem(TOKEN_STORAGE_KEY, token);
|
|
18
|
+
else
|
|
19
|
+
localStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
20
|
+
}
|
|
21
|
+
getToken() {
|
|
22
|
+
return this.token;
|
|
23
|
+
}
|
|
24
|
+
headers(accept = "application/json") {
|
|
25
|
+
const headers = { Accept: accept };
|
|
26
|
+
if (this.token)
|
|
27
|
+
headers.Authorization = "Bearer " + this.token;
|
|
28
|
+
return headers;
|
|
29
|
+
}
|
|
30
|
+
async json(url) {
|
|
31
|
+
const response = await fetch(url, { headers: this.headers() });
|
|
32
|
+
if (!response.ok)
|
|
33
|
+
throw new Error("HTTP " + response.status + " for " + url);
|
|
34
|
+
return response.json();
|
|
35
|
+
}
|
|
36
|
+
async universe() { return this.json("/api/graph/universe"); }
|
|
37
|
+
async clusters(repoId) { return this.json("/api/graph/repo/" + encodeURIComponent(repoId) + "/clusters"); }
|
|
38
|
+
async clusterEdges(repoId) { return this.json("/api/graph/repo/" + encodeURIComponent(repoId) + "/edges?scope=clusters"); }
|
|
39
|
+
async layout(repoId, lod, clusterId) {
|
|
40
|
+
const params = new URLSearchParams({ lod });
|
|
41
|
+
if (clusterId)
|
|
42
|
+
params.set("clusterId", clusterId);
|
|
43
|
+
return this.json("/api/graph/repo/" + encodeURIComponent(repoId) + "/layout?" + params.toString());
|
|
44
|
+
}
|
|
45
|
+
async symbolEdges(repoId, clusterId, filters) {
|
|
46
|
+
const params = new URLSearchParams({ clusterId });
|
|
47
|
+
if (filters?.minConfidence !== undefined)
|
|
48
|
+
params.set("minConfidence", String(filters.minConfidence));
|
|
49
|
+
if (filters?.exactOnly)
|
|
50
|
+
params.set("exactOnly", "1");
|
|
51
|
+
if (filters?.kinds?.length)
|
|
52
|
+
params.set("kinds", filters.kinds.join(","));
|
|
53
|
+
return this.json("/api/graph/repo/" + encodeURIComponent(repoId) + "/edges?" + params.toString());
|
|
54
|
+
}
|
|
55
|
+
async card(repoId, symbolId) { return this.json("/api/graph/repo/" + encodeURIComponent(repoId) + "/symbol/" + encodeURIComponent(symbolId) + "/card"); }
|
|
56
|
+
async search(repoId, query) { return this.json("/api/graph/repo/" + encodeURIComponent(repoId) + "/search?q=" + encodeURIComponent(query)); }
|
|
57
|
+
async impact(repoId, fromVersion, toVersion) {
|
|
58
|
+
return this.json("/api/graph/repo/" + encodeURIComponent(repoId) + "/impact?fromVersion=" + encodeURIComponent(fromVersion) + "&toVersion=" + encodeURIComponent(toVersion));
|
|
59
|
+
}
|
|
60
|
+
async skins() { return this.json("/api/graph/skins"); }
|
|
61
|
+
async skinBytes(id) {
|
|
62
|
+
const response = await fetch("/api/graph/skins/" + encodeURIComponent(id), { headers: this.headers("application/zip") });
|
|
63
|
+
if (!response.ok)
|
|
64
|
+
throw new Error("HTTP " + response.status + " for skin " + id);
|
|
65
|
+
return response.arrayBuffer();
|
|
66
|
+
}
|
|
67
|
+
async graphStream(onEvent, signal) {
|
|
68
|
+
const response = await fetch("/api/observability/stream?types=graph", { headers: this.headers("text/event-stream"), signal });
|
|
69
|
+
if (!response.ok || !response.body)
|
|
70
|
+
throw new Error("SSE failed: HTTP " + response.status);
|
|
71
|
+
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
72
|
+
let buffer = "";
|
|
73
|
+
while (!signal.aborted) {
|
|
74
|
+
const chunk = await reader.read();
|
|
75
|
+
if (chunk.done)
|
|
76
|
+
break;
|
|
77
|
+
buffer += chunk.value;
|
|
78
|
+
const events = buffer.split("\n\n");
|
|
79
|
+
buffer = events.pop() ?? "";
|
|
80
|
+
for (const event of events) {
|
|
81
|
+
const data = event.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("\n");
|
|
82
|
+
if (!data)
|
|
83
|
+
continue;
|
|
84
|
+
try {
|
|
85
|
+
onEvent(JSON.parse(data));
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.warn("[viewer] bad SSE event", error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { setActiveLens, setVisibleRepos, subscribe } from "./state.js";
|
|
2
|
+
export class ViewerChrome {
|
|
3
|
+
searchInput;
|
|
4
|
+
lensSelect;
|
|
5
|
+
skinSelect;
|
|
6
|
+
fpsSelect;
|
|
7
|
+
ambientButton;
|
|
8
|
+
status;
|
|
9
|
+
repoHost;
|
|
10
|
+
constructor(host) {
|
|
11
|
+
host.innerHTML = '<div class="brand"><span class="mark"></span><div><h1>SDL Galaxy</h1><p>graph viewer</p></div></div>' +
|
|
12
|
+
'<div class="toolbar"><div class="repo-filter"></div><input class="search" type="search" placeholder="Search symbols" aria-label="Search symbols" />' +
|
|
13
|
+
'<select class="lens" aria-label="Lens"><option value="none">Normal</option><option value="community">Community</option><option value="impact">Impact</option><option value="edges">Edges</option></select>' +
|
|
14
|
+
'<select class="skin" aria-label="Skin"><option value="default">Default</option></select>' +
|
|
15
|
+
'<select class="fps" aria-label="FPS"><option>15</option><option>30</option><option selected>60</option><option>90</option><option>120</option></select>' +
|
|
16
|
+
'<button class="ambient" type="button" aria-pressed="false">Ambient</button><span class="status">booting</span></div>';
|
|
17
|
+
this.searchInput = host.querySelector(".search");
|
|
18
|
+
this.lensSelect = host.querySelector(".lens");
|
|
19
|
+
this.skinSelect = host.querySelector(".skin");
|
|
20
|
+
this.fpsSelect = host.querySelector(".fps");
|
|
21
|
+
this.ambientButton = host.querySelector(".ambient");
|
|
22
|
+
this.status = host.querySelector(".status");
|
|
23
|
+
this.repoHost = host.querySelector(".repo-filter");
|
|
24
|
+
this.lensSelect.addEventListener("change", () => setActiveLens(this.lensSelect.value));
|
|
25
|
+
subscribe((snapshot) => { this.ambientButton.setAttribute("aria-pressed", String(snapshot.ambient)); });
|
|
26
|
+
}
|
|
27
|
+
setRepos(repos) {
|
|
28
|
+
this.repoHost.innerHTML = repos.map((repo) => '<label><input type="checkbox" value="' + repo.repoId + '" checked />' + repo.repoId + '</label>').join("");
|
|
29
|
+
this.repoHost.addEventListener("change", () => {
|
|
30
|
+
setVisibleRepos([...this.repoHost.querySelectorAll('input:checked')].map((input) => input.value));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
setSettings(settings) { this.fpsSelect.value = String(settings.fps); }
|
|
34
|
+
setSkins(skins) { this.skinSelect.innerHTML = '<option value="default">Default</option>' + skins.map((skin) => '<option value="' + skin.id + '">' + skin.id + '</option>').join(""); }
|
|
35
|
+
setStatus(value) { this.status.textContent = value; }
|
|
36
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { KIND_COLORS } from "./theme.js";
|
|
3
|
+
export function createSymbolEdges(edges, positions, minConfidence = 0, exactOnly = false, kinds = new Set()) {
|
|
4
|
+
const points = [];
|
|
5
|
+
const colors = [];
|
|
6
|
+
for (const edge of edges) {
|
|
7
|
+
const confidence = edge.confidence ?? 1;
|
|
8
|
+
const kind = edge.kind ?? "call";
|
|
9
|
+
if (confidence < minConfidence)
|
|
10
|
+
continue;
|
|
11
|
+
if (exactOnly && edge.resolution && edge.resolution !== "exact")
|
|
12
|
+
continue;
|
|
13
|
+
if (kinds.size > 0 && !kinds.has(kind))
|
|
14
|
+
continue;
|
|
15
|
+
const from = positions.get(edge.from);
|
|
16
|
+
const to = positions.get(edge.to);
|
|
17
|
+
if (!from || !to)
|
|
18
|
+
continue;
|
|
19
|
+
const color = new THREE.Color(KIND_COLORS[kind] ?? "#93a7c8").multiplyScalar(Math.max(0.65, confidence));
|
|
20
|
+
points.push(from.x, from.y, from.z, to.x, to.y, to.z);
|
|
21
|
+
colors.push(color.r, color.g, color.b, color.r, color.g, color.b);
|
|
22
|
+
}
|
|
23
|
+
const geometry = new THREE.BufferGeometry();
|
|
24
|
+
geometry.setAttribute("position", new THREE.Float32BufferAttribute(points, 3));
|
|
25
|
+
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
|
|
26
|
+
return new THREE.LineSegments(geometry, new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, opacity: 0.85, blending: THREE.AdditiveBlending, depthWrite: false }));
|
|
27
|
+
}
|
|
28
|
+
export function edgeCounts(edges) {
|
|
29
|
+
const counts = new Map();
|
|
30
|
+
for (const edge of edges)
|
|
31
|
+
counts.set(edge.kind ?? "call", (counts.get(edge.kind ?? "call") ?? 0) + 1);
|
|
32
|
+
return counts;
|
|
33
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>SDL Galaxy</title>
|
|
7
|
+
<link rel="stylesheet" href="/ui/observability.css" />
|
|
8
|
+
<link rel="stylesheet" href="/ui/viewer/viewer.css" />
|
|
9
|
+
<script type="importmap">
|
|
10
|
+
{
|
|
11
|
+
"imports": {
|
|
12
|
+
"three": "/ui/vendor/three.module.min.js",
|
|
13
|
+
"three/addons/": "/ui/vendor/jsm/",
|
|
14
|
+
"fflate": "/ui/vendor/fflate.js"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
18
|
+
</head>
|
|
19
|
+
<body class="viewer-body">
|
|
20
|
+
<header id="viewerChrome" class="viewer-chrome"></header>
|
|
21
|
+
<main class="viewer-shell">
|
|
22
|
+
<section id="viewerCanvas" class="viewer-canvas" aria-label="SDL Galaxy graph canvas"></section>
|
|
23
|
+
<aside id="viewerInspector" class="panel viewer-inspector" aria-label="Inspector"></aside>
|
|
24
|
+
<section class="panel viewer-results" aria-label="Search and lens results">
|
|
25
|
+
<header class="panel-head"><h2>RESULTS</h2><span class="panel-sub">live</span></header>
|
|
26
|
+
<div id="viewerResults" class="result-list"></div>
|
|
27
|
+
</section>
|
|
28
|
+
</main>
|
|
29
|
+
<div id="viewerCaption" class="viewer-caption" role="status" aria-live="polite"></div>
|
|
30
|
+
<script type="module" src="/ui/viewer/main.js"></script>
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { subscribe } from "./state.js";
|
|
2
|
+
function text(value) { return value === undefined || value === null || value === "" ? "-" : String(value); }
|
|
3
|
+
export class InspectorPanel {
|
|
4
|
+
host;
|
|
5
|
+
constructor(host) {
|
|
6
|
+
this.host = host;
|
|
7
|
+
subscribe((state) => this.render(state.selection?.card ?? null, state.selection?.clusterId));
|
|
8
|
+
}
|
|
9
|
+
render(card, clusterId) {
|
|
10
|
+
if (!card && !clusterId) {
|
|
11
|
+
this.host.innerHTML = '<header class="panel-head"><h2>INSPECTOR</h2><span class="panel-sub">no selection</span></header><div class="empty">Select a cluster or symbol.</div>';
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (!card) {
|
|
15
|
+
this.host.innerHTML = '<header class="panel-head"><h2>CLUSTER</h2><span class="panel-sub">' + text(clusterId) + '</span></header><dl class="inspector-dl"><dt>Cluster ID</dt><dd>' + text(clusterId) + '</dd></dl>';
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const deps = [...(card.deps?.out ?? []), ...(card.deps?.in ?? [])].slice(0, 8).map((dep) => '<button type="button" data-symbol="' + text(dep.symbolId) + '">' + text(dep.name ?? dep.symbolId) + '</button>').join("");
|
|
19
|
+
this.host.innerHTML = '<header class="panel-head"><h2>' + text(card.name ?? card.symbolId) + '</h2><span class="panel-sub">' + text(card.kind) + '</span></header>' +
|
|
20
|
+
'<dl class="inspector-dl"><dt>Signature</dt><dd><code>' + text(typeof card.signature === "string" ? card.signature : JSON.stringify(card.signature ?? "")) + '</code></dd><dt>Summary</dt><dd>' + text(card.summary) + '</dd></dl>' +
|
|
21
|
+
'<div class="dep-list">' + deps + '</div>';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { pushGraphEvent } from "../state.js";
|
|
2
|
+
export class ActivityLens {
|
|
3
|
+
api;
|
|
4
|
+
universe;
|
|
5
|
+
setCaption;
|
|
6
|
+
abortController = null;
|
|
7
|
+
constructor(api, universe, setCaption) {
|
|
8
|
+
this.api = api;
|
|
9
|
+
this.universe = universe;
|
|
10
|
+
this.setCaption = setCaption;
|
|
11
|
+
}
|
|
12
|
+
start() {
|
|
13
|
+
this.abortController?.abort();
|
|
14
|
+
this.abortController = new AbortController();
|
|
15
|
+
void this.api.graphStream((event) => this.handle(event), this.abortController.signal).catch((error) => console.warn("[viewer] graph stream", error));
|
|
16
|
+
}
|
|
17
|
+
stop() { this.abortController?.abort(); }
|
|
18
|
+
handle(event) {
|
|
19
|
+
pushGraphEvent(event);
|
|
20
|
+
if (event.repoId && event.clusterId)
|
|
21
|
+
this.universe.pulseCluster(event.repoId, event.clusterId);
|
|
22
|
+
this.setCaption(event.type + (event.count ? ": " + event.count : ""));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { edgeCounts } from "../edges.js";
|
|
2
|
+
export function renderEdgeCounts(host, edges) {
|
|
3
|
+
host.innerHTML = [...edgeCounts(edges)].map(([kind, count]) => '<label><input type="checkbox" value="' + kind + '" checked />' + kind + ' <span>' + count + '</span></label>').join("");
|
|
4
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export async function runImpactLens(api, repoId, fromVersion, toVersion, renderList) {
|
|
2
|
+
const result = await api.impact(repoId, fromVersion, toVersion);
|
|
3
|
+
const changed = (result.changed ?? []).map((symbolId) => symbolId);
|
|
4
|
+
const blast = (result.blastRadius ?? []).map((item) => item.name ?? item.symbolId);
|
|
5
|
+
renderList([...changed, ...blast]);
|
|
6
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { setSearchResults } from "../state.js";
|
|
2
|
+
function escapeHtml(value) {
|
|
3
|
+
return String(value ?? "")
|
|
4
|
+
.replaceAll("&", "&")
|
|
5
|
+
.replaceAll("<", "<")
|
|
6
|
+
.replaceAll(">", ">")
|
|
7
|
+
.replaceAll('"', """);
|
|
8
|
+
}
|
|
9
|
+
export function renderSearchResultsHtml(results) {
|
|
10
|
+
if (results.length === 0)
|
|
11
|
+
return '<div class="empty">No results</div>';
|
|
12
|
+
return results.map((result, index) => {
|
|
13
|
+
const score = typeof result.score === "number" ? " · " + result.score.toFixed(3) : "";
|
|
14
|
+
const path = result.relPath ? '<span class="result-path">' + escapeHtml(result.relPath) + '</span>' : "";
|
|
15
|
+
return '<button type="button" class="result-item" data-index="' + String(index) + '" data-symbol-id="' + escapeHtml(result.symbolId) + '" data-cluster-id="' + escapeHtml(result.clusterId ?? "") + '">' +
|
|
16
|
+
'<span class="result-name">' + escapeHtml(result.name || result.symbolId) + '</span>' +
|
|
17
|
+
'<span class="result-meta">' + escapeHtml(result.kind) + score + '</span>' +
|
|
18
|
+
path +
|
|
19
|
+
'</button>';
|
|
20
|
+
}).join("");
|
|
21
|
+
}
|
|
22
|
+
export function attachSearch(input, resultsHost, api, getRepoId, onSelect) {
|
|
23
|
+
let timer = 0;
|
|
24
|
+
let currentResults = [];
|
|
25
|
+
const clear = () => {
|
|
26
|
+
currentResults = [];
|
|
27
|
+
setSearchResults([]);
|
|
28
|
+
resultsHost.innerHTML = "";
|
|
29
|
+
};
|
|
30
|
+
const choose = async (result) => {
|
|
31
|
+
if (!result)
|
|
32
|
+
return;
|
|
33
|
+
await onSelect(result);
|
|
34
|
+
};
|
|
35
|
+
input.addEventListener("keydown", (event) => {
|
|
36
|
+
if (event.key === "Escape") {
|
|
37
|
+
input.value = "";
|
|
38
|
+
clear();
|
|
39
|
+
}
|
|
40
|
+
if (event.key === "Enter") {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
void choose(currentResults[0]);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
window.addEventListener("keydown", (event) => { if (event.key === "/" && document.activeElement !== input) {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
input.focus();
|
|
48
|
+
} });
|
|
49
|
+
resultsHost.addEventListener("click", (event) => {
|
|
50
|
+
const button = event.target?.closest("button[data-index]");
|
|
51
|
+
if (!button)
|
|
52
|
+
return;
|
|
53
|
+
void choose(currentResults[Number(button.dataset.index)]);
|
|
54
|
+
});
|
|
55
|
+
input.addEventListener("input", () => {
|
|
56
|
+
window.clearTimeout(timer);
|
|
57
|
+
timer = window.setTimeout(async () => {
|
|
58
|
+
const query = input.value.trim();
|
|
59
|
+
if (!query) {
|
|
60
|
+
clear();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const response = await api.search(getRepoId(), query);
|
|
64
|
+
currentResults = response.results ?? [];
|
|
65
|
+
setSearchResults(currentResults);
|
|
66
|
+
resultsHost.innerHTML = renderSearchResultsHtml(currentResults);
|
|
67
|
+
}, 180);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { createSymbolEdges } from "./edges.js";
|
|
3
|
+
import { touchExpandedCluster, removeExpandedCluster, state } from "./state.js";
|
|
4
|
+
import { KIND_COLORS, hashColor } from "./theme.js";
|
|
5
|
+
export class LodController {
|
|
6
|
+
api;
|
|
7
|
+
viewer;
|
|
8
|
+
universe;
|
|
9
|
+
expanded = new Map();
|
|
10
|
+
constructor(api, viewer, universe) {
|
|
11
|
+
this.api = api;
|
|
12
|
+
this.viewer = viewer;
|
|
13
|
+
this.universe = universe;
|
|
14
|
+
}
|
|
15
|
+
async maybeExpandNearest() {
|
|
16
|
+
let nearest = null;
|
|
17
|
+
for (const cluster of this.universe.getClusters()) {
|
|
18
|
+
const distance = cluster.position.distanceTo(this.viewer.camera.position);
|
|
19
|
+
if (distance < 520 && (!nearest || distance < nearest.distance))
|
|
20
|
+
nearest = { key: cluster.repoId + ":" + cluster.clusterId, distance };
|
|
21
|
+
}
|
|
22
|
+
if (nearest && !this.expanded.has(nearest.key))
|
|
23
|
+
await this.expand(nearest.key);
|
|
24
|
+
for (const [key, expanded] of this.expanded) {
|
|
25
|
+
const cluster = this.universe.getCluster(key);
|
|
26
|
+
if (!cluster || cluster.position.distanceTo(this.viewer.camera.position) > 1150 || !state.expandedClusters.has(key))
|
|
27
|
+
this.collapse(key, expanded);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async expand(key) {
|
|
31
|
+
const cluster = this.universe.getCluster(key);
|
|
32
|
+
if (!cluster)
|
|
33
|
+
return;
|
|
34
|
+
touchExpandedCluster(key);
|
|
35
|
+
const limit = state.settings?.layout.maxSymbolsPerClusterExpand ?? 5000;
|
|
36
|
+
if (cluster.symbolCount > limit)
|
|
37
|
+
return;
|
|
38
|
+
const [layout, edgeResponse] = await Promise.all([
|
|
39
|
+
this.api.layout(cluster.repoId, "symbol", cluster.clusterId),
|
|
40
|
+
this.api.symbolEdges(cluster.repoId, cluster.clusterId),
|
|
41
|
+
]);
|
|
42
|
+
const nodes = edgeResponse.nodes ?? layout.positions.map((position) => ({ id: position.id }));
|
|
43
|
+
const geometry = new THREE.SphereGeometry(3.5, 10, 8);
|
|
44
|
+
const material = new THREE.MeshBasicMaterial();
|
|
45
|
+
const mesh = new THREE.InstancedMesh(geometry, material, Math.max(nodes.length, 1));
|
|
46
|
+
mesh.name = "symbols:" + key;
|
|
47
|
+
const matrix = new THREE.Matrix4();
|
|
48
|
+
const color = new THREE.Color();
|
|
49
|
+
const positions = new Map();
|
|
50
|
+
const layoutPositions = new Map(layout.positions.map((position) => [position.id, position]));
|
|
51
|
+
nodes.forEach((node, index) => {
|
|
52
|
+
const p = layoutPositions.get(node.id);
|
|
53
|
+
const position = new THREE.Vector3(p?.x ?? 0, p?.y ?? 0, p?.z ?? 0).multiplyScalar(0.45).add(cluster.position);
|
|
54
|
+
positions.set(node.id, position);
|
|
55
|
+
const scale = 0.5 + Math.log2(1 + (node.fanIn ?? 0)) * 0.25;
|
|
56
|
+
matrix.compose(position, new THREE.Quaternion(), new THREE.Vector3(scale, scale, scale));
|
|
57
|
+
mesh.setMatrixAt(index, matrix);
|
|
58
|
+
mesh.setColorAt(index, this.nodeColor(node, cluster.clusterId, color));
|
|
59
|
+
});
|
|
60
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
61
|
+
if (mesh.instanceColor)
|
|
62
|
+
mesh.instanceColor.needsUpdate = true;
|
|
63
|
+
const lines = createSymbolEdges(edgeResponse.edges, positions);
|
|
64
|
+
const group = new THREE.Group();
|
|
65
|
+
group.add(mesh);
|
|
66
|
+
group.add(lines);
|
|
67
|
+
this.viewer.root.add(group);
|
|
68
|
+
this.expanded.set(key, { key, clusterId: cluster.clusterId, group, positions, nodes, mesh, lines });
|
|
69
|
+
}
|
|
70
|
+
collapse(key, expanded = this.expanded.get(key)) {
|
|
71
|
+
if (!expanded)
|
|
72
|
+
return;
|
|
73
|
+
this.viewer.disposeObject(expanded.group);
|
|
74
|
+
this.expanded.delete(key);
|
|
75
|
+
removeExpandedCluster(key);
|
|
76
|
+
}
|
|
77
|
+
applyLens() {
|
|
78
|
+
const color = new THREE.Color();
|
|
79
|
+
for (const expanded of this.expanded.values()) {
|
|
80
|
+
expanded.nodes.forEach((node, index) => {
|
|
81
|
+
expanded.mesh.setColorAt(index, this.nodeColor(node, expanded.clusterId, color));
|
|
82
|
+
});
|
|
83
|
+
if (expanded.mesh.instanceColor)
|
|
84
|
+
expanded.mesh.instanceColor.needsUpdate = true;
|
|
85
|
+
expanded.lines.material.opacity = state.activeLens === "edges" ? 1 : 0.85;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
nodeColor(node, clusterId, target) {
|
|
89
|
+
const lens = state.activeLens;
|
|
90
|
+
if (lens === "community")
|
|
91
|
+
return target.set(hashColor(clusterId));
|
|
92
|
+
if (lens === "impact") {
|
|
93
|
+
const heat = Math.min(1, Math.log2(1 + (node.fanIn ?? 0)) / 8);
|
|
94
|
+
return target.set("#38bdf8").lerp(new THREE.Color("#fb7185"), heat);
|
|
95
|
+
}
|
|
96
|
+
target.set(KIND_COLORS[node.kind ?? "function"] ?? "#e5e7eb");
|
|
97
|
+
return lens === "edges" ? target.multiplyScalar(0.35) : target;
|
|
98
|
+
}
|
|
99
|
+
getExpanded() { return [...this.expanded.values()]; }
|
|
100
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { ViewerApi } from "./api.js";
|
|
3
|
+
import { AmbientController } from "./ambient.js";
|
|
4
|
+
import { ViewerChrome } from "./chrome.js";
|
|
5
|
+
import { InspectorPanel } from "./inspector.js";
|
|
6
|
+
import { ActivityLens } from "./lenses/activity.js";
|
|
7
|
+
import { attachSearch } from "./lenses/search.js";
|
|
8
|
+
import { LodController } from "./lod.js";
|
|
9
|
+
import { PickingController } from "./picking.js";
|
|
10
|
+
import { ViewerScene } from "./scene.js";
|
|
11
|
+
import { setSettings, loadVisibleRepos, state, subscribe, setAmbient, setSelection } from "./state.js";
|
|
12
|
+
import { loadSkinZip, applySkinCssVars } from "./skins/loader.js";
|
|
13
|
+
import { UniverseRenderer } from "./universe.js";
|
|
14
|
+
const canvasHost = document.getElementById("viewerCanvas");
|
|
15
|
+
const chromeHost = document.getElementById("viewerChrome");
|
|
16
|
+
const inspectorHost = document.getElementById("viewerInspector");
|
|
17
|
+
const captionHost = document.getElementById("viewerCaption");
|
|
18
|
+
if (!canvasHost || !chromeHost || !inspectorHost || !captionHost)
|
|
19
|
+
throw new Error("viewer shell missing required elements");
|
|
20
|
+
const api = new ViewerApi();
|
|
21
|
+
const scene = new ViewerScene(canvasHost);
|
|
22
|
+
const universeRenderer = new UniverseRenderer(scene);
|
|
23
|
+
const chrome = new ViewerChrome(chromeHost);
|
|
24
|
+
new InspectorPanel(inspectorHost);
|
|
25
|
+
new PickingController(api, scene, universeRenderer);
|
|
26
|
+
const lod = new LodController(api, scene, universeRenderer);
|
|
27
|
+
const ambient = new AmbientController(scene, universeRenderer, captionHost);
|
|
28
|
+
const activity = new ActivityLens(api, universeRenderer, (text) => ambient.setCaption(text));
|
|
29
|
+
function firstVisibleRepo() { return [...state.visibleRepos][0] ?? "sdl-mcp"; }
|
|
30
|
+
const resultsHost = document.getElementById("viewerResults");
|
|
31
|
+
attachSearch(chrome.searchInput, resultsHost, api, firstVisibleRepo, async (result) => {
|
|
32
|
+
const repoId = firstVisibleRepo();
|
|
33
|
+
const cluster = result.clusterId
|
|
34
|
+
? universeRenderer.getClusters().find((item) => item.repoId === repoId && item.clusterId === result.clusterId)
|
|
35
|
+
: undefined;
|
|
36
|
+
if (cluster)
|
|
37
|
+
scene.flyTo(cluster.position);
|
|
38
|
+
const card = await api.card(repoId, result.symbolId);
|
|
39
|
+
setSelection({ repoId, clusterId: result.clusterId ?? undefined, symbolId: result.symbolId, card });
|
|
40
|
+
});
|
|
41
|
+
chrome.ambientButton.addEventListener("click", () => setAmbient(!state.ambient));
|
|
42
|
+
chrome.fpsSelect.addEventListener("change", () => scene.setFpsCap(Number(chrome.fpsSelect.value)));
|
|
43
|
+
chrome.skinSelect.addEventListener("change", async () => {
|
|
44
|
+
if (chrome.skinSelect.value === "default")
|
|
45
|
+
return;
|
|
46
|
+
const bytes = await api.skinBytes(chrome.skinSelect.value);
|
|
47
|
+
const caps = state.settings?.skins ?? { maxZipBytes: 52_428_800, maxEntries: 500, maxDecompressedBytes: 209_715_200 };
|
|
48
|
+
const skin = loadSkinZip(new Uint8Array(bytes), caps);
|
|
49
|
+
applySkinCssVars(canvasHost, skin.manifest);
|
|
50
|
+
});
|
|
51
|
+
let lastLens = state.activeLens;
|
|
52
|
+
subscribe((snapshot) => {
|
|
53
|
+
chrome.lensSelect.value = snapshot.activeLens;
|
|
54
|
+
if (snapshot.settings)
|
|
55
|
+
scene.setFpsCap(snapshot.ambient ? snapshot.settings.ambient.fps : Number(chrome.fpsSelect.value));
|
|
56
|
+
if (snapshot.activeLens !== lastLens) {
|
|
57
|
+
lastLens = snapshot.activeLens;
|
|
58
|
+
universeRenderer.applyLens(snapshot.activeLens);
|
|
59
|
+
lod.applyLens();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
async function boot() {
|
|
63
|
+
const universe = await api.universe();
|
|
64
|
+
setSettings(universe.settings);
|
|
65
|
+
chrome.setSettings(universe.settings);
|
|
66
|
+
chrome.setRepos(universe.repos);
|
|
67
|
+
loadVisibleRepos(universe.repos.map((repo) => repo.repoId));
|
|
68
|
+
const skins = await api.skins().catch(() => ({ skins: [] }));
|
|
69
|
+
chrome.setSkins(skins.skins);
|
|
70
|
+
const rendered = [];
|
|
71
|
+
for (const repo of universe.repos.filter((item) => state.visibleRepos.has(item.repoId))) {
|
|
72
|
+
try {
|
|
73
|
+
const [clustersResponse, layout, edgeResponse] = await Promise.all([api.clusters(repo.repoId), api.layout(repo.repoId, "cluster"), api.clusterEdges(repo.repoId)]);
|
|
74
|
+
const clusters = clustersResponse.clusters ?? [];
|
|
75
|
+
universeRenderer.renderRepo(repo, clusters, layout);
|
|
76
|
+
universeRenderer.renderClusterEdges(repo.repoId, edgeResponse.edges ?? []);
|
|
77
|
+
rendered.push(repo);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.warn(`[viewer] skipping repo ${repo.repoId}`, error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const home = rendered.reduce((best, repo) => (!best || repo.symbolCount > best.symbolCount ? repo : best), undefined);
|
|
84
|
+
if (home)
|
|
85
|
+
scene.flyTo(new THREE.Vector3(home.galaxy.position[0], home.galaxy.position[1], home.galaxy.position[2]), Math.max(420, home.galaxy.radius * 1.6));
|
|
86
|
+
else
|
|
87
|
+
scene.flyTo(new THREE.Vector3(0, 0, 0));
|
|
88
|
+
scene.onFrame(() => { void lod.maybeExpandNearest(); });
|
|
89
|
+
activity.start();
|
|
90
|
+
chrome.setStatus("live");
|
|
91
|
+
}
|
|
92
|
+
boot().catch((error) => { console.error("[viewer] boot failed", error); chrome.setStatus("error"); });
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { setHovered, setSelection } from "./state.js";
|
|
3
|
+
export class PickingController {
|
|
4
|
+
viewer;
|
|
5
|
+
universe;
|
|
6
|
+
raycaster = new THREE.Raycaster();
|
|
7
|
+
pointer = new THREE.Vector2();
|
|
8
|
+
constructor(_api, viewer, universe) {
|
|
9
|
+
this.viewer = viewer;
|
|
10
|
+
this.universe = universe;
|
|
11
|
+
viewer.renderer.domElement.addEventListener("pointermove", this.onMove);
|
|
12
|
+
viewer.renderer.domElement.addEventListener("click", this.onClick);
|
|
13
|
+
}
|
|
14
|
+
pick(event) {
|
|
15
|
+
const rect = this.viewer.renderer.domElement.getBoundingClientRect();
|
|
16
|
+
this.pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|
17
|
+
this.pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
|
18
|
+
this.raycaster.setFromCamera(this.pointer, this.viewer.camera);
|
|
19
|
+
return this.raycaster.intersectObjects(this.viewer.root.children, true)[0] ?? null;
|
|
20
|
+
}
|
|
21
|
+
onMove = (event) => {
|
|
22
|
+
const hit = this.pick(event);
|
|
23
|
+
setHovered(hit?.object.name ?? null);
|
|
24
|
+
};
|
|
25
|
+
onClick = async (event) => {
|
|
26
|
+
const hit = this.pick(event);
|
|
27
|
+
if (!hit) {
|
|
28
|
+
setSelection(null);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (hit.object.name.startsWith("clusters:")) {
|
|
32
|
+
const repoId = hit.object.name.slice("clusters:".length);
|
|
33
|
+
const instanceId = hit.instanceId ?? 0;
|
|
34
|
+
const cluster = this.universe.getClusters().filter((item) => item.repoId === repoId)[instanceId];
|
|
35
|
+
if (cluster) {
|
|
36
|
+
this.viewer.flyTo(cluster.position);
|
|
37
|
+
setSelection({ repoId, clusterId: cluster.clusterId });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
dispose() {
|
|
42
|
+
this.viewer.renderer.domElement.removeEventListener("pointermove", this.onMove);
|
|
43
|
+
this.viewer.renderer.domElement.removeEventListener("click", this.onClick);
|
|
44
|
+
}
|
|
45
|
+
}
|