From 64bd6f59e0545bf1c6ed64eb0ae409a7681aa269 Mon Sep 17 00:00:00 2001 From: Joel Chan Date: Fri, 3 Jul 2026 16:23:03 +0800 Subject: [PATCH 1/2] chore: add three.js and @types/three dependencies Co-Authored-By: Claude Sonnet 4.6 --- bun.lock | 18 ++++++++++++++++++ package.json | 6 ++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/bun.lock b/bun.lock index 300795a..246b8e1 100644 --- a/bun.lock +++ b/bun.lock @@ -13,9 +13,11 @@ "minimatch": "^10.2.5", "node-html-parser": "^7.1.0", "sigma": "^3.0.0", + "three": "^0.185.0", }, "devDependencies": { "@types/bun": "latest", + "@types/three": "^0.185.0", }, "peerDependencies": { "typescript": "^5", @@ -23,6 +25,8 @@ }, }, "packages": { + "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], + "@huggingface/jinja": ["@huggingface/jinja@0.2.2", "", {}, "sha512-/KPde26khDUIPkTGU82jdtTW9UAuvUTumCAbFs/7giR0SxsvZC4hru51PBvpijH6BVkHcROcvZM/lpy5h1jRRA=="], "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], @@ -45,12 +49,20 @@ "@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="], + "@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="], + "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], "@types/long": ["@types/long@4.0.2", "", {}, "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="], "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + "@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="], + + "@types/three": ["@types/three@0.185.0", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", "fflate": "~0.8.2", "meshoptimizer": "~1.1.1" } }, "sha512-O2Uy8Cj4Nonr8dWUUbifMdPe8B0Mq7EdOHb89S4+kjUw/KhbjTZrUuYlrQ1bpUKG+EP9QJnN7qNxbHGlGoLHMA=="], + + "@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="], + "@xenova/transformers": ["@xenova/transformers@2.17.2", "", { "dependencies": { "@huggingface/jinja": "^0.2.2", "onnxruntime-web": "1.14.0", "sharp": "^0.32.0" }, "optionalDependencies": { "onnxruntime-node": "1.14.0" } }, "sha512-lZmHqzrVIkSvZdKZEx7IYY51TK0WDrC8eR0c5IMnBsO8di8are1zzw8BlLhyO2TklZKLN5UffNGs1IJwT6oOqQ=="], "b4a": ["b4a@1.8.1", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw=="], @@ -121,6 +133,8 @@ "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], + "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], + "flatbuffers": ["flatbuffers@1.12.0", "", {}, "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ=="], "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], @@ -155,6 +169,8 @@ "long": ["long@4.0.0", "", {}, "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="], + "meshoptimizer": ["meshoptimizer@1.1.1", "", {}, "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g=="], + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], @@ -229,6 +245,8 @@ "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="], + "three": ["three@0.185.0", "", {}, "sha512-+yRrcRO2iZa8uzvNNl0d7cL4huhgKgBvVJ0njcTe8xFqZ6DMAFZdCKDP91SEAuj25bNAj7k1QQdf+srZywVK6w=="], + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], diff --git a/package.json b/package.json index ebfd174..ac1f4ae 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "start": "bun src/index.ts" }, "devDependencies": { - "@types/bun": "latest" + "@types/bun": "latest", + "@types/three": "^0.185.0" }, "peerDependencies": { "typescript": "^5" @@ -23,6 +24,7 @@ "graphology-layout-noverlap": "^0.4.2", "minimatch": "^10.2.5", "node-html-parser": "^7.1.0", - "sigma": "^3.0.0" + "sigma": "^3.0.0", + "three": "^0.185.0" } } From 5ab7c3d517ef36978d9a22367f964bb286529545 Mon Sep 17 00:00:00 2001 From: Joel Chan Date: Fri, 3 Jul 2026 16:25:23 +0800 Subject: [PATCH 2/2] feat: replace Sigma 2D with Three.js 3D graph renderer Move all layout computation (orbital seed + ForceAtlas2 + noverlap) to server-side Bun pipeline. Bake x/y/z positions into GraphNode JSON. Replace Sigma.js + bundled FA2/noverlap in HTML with vanilla Three.js WebGLRenderer (InstancedMesh spheres, LineSegments edges, OrbitControls, Raycaster picking). Depth encoded on z-axis (depth * -60). - types.ts: add x/y/z to GraphNode - orbital-layout.ts: rewrite to arc-per-community algorithm + computeLayout (FA2 + noverlap server-side via graphology deps) - graph-builder.ts: call computeLayout() at end of buildGraph() - html-report.ts: remove ~600 lines of client-side layout/Sigma code, add Three.js 3D renderer with sun glow, instanced nodes, edge lines - vendor/: delete forceatlas2.bundle.js + noverlap.bundle.js, add three.bundle.js (Three.js + OrbitControls bundled for browser) - 96 tests pass (2 new z-depth encoding tests) Co-Authored-By: Claude Sonnet 4.6 --- src/graph-builder.test.ts | 3 + src/graph-builder.ts | 7 + src/html-report.ts | 633 ++--- src/orbital-layout.test.ts | 45 +- src/orbital-layout.ts | 206 +- src/types.ts | 3 + src/vendor/build-three-bundle.ts | 6 + src/vendor/forceatlas2.bundle.js | 733 ------ src/vendor/noverlap.bundle.js | 261 -- src/vendor/three.bundle.js | 4108 ++++++++++++++++++++++++++++++ 10 files changed, 4439 insertions(+), 1566 deletions(-) create mode 100644 src/vendor/build-three-bundle.ts delete mode 100644 src/vendor/forceatlas2.bundle.js delete mode 100644 src/vendor/noverlap.bundle.js create mode 100644 src/vendor/three.bundle.js diff --git a/src/graph-builder.test.ts b/src/graph-builder.test.ts index 1c95159..91eb010 100644 --- a/src/graph-builder.test.ts +++ b/src/graph-builder.test.ts @@ -21,6 +21,9 @@ function makeNode(id: string, section: string): GraphNode { navSource: null, navSection: null, role: 'moon', + x: 0, + y: 0, + z: 0, } } diff --git a/src/graph-builder.ts b/src/graph-builder.ts index 24d06e2..9193016 100644 --- a/src/graph-builder.ts +++ b/src/graph-builder.ts @@ -1,5 +1,6 @@ import { parse } from "node-html-parser" import type { CrawlRecord, Graph, GraphEdge, GraphNode, GraphStats, LangStat, LangVariant, NodeRole, SectionStat, TemplateStat } from "./types" +import { computeLayout } from "./orbital-layout" const LANG_RE = /^[a-z]{2}(-[a-z]{2,4})?$/i const LANG_OVERLAP_THRESHOLD = 0.30 @@ -626,6 +627,9 @@ export async function buildGraph( navSource: null, navSection: null, role: 'moon', + x: 0, + y: 0, + z: 0, langVariants: rec.langVariants, missingLangs: rec.missingLangs, }) @@ -704,6 +708,9 @@ export async function buildGraph( delete node._embedding } + // ── Layout: compute 2D positions server-side (orbital seed → FA2 → Noverlap) ── + computeLayout(nodes, edges) + // ── Stats ─────────────────────────────────────────────────────────────── const sectionPageMap = new Map>() diff --git a/src/html-report.ts b/src/html-report.ts index dd25f05..c4d2225 100644 --- a/src/html-report.ts +++ b/src/html-report.ts @@ -6,19 +6,16 @@ import type { Graph } from "./types" const __dirname = dirname(fileURLToPath(import.meta.url)) export async function generateReport(graph: Graph, domain: string): Promise { - const sigmaSource = readFileSync(join(__dirname, "../node_modules/sigma/dist/sigma.min.js"), "utf-8") - const graphologySource = readFileSync(join(__dirname, "../node_modules/graphology/dist/graphology.umd.min.js"), "utf-8") - const forceAtlas2Source = readFileSync(join(__dirname, "vendor/forceatlas2.bundle.js"), "utf-8") - const noverlapSource = readFileSync(join(__dirname, "vendor/noverlap.bundle.js"), "utf-8") + const threeSource = readFileSync(join(__dirname, "vendor/three.bundle.js"), "utf-8") const graphJson = JSON.stringify({ nodes: graph.nodes, edges: graph.edges, stats: graph.stats }) const timestamp = new Date().toISOString() const outputFile = `${domain.replace(/\./g, "-")}-scan.html` - const html = buildHtml(graphJson, sigmaSource, graphologySource, forceAtlas2Source, noverlapSource, domain, timestamp, graph.stats) + const html = buildHtml(graphJson, threeSource, domain, timestamp, graph.stats) await Bun.write(outputFile, html) } -function buildHtml(graphJson: string, sigmaSource: string, graphologySource: string, forceAtlas2Source: string, noverlapSource: string, domain: string, timestamp: string, stats: any): string { +function buildHtml(graphJson: string, threeSource: string, domain: string, timestamp: string, stats: any): string { return ` @@ -60,9 +57,7 @@ input[type=range] { width: 100%; accent-color: #a78bfa; } .toggle-btn.active { background: #2d2455; border-color: #7c3aed; color: #a78bfa; } #graph-canvas { flex: 1; position: relative; overflow: hidden; } -#sigma-container { width: 100%; height: 100%; position: relative; } -#hull-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 0; } -#sigma-container > canvas { z-index: 1; } +#graph-canvas canvas { display: block; width: 100% !important; height: 100% !important; } /* Tooltip */ #tooltip { position: absolute; background: #1a1d27; border: 1px solid #2d3148; border-radius: 8px; padding: 10px 14px; font-size: 12px; pointer-events: none; max-width: 280px; z-index: 100; opacity: 0; transition: opacity 0.1s; } @@ -166,7 +161,6 @@ h3.section-title { font-size: 14px; font-weight: 600; color: #e2e8f0; margin-bot
-
@@ -207,16 +201,7 @@ h3.section-title { font-size: 14px; font-weight: 600; color: #e2e8f0; margin-bot const GRAPH_DATA = ${graphJson}; - - -