This commit is contained in:
Boris 2026-01-13 13:47:14 +00:00 committed by GitHub
commit b42b71d79c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 3619 additions and 566 deletions

View file

@ -12,11 +12,14 @@
"classnames": "^2.5.1",
"culori": "^4.0.1",
"d3-force-3d": "^3.0.6",
"next": "^16.1.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"next": "15.3.3",
"ngraph.forcelayout": "^3.3.1",
"ngraph.graph": "^20.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-force-graph-2d": "^1.27.1",
"react-markdown": "^10.1.0",
"three": "^0.175.0",
"troika-three-text": "^0.52.4",
"uuid": "^9.0.1"
},
"devDependencies": {
@ -24,8 +27,9 @@
"@tailwindcss/postcss": "^4.1.7",
"@types/culori": "^4.0.0",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/three": "^0.175.0",
"@types/uuid": "^9.0.8",
"eslint": "^9",
"eslint-config-next": "^16.0.4",
@ -1678,11 +1682,31 @@
"@types/react": "^19.2.0"
}
},
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
"node_modules/@types/stats.js": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
"dev": true
},
"node_modules/@types/three": {
"version": "0.175.0",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.175.0.tgz",
"integrity": "sha512-ldMSBgtZOZ3g9kJ3kOZSEtZIEITmJOzu8eKVpkhf036GuNkM4mt0NXecrjCn5tMm1OblOF7dZehlaDypBfNokw==",
"dev": true,
"dependencies": {
"@tweenjs/tween.js": "~23.1.3",
"@types/stats.js": "*",
"@types/webxr": "*",
"@webgpu/types": "*",
"fflate": "~0.8.2",
"meshoptimizer": "~0.18.1"
}
},
"node_modules/@types/three/node_modules/@tweenjs/tween.js": {
"version": "23.1.3",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
"dev": true
},
"node_modules/@types/uuid": {
"version": "9.0.8",
@ -1691,6 +1715,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/webxr": {
"version": "0.5.24",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz",
@ -2237,6 +2267,12 @@
"win32"
]
},
"node_modules/@webgpu/types": {
"version": "0.1.66",
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.66.tgz",
"integrity": "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==",
"dev": true
},
"node_modules/accessor-fn": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz",
@ -2569,6 +2605,14 @@
"url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md"
}
},
"node_modules/bidi-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
"dependencies": {
"require-from-string": "^2.0.2"
}
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@ -4031,6 +4075,12 @@
"reusify": "^1.0.4"
}
},
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"dev": true
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@ -5763,447 +5813,11 @@
"node": ">= 8"
}
},
"node_modules/micromark": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
"integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"@types/debug": "^4.0.0",
"debug": "^4.0.0",
"decode-named-character-reference": "^1.0.0",
"devlop": "^1.0.0",
"micromark-core-commonmark": "^2.0.0",
"micromark-factory-space": "^2.0.0",
"micromark-util-character": "^2.0.0",
"micromark-util-chunked": "^2.0.0",
"micromark-util-combine-extensions": "^2.0.0",
"micromark-util-decode-numeric-character-reference": "^2.0.0",
"micromark-util-encode": "^2.0.0",
"micromark-util-normalize-identifier": "^2.0.0",
"micromark-util-resolve-all": "^2.0.0",
"micromark-util-sanitize-uri": "^2.0.0",
"micromark-util-subtokenize": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-core-commonmark": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
"integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"decode-named-character-reference": "^1.0.0",
"devlop": "^1.0.0",
"micromark-factory-destination": "^2.0.0",
"micromark-factory-label": "^2.0.0",
"micromark-factory-space": "^2.0.0",
"micromark-factory-title": "^2.0.0",
"micromark-factory-whitespace": "^2.0.0",
"micromark-util-character": "^2.0.0",
"micromark-util-chunked": "^2.0.0",
"micromark-util-classify-character": "^2.0.0",
"micromark-util-html-tag-name": "^2.0.0",
"micromark-util-normalize-identifier": "^2.0.0",
"micromark-util-resolve-all": "^2.0.0",
"micromark-util-subtokenize": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-factory-destination": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
"integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-util-character": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-factory-label": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
"integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"devlop": "^1.0.0",
"micromark-util-character": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-factory-space": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
"integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-util-character": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-factory-title": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
"integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-factory-space": "^2.0.0",
"micromark-util-character": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-factory-whitespace": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
"integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-factory-space": "^2.0.0",
"micromark-util-character": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-util-character": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
"integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-util-chunked": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
"integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-util-symbol": "^2.0.0"
}
},
"node_modules/micromark-util-classify-character": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
"integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-util-character": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-util-combine-extensions": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
"integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-util-chunked": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-util-decode-numeric-character-reference": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
"integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-util-symbol": "^2.0.0"
}
},
"node_modules/micromark-util-decode-string": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
"integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"decode-named-character-reference": "^1.0.0",
"micromark-util-character": "^2.0.0",
"micromark-util-decode-numeric-character-reference": "^2.0.0",
"micromark-util-symbol": "^2.0.0"
}
},
"node_modules/micromark-util-encode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
"integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT"
},
"node_modules/micromark-util-html-tag-name": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
"integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT"
},
"node_modules/micromark-util-normalize-identifier": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
"integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-util-symbol": "^2.0.0"
}
},
"node_modules/micromark-util-resolve-all": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
"integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-util-sanitize-uri": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
"integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-util-character": "^2.0.0",
"micromark-util-encode": "^2.0.0",
"micromark-util-symbol": "^2.0.0"
}
},
"node_modules/micromark-util-subtokenize": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
"integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"devlop": "^1.0.0",
"micromark-util-chunked": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-util-symbol": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
"integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT"
},
"node_modules/micromark-util-types": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
"integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT"
"node_modules/meshoptimizer": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
"integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
"dev": true
},
"node_modules/micromatch": {
"version": "4.0.8",
@ -6371,12 +5985,38 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"dev": true,
"license": "MIT"
"node_modules/ngraph.events": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.4.0.tgz",
"integrity": "sha512-NeDGI4DSyjBNBRtA86222JoYietsmCXbs8CEB0dZ51Xeh4lhVl1y3wpWLumczvnha8sFQIW4E0vvVWwgmX2mGw=="
},
"node_modules/ngraph.forcelayout": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/ngraph.forcelayout/-/ngraph.forcelayout-3.3.1.tgz",
"integrity": "sha512-MKBuEh1wujyQHFTW57y5vd/uuEOK0XfXYxm3lC7kktjJLRdt/KEKEknyOlc6tjXflqBKEuYBBcu7Ax5VY+S6aw==",
"dependencies": {
"ngraph.events": "^1.0.0",
"ngraph.merge": "^1.0.0",
"ngraph.random": "^1.0.0"
}
},
"node_modules/ngraph.graph": {
"version": "20.1.0",
"resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-20.1.0.tgz",
"integrity": "sha512-1jorNgIc0Kg0L9bTNN4+RCrVvbZ+4pqGVMrbhX3LLyqYcRdLvAQRRnxddmfj9l5f6Eq59SUTfbYZEm8cktiE7Q==",
"dependencies": {
"ngraph.events": "^1.2.1"
}
},
"node_modules/ngraph.merge": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/ngraph.merge/-/ngraph.merge-1.0.0.tgz",
"integrity": "sha512-5J8YjGITUJeapsomtTALYsw7rFveYkM+lBj3QiYZ79EymQcuri65Nw3knQtFxQBU1r5iOaVRXrSwMENUPK62Vg=="
},
"node_modules/ngraph.random": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/ngraph.random/-/ngraph.random-1.2.0.tgz",
"integrity": "sha512-4EUeAGbB2HWX9njd6bP6tciN6ByJfoaAvmVL9QTaZSeXrW46eNGA9GajiXiPBbvFqxUWFkEbyo6x5qsACUuVfA=="
},
"node_modules/oauth4webapi": {
"version": "3.8.3",
@ -6917,37 +6557,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/remark-parse": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
"integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"mdast-util-from-markdown": "^2.0.0",
"micromark-util-types": "^2.0.0",
"unified": "^11.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-rehype": {
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
"integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/mdast": "^4.0.0",
"mdast-util-to-hast": "^13.0.0",
"unified": "^11.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve": {
@ -7594,6 +7209,12 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/three": {
"version": "0.175.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.175.0.tgz",
"integrity": "sha512-nNE3pnTHxXN/Phw768u0Grr7W4+rumGg/H6PgeseNJojkJtmeHJfZWi41Gp2mpXl1pg1pf1zjwR4McM1jTqkpg==",
"license": "MIT"
},
"node_modules/tinycolor2": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
@ -7662,26 +7283,33 @@
"node": ">=8.0"
}
},
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
"integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
"node_modules/troika-three-text": {
"version": "0.52.4",
"resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz",
"integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==",
"dependencies": {
"bidi-js": "^1.0.2",
"troika-three-utils": "^0.52.4",
"troika-worker-utils": "^0.52.0",
"webgl-sdf-generator": "1.1.1"
},
"peerDependencies": {
"three": ">=0.125.0"
}
},
"node_modules/trough": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
"integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
"node_modules/troika-three-utils": {
"version": "0.52.4",
"resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz",
"integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==",
"peerDependencies": {
"three": ">=0.125.0"
}
},
"node_modules/troika-worker-utils": {
"version": "0.52.0",
"resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz",
"integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw=="
},
"node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@ -8068,33 +7696,10 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
"integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"vfile-message": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/vfile-message": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
"integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-stringify-position": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
"node_modules/webgl-sdf-generator": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz",
"integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA=="
},
"node_modules/which": {
"version": "2.0.2",

View file

@ -13,11 +13,14 @@
"classnames": "^2.5.1",
"culori": "^4.0.1",
"d3-force-3d": "^3.0.6",
"next": "^16.1.7",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"next": "15.3.3",
"ngraph.forcelayout": "^3.3.1",
"ngraph.graph": "^20.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-force-graph-2d": "^1.27.1",
"react-markdown": "^10.1.0",
"three": "^0.175.0",
"troika-three-text": "^0.52.4",
"uuid": "^9.0.1"
},
"devDependencies": {
@ -25,8 +28,9 @@
"@tailwindcss/postcss": "^4.1.7",
"@types/culori": "^4.0.0",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/three": "^0.175.0",
"@types/uuid": "^9.0.8",
"eslint": "^9",
"eslint-config-next": "^16.0.4",

View file

@ -1,5 +1,6 @@
"use client";
import Link from "next/link";
import { ChangeEvent, useCallback, useEffect, useState } from "react";
import { useBoolean } from "@/utils";
import { Accordion, CTAButton, GhostButton, IconButton, Input, Modal, PopupMenu } from "@/ui/elements";
@ -258,15 +259,12 @@ export default function DatasetsAccordion({
tools={(
<IconButton className="relative">
<PopupMenu>
<div className="flex flex-col gap-0.5">
<div className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer relative">
<input tabIndex={-1} type="file" multiple onChange={handleAddFiles.bind(null, dataset)} className="absolute w-full h-full cursor-pointer opacity-0" />
<span>add data</span>
</div>
</div>
<div className="flex flex-col gap-0.5 items-start">
<div onClick={() => handleDatasetRemove(dataset)} className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer">delete</div>
<div className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer relative">
<input tabIndex={-1} type="file" multiple onChange={handleAddFiles.bind(null, dataset)} className="absolute w-full h-full cursor-pointer opacity-0" />
<span>add data</span>
</div>
<Link target="_blank" href={`/visualize/${dataset.id}`}>visualize</Link>
<div onClick={() => handleDatasetRemove(dataset)} className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer">delete</div>
</PopupMenu>
</IconButton>
)}

View file

@ -0,0 +1,95 @@
"use client";
import { useEffect, useState } from "react";
import { fetch } from "@/utils";
import { adaptCogneeGraphData, validateCogneeGraphResponse } from "@/lib/adaptCogneeGraphData";
import { CogneeGraphResponse } from "@/types/CogneeAPI";
import MemoryGraphVisualization from "@/ui/elements/MemoryGraphVisualization";
import { Edge, Node } from "@/ui/rendering/graph/types";
interface VisualizePageProps {
params: { datasetId: string };
}
export default function Page({ params }: VisualizePageProps) {
const [graphData, setGraphData] = useState<{ nodes: Node[], edges: Edge[] } | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function getData() {
try {
setLoading(true);
setError(null);
const datasetId = (await params).datasetId;
const response = await fetch(`/v1/datasets/${datasetId}/graph`);
if (!response.ok) {
throw new Error(`Failed to fetch graph data: ${response.statusText}`);
}
const apiData = await response.json();
// Validate API response
if (!validateCogneeGraphResponse(apiData)) {
throw new Error("Invalid graph data format from API");
}
// Adapt Cognee API format to visualization format
const adaptedData = adaptCogneeGraphData(apiData as CogneeGraphResponse);
setGraphData(adaptedData);
} catch (err) {
console.error("Error loading graph data:", err);
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
setLoading(false);
}
}
getData();
}, [params]);
if (loading) {
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-gray-900 via-black to-purple-900">
<div className="text-center">
<div className="text-6xl mb-4 animate-spin"></div>
<div className="text-2xl font-bold text-white">Loading graph data...</div>
</div>
</div>
);
}
if (error) {
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-gray-900 via-black to-purple-900">
<div className="text-center max-w-md p-6">
<div className="text-6xl mb-4"></div>
<div className="text-2xl font-bold text-white mb-2">Error Loading Graph</div>
<div className="text-gray-400">{error}</div>
</div>
</div>
);
}
if (!graphData || graphData.nodes.length === 0) {
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-gray-900 via-black to-purple-900">
<div className="text-center max-w-md p-6">
<div className="text-6xl mb-4">📊</div>
<div className="text-2xl font-bold text-white mb-2">No Graph Data</div>
<div className="text-gray-400">This dataset has no graph data to visualize.</div>
</div>
</div>
);
}
return (
<MemoryGraphVisualization
nodes={graphData.nodes}
edges={graphData.edges}
title="Cognee Memory Graph"
showControls={true}
/>
);
}

View file

@ -0,0 +1,184 @@
# Graph Visualization Demo
An isolated, interactive demo of Cognee's Three.js-based graph visualization with a rich AI/ML knowledge graph dataset.
## Features
### 🎨 Visual Design
- **Vibrant Color Palette**: 10 distinct colors for different node types
- **Dark Theme**: Optimized background (#0a0a0f) for maximum contrast
- **Metaball Rendering**: Smooth, organic blob visualization of node clusters
- **Responsive Labels**: Context-aware labels that appear on hover and zoom
### 🎯 Interactive Controls
- **Pan**: Click and drag to move around the graph
- **Zoom**: Scroll to zoom in (6x max) or out (0.5x min)
- **Hover**: Mouse over nodes to see labels and connections
- **Click**: Select nodes to highlight their relationships
- **Smooth Animation**: Fluid camera motion with optimized damping
### 📊 UI Components
- **Legend Panel**: Categorizes nodes by type with color coding
- **Statistics**: Real-time graph metrics (nodes, edges, connections)
- **Instructions Overlay**: Quick reference for interaction methods
- **Toggle Controls**: Show/hide legend and stats as needed
## Dataset
The demo includes a comprehensive **AI/ML Knowledge Graph** with:
### Node Types (52 total)
- **Concepts** (6): AI, Machine Learning, Deep Learning, NLP, CV, RL
- **Algorithms** (10): SVM, Decision Trees, K-Means, Q-Learning, etc.
- **Architectures** (12): CNN, RNN, Transformer, GAN, VAE, etc.
- **Technologies** (9): BERT, GPT, ResNet, YOLO, Word2Vec, etc.
- **Applications** (5): Chatbots, Autonomous Vehicles, Medical Imaging, etc.
- **Data** (4): Datasets, Feature Engineering, Augmentation, Normalization
- **Optimization** (5): Gradient Descent, Adam, Backprop, Regularization, Dropout
### Relationships (56 edges)
- Hierarchical: "is subfield of", "type of", "variant of"
- Functional: "implements", "uses", "powered by", "trains"
- Application: "application of", "task in", "used in"
## Technical Implementation
### Architecture
```
GraphVisualization Component
animate.ts (Main Render Loop)
┌─────────────────┬─────────────────┬─────────────────┐
│ Graph Layout │ Rendering │ Interaction │
│ (ngraph) │ (Three.js) │ (Picking) │
├─────────────────┼─────────────────┼─────────────────┤
│ • Force layout │ • Node swarm │ • Mouse hover │
│ • 800 iterations│ • Edge mesh │ • Click select │
│ • Spring physics│ • Metaballs │ • Label display │
│ │ • Density cloud │ • Pan/Zoom │
└─────────────────┴─────────────────┴─────────────────┘
```
### Performance Optimizations
- **GPU-Accelerated**: All rendering uses WebGL shaders
- **Instanced Rendering**: Nodes rendered in a single draw call
- **Texture-Based Positions**: Node positions stored in GPU texture
- **Culling**: Labels only shown for visible/hovered nodes
- **Adaptive Layout**: Physics stabilizes after initial iterations
### Scalability
The implementation is designed to handle:
- ✅ **100+ nodes**: Excellent performance
- ✅ **500+ nodes**: Good performance with metaballs
- ✅ **1000+ nodes**: Recommended to reduce metaball density
- ⚠️ **5000+ nodes**: Consider simplified rendering mode
## Configuration Options
The visualization accepts a `config` prop:
```typescript
config={{
fontSize: 12, // Label font size (default: 10)
}}
```
### Force Layout Parameters (in animate.ts)
```typescript
{
dragCoefficient: 0.8, // Node movement resistance
springLength: 180, // Ideal distance between connected nodes
springCoefficient: 0.25, // Connection strength
gravity: -1200, // Repulsion force
}
```
### Camera Controls
```typescript
{
minZoom: 0.5, // Maximum zoom out
maxZoom: 6, // Maximum zoom in
dampingFactor: 0.08, // Camera smoothness
}
```
## How to Use in Development
1. **Start the frontend**:
```bash
cd cognee-frontend
npm run dev
```
2. **Navigate to the demo**:
```
http://localhost:3000/visualize/demo
```
3. **Interact with the graph**:
- Hover over nodes to see labels
- Zoom in to see more connections
- Click to select nodes
- Toggle legend/stats panels
## Extending the Demo
### Add More Nodes
```typescript
mockNodes.push({
id: "new-concept",
label: "New Concept",
type: "Concept"
});
```
### Add Connections
```typescript
mockEdges.push({
id: "e-new",
source: "new-concept",
target: "ai",
label: "related to"
});
```
### Customize Colors
Update the `typeColors` mapping in the demo page:
```typescript
const typeColors: Record<string, string> = {
"YourType": "#YOUR_COLOR",
// ...
};
```
## Future Enhancements
Potential improvements:
- [ ] Search functionality to find and highlight nodes
- [ ] Filter nodes by type
- [ ] Export graph as image/SVG
- [ ] Node clustering by community detection
- [ ] Time-based animation of graph evolution
- [ ] 3D visualization mode
- [ ] Multi-graph comparison view
## Related Files
- `src/ui/elements/GraphVisualization.tsx` - Main component wrapper
- `src/ui/rendering/animate.ts` - Render loop and Three.js setup
- `src/ui/rendering/graph/createGraph.ts` - Graph creation from data
- `src/ui/rendering/materials/` - Shader materials for visual effects
- `src/ui/rendering/meshes/` - Mesh generation for nodes/edges/labels
## Performance Tips
For large graphs (1000+ nodes):
1. Reduce `densityCloudTarget` resolution (line 135 in animate.ts)
2. Decrease label display limit (line 372)
3. Consider disabling metaball rendering for very large graphs
4. Use node clustering/aggregation for massive datasets
## License
Part of the Cognee project - Apache 2.0 License

View file

@ -0,0 +1,87 @@
"use client";
import { useState, useMemo } from "react";
import { generateOntologyGraph } from "@/lib/generateOntologyGraph";
import MemoryGraphVisualization from "@/ui/elements/MemoryGraphVisualization";
type GraphMode = "small" | "medium" | "large";
export default function VisualizationDemoPage() {
const [graphMode, setGraphMode] = useState<GraphMode>("medium");
const [isGenerating, setIsGenerating] = useState(false);
// Generate graph based on mode
const { nodes, edges } = useMemo(() => {
console.log(`Generating ${graphMode} ontology graph...`);
setIsGenerating(true);
let result;
switch (graphMode) {
case "small":
result = { ...generateOntologyGraph("simple"), clusters: new Map() };
break;
case "medium":
result = generateOntologyGraph("medium");
break;
case "large":
result = generateOntologyGraph("complex");
break;
}
setTimeout(() => setIsGenerating(false), 500);
return result;
}, [graphMode]);
return (
<div className="relative min-h-screen">
{isGenerating ? (
<div className="absolute inset-0 flex items-center justify-center bg-black/90 z-50 backdrop-blur-sm">
<div className="text-center">
<div className="relative">
<div className="text-6xl mb-4 animate-spin"></div>
<div className="absolute inset-0 text-6xl mb-4 animate-ping opacity-20"></div>
</div>
<div className="text-2xl font-bold mb-2 bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
Building Knowledge Graph...
</div>
<div className="text-gray-400">
Creating {
graphMode === "small" ? "~500" :
graphMode === "medium" ? "~1,000" :
"~1,500"
} interconnected nodes
</div>
</div>
</div>
) : null}
{/* Mode Selector Overlay */}
<div className="absolute top-6 left-6 z-10 pointer-events-auto">
<div className="flex gap-1 bg-black/70 backdrop-blur-md rounded-lg p-1 border border-purple-500/30">
{(["small", "medium", "large"] as GraphMode[]).map((mode) => (
<button
key={mode}
onClick={() => setGraphMode(mode)}
className={`flex-1 px-3 py-2 rounded transition-all text-sm font-medium ${
graphMode === mode
? "bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg shadow-purple-500/50"
: "hover:bg-white/10 text-gray-300"
}`}
>
{mode === "small" && "500"}
{mode === "medium" && "1K"}
{mode === "large" && "1.5K"}
</button>
))}
</div>
</div>
<MemoryGraphVisualization
nodes={nodes}
edges={edges}
title="Memory Retrieval Debugger (Demo)"
showControls={true}
/>
</div>
);
}

View file

@ -0,0 +1,45 @@
/**
* Type definitions for Cognee API responses
* Based on Cognee SDK data point model and graph API
*/
/**
* Cognee DataPoint representation from API
* Corresponds to: cognee/infrastructure/engine/models/DataPoint.py
*/
export interface CogneeDataPoint {
id: string; // UUID
label: string; // Display name
type: string; // Node type (Entity, EntityType, DocumentChunk, etc.)
properties?: Record<string, any>; // Additional metadata
}
/**
* Cognee Edge representation from API
* Corresponds to: cognee/infrastructure/engine/models/Edge.py
*/
export interface CogneeEdge {
source: string; // Source node UUID
target: string; // Target node UUID
label: string; // Relationship type
weight?: number; // Optional weight
weights?: Record<string, number>; // Optional multiple weights
properties?: Record<string, any>; // Additional properties
}
/**
* Cognee Graph API response format
* From: /api/v1/datasets/{dataset_id}/graph
*/
export interface CogneeGraphResponse {
nodes: CogneeDataPoint[];
edges: CogneeEdge[];
}
/**
* Cognee API error response
*/
export interface CogneeAPIError {
detail: string;
status_code: number;
}

View file

@ -0,0 +1,165 @@
/**
* Node Sets: The primary abstraction for grouping nodes
* Replaces fixed types with dynamic, inferred, overlapping groups
*/
export type NodeSetSource =
| "model-inferred" // Created by AI/ML model
| "user-defined" // Manually created by user
| "query-result" // Result of a search query
| "imported" // From external source
| "set-algebra"; // Created by combining other sets
export type NodeSetStability =
| "stable" // Won't change often
| "evolving" // Changes gradually
| "ephemeral"; // Temporary, will be removed
export interface NodeSet {
id: string;
name: string;
description?: string;
// Required properties
nodeIds: string[]; // Member node IDs
size: number; // Number of nodes
definition: string; // How it was created (e.g., "semantic cluster around 'AI'")
stability: NodeSetStability;
source: NodeSetSource;
lastUpdated: Date;
// Confidence metrics
confidence?: number; // 0-1, how confident we are in this grouping
cohesion?: number; // 0-1, how tightly connected members are
// Set algebra metadata
parentSets?: string[]; // If created from other sets
operation?: "union" | "intersect" | "diff";
// Retrieval metadata
retrievalScore?: number; // If this set was retrieved
retrievalSignals?: string[]; // Why it was retrieved
// Visual properties (for rendering)
color?: string;
visible?: boolean;
}
/**
* Set operations
*/
export function unionSets(sets: NodeSet[], name: string): NodeSet {
const allNodeIds = new Set<string>();
sets.forEach(set => set.nodeIds.forEach(id => allNodeIds.add(id)));
return {
id: `union_${Date.now()}`,
name,
nodeIds: Array.from(allNodeIds),
size: allNodeIds.size,
definition: `Union of: ${sets.map(s => s.name).join(", ")}`,
stability: "ephemeral",
source: "set-algebra",
lastUpdated: new Date(),
parentSets: sets.map(s => s.id),
operation: "union",
};
}
export function intersectSets(sets: NodeSet[], name: string): NodeSet {
if (sets.length === 0) {
return {
id: `intersect_${Date.now()}`,
name,
nodeIds: [],
size: 0,
definition: "Empty intersection",
stability: "ephemeral",
source: "set-algebra",
lastUpdated: new Date(),
};
}
const intersection = new Set(sets[0].nodeIds);
sets.slice(1).forEach(set => {
const setIds = new Set(set.nodeIds);
intersection.forEach(id => {
if (!setIds.has(id)) intersection.delete(id);
});
});
return {
id: `intersect_${Date.now()}`,
name,
nodeIds: Array.from(intersection),
size: intersection.size,
definition: `Intersection of: ${sets.map(s => s.name).join(", ")}`,
stability: "ephemeral",
source: "set-algebra",
lastUpdated: new Date(),
parentSets: sets.map(s => s.id),
operation: "intersect",
};
}
export function diffSets(setA: NodeSet, setB: NodeSet, name: string): NodeSet {
const diff = new Set(setA.nodeIds);
setB.nodeIds.forEach(id => diff.delete(id));
return {
id: `diff_${Date.now()}`,
name,
nodeIds: Array.from(diff),
size: diff.size,
definition: `${setA.name} minus ${setB.name}`,
stability: "ephemeral",
source: "set-algebra",
lastUpdated: new Date(),
parentSets: [setA.id, setB.id],
operation: "diff",
};
}
/**
* Retrieval result with explanation
*/
export interface RetrievalResult {
type: "node" | "nodeSet" | "suggestedSet";
// For nodes
nodeId?: string;
nodeLabel?: string;
// For node sets
nodeSet?: NodeSet;
// For suggested sets
suggestedSetDefinition?: string;
suggestedNodeIds?: string[];
// Explanation (critical for trust)
why: string; // Human-readable explanation
similarityScore: number; // 0-1
signals: {
name: string; // e.g., "semantic", "recency", "provenance"
weight: number; // Contribution to final score
value: string | number; // The actual value
}[];
// Confidence
confidence: number; // 0-1, how confident we are in this retrieval
}
/**
* Recall simulation: "If the agent were asked X, what would be retrieved?"
*/
export interface RecallSimulation {
query: string;
rankedMemories: RetrievalResult[];
activatedSets: NodeSet[];
conflicts?: {
nodeId: string;
reason: string;
conflictingSets: string[];
}[];
}

View file

@ -0,0 +1,61 @@
"use client";
import classNames from 'classnames';
import { useEffect, useRef } from "react";
import { Edge, Node } from "@/ui/rendering/graph/types";
import animate from "@/ui/rendering/animate";
// IMPROVEMENT #8: Extended config for layered view controls
interface GraphVisualizationProps {
nodes: Node[];
edges: Edge[];
className?: string;
config?: {
fontSize?: number;
showNodes?: boolean; // Toggle node visibility
showEdges?: boolean; // Toggle edge/path visibility
showMetaballs?: boolean; // Toggle density cloud visibility
highlightedNodeIds?: Set<string>; // Nodes to highlight (neutral-by-default)
};
}
export default function GraphVisualization({
nodes,
edges,
className,
config,
}: GraphVisualizationProps) {
const visualizationRef = useRef<HTMLDivElement>(null);
const cleanupRef = useRef<(() => void) | null>(null);
useEffect(() => {
const visualizationContainer = visualizationRef.current;
if (visualizationContainer) {
// Clean up previous visualization
if (cleanupRef.current) {
cleanupRef.current();
}
// Clear the container
while (visualizationContainer.firstChild) {
visualizationContainer.removeChild(visualizationContainer.firstChild);
}
// Create new visualization
const cleanup = animate(nodes, edges, visualizationContainer, config);
cleanupRef.current = cleanup;
}
return () => {
if (cleanupRef.current) {
cleanupRef.current();
}
};
}, [config, edges, nodes]);
return (
<div className={classNames("min-w-full min-h-full", className)} ref={visualizationRef} />
);
}

View file

@ -0,0 +1,374 @@
/**
* Memory Graph Visualization
*
* Reusable visualization component with retrieval-first features:
* - Node set inference and display
* - Retrieval search with explanations
* - Neutral-by-default highlighting
* - Type attributes vs. inferred sets separation
*
* Works with any graph data (mock or real Cognee API data)
*/
"use client";
import { useState, useMemo } from "react";
import GraphVisualization from "@/ui/elements/GraphVisualization";
import { Edge, Node } from "@/ui/rendering/graph/types";
import { NodeSet } from "@/types/NodeSet";
import { inferNodeSets, mockRetrievalSearch } from "@/lib/inferNodeSets";
import type { RetrievalResult } from "@/types/NodeSet";
interface MemoryGraphVisualizationProps {
nodes: Node[];
edges: Edge[];
title?: string;
showControls?: boolean;
}
export default function MemoryGraphVisualization({
nodes,
edges,
title = "Memory Retrieval Debugger",
showControls = true,
}: MemoryGraphVisualizationProps) {
const [showLegend, setShowLegend] = useState(true);
// Retrieval-first: search replaces static filtering
const [searchQuery, setSearchQuery] = useState("");
const [retrievalResults, setRetrievalResults] = useState<RetrievalResult[]>([]);
// Node sets: primary abstraction
const [selectedNodeSet, setSelectedNodeSet] = useState<NodeSet | null>(null);
// Layer visibility controls
const [showNodes, setShowNodes] = useState(true);
const [showEdges, setShowEdges] = useState(true);
const [showMetaballs, setShowMetaballs] = useState(false);
// Node Attributes section collapsed by default (secondary concern)
const [showNodeAttributes, setShowNodeAttributes] = useState(false);
// Infer node sets from graph structure (CRITICAL: separate attributes from sets)
const { typeAttributes, inferredSets } = useMemo(() => {
return inferNodeSets(nodes, edges, {
minSetSize: 5,
maxSets: 15,
});
}, [nodes, edges]);
// Neutral-by-default: only highlight nodes that are selected or retrieved
const highlightedNodeIds = useMemo(() => {
const ids = new Set<string>();
// Nodes from retrieval results
retrievalResults.forEach(result => {
if (result.type === "node" && result.nodeId) {
ids.add(result.nodeId);
} else if (result.type === "nodeSet" && result.nodeSet) {
result.nodeSet.nodeIds.forEach(id => ids.add(id));
}
});
// Nodes from selected set
if (selectedNodeSet) {
selectedNodeSet.nodeIds.forEach(id => ids.add(id));
}
return ids;
}, [retrievalResults, selectedNodeSet]);
// Handle retrieval search
const handleSearch = (query: string) => {
setSearchQuery(query);
if (query.trim()) {
const results = mockRetrievalSearch(query, nodes, inferredSets);
setRetrievalResults(results);
} else {
setRetrievalResults([]);
}
};
const handleReset = () => {
setSearchQuery("");
setRetrievalResults([]);
setSelectedNodeSet(null);
};
return (
<div className="flex min-h-screen bg-gradient-to-br from-gray-900 via-black to-purple-900 text-white">
{/* Main Visualization */}
<div className="flex-1 relative">
<GraphVisualization
nodes={nodes}
edges={edges}
config={{
fontSize: 11,
showNodes,
showEdges,
showMetaballs,
highlightedNodeIds,
}}
/>
{/* Header */}
<div className="absolute top-0 left-0 right-0 p-6 bg-gradient-to-b from-black/90 via-black/50 to-transparent pointer-events-none">
<h1 className="text-4xl font-bold mb-2 bg-gradient-to-r from-purple-400 via-pink-400 to-cyan-400 bg-clip-text text-transparent">
{title}
</h1>
<p className="text-gray-300">
{highlightedNodeIds.size > 0 ? (
<>
<span className="text-purple-400 font-semibold">{highlightedNodeIds.size}</span> retrieved
{" / "}
<span className="text-gray-500">{nodes.length.toLocaleString()} total</span>
{" • "}
<span className="text-sm">{inferredSets.length} inferred sets</span>
</>
) : (
<>
<span>{nodes.length.toLocaleString()} nodes</span>
{" • "}
<span>{inferredSets.length} inferred sets</span>
{" • "}
<span className="text-gray-500">Search to retrieve</span>
</>
)}
</p>
</div>
{showControls && (
<div className="absolute top-6 right-6 flex flex-col gap-3 pointer-events-auto max-w-md">
{/* Retrieval Search */}
<div className="relative">
<input
type="text"
placeholder="🔍 Retrieve memories... (e.g., 'AI', 'Physics')"
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
className="w-full px-4 py-2 bg-black/70 backdrop-blur-md border border-purple-500/30 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20"
/>
{searchQuery && (
<button
onClick={() => handleSearch("")}
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-white"
>
</button>
)}
</div>
{/* View Controls */}
<div className="flex gap-2">
<button
onClick={() => setShowLegend(!showLegend)}
className="flex-1 px-4 py-2 bg-black/70 hover:bg-black/90 backdrop-blur-md rounded-lg border border-purple-500/30 transition-all"
>
{showLegend ? "Hide" : "Show"} Panel
</button>
{(searchQuery || selectedNodeSet || retrievalResults.length > 0) && (
<button
onClick={handleReset}
className="px-4 py-2 bg-gradient-to-r from-red-600 to-orange-600 hover:from-red-500 hover:to-orange-500 rounded-lg transition-all shadow-lg"
>
Reset
</button>
)}
</div>
{/* Layer Visibility Controls */}
<div className="bg-black/70 backdrop-blur-md rounded-lg p-3 border border-purple-500/30">
<div className="text-xs font-semibold text-gray-400 mb-2">Layers</div>
<div className="flex flex-col gap-2">
<button
onClick={() => setShowNodes(!showNodes)}
className={`flex items-center justify-between px-3 py-2 rounded-lg transition-all text-sm ${
showNodes
? "bg-purple-600/30 border border-purple-500/50"
: "bg-white/5 border border-gray-600/30"
}`}
>
<span> Nodes</span>
<span className="text-xs">{showNodes ? "ON" : "OFF"}</span>
</button>
<button
onClick={() => setShowEdges(!showEdges)}
className={`flex items-center justify-between px-3 py-2 rounded-lg transition-all text-sm ${
showEdges
? "bg-amber-600/30 border border-amber-500/50"
: "bg-white/5 border border-gray-600/30"
}`}
>
<span> Paths</span>
<span className="text-xs">{showEdges ? "ON" : "OFF"}</span>
</button>
<button
onClick={() => setShowMetaballs(!showMetaballs)}
className={`flex items-center justify-between px-3 py-2 rounded-lg transition-all text-sm ${
showMetaballs
? "bg-purple-600/20 border border-purple-500/30"
: "bg-white/5 border border-gray-600/30"
}`}
>
<span> Clouds</span>
<span className="text-xs">{showMetaballs ? "ON" : "OFF"}</span>
</button>
</div>
</div>
</div>
)}
</div>
{/* Side Panel */}
{showLegend && (
<div className="w-96 bg-black/95 backdrop-blur-xl border-l border-purple-500/20 overflow-y-auto">
<div className="p-6 space-y-6">
{/* Retrieval Results */}
{retrievalResults.length > 0 && (
<div>
<h2 className="text-2xl font-bold mb-4 bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
Retrieved Memories
</h2>
<div className="space-y-3">
{retrievalResults.slice(0, 10).map((result, idx) => (
<div
key={idx}
className="bg-white/5 hover:bg-white/10 p-3 rounded-lg border border-purple-500/20 transition-all"
>
<div className="flex items-start justify-between gap-2 mb-2">
<div className="font-medium text-sm">
{result.type === "node" ? result.nodeLabel : result.nodeSet?.name}
</div>
<div className="text-xs px-2 py-0.5 bg-purple-600/30 rounded">
{(result.similarityScore * 100).toFixed(0)}%
</div>
</div>
<div className="text-xs text-gray-400 mb-2">
{result.why}
</div>
<div className="flex gap-2 flex-wrap">
{result.signals.map((signal, sidx) => (
<span
key={sidx}
className="text-xs px-2 py-0.5 bg-black/30 rounded"
>
{signal.name}: {(signal.weight * 100).toFixed(0)}%
</span>
))}
</div>
</div>
))}
</div>
</div>
)}
{/* Node Attributes - Secondary (NOT sets, just metadata) */}
<div>
<button
onClick={() => setShowNodeAttributes(!showNodeAttributes)}
className="w-full flex items-center justify-between mb-3 text-left"
>
<h3 className="text-sm font-semibold text-gray-400">
Node Attributes {showNodeAttributes ? "▼" : "▶"}
</h3>
<span className="text-xs text-gray-500">
{typeAttributes.size} types
</span>
</button>
{showNodeAttributes && (
<div className="space-y-1 mb-6 pl-2">
{Array.from(typeAttributes.entries())
.sort((a, b) => b[1] - a[1])
.map(([type, count]) => (
<div
key={type}
className="flex items-center justify-between text-xs px-3 py-1.5 bg-white/5 rounded"
>
<span className="text-gray-400">type: {type}</span>
<span className="text-gray-500">({count})</span>
</div>
))}
</div>
)}
</div>
{/* Inferred Node Sets - PRIMARY ABSTRACTION */}
<div>
<h2 className="text-xl font-bold mb-3 flex items-center justify-between">
<span className="bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
Node Sets
</span>
{selectedNodeSet && (
<button
onClick={() => setSelectedNodeSet(null)}
className="text-xs px-2 py-1 bg-red-600 hover:bg-red-500 rounded"
>
Clear
</button>
)}
</h2>
<div className="space-y-2">
{inferredSets.map((nodeSet) => {
const isSelected = selectedNodeSet?.id === nodeSet.id;
return (
<button
key={nodeSet.id}
onClick={() => setSelectedNodeSet(isSelected ? null : nodeSet)}
className={`w-full text-left p-3 rounded-lg transition-all ${
isSelected
? "bg-gradient-to-r from-purple-600 to-pink-600 shadow-lg"
: "bg-white/5 hover:bg-white/10"
}`}
>
<div className="flex items-start justify-between gap-2">
<div className="font-medium text-sm">{nodeSet.name}</div>
<div className="text-xs px-2 py-0.5 bg-black/30 rounded">
{nodeSet.size}
</div>
</div>
</button>
);
})}
</div>
</div>
{/* Enhanced explanatory section with explicit semantics */}
<div className="pt-6 border-t border-purple-500/20">
<h3 className="font-semibold mb-3 text-purple-400">Visual Elements</h3>
<div className="space-y-2 text-sm text-gray-400">
<div className="flex items-start gap-2">
<span className="text-purple-400 mt-0.5 font-bold text-lg"></span>
<div>
<strong className="text-gray-300">Node Size = Importance:</strong>
<div className="text-xs mt-0.5">Larger = Domain/Field (structural), Smaller = Application (leaf)</div>
</div>
</div>
<div className="flex items-start gap-2">
<span className="text-amber-400 mt-0.5"></span>
<div>
<strong className="text-gray-300">Paths (zoom to see):</strong>
<div className="text-xs mt-0.5">Relationships Hover node to highlight connections</div>
</div>
</div>
<div className="flex items-start gap-2">
<span className="text-purple-400/40 mt-0.5"></span>
<div>
<strong className="text-gray-300">Background Clouds:</strong>
<div className="text-xs mt-0.5">Conceptual Density Visible at far zoom for cluster overview</div>
</div>
</div>
<div className="flex items-start gap-2">
<span className="text-cyan-400/60 mt-0.5"></span>
<div>
<strong className="text-gray-300">Boundary Rings:</strong>
<div className="text-xs mt-0.5">Type Clusters Spatial grouping by semantic category</div>
</div>
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
}

View file

@ -14,6 +14,7 @@ import GraphVisualization, { GraphVisualizationAPI } from "@/app/(graph)/GraphVi
import NotebookCellHeader from "./NotebookCellHeader";
import MarkdownPreview from "./MarkdownPreview";
import { Cell, Notebook as NotebookType } from "./types";
import GraphVisualization from "../GraphVisualization";
interface NotebookProps {
notebook: NotebookType;
@ -371,14 +372,18 @@ function CellResult({ content }: { content: [] }) {
if (Array.isArray(line)) {
// Insights search returns uncommon graph data structure
if (Array.from(line).length > 0 && Array.isArray(line[0]) && line[0][1]["relationship_name"]) {
const data = transformInsightsGraphData(line);
parsedContent.push(
<div key={line[0][1]["relationship_name"]} className="w-full h-full bg-white">
<div key={line[0][1]["relationship_name"]} className="flex flex-col w-full h-full min-h-80 bg-white">
<span className="text-sm pl-2 mb-4">reasoning graph</span>
<GraphVisualization
data={transformInsightsGraphData(line)}
ref={graphRef as MutableRefObject<GraphVisualizationAPI>}
graphControls={graphControls}
className="min-h-80"
nodes={data.nodes}
edges={data.edges}
className="flex-1"
config={{
fontSize: 24,
}}
/>
</div>
);
@ -420,13 +425,15 @@ function CellResult({ content }: { content: [] }) {
if (typeof item === "object" && item["graphs"] && typeof item["graphs"] === "object") {
Object.entries<{ nodes: []; edges: []; }>(item["graphs"]).forEach(([datasetName, graph]) => {
parsedContent.push(
<div key={datasetName} className="w-full h-full bg-white">
<div key={datasetName} className="flex flex-col w-full h-full min-h-80 bg-white">
<span className="text-sm pl-2 mb-4">reasoning graph (datasets: {datasetName})</span>
<GraphVisualization
data={transformToVisualizationData(graph)}
ref={graphRef as MutableRefObject<GraphVisualizationAPI>}
graphControls={graphControls}
className="min-h-80"
nodes={graph.nodes}
edges={graph.edges}
className="flex-1"
config={{
fontSize: 24,
}}
/>
</div>
);
@ -501,13 +508,6 @@ function CellResult({ content }: { content: [] }) {
));
};
function transformToVisualizationData(graph: { nodes: [], edges: [] }) {
return {
nodes: graph.nodes,
links: graph.edges,
};
}
type Triplet = [{
id: string,
name: string,
@ -528,8 +528,9 @@ function transformInsightsGraphData(triplets: Triplet[]) {
type: string,
}
} = {};
const links: {
const edges: {
[key: string]: {
id: string,
source: string,
target: string,
label: string,
@ -548,7 +549,8 @@ function transformInsightsGraphData(triplets: Triplet[]) {
type: triplet[2].type,
};
const linkKey = `${triplet[0]["id"]}_${triplet[1]["relationship_name"]}_${triplet[2]["id"]}`;
links[linkKey] = {
edges[linkKey] = {
id: linkKey,
source: triplet[0].id,
target: triplet[2].id,
label: triplet[1]["relationship_name"],
@ -557,6 +559,6 @@ function transformInsightsGraphData(triplets: Triplet[]) {
return {
nodes: Object.values(nodes),
links: Object.values(links),
edges: Object.values(edges),
};
}

View file

@ -0,0 +1,643 @@
import { Graph, Node as GraphNode, Link as GraphLink } from "ngraph.graph";
import * as three from "three";
import {
Color,
DataTexture,
OrthographicCamera,
RGBAFormat,
Scene,
UnsignedByteType,
Vector2,
WebGLRenderer,
WebGLRenderTarget,
} from "three";
import { OrbitControls } from "three/examples/jsm/Addons.js";
import createForceLayout, { Layout } from "ngraph.forcelayout";
import { Edge, Node } from "./graph/types";
import createGraph from "./graph/createGraph";
import createLabel from "./meshes/createLabel";
import pickNodeIndex from "./picking/pickNodeIndex";
import createEdgeMesh from "./meshes/createEdgeMesh";
import createPickingMesh from "./meshes/createPickingMesh";
import createNodeSwarmMesh from "./meshes/createNodeSwarmMesh";
import createNodePositionsTexture from "./textures/createNodePositionsTexture";
import createDensityRenderTarget from "./render-targets/createDensityRenderTarget";
import createDensityAccumulatorMesh from "./meshes/createDensityAccumulatorMesh";
import createMetaballMesh from "./meshes/createMetaballMesh";
import createClusterBoundaryMesh, { ClusterInfo } from "./meshes/createClusterBoundaryMesh";
const INITIAL_CAMERA_DISTANCE = 2000;
// Extended config for layered view controls + zoom semantics
interface Config {
fontSize?: number;
showNodes?: boolean;
showEdges?: boolean;
showMetaballs?: boolean;
pathFilterMode?: "all" | "hoverOnly" | "strongOnly"; // Path filtering
zoomLevel?: "far" | "mid" | "near"; // Zoom-based semantics
highlightedNodeIds?: Set<string>; // Nodes to highlight (neutral-by-default)
}
export default function animate(
nodes: Node[],
edges: Edge[],
parentElement: HTMLElement,
config?: Config
): () => void {
const nodeLabelMap = new Map();
const edgeLabelMap = new Map();
// Semantic color encoding: hierarchy drives saturation + brightness
const typeColorMap: Record<string, string> = {
"Domain": "#C4B5FD", // Bright Purple - Highest importance
"Field": "#67E8F9", // Bright Cyan - High importance
"Subfield": "#A78BFA", // Medium Purple - Medium-high importance
"Concept": "#5EEAD4", // Teal - Medium importance
"Method": "#6EE7B7", // Green - Medium importance
"Theory": "#F9A8D4", // Pink - Medium importance
"Technology": "#FCA5A5", // Soft Red - Lower importance
"Application": "#71717A", // Desaturated Gray - Background/lowest importance
};
// Size hierarchy: more important = larger
const typeSizeMap: Record<string, number> = {
"Domain": 2.5, // Largest
"Field": 2.0,
"Subfield": 1.6,
"Concept": 1.2,
"Method": 1.1,
"Theory": 1.0,
"Technology": 0.9,
"Application": 0.6, // Smallest
};
function getColorForType(nodeType: string): Color {
const colorHex = typeColorMap[nodeType];
if (colorHex) {
return new Color(colorHex);
}
// Fallback for unknown types
return new Color("#9CA3AF"); // Gray for unknown types
}
const mousePosition = new Vector2();
// Node related data
const nodeColors = new Float32Array(nodes.length * 3);
const nodeSizes = new Float32Array(nodes.length); // Size per node for hierarchy
const nodeHighlights = new Float32Array(nodes.length); // 1.0 = highlighted, 0.3 = dimmed
const nodeIndices = new Map();
const textureSize = Math.ceil(Math.sqrt(nodes.length));
const nodePositionsData = new Float32Array(textureSize * textureSize * 4);
// Determine which nodes are highlighted
const highlightedIds = config?.highlightedNodeIds;
const hasHighlights = highlightedIds && highlightedIds.size > 0;
let nodeIndex = 0;
function forNode(node: Node) {
const color = getColorForType(node.type);
nodeColors[nodeIndex * 3 + 0] = color.r;
nodeColors[nodeIndex * 3 + 1] = color.g;
nodeColors[nodeIndex * 3 + 2] = color.b;
// Set highlight state: if no highlights, all at 1.0; if highlights exist, dim non-highlighted
if (hasHighlights) {
nodeHighlights[nodeIndex] = highlightedIds!.has(node.id) ? 1.0 : 0.3;
} else {
nodeHighlights[nodeIndex] = 1.0; // All visible when no highlights
}
// Store size multiplier based on type
nodeSizes[nodeIndex] = typeSizeMap[node.type] || 1.0;
nodePositionsData[nodeIndex * 4 + 0] = 0.0;
nodePositionsData[nodeIndex * 4 + 1] = 0.0;
nodePositionsData[nodeIndex * 4 + 2] = 0.0;
nodePositionsData[nodeIndex * 4 + 3] = 1.0;
nodeIndices.set(node.id, nodeIndex);
nodeIndex += 1;
}
// Node related data
const edgeIndices = new Float32Array(edges.length * 2);
let edgeIndex = 0;
function forEdge(edge: Edge) {
const fromIndex = nodeIndices.get(edge.source);
const toIndex = nodeIndices.get(edge.target);
edgeIndices[edgeIndex * 2 + 0] = fromIndex;
edgeIndices[edgeIndex * 2 + 1] = toIndex;
edgeIndex += 1;
}
// Graph creation and layout
const graph = createGraph(nodes, edges, forNode, forEdge);
// Adaptive layout parameters based on graph size
const nodeCount = nodes.length;
const isLargeGraph = nodeCount > 5000;
const isMassiveGraph = nodeCount > 15000;
// Apple embedding atlas style: stronger repulsion for clear cluster separation
const graphLayout = createForceLayout(graph, {
dragCoefficient: isMassiveGraph ? 0.95 : 0.85,
springLength: isMassiveGraph ? 120 : isLargeGraph ? 180 : 220, // Longer springs for spacing
springCoefficient: isMassiveGraph ? 0.12 : isLargeGraph ? 0.15 : 0.18, // Weaker springs
gravity: isMassiveGraph ? -1200 : isLargeGraph ? -1500 : -1800, // Stronger repulsion
});
// Node Mesh
const nodePositionsTexture = createNodePositionsTexture(
nodes,
nodePositionsData
);
const nodeSwarmMesh = createNodeSwarmMesh(
nodes,
nodePositionsTexture,
nodeColors,
nodeSizes,
nodeHighlights,
INITIAL_CAMERA_DISTANCE
);
const edgeMesh = createEdgeMesh(
edges,
nodePositionsTexture,
edgeIndices,
INITIAL_CAMERA_DISTANCE
);
// Density cloud setup - adaptive resolution for performance
const densityCloudScene = new Scene();
const densityResolution = isMassiveGraph ? 256 : isLargeGraph ? 384 : 512;
const densityCloudTarget = createDensityRenderTarget(densityResolution);
const densityAccumulatorMesh = createDensityAccumulatorMesh(
nodes,
nodeColors,
nodePositionsTexture,
INITIAL_CAMERA_DISTANCE
);
const metaballMesh = createMetaballMesh(densityCloudTarget);
// const densityCloudDebugMesh = createDebugViewMesh(densityCloudTarget);
// Density cloud setup end
let pickedNodeIndex = -1;
const lastPickedNodeIndex = -1;
const pickNodeFromScene = (event: unknown) => {
pickedNodeIndex = pickNodeIndexFromScene(event as MouseEvent);
};
parentElement.addEventListener("mousemove", (event) => {
const rect = parentElement.getBoundingClientRect();
mousePosition.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mousePosition.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
pickNodeFromScene(event);
});
// Group nodes by type for cluster boundaries
const nodesByType = new Map<string, Node[]>();
nodes.forEach(node => {
if (!nodesByType.has(node.type)) {
nodesByType.set(node.type, []);
}
nodesByType.get(node.type)!.push(node);
});
const scene = new Scene();
// Apple embedding atlas style: pure black background
scene.background = new Color("#000000");
const renderer = new WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(parentElement.clientWidth, parentElement.clientHeight);
if (parentElement.children.length === 0) {
parentElement.appendChild(renderer.domElement);
}
// Setup camera
const aspect = parentElement.clientWidth / parentElement.clientHeight;
const frustumSize = INITIAL_CAMERA_DISTANCE;
const camera = new OrthographicCamera(
(-frustumSize * aspect) / 2,
(frustumSize * aspect) / 2,
frustumSize / 2,
-frustumSize / 2,
1,
5000
);
camera.position.set(0, 0, INITIAL_CAMERA_DISTANCE);
camera.lookAt(0, 0, 0);
// Setup controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableRotate = false;
controls.enablePan = true;
controls.enableZoom = true;
controls.screenSpacePanning = true;
controls.minZoom = 0.5; // Allow zooming out more
controls.maxZoom = 6; // Allow closer zoom for detail
controls.enableDamping = true;
controls.dampingFactor = 0.08; // Smoother, more fluid motion
controls.target.set(0, 0, 0);
controls.update();
// Handle resizing
window.addEventListener("resize", () => {
const aspect = parentElement.clientWidth / parentElement.clientHeight;
camera.left = (-frustumSize * aspect) / 2;
camera.right = (frustumSize * aspect) / 2;
camera.top = frustumSize / 2;
camera.bottom = -frustumSize / 2;
camera.updateProjectionMatrix();
renderer.setSize(parentElement.clientWidth, parentElement.clientHeight);
});
// Node picking setup
const pickingTarget = new WebGLRenderTarget(
window.innerWidth,
window.innerHeight,
{
format: RGBAFormat,
type: UnsignedByteType,
depthBuffer: true,
stencilBuffer: false,
}
);
const pickingScene = new Scene();
function pickNodeIndexFromScene(event: MouseEvent): number {
pickingScene.add(pickingMesh);
const pickedNodeIndex = pickNodeIndex(
event,
renderer,
pickingScene,
camera,
pickingTarget
);
return pickedNodeIndex;
}
const pickingMesh = createPickingMesh(
nodes,
nodePositionsTexture,
nodeColors,
INITIAL_CAMERA_DISTANCE
);
renderer.domElement.addEventListener("mousedown", (event) => {
const pickedNodeIndex = pickNodeIndexFromScene(event);
console.log("Picked node index: ", pickedNodeIndex);
});
// Node picking setup end
// Adaptive layout iterations based on graph size
const layoutIterations = isMassiveGraph ? 300 : isLargeGraph ? 500 : 800;
console.log(`Running ${layoutIterations} layout iterations for ${nodeCount} nodes...`);
for (let i = 0; i < layoutIterations; i++) {
graphLayout.step();
// Progress logging for large graphs
if (isMassiveGraph && i % 50 === 0) {
console.log(`Layout progress: ${((i / layoutIterations) * 100).toFixed(0)}%`);
}
}
console.log("Layout complete!");
let visibleLabels: unknown[] = [];
// Only create entity type labels for smaller graphs (performance optimization)
const entityTypeLabels: [string, unknown][] = [];
if (!isMassiveGraph) {
for (const node of nodes) {
if (node.type === "EntityType") {
const label = createLabel(node.label, config?.fontSize);
entityTypeLabels.push([node.id, label]);
}
}
}
// const processingStep = 0;
// Performance monitoring
let frameCount = 0;
let lastFpsUpdate = performance.now();
let currentFps = 60;
// Cluster boundaries
let clusterBoundariesCreated = false;
const clusterBoundaryMeshes: three.Mesh[] = [];
function calculateClusterBoundaries(): ClusterInfo[] {
const clusters: ClusterInfo[] = [];
nodesByType.forEach((typeNodes, nodeType) => {
if (typeNodes.length < 3) return; // Skip small clusters
// Calculate center and radius from actual node positions
let sumX = 0;
let sumY = 0;
typeNodes.forEach(node => {
const pos = graphLayout.getNodePosition(node.id);
sumX += pos.x;
sumY += pos.y;
});
const center = {
x: sumX / typeNodes.length,
y: sumY / typeNodes.length
};
// Calculate radius as max distance from center + padding
let maxDist = 0;
typeNodes.forEach(node => {
const pos = graphLayout.getNodePosition(node.id);
const dist = Math.sqrt(
Math.pow(pos.x - center.x, 2) + Math.pow(pos.y - center.y, 2)
);
maxDist = Math.max(maxDist, dist);
});
clusters.push({
center,
radius: maxDist + 350, // Apple style: more spacing between clusters
color: getColorForType(nodeType)
});
});
return clusters;
}
// Render loop
function render() {
// Adaptive physics updates - skip for large graphs after stabilization
if (!isMassiveGraph || frameCount < 100) {
graphLayout.step();
} else if (frameCount % 2 === 0) {
// Update physics every other frame for massive graphs
graphLayout.step();
}
// FPS monitoring
frameCount++;
const now = performance.now();
if (now - lastFpsUpdate > 1000) {
currentFps = Math.round((frameCount * 1000) / (now - lastFpsUpdate));
frameCount = 0;
lastFpsUpdate = now;
if (isMassiveGraph && currentFps < 30) {
console.warn(`Low FPS detected: ${currentFps} fps`);
}
}
controls.update();
// Create cluster boundaries after layout stabilizes
if (!clusterBoundariesCreated && frameCount === 60) {
const clusters = calculateClusterBoundaries();
clusters.forEach(cluster => {
const mesh = createClusterBoundaryMesh(cluster);
clusterBoundaryMeshes.push(mesh);
scene.add(mesh);
});
clusterBoundariesCreated = true;
}
updateNodePositions(
nodes,
graphLayout,
nodePositionsData,
nodePositionsTexture
);
const textScale = Math.max(1, 4 / camera.zoom);
nodeSwarmMesh.material.uniforms.camDist.value = Math.floor(
camera.zoom * 500
);
nodeSwarmMesh.material.uniforms.mousePos.value.set(
mousePosition.x,
mousePosition.y
);
// @ts-expect-error uniforms does exist on material
edgeMesh.material.uniforms.camDist.value = Math.floor(camera.zoom * 500);
// @ts-expect-error uniforms does exist on material
edgeMesh.material.uniforms.mousePos.value.set(
mousePosition.x,
mousePosition.y
);
// @ts-expect-error uniforms does exist on material
pickingMesh.material.uniforms.camDist.value = Math.floor(camera.zoom * 500);
// Zoom-level semantics: determine what to show based on zoom
let zoomLevel: "far" | "mid" | "near" = "mid";
if (camera.zoom < 1.0) {
zoomLevel = "far"; // Show clusters, domains, density
} else if (camera.zoom > 3.0) {
zoomLevel = "near"; // Show applications, paths, labels
}
edgeMesh.renderOrder = 1;
nodeSwarmMesh.renderOrder = 2;
// IMPROVEMENT #8: Conditional layer rendering based on config and zoom
const showEdges = config?.showEdges !== false && zoomLevel !== "far"; // Hide edges when far
const showNodes = config?.showNodes !== false; // Always show nodes
const showMetaballs = config?.showMetaballs !== false && zoomLevel === "far"; // Only show at far zoom
// Path filtering based on hover
const pathFilterMode = config?.pathFilterMode || "all";
const shouldShowPath = pathFilterMode === "all" || (pathFilterMode === "hoverOnly" && pickedNodeIndex >= 0);
if (showEdges) {
scene.add(edgeMesh);
}
if (showNodes) {
scene.add(nodeSwarmMesh);
}
// Metaball rendering - reduce frequency for massive graphs
const shouldRenderMetaballs = showMetaballs && (!isMassiveGraph || frameCount % 2 === 0);
if (shouldRenderMetaballs) {
// Pass 1: draw points into density texture
renderer.setRenderTarget(densityCloudTarget);
renderer.clear();
densityCloudScene.clear();
densityCloudScene.add(densityAccumulatorMesh);
renderer.render(densityCloudScene, camera);
// Pass 2: render density map to screen
renderer.setRenderTarget(null);
renderer.clear();
metaballMesh.renderOrder = 0;
scene.add(metaballMesh);
} else {
renderer.setRenderTarget(null);
renderer.clear();
}
for (const [nodeId, label] of entityTypeLabels) {
const nodePosition = graphLayout.getNodePosition(nodeId);
// @ts-expect-error label is Text from troika-three-text
label.position.set(nodePosition.x, nodePosition.y, 1.0);
// @ts-expect-error label is Text from troika-three-text
label.scale.setScalar(textScale);
// @ts-expect-error label is Text from troika-three-text
scene.add(label);
}
if (pickedNodeIndex >= 0) {
if (pickedNodeIndex !== lastPickedNodeIndex) {
for (const label of visibleLabels) {
// @ts-expect-error label is Text from troika-three-text
label.visible = false;
}
visibleLabels = [];
}
const pickedNode = nodes[pickedNodeIndex];
parentElement.style.cursor = "pointer";
const pickedNodePosition = graphLayout.getNodePosition(pickedNode.id);
let pickedNodeLabel = nodeLabelMap.get(pickedNode.id);
if (!pickedNodeLabel) {
pickedNodeLabel = createLabel(pickedNode.label, config?.fontSize);
nodeLabelMap.set(pickedNode.id, pickedNodeLabel);
}
pickedNodeLabel.position.set(
pickedNodePosition.x,
pickedNodePosition.y,
1.0
);
pickedNodeLabel.scale.setScalar(textScale);
// Adaptive label display based on graph size and zoom
const minZoomForLabels = isMassiveGraph ? 4 : isLargeGraph ? 3 : 2;
const maxLabels = isMassiveGraph ? 5 : isLargeGraph ? 10 : 15;
if (camera.zoom > minZoomForLabels) {
graph.forEachLinkedNode(
pickedNode.id,
(otherNode: GraphNode, edge: GraphLink) => {
if (visibleLabels.length > maxLabels) {
return;
}
let otherNodeLabel = nodeLabelMap.get(otherNode.id);
if (!otherNodeLabel) {
otherNodeLabel = createLabel(otherNode.data.label, config?.fontSize);
nodeLabelMap.set(otherNode.id, otherNodeLabel);
}
const otherNodePosition = graphLayout.getNodePosition(otherNode.id);
otherNodeLabel.position.set(
otherNodePosition.x,
otherNodePosition.y,
1.0
);
let linkLabel = edgeLabelMap.get(edge.id);
if (!linkLabel) {
linkLabel = createLabel(edge.data.label, config?.fontSize);
edgeLabelMap.set(edge.id, linkLabel);
}
const linkPosition = graphLayout.getLinkPosition(edge.id);
const middleLinkPosition = new Vector2(
(linkPosition.from.x + linkPosition.to.x) / 2,
(linkPosition.from.y + linkPosition.to.y) / 2
);
linkLabel.position.set(
middleLinkPosition.x,
middleLinkPosition.y,
1.0
);
linkLabel.visible = true;
linkLabel.scale.setScalar(textScale);
visibleLabels.push(linkLabel);
otherNodeLabel.visible = true;
otherNodeLabel.scale.setScalar(textScale);
visibleLabels.push(otherNodeLabel);
scene.add(linkLabel);
scene.add(otherNodeLabel);
}
);
}
pickedNodeLabel.visible = true;
visibleLabels.push(pickedNodeLabel);
scene.add(pickedNodeLabel);
} else {
parentElement.style.cursor = "default";
for (const label of visibleLabels) {
// @ts-expect-error label is Text from troika-three-text
label.visible = false;
}
visibleLabels = [];
}
renderer.render(scene, camera);
animationFrameId = requestAnimationFrame(render);
}
let animationFrameId: number;
render();
// Return cleanup function
return () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
// Clean up cluster boundaries
clusterBoundaryMeshes.forEach(mesh => {
scene.remove(mesh);
mesh.geometry.dispose();
if (mesh.material instanceof three.Material) {
mesh.material.dispose();
}
});
graphLayout.dispose();
renderer.dispose();
controls.dispose();
};
}
function updateNodePositions(
nodes: Node[],
graphLayout: Layout<Graph>,
nodePositionsData: Float32Array,
nodePositionsTexture: DataTexture
) {
for (let i = 0; i < nodes.length; i++) {
const p = graphLayout.getNodePosition(nodes[i].id);
nodePositionsData[i * 4 + 0] = p.x;
nodePositionsData[i * 4 + 1] = p.y;
nodePositionsData[i * 4 + 2] = 0.0;
nodePositionsData[i * 4 + 3] = 1.0;
}
nodePositionsTexture.needsUpdate = true;
}

View file

@ -0,0 +1,28 @@
import createNgraph, { Graph } from "ngraph.graph";
import { Edge, Node } from "./types";
export default function createGraph(
nodes: Node[],
edges: Edge[],
forNode?: (node: Node) => void,
forEdge?: (node: Edge) => void
): Graph {
const graph = createNgraph();
for (const node of nodes) {
graph.addNode(node.id, {
id: node.id,
label: node.label,
});
forNode?.(node);
}
for (const edge of edges) {
graph.addLink(edge.source, edge.target, {
id: edge.id,
label: edge.label,
});
forEdge?.(edge);
}
return graph;
}

View file

@ -0,0 +1,12 @@
export interface Node {
id: string;
label: string;
type: string;
}
export interface Edge {
id: string;
label: string;
source: string;
target: string;
}

View file

@ -0,0 +1,46 @@
import { ShaderMaterial, Texture, Vector2 } from "three";
export function createBlurPassMaterial(
texture: Texture,
direction = new Vector2(1.0, 0.0)
) {
return new ShaderMaterial({
uniforms: {
densityTex: { value: texture },
direction: { value: direction }, // (1,0) = horizontal, (0,1) = vertical
texSize: { value: new Vector2(512, 512) },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position.xy, 0.0, 1.0);
}
`,
fragmentShader: `
precision highp float;
uniform sampler2D densityTex;
uniform vec2 direction;
uniform vec2 texSize;
varying vec2 vUv;
void main() {
vec2 texel = direction / texSize;
float kernel[5];
kernel[0] = 0.204164;
kernel[1] = 0.304005;
kernel[2] = 0.193783;
kernel[3] = 0.072184;
kernel[4] = 0.025864;
vec4 sum = texture2D(densityTex, vUv) * kernel[0];
for (int i = 1; i < 5; i++) {
sum += texture2D(densityTex, vUv + texel * float(i)) * kernel[i];
sum += texture2D(densityTex, vUv - texel * float(i)) * kernel[i];
}
gl_FragColor = sum;
}
`,
});
}

View file

@ -0,0 +1,48 @@
import * as three from "three";
export default function createClusterBoundaryMaterial(
clusterColor: three.Color
): three.ShaderMaterial {
const material = new three.ShaderMaterial({
transparent: true,
depthWrite: false,
side: three.DoubleSide,
uniforms: {
clusterColor: { value: clusterColor },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
precision highp float;
uniform vec3 clusterColor;
varying vec2 vUv;
void main() {
// Apple embedding atlas style: soft circular regions
vec2 center = vec2(0.5, 0.5);
float dist = length(vUv - center);
// Soft radial gradient background
float alpha = smoothstep(0.5, 0.25, dist) * 0.12; // More visible background
// Prominent boundary ring (Apple style)
float ring = smoothstep(0.49, 0.47, dist) - smoothstep(0.51, 0.49, dist);
alpha += ring * 0.25; // More prominent border
// Lighter, more vibrant colors for Apple aesthetic
vec3 bgColor = clusterColor * 1.1;
gl_FragColor = vec4(bgColor, alpha);
}
`,
});
return material;
}

View file

@ -0,0 +1,37 @@
import { ShaderMaterial, Texture } from "three";
export function createDebugViewMaterial(fieldTexture: Texture) {
return new ShaderMaterial({
uniforms: {
fieldTex: { value: fieldTexture },
},
vertexShader: `
// void main() {
// gl_Position = vec4(position, 1.0);
// }
varying vec2 vUv;
void main() { vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }
`,
fragmentShader: `
uniform sampler2D fieldTex;
varying vec2 vUv;
void main() {
// gl_FragColor = texture2D(fieldTex, vUv);
float field = texture2D(fieldTex, vUv).r;
field = pow(field * 2.0, 0.5); // optional tone mapping
gl_FragColor = vec4(vec3(field), 1.0);
}
// precision highp float;
// uniform sampler2D fieldTex;
// void main() {
// vec2 uv = gl_FragCoord.xy / vec2(textureSize(fieldTex, 0));
// float field = texture2D(fieldTex, uv).r;
// // visualize the field as grayscale
// gl_FragColor = vec4(vec3(field), 1.0);
// }
`,
});
}

View file

@ -0,0 +1,81 @@
import { AdditiveBlending, DataTexture, ShaderMaterial } from "three";
export default function createDensityAccumulatorMaterial(
nodePositionsTexture: DataTexture,
initialCameraDistance: number
) {
const densityCloudMaterial = new ShaderMaterial({
depthWrite: false,
depthTest: false,
transparent: true,
blending: AdditiveBlending,
uniforms: {
nodePositionsTexture: {
value: nodePositionsTexture,
},
textureSize: {
value: nodePositionsTexture.image.width,
},
camDist: {
value: initialCameraDistance,
},
radius: { value: 0.05 },
},
vertexShader: `
uniform sampler2D nodePositionsTexture;
uniform float textureSize;
uniform float camDist;
attribute vec3 nodeColor;
varying vec3 vColor;
varying vec2 vUv;
varying float nodeSize;
vec3 getNodePos(float idx) {
float fx = mod(idx, textureSize);
float fy = floor(idx / textureSize);
vec2 uv = (vec2(fx, fy) + 0.5) / textureSize;
return texture2D(nodePositionsTexture, uv).xyz;
}
void main() {
vUv = uv;
vColor = nodeColor;
vec3 nodePos = getNodePos(float(gl_InstanceID));
float baseNodeSize = 8.0;
// Normalize camera distance into [0,1]
float t = clamp((camDist - 500.0) / (2000.0 - 500.0), 0.0, 1.0);
nodeSize = baseNodeSize * mix(10.0, 12.0, t);
vec3 transformed = nodePos + position * nodeSize;
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
}
`,
fragmentShader: `
precision highp float;
varying vec2 vUv;
varying float nodeSize;
varying vec3 vColor;
void main() {
vec2 pCoord = vUv - 0.5;
float distSq = dot(pCoord, pCoord) * 4.0;
if (distSq > 1.0) {
discard;
}
float radiusSq = (nodeSize / 2.0) * (nodeSize / 2.0);
float falloff = max(0.0, 1.0 - distSq);
float influence = radiusSq * falloff * falloff;
vec3 accumulatedColor = vColor * influence;
gl_FragColor = vec4(accumulatedColor, influence);
}
`,
});
return densityCloudMaterial;
}

View file

@ -0,0 +1,87 @@
import * as three from "three";
export default function createEdgeMaterial(
texture: three.DataTexture,
initialCameraDistance: number
): three.ShaderMaterial {
const material = new three.ShaderMaterial({
transparent: true,
depthWrite: false,
blending: three.AdditiveBlending,
uniforms: {
nodePosTex: { value: texture },
textureSize: { value: texture.image.width },
camDist: { value: initialCameraDistance },
mousePos: { value: new three.Vector2(9999, 9999) }, // start offscreen
// Apple embedding atlas style: soft pastel edges
color: { value: new three.Color("#FCD34D") }, // Soft amber for minimalist aesthetic
},
vertexShader: `
attribute vec2 edgeIndices;
uniform sampler2D nodePosTex;
uniform float textureSize;
uniform float camDist;
uniform vec2 mousePos;
varying float vFade;
varying float vHighlight;
varying float vEdgePosition; // IMPROVEMENT #2: For directional gradient
vec3 getNodePos(float idx) {
float x = mod(idx, textureSize);
float y = floor(idx / textureSize);
vec2 uv = (vec2(x, y) + 0.5) / textureSize;
return texture2D(nodePosTex, uv).xyz;
}
void main() {
vec3 start = getNodePos(edgeIndices.x);
vec3 end = getNodePos(edgeIndices.y);
vec3 nodePos = mix(start, end, position.x);
// IMPROVEMENT #2: Pass edge position for gradient
vEdgePosition = position.x;
// Project world-space position to clip-space
vec4 clipPos = projectionMatrix * modelViewMatrix * vec4(nodePos, 1.0);
vec3 ndc = clipPos.xyz / clipPos.w; // normalized device coordinates [-1,1]
float distanceFromMouse = length(ndc.xy - mousePos);
vHighlight = smoothstep(0.2, 0.0, distanceFromMouse);
// Apple embedding atlas style: subtle edge opacity
vFade = smoothstep(500.0, 1500.0, camDist);
vFade = 0.25 * clamp(vFade, 0.0, 1.0); // Subtle for clean aesthetic
gl_Position = projectionMatrix * modelViewMatrix * vec4(nodePos, 1.0);
}
`,
fragmentShader: `
precision highp float;
uniform vec3 color;
varying float vFade;
varying float vHighlight;
varying float vEdgePosition; // IMPROVEMENT #2: For directional gradient
void main() {
// IMPROVEMENT #2: Directional gradient from start to end
// Brighter at start, slightly darker at end for flow direction
float gradientFactor = 1.0 - (vEdgePosition * 0.3); // 30% dimming from start to end
// IMPROVEMENT #2: Add subtle glow effect
vec3 glowColor = vec3(1.0, 0.9, 0.7); // Warm white glow
vec3 baseColor = color * gradientFactor;
vec3 finalColor = mix(baseColor, glowColor, vHighlight * 0.9);
// IMPROVEMENT #2: Increased visibility and glow
float baseAlpha = vFade * 1.5; // Increased visibility
float alpha = mix(baseAlpha, 0.95, vHighlight); // Stronger highlight
gl_FragColor = vec4(finalColor, alpha);
}
`,
});
return material;
}

View file

@ -0,0 +1,50 @@
import { ShaderMaterial, Texture } from "three";
export function createMetaballMaterial(fieldTexture: Texture) {
return new ShaderMaterial({
transparent: true,
uniforms: {
fieldTex: { value: fieldTexture },
threshold: { value: 25000.0 },
smoothing: { value: 5000.0 },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 1.0);
}
`,
fragmentShader: `
precision highp float;
varying vec2 vUv;
uniform float threshold;
uniform float smoothing;
uniform sampler2D fieldTex;
void main() {
vec4 fieldData = texture2D(fieldTex, vUv);
vec3 accumulatedColor = fieldData.rgb;
float totalInfluence = fieldData.a;
vec3 finalColor = vec3(0.0);
if (totalInfluence > 0.0) {
finalColor = accumulatedColor / totalInfluence;
}
// Apple embedding atlas style: very subtle density clouds
float alphaEdge = smoothstep(threshold - smoothing, threshold + smoothing, totalInfluence);
float alpha = alphaEdge * 0.08; // Very subtle for clean Apple aesthetic
if (alpha < 0.01) {
discard;
}
gl_FragColor = vec4(finalColor, alpha);
}
`,
});
}

View file

@ -0,0 +1,104 @@
import * as three from "three";
export default function createNodeSwarmMaterial(
nodePositionsTexture: three.DataTexture,
initialCameraDistance: number
) {
const material = new three.ShaderMaterial({
transparent: true,
uniforms: {
nodePosTex: { value: nodePositionsTexture },
textureSize: { value: nodePositionsTexture.image.width },
camDist: { value: initialCameraDistance },
mousePos: { value: new three.Vector2(9999, 9999) }, // start offscreen
},
vertexShader: `
precision highp float;
uniform sampler2D nodePosTex;
uniform float textureSize;
uniform float camDist;
uniform vec2 mousePos;
attribute vec3 nodeColor;
attribute float nodeSize; // Hierarchy-based size multiplier
attribute float nodeHighlight; // Selection-based highlight (1.0 = selected, 0.3 = dimmed)
varying vec3 vColor;
varying float vHighlight;
varying float vSelectionHighlight;
varying vec2 vUv; // IMPROVEMENT #4: For radial halo effect
vec3 getNodePos(float idx) {
float size = textureSize;
float fx = mod(idx, size);
float fy = floor(idx / size);
vec2 uv = (vec2(fx, fy) + 0.5) / size;
return texture2D(nodePosTex, uv).xyz;
}
void main() {
vColor = nodeColor;
vSelectionHighlight = nodeHighlight;
vec3 nodePos = getNodePos(float(gl_InstanceID));
// IMPROVEMENT #4: Pass UV coordinates for halo effect
vUv = position.xy * 0.5 + 0.5; // Convert from [-1,1] to [0,1]
// Project world-space position to clip-space
vec4 clipPos = projectionMatrix * modelViewMatrix * vec4(nodePos, 1.0);
vec3 ndc = clipPos.xyz / clipPos.w; // normalized device coordinates [-1,1]
float distanceFromMouse = length(ndc.xy - mousePos);
vHighlight = smoothstep(0.2, 0.0, distanceFromMouse);
// Hierarchy-based sizing: base size * type size multiplier
float baseNodeSize = 7.0;
// Normalize camera distance into [0,1]
float t = clamp((camDist - 500.0) / (2000.0 - 500.0), 0.0, 1.0);
float finalSize = baseNodeSize * nodeSize * mix(1.0, 1.2, t); // Apply hierarchy multiplier
vec3 transformed = nodePos + position * finalSize;
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
}
`,
fragmentShader: `
precision highp float;
varying vec3 vColor;
varying float vHighlight;
varying float vSelectionHighlight;
varying vec2 vUv; // IMPROVEMENT #4: For radial halo effect
void main() {
// Apple embedding atlas style: subtle radial glow
vec2 center = vec2(0.5, 0.5);
float distFromCenter = length(vUv - center) * 2.0;
// Create sharp node with very subtle glow
float coreRadius = 0.75; // Slightly larger core
float haloRadius = 1.0;
// Core node (solid)
float core = 1.0 - smoothstep(0.0, coreRadius, distFromCenter);
// Very subtle outer glow (Apple aesthetic)
float halo = smoothstep(haloRadius, coreRadius, distFromCenter);
// Subtle color mixing
vec3 haloColor = vColor * 1.15; // Subtle brightness increase
vec3 baseColor = mix(vColor, vec3(1.0), vHighlight * 0.4);
vec3 finalColor = mix(haloColor, baseColor, core);
// Alpha with subtle glow
float alpha = mix(halo * 0.4, 1.0, core); // Reduced halo opacity
// Apply selection-based dimming (neutral-by-default)
alpha *= vSelectionHighlight;
gl_FragColor = vec4(finalColor, alpha);
}
`,
});
return material;
}

View file

@ -0,0 +1,65 @@
import * as three from "three";
export default function createPickingMaterial(
nodePositionsTexture: three.DataTexture,
initialCameraDistance: number
) {
const pickingMaterial = new three.ShaderMaterial({
depthTest: true,
depthWrite: true,
transparent: false,
blending: three.NoBlending,
uniforms: {
nodePosTex: { value: nodePositionsTexture },
textureSize: { value: nodePositionsTexture.image.width },
camDist: { value: initialCameraDistance },
},
vertexShader: `
precision highp float;
uniform sampler2D nodePosTex;
uniform float textureSize;
uniform float camDist;
varying vec3 vColor;
vec3 getNodePos(float idx) {
float size = textureSize;
float fx = mod(idx, size);
float fy = floor(idx / size);
vec2 uv = (vec2(fx, fy) + 0.5) / size;
return texture2D(nodePosTex, uv).xyz;
}
void main() {
float id = float(gl_InstanceID);
vec3 nodePos = getNodePos(id);
vColor = vec3(
mod(id, 256.0) / 255.0,
mod(floor(id / 256.0), 256.0) / 255.0,
floor(id / 65536.0) / 255.0
);
// vColor = vec3(fract(sin(id * 12.9898) * 43758.5453));
float baseNodeSize = 4.0;
// Normalize camera distance into [0,1]
float t = clamp((camDist - 500.0) / (2000.0 - 500.0), 0.0, 1.0);
float nodeSize = baseNodeSize * mix(1.0, 2.0, t);
vec3 transformed = nodePos + position * nodeSize;
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
}
`,
fragmentShader: `
precision highp float;
varying vec3 vColor;
void main() {
gl_FragColor = vec4(vColor, 1.0);
}
`,
});
return pickingMaterial;
}

View file

@ -0,0 +1,27 @@
import * as three from "three";
import createClusterBoundaryMaterial from "../materials/createClusterBoundaryMaterial";
export interface ClusterInfo {
center: { x: number; y: number };
radius: number;
color: three.Color;
}
export default function createClusterBoundaryMesh(
cluster: ClusterInfo
): three.Mesh {
// Create a circle geometry for the cluster boundary
const geometry = new three.PlaneGeometry(
cluster.radius * 2.5, // Make it larger to encompass the cluster
cluster.radius * 2.5
);
const material = createClusterBoundaryMaterial(cluster.color);
const mesh = new three.Mesh(geometry, material);
// Position the mesh at the cluster center
mesh.position.set(cluster.center.x, cluster.center.y, -100); // Behind everything else
mesh.renderOrder = -1;
return mesh;
}

View file

@ -0,0 +1,13 @@
import { Mesh, PlaneGeometry, WebGLRenderTarget } from "three";
import { createDebugViewMaterial } from "../materials/createDebugViewMaterial";
export default function createDebugViewMesh(renderTarget: WebGLRenderTarget) {
const debugQuad = new Mesh(
new PlaneGeometry(2, 2),
createDebugViewMaterial(renderTarget.texture)
);
debugQuad.frustumCulled = false;
return debugQuad;
}

View file

@ -0,0 +1,33 @@
import {
InstancedBufferAttribute,
InstancedMesh,
DataTexture,
PlaneGeometry,
} from "three";
import { Node } from "../graph/types";
import createDensityAccumulatorMaterial from "../materials/createDensityAccumulatorMaterial";
export default function createDensityAccumulatorMesh(
nodes: Node[],
nodeColors: Float32Array,
nodePositionsTexture: DataTexture,
initialCameraDistance: number
) {
const geometry = new PlaneGeometry(2, 2);
const material = createDensityAccumulatorMaterial(
nodePositionsTexture,
initialCameraDistance
);
geometry.setAttribute(
"nodeColor",
new InstancedBufferAttribute(nodeColors, 3)
);
const mesh = new InstancedMesh(geometry, material, nodes.length);
mesh.frustumCulled = false;
return mesh;
}

View file

@ -0,0 +1,35 @@
import * as three from "three";
import createEdgeMaterial from "../materials/createEdgeMaterial";
import { Edge } from "../graph/types";
export default function createEdgeMesh(
edges: Edge[],
nodePositionTexture: three.DataTexture,
edgeIndices: Float32Array,
initialCameraDistance: number
): three.LineSegments {
const numberOfEdges = edges.length;
const instGeom = new three.InstancedBufferGeometry();
instGeom.setAttribute(
"position",
new three.BufferAttribute(new Float32Array([0, 0, 0, 1, 0, 0]), 3)
);
// instGeom.index = baseGeom.index;
instGeom.instanceCount = numberOfEdges;
instGeom.setAttribute(
"edgeIndices",
new three.InstancedBufferAttribute(edgeIndices, 2)
);
const material = createEdgeMaterial(
nodePositionTexture,
initialCameraDistance
);
const edgeMesh = new three.LineSegments(instGeom, material);
edgeMesh.frustumCulled = false;
return edgeMesh;
}

View file

@ -0,0 +1,24 @@
import { Color } from "three";
import { Text } from "troika-three-text";
const LABEL_FONT_SIZE = 14;
export default function createLabel(text = "", fontSize = LABEL_FONT_SIZE): Text {
const label = new Text();
label.text = text;
label.fontSize = fontSize;
label.color = new Color("#ffffff");
label.strokeColor = new Color("#ffffff");
label.outlineWidth = 2;
label.outlineColor = new Color("#000000");
label.outlineOpacity = 0.5;
label.anchorX = "center";
label.anchorY = "middle";
label.visible = true;
label.frustumCulled = false;
label.renderOrder = 5;
label.maxWidth = 200;
label.sync();
return label;
}

View file

@ -0,0 +1,15 @@
import { Mesh, PlaneGeometry, WebGLRenderTarget } from "three";
import { createMetaballMaterial } from "../materials/createMetaballMaterial";
export default function createMetaballMesh(
fieldRenderTarget: WebGLRenderTarget
) {
const quadGeo = new PlaneGeometry(2, 2);
const metaballMat = createMetaballMaterial(fieldRenderTarget.texture);
const quad = new Mesh(quadGeo, metaballMat);
quad.frustumCulled = false;
return quad;
}

View file

@ -0,0 +1,39 @@
import {
Mesh,
DataTexture,
CircleGeometry,
InstancedBufferAttribute,
InstancedBufferGeometry,
} from "three";
import { Node } from "../graph/types";
import createNodeSwarmMaterial from "../materials/createNodeSwarmMaterial";
export default function createNodeSwarmMesh(
nodes: Node[],
nodePositionsTexture: DataTexture,
nodeColors: Float32Array,
nodeSizes: Float32Array,
nodeHighlights: Float32Array,
initialCameraDistance: number
) {
const nodeGeom = new CircleGeometry(2, 16);
const geom = new InstancedBufferGeometry();
geom.index = nodeGeom.index;
geom.instanceCount = nodes.length;
geom.setAttribute("position", nodeGeom.attributes.position);
geom.setAttribute("uv", nodeGeom.attributes.uv);
geom.setAttribute("nodeColor", new InstancedBufferAttribute(nodeColors, 3));
geom.setAttribute("nodeSize", new InstancedBufferAttribute(nodeSizes, 1));
geom.setAttribute("nodeHighlight", new InstancedBufferAttribute(nodeHighlights, 1));
const material = createNodeSwarmMaterial(
nodePositionsTexture,
initialCameraDistance
);
const nodeSwarmMesh = new Mesh(geom, material);
nodeSwarmMesh.frustumCulled = false;
return nodeSwarmMesh;
}

View file

@ -0,0 +1,35 @@
import {
Mesh,
DataTexture,
CircleGeometry,
InstancedBufferGeometry,
InstancedBufferAttribute,
} from "three";
import { Node } from "../graph/types";
import createPickingMaterial from "../materials/createPickingMaterial";
export default function createPickingMesh(
nodes: Node[],
nodePositionsTexture: DataTexture,
nodeColors: Float32Array,
initialCameraDistance: number
): Mesh {
const nodeGeom = new CircleGeometry(2, 16);
const geom = new InstancedBufferGeometry();
geom.index = nodeGeom.index;
geom.instanceCount = nodes.length;
geom.setAttribute("position", nodeGeom.attributes.position);
geom.setAttribute("uv", nodeGeom.attributes.uv);
geom.setAttribute("nodeColor", new InstancedBufferAttribute(nodeColors, 3));
const pickingMaterial = createPickingMaterial(
nodePositionsTexture,
initialCameraDistance
);
const pickingMesh = new Mesh(geom, pickingMaterial);
pickingMesh.frustumCulled = false;
return pickingMesh;
}

View file

@ -0,0 +1,40 @@
import {
OrthographicCamera,
Scene,
WebGLRenderer,
WebGLRenderTarget,
} from "three";
const pixelBuffer = new Uint8Array(4);
export default function pickNodeIndex(
event: MouseEvent,
renderer: WebGLRenderer,
pickingScene: Scene,
camera: OrthographicCamera,
pickingRenderTarget: WebGLRenderTarget
) {
const rect = renderer.domElement.getBoundingClientRect();
// Convert from client coords to pixel coords in render target
const x =
((event.clientX - rect.left) / rect.width) * pickingRenderTarget.width;
const y =
pickingRenderTarget.height -
((event.clientY - rect.top) / rect.height) * pickingRenderTarget.height;
renderer.setRenderTarget(pickingRenderTarget);
renderer.clear();
renderer.render(pickingScene, camera);
renderer.readRenderTargetPixels(
pickingRenderTarget,
Math.floor(x),
Math.floor(y),
1,
1,
pixelBuffer
);
renderer.setRenderTarget(null);
const id = pixelBuffer[0] + pixelBuffer[1] * 256 + pixelBuffer[2] * 256 * 256;
return id || -1;
}

View file

@ -0,0 +1,12 @@
import { FloatType, LinearFilter, RGBAFormat, WebGLRenderTarget } from "three";
export default function createDensityRenderTarget(size = 512) {
return new WebGLRenderTarget(size, size, {
format: RGBAFormat,
type: FloatType,
minFilter: LinearFilter,
magFilter: LinearFilter,
depthBuffer: false,
stencilBuffer: false,
});
}

View file

@ -0,0 +1,28 @@
import * as three from "three";
import { Node } from "../graph/types";
export default function createNodePositionsTexture(
nodes: Node[],
nodePositionData: Float32Array
): three.DataTexture {
const textureSize = Math.ceil(Math.sqrt(nodes.length));
for (let i = 0; i < nodes.length; i++) {
nodePositionData[i * 4 + 0] = 0.0;
nodePositionData[i * 4 + 1] = 0.0;
nodePositionData[i * 4 + 2] = 0.0;
nodePositionData[i * 4 + 3] = 1.0;
}
const texture = new three.DataTexture(
nodePositionData,
textureSize,
textureSize,
three.RGBAFormat,
three.FloatType
);
texture.needsUpdate = true;
texture.minFilter = three.NearestFilter;
texture.magFilter = three.NearestFilter;
return texture;
}

View file

@ -0,0 +1,465 @@
// /* eslint-disable @typescript-eslint/no-explicit-any */
declare module "troika-three-text";
// import type { Color, Material, Object3D, Object3DEventMap } from "three";
// export class BatchedText {
// constructor(...args: any[]);
// add(...args: any[]): void;
// addText(...args: any[]): void;
// copy(...args: any[]): void;
// createDerivedMaterial(...args: any[]): void;
// dispose(...args: any[]): void;
// hasOutline(...args: any[]): void;
// remove(...args: any[]): void;
// removeText(...args: any[]): void;
// sync(...args: any[]): void;
// updateBounds(...args: any[]): void;
// updateMatrixWorld(...args: any[]): void;
// static DEFAULT_MATRIX_AUTO_UPDATE: boolean;
// static DEFAULT_MATRIX_WORLD_AUTO_UPDATE: boolean;
// }
// export class GlyphsGeometry {
// constructor(...args: any[]);
// applyClipRect(...args: any[]): void;
// computeBoundingBox(...args: any[]): void;
// computeBoundingSphere(...args: any[]): void;
// updateAttributeData(...args: any[]): void;
// updateGlyphs(...args: any[]): void;
// }
// export class Text extends Object3D<Object3DEventMap> {
// public text: string;
// public fontSize: number;
// public color: Color;
// public anchorX;
// public anchorY;
// public font: string;
// public material: Material;
// constructor(...args: any[]): Object3D<Object3DEventMap>;
// clone(...args: any[]): Object3D<Object3DEventMap>;
// copy(...args: any[]): Object3D<Object3DEventMap>;
// createDerivedMaterial(...args: any[]): void;
// dispose(...args: any[]): void;
// hasOutline(...args: any[]): void;
// localPositionToTextCoords(...args: any[]): void;
// onBeforeRender(...args: any[]): void;
// raycast(...args: any[]): void;
// sync(...args: any[]): void;
// worldPositionToTextCoords(...args: any[]): void;
// static DEFAULT_MATRIX_AUTO_UPDATE: boolean;
// static DEFAULT_MATRIX_WORLD_AUTO_UPDATE: boolean;
// }
// export function configureTextBuilder(config: any): void;
// export function createTextDerivedMaterial(baseMaterial: any): any;
// export function dumpSDFTextures(): void;
// export function fontResolverWorkerModule(...args: any[]): any;
// export function getCaretAtPoint(textRenderInfo: any, x: any, y: any): any;
// export function getSelectionRects(
// textRenderInfo: any,
// start: any,
// end: any
// ): any;
// export function getTextRenderInfo(args: any, callback: any): any;
// export function preloadFont(
// { font, characters, sdfGlyphSize }: any,
// callback: any
// ): void;
// export function typesetterWorkerModule(...args: any[]): any;
// export namespace BatchedText {
// namespace DEFAULT_UP {
// const isVector3: boolean;
// const x: number;
// const y: number;
// const z: number;
// function add(...args: any[]): void;
// function addScalar(...args: any[]): void;
// function addScaledVector(...args: any[]): void;
// function addVectors(...args: any[]): void;
// function angleTo(...args: any[]): void;
// function applyAxisAngle(...args: any[]): void;
// function applyEuler(...args: any[]): void;
// function applyMatrix3(...args: any[]): void;
// function applyMatrix4(...args: any[]): void;
// function applyNormalMatrix(...args: any[]): void;
// function applyQuaternion(...args: any[]): void;
// function ceil(...args: any[]): void;
// function clamp(...args: any[]): void;
// function clampLength(...args: any[]): void;
// function clampScalar(...args: any[]): void;
// function clone(...args: any[]): void;
// function copy(...args: any[]): void;
// function cross(...args: any[]): void;
// function crossVectors(...args: any[]): void;
// function distanceTo(...args: any[]): void;
// function distanceToSquared(...args: any[]): void;
// function divide(...args: any[]): void;
// function divideScalar(...args: any[]): void;
// function dot(...args: any[]): void;
// function equals(...args: any[]): void;
// function floor(...args: any[]): void;
// function fromArray(...args: any[]): void;
// function fromBufferAttribute(...args: any[]): void;
// function getComponent(...args: any[]): void;
// function length(...args: any[]): void;
// function lengthSq(...args: any[]): void;
// function lerp(...args: any[]): void;
// function lerpVectors(...args: any[]): void;
// function manhattanDistanceTo(...args: any[]): void;
// function manhattanLength(...args: any[]): void;
// function max(...args: any[]): void;
// function min(...args: any[]): void;
// function multiply(...args: any[]): void;
// function multiplyScalar(...args: any[]): void;
// function multiplyVectors(...args: any[]): void;
// function negate(...args: any[]): void;
// function normalize(...args: any[]): void;
// function project(...args: any[]): void;
// function projectOnPlane(...args: any[]): void;
// function projectOnVector(...args: any[]): void;
// function random(...args: any[]): void;
// function randomDirection(...args: any[]): void;
// function reflect(...args: any[]): void;
// function round(...args: any[]): void;
// function roundToZero(...args: any[]): void;
// function set(...args: any[]): void;
// function setComponent(...args: any[]): void;
// function setFromColor(...args: any[]): void;
// function setFromCylindrical(...args: any[]): void;
// function setFromCylindricalCoords(...args: any[]): void;
// function setFromEuler(...args: any[]): void;
// function setFromMatrix3Column(...args: any[]): void;
// function setFromMatrixColumn(...args: any[]): void;
// function setFromMatrixPosition(...args: any[]): void;
// function setFromMatrixScale(...args: any[]): void;
// function setFromSpherical(...args: any[]): void;
// function setFromSphericalCoords(...args: any[]): void;
// function setLength(...args: any[]): void;
// function setScalar(...args: any[]): void;
// function setX(...args: any[]): void;
// function setY(...args: any[]): void;
// function setZ(...args: any[]): void;
// function sub(...args: any[]): void;
// function subScalar(...args: any[]): void;
// function subVectors(...args: any[]): void;
// function toArray(...args: any[]): void;
// function transformDirection(...args: any[]): void;
// function unproject(...args: any[]): void;
// }
// }
// export namespace Text {
// namespace DEFAULT_UP {
// const isVector3: boolean;
// const x: number;
// const y: number;
// const z: number;
// function add(...args: any[]): void;
// function addScalar(...args: any[]): void;
// function addScaledVector(...args: any[]): void;
// function addVectors(...args: any[]): void;
// function angleTo(...args: any[]): void;
// function applyAxisAngle(...args: any[]): void;
// function applyEuler(...args: any[]): void;
// function applyMatrix3(...args: any[]): void;
// function applyMatrix4(...args: any[]): void;
// function applyNormalMatrix(...args: any[]): void;
// function applyQuaternion(...args: any[]): void;
// function ceil(...args: any[]): void;
// function clamp(...args: any[]): void;
// function clampLength(...args: any[]): void;
// function clampScalar(...args: any[]): void;
// function clone(...args: any[]): void;
// function copy(...args: any[]): void;
// function cross(...args: any[]): void;
// function crossVectors(...args: any[]): void;
// function distanceTo(...args: any[]): void;
// function distanceToSquared(...args: any[]): void;
// function divide(...args: any[]): void;
// function divideScalar(...args: any[]): void;
// function dot(...args: any[]): void;
// function equals(...args: any[]): void;
// function floor(...args: any[]): void;
// function fromArray(...args: any[]): void;
// function fromBufferAttribute(...args: any[]): void;
// function getComponent(...args: any[]): void;
// function length(...args: any[]): void;
// function lengthSq(...args: any[]): void;
// function lerp(...args: any[]): void;
// function lerpVectors(...args: any[]): void;
// function manhattanDistanceTo(...args: any[]): void;
// function manhattanLength(...args: any[]): void;
// function max(...args: any[]): void;
// function min(...args: any[]): void;
// function multiply(...args: any[]): void;
// function multiplyScalar(...args: any[]): void;
// function multiplyVectors(...args: any[]): void;
// function negate(...args: any[]): void;
// function normalize(...args: any[]): void;
// function project(...args: any[]): void;
// function projectOnPlane(...args: any[]): void;
// function projectOnVector(...args: any[]): void;
// function random(...args: any[]): void;
// function randomDirection(...args: any[]): void;
// function reflect(...args: any[]): void;
// function round(...args: any[]): void;
// function roundToZero(...args: any[]): void;
// function set(...args: any[]): void;
// function setComponent(...args: any[]): void;
// function setFromColor(...args: any[]): void;
// function setFromCylindrical(...args: any[]): void;
// function setFromCylindricalCoords(...args: any[]): void;
// function setFromEuler(...args: any[]): void;
// function setFromMatrix3Column(...args: any[]): void;
// function setFromMatrixColumn(...args: any[]): void;
// function setFromMatrixPosition(...args: any[]): void;
// function setFromMatrixScale(...args: any[]): void;
// function setFromSpherical(...args: any[]): void;
// function setFromSphericalCoords(...args: any[]): void;
// function setLength(...args: any[]): void;
// function setScalar(...args: any[]): void;
// function setX(...args: any[]): void;
// function setY(...args: any[]): void;
// function setZ(...args: any[]): void;
// function sub(...args: any[]): void;
// function subScalar(...args: any[]): void;
// function subVectors(...args: any[]): void;
// function toArray(...args: any[]): void;
// function transformDirection(...args: any[]): void;
// function unproject(...args: any[]): void;
// }
// }
// export namespace fontResolverWorkerModule {
// const workerModuleData: {
// dependencies: {
// dependencies: any;
// getTransferables: any;
// id: string;
// init: string;
// isWorkerModule: boolean;
// name: string;
// }[];
// getTransferables: any;
// id: string;
// init: string;
// isWorkerModule: boolean;
// name: string;
// };
// function onMainThread(...args: any[]): any;
// }
// export namespace typesetterWorkerModule {
// const workerModuleData: {
// dependencies: {
// dependencies: any;
// getTransferables: any;
// id: string;
// init: string;
// isWorkerModule: boolean;
// name: string;
// }[];
// getTransferables: any;
// id: string;
// init: string;
// isWorkerModule: boolean;
// name: string;
// };
// function onMainThread(...args: any[]): any;
// }

View file

@ -0,0 +1,465 @@
// /* eslint-disable @typescript-eslint/no-explicit-any */
declare module "troika-three-utils";
// import type { Color, Material, Object3D, Object3DEventMap } from "three";
// export class BatchedText {
// constructor(...args: any[]);
// add(...args: any[]): void;
// addText(...args: any[]): void;
// copy(...args: any[]): void;
// createDerivedMaterial(...args: any[]): void;
// dispose(...args: any[]): void;
// hasOutline(...args: any[]): void;
// remove(...args: any[]): void;
// removeText(...args: any[]): void;
// sync(...args: any[]): void;
// updateBounds(...args: any[]): void;
// updateMatrixWorld(...args: any[]): void;
// static DEFAULT_MATRIX_AUTO_UPDATE: boolean;
// static DEFAULT_MATRIX_WORLD_AUTO_UPDATE: boolean;
// }
// export class GlyphsGeometry {
// constructor(...args: any[]);
// applyClipRect(...args: any[]): void;
// computeBoundingBox(...args: any[]): void;
// computeBoundingSphere(...args: any[]): void;
// updateAttributeData(...args: any[]): void;
// updateGlyphs(...args: any[]): void;
// }
// export class Text extends Object3D<Object3DEventMap> {
// public text: string;
// public fontSize: number;
// public color: Color;
// public anchorX;
// public anchorY;
// public font: string;
// public material: Material;
// constructor(...args: any[]): Object3D<Object3DEventMap>;
// clone(...args: any[]): Object3D<Object3DEventMap>;
// copy(...args: any[]): Object3D<Object3DEventMap>;
// createDerivedMaterial(...args: any[]): void;
// dispose(...args: any[]): void;
// hasOutline(...args: any[]): void;
// localPositionToTextCoords(...args: any[]): void;
// onBeforeRender(...args: any[]): void;
// raycast(...args: any[]): void;
// sync(...args: any[]): void;
// worldPositionToTextCoords(...args: any[]): void;
// static DEFAULT_MATRIX_AUTO_UPDATE: boolean;
// static DEFAULT_MATRIX_WORLD_AUTO_UPDATE: boolean;
// }
// export function configureTextBuilder(config: any): void;
// export function createTextDerivedMaterial(baseMaterial: any): any;
// export function dumpSDFTextures(): void;
// export function fontResolverWorkerModule(...args: any[]): any;
// export function getCaretAtPoint(textRenderInfo: any, x: any, y: any): any;
// export function getSelectionRects(
// textRenderInfo: any,
// start: any,
// end: any
// ): any;
// export function getTextRenderInfo(args: any, callback: any): any;
// export function preloadFont(
// { font, characters, sdfGlyphSize }: any,
// callback: any
// ): void;
// export function typesetterWorkerModule(...args: any[]): any;
// export namespace BatchedText {
// namespace DEFAULT_UP {
// const isVector3: boolean;
// const x: number;
// const y: number;
// const z: number;
// function add(...args: any[]): void;
// function addScalar(...args: any[]): void;
// function addScaledVector(...args: any[]): void;
// function addVectors(...args: any[]): void;
// function angleTo(...args: any[]): void;
// function applyAxisAngle(...args: any[]): void;
// function applyEuler(...args: any[]): void;
// function applyMatrix3(...args: any[]): void;
// function applyMatrix4(...args: any[]): void;
// function applyNormalMatrix(...args: any[]): void;
// function applyQuaternion(...args: any[]): void;
// function ceil(...args: any[]): void;
// function clamp(...args: any[]): void;
// function clampLength(...args: any[]): void;
// function clampScalar(...args: any[]): void;
// function clone(...args: any[]): void;
// function copy(...args: any[]): void;
// function cross(...args: any[]): void;
// function crossVectors(...args: any[]): void;
// function distanceTo(...args: any[]): void;
// function distanceToSquared(...args: any[]): void;
// function divide(...args: any[]): void;
// function divideScalar(...args: any[]): void;
// function dot(...args: any[]): void;
// function equals(...args: any[]): void;
// function floor(...args: any[]): void;
// function fromArray(...args: any[]): void;
// function fromBufferAttribute(...args: any[]): void;
// function getComponent(...args: any[]): void;
// function length(...args: any[]): void;
// function lengthSq(...args: any[]): void;
// function lerp(...args: any[]): void;
// function lerpVectors(...args: any[]): void;
// function manhattanDistanceTo(...args: any[]): void;
// function manhattanLength(...args: any[]): void;
// function max(...args: any[]): void;
// function min(...args: any[]): void;
// function multiply(...args: any[]): void;
// function multiplyScalar(...args: any[]): void;
// function multiplyVectors(...args: any[]): void;
// function negate(...args: any[]): void;
// function normalize(...args: any[]): void;
// function project(...args: any[]): void;
// function projectOnPlane(...args: any[]): void;
// function projectOnVector(...args: any[]): void;
// function random(...args: any[]): void;
// function randomDirection(...args: any[]): void;
// function reflect(...args: any[]): void;
// function round(...args: any[]): void;
// function roundToZero(...args: any[]): void;
// function set(...args: any[]): void;
// function setComponent(...args: any[]): void;
// function setFromColor(...args: any[]): void;
// function setFromCylindrical(...args: any[]): void;
// function setFromCylindricalCoords(...args: any[]): void;
// function setFromEuler(...args: any[]): void;
// function setFromMatrix3Column(...args: any[]): void;
// function setFromMatrixColumn(...args: any[]): void;
// function setFromMatrixPosition(...args: any[]): void;
// function setFromMatrixScale(...args: any[]): void;
// function setFromSpherical(...args: any[]): void;
// function setFromSphericalCoords(...args: any[]): void;
// function setLength(...args: any[]): void;
// function setScalar(...args: any[]): void;
// function setX(...args: any[]): void;
// function setY(...args: any[]): void;
// function setZ(...args: any[]): void;
// function sub(...args: any[]): void;
// function subScalar(...args: any[]): void;
// function subVectors(...args: any[]): void;
// function toArray(...args: any[]): void;
// function transformDirection(...args: any[]): void;
// function unproject(...args: any[]): void;
// }
// }
// export namespace Text {
// namespace DEFAULT_UP {
// const isVector3: boolean;
// const x: number;
// const y: number;
// const z: number;
// function add(...args: any[]): void;
// function addScalar(...args: any[]): void;
// function addScaledVector(...args: any[]): void;
// function addVectors(...args: any[]): void;
// function angleTo(...args: any[]): void;
// function applyAxisAngle(...args: any[]): void;
// function applyEuler(...args: any[]): void;
// function applyMatrix3(...args: any[]): void;
// function applyMatrix4(...args: any[]): void;
// function applyNormalMatrix(...args: any[]): void;
// function applyQuaternion(...args: any[]): void;
// function ceil(...args: any[]): void;
// function clamp(...args: any[]): void;
// function clampLength(...args: any[]): void;
// function clampScalar(...args: any[]): void;
// function clone(...args: any[]): void;
// function copy(...args: any[]): void;
// function cross(...args: any[]): void;
// function crossVectors(...args: any[]): void;
// function distanceTo(...args: any[]): void;
// function distanceToSquared(...args: any[]): void;
// function divide(...args: any[]): void;
// function divideScalar(...args: any[]): void;
// function dot(...args: any[]): void;
// function equals(...args: any[]): void;
// function floor(...args: any[]): void;
// function fromArray(...args: any[]): void;
// function fromBufferAttribute(...args: any[]): void;
// function getComponent(...args: any[]): void;
// function length(...args: any[]): void;
// function lengthSq(...args: any[]): void;
// function lerp(...args: any[]): void;
// function lerpVectors(...args: any[]): void;
// function manhattanDistanceTo(...args: any[]): void;
// function manhattanLength(...args: any[]): void;
// function max(...args: any[]): void;
// function min(...args: any[]): void;
// function multiply(...args: any[]): void;
// function multiplyScalar(...args: any[]): void;
// function multiplyVectors(...args: any[]): void;
// function negate(...args: any[]): void;
// function normalize(...args: any[]): void;
// function project(...args: any[]): void;
// function projectOnPlane(...args: any[]): void;
// function projectOnVector(...args: any[]): void;
// function random(...args: any[]): void;
// function randomDirection(...args: any[]): void;
// function reflect(...args: any[]): void;
// function round(...args: any[]): void;
// function roundToZero(...args: any[]): void;
// function set(...args: any[]): void;
// function setComponent(...args: any[]): void;
// function setFromColor(...args: any[]): void;
// function setFromCylindrical(...args: any[]): void;
// function setFromCylindricalCoords(...args: any[]): void;
// function setFromEuler(...args: any[]): void;
// function setFromMatrix3Column(...args: any[]): void;
// function setFromMatrixColumn(...args: any[]): void;
// function setFromMatrixPosition(...args: any[]): void;
// function setFromMatrixScale(...args: any[]): void;
// function setFromSpherical(...args: any[]): void;
// function setFromSphericalCoords(...args: any[]): void;
// function setLength(...args: any[]): void;
// function setScalar(...args: any[]): void;
// function setX(...args: any[]): void;
// function setY(...args: any[]): void;
// function setZ(...args: any[]): void;
// function sub(...args: any[]): void;
// function subScalar(...args: any[]): void;
// function subVectors(...args: any[]): void;
// function toArray(...args: any[]): void;
// function transformDirection(...args: any[]): void;
// function unproject(...args: any[]): void;
// }
// }
// export namespace fontResolverWorkerModule {
// const workerModuleData: {
// dependencies: {
// dependencies: any;
// getTransferables: any;
// id: string;
// init: string;
// isWorkerModule: boolean;
// name: string;
// }[];
// getTransferables: any;
// id: string;
// init: string;
// isWorkerModule: boolean;
// name: string;
// };
// function onMainThread(...args: any[]): any;
// }
// export namespace typesetterWorkerModule {
// const workerModuleData: {
// dependencies: {
// dependencies: any;
// getTransferables: any;
// id: string;
// init: string;
// isWorkerModule: boolean;
// name: string;
// }[];
// getTransferables: any;
// id: string;
// init: string;
// isWorkerModule: boolean;
// name: string;
// };
// function onMainThread(...args: any[]): any;
// }

View file

@ -58,6 +58,7 @@ class DataDTO(OutDTO):
class GraphNodeDTO(OutDTO):
id: UUID
label: str
type: str
properties: dict