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" } } 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}; - - -