Improve self-hosted worker mesh generation

Co-authored-by: Luke The Dev <iamlukethedev@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-04-19 16:35:21 +00:00
co-authored by Luke The Dev
parent 2698cbf2a5
commit 9a7f923266
3 changed files with 273 additions and 234 deletions
+34 -221
View File
@@ -1,24 +1,25 @@
{
"name": "claw3d",
"version": "0.1.0",
"version": "0.1.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claw3d",
"version": "0.1.0",
"version": "0.1.5",
"license": "MIT",
"dependencies": {
"@noble/ed25519": "^3.0.0",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@vercel/otel": "^2.1.0",
"canvas-confetti": "^1.9.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"jpeg-js": "^0.4.4",
"lucide-react": "^0.563.0",
"next": "16.1.7",
"phaser": "^3.90.0",
"pngjs": "^7.0.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-markdown": "^10.1.0",
@@ -163,7 +164,6 @@
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.6",
@@ -470,7 +470,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -511,7 +510,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1964,136 +1962,12 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"optional": true,
"peer": true,
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@opentelemetry/api-logs": {
"version": "0.211.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.211.0.tgz",
"integrity": "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/api": "^1.3.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@opentelemetry/core": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz",
"integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/instrumentation": {
"version": "0.211.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.211.0.tgz",
"integrity": "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/api-logs": "0.211.0",
"import-in-the-middle": "^2.0.0",
"require-in-the-middle": "^8.0.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/resources": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz",
"integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.5.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-logs": {
"version": "0.211.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.211.0.tgz",
"integrity": "sha512-O5nPwzgg2JHzo59kpQTPUOTzFi0Nv5LxryG27QoXBciX3zWM3z83g+SNOHhiQVYRWFSxoWn1JM2TGD5iNjOwdA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/api-logs": "0.211.0",
"@opentelemetry/core": "2.5.0",
"@opentelemetry/resources": "2.5.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.4.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-metrics": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz",
"integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.5.0",
"@opentelemetry/resources": "2.5.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.9.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-trace-base": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz",
"integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.5.0",
"@opentelemetry/resources": "2.5.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/semantic-conventions": {
"version": "1.39.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.39.0.tgz",
"integrity": "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/@peculiar/asn1-cms": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz",
@@ -2259,7 +2133,6 @@
"integrity": "sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"playwright": "1.58.0"
},
@@ -2315,7 +2188,6 @@
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz",
"integrity": "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.17.8",
"@types/webxr": "*",
@@ -3069,6 +2941,7 @@
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -3089,6 +2962,7 @@
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"dequal": "^2.0.3"
}
@@ -3170,7 +3044,8 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/canvas-confetti": {
"version": "1.9.0",
@@ -3286,7 +3161,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz",
"integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -3297,7 +3171,6 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -3322,7 +3195,6 @@
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz",
"integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@dimforge/rapier3d-compat": "~0.12.0",
"@tweenjs/tween.js": "~23.1.3",
@@ -3400,7 +3272,6 @@
"integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.53.1",
"@typescript-eslint/types": "8.53.1",
@@ -3918,24 +3789,6 @@
"react": ">= 16.8.0"
}
},
"node_modules/@vercel/otel": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vercel/otel/-/otel-2.1.0.tgz",
"integrity": "sha512-Zwu2Cu4t46DzBnY1DQSTxZ4MBLVfYsOjnlWuZuLRWnmVPX+SNrVHbs3ssiJ6uvY1J1JJswor4zSn8mHYxzYeBA==",
"license": "MIT",
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.9.0 <2.0.0",
"@opentelemetry/api-logs": ">=0.200.0 <0.300.0",
"@opentelemetry/instrumentation": ">=0.200.0 <0.300.0",
"@opentelemetry/resources": ">=2.0.0 <3.0.0",
"@opentelemetry/sdk-logs": ">=0.200.0 <0.300.0",
"@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0",
"@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0"
}
},
"node_modules/@vitest/expect": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
@@ -4057,8 +3910,8 @@
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4066,15 +3919,6 @@
"node": ">=0.4.0"
}
},
"node_modules/acorn-import-attributes": {
"version": "1.9.5",
"resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
"integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
"license": "MIT",
"peerDependencies": {
"acorn": "^8"
}
},
"node_modules/acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
@@ -4118,6 +3962,7 @@
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
}
@@ -4495,7 +4340,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -4724,18 +4568,11 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/cjs-module-lexer": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz",
"integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==",
"license": "MIT"
},
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"clsx": "^2.1.1"
},
@@ -4754,7 +4591,6 @@
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
}
@@ -5112,7 +4948,8 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/draco3d": {
"version": "1.5.7",
@@ -5431,7 +5268,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5633,7 +5469,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -6532,18 +6367,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/import-in-the-middle": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.5.tgz",
"integrity": "sha512-0InH9/4oDCBRzWXhpOqusspLBrVfK1vPvbn9Wxl8DAQ8yyx5fWJRETICSwkiAMaYntjJAMBP1R4B6cQnEUYVEA==",
"license": "Apache-2.0",
"dependencies": {
"acorn": "^8.15.0",
"acorn-import-attributes": "^1.9.5",
"cjs-module-lexer": "^2.2.0",
"module-details-from-path": "^1.0.4"
}
},
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -7122,6 +6945,12 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/jpeg-js": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
"license": "BSD-3-Clause"
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -7148,7 +6977,6 @@
"integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@acemir/cssom": "^0.9.28",
"@asamuzakjp/dom-selector": "^6.7.6",
@@ -7631,6 +7459,7 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -8599,12 +8428,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/module-details-from-path": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz",
"integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -9117,6 +8940,15 @@
"node": ">=18"
}
},
"node_modules/pngjs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
"license": "MIT",
"engines": {
"node": ">=14.19.0"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -9178,6 +9010,7 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -9193,6 +9026,7 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -9205,7 +9039,8 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/promise-worker-transferable": {
"version": "1.0.4",
@@ -9295,7 +9130,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -9305,7 +9139,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -9518,19 +9351,6 @@
"node": ">=0.10.0"
}
},
"node_modules/require-in-the-middle": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz",
"integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.5",
"module-details-from-path": "^1.0.3"
},
"engines": {
"node": ">=9.3.0 || >=8.10.0 <9.0.0"
}
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -10292,7 +10112,6 @@
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
"integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
@@ -10323,8 +10142,7 @@
"version": "0.183.2",
"resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
"integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/three-mesh-bvh": {
"version": "0.8.3",
@@ -10416,7 +10234,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -10752,7 +10569,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -11026,7 +10842,6 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -11135,7 +10950,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -11487,7 +11301,6 @@
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+2
View File
@@ -27,9 +27,11 @@
"canvas-confetti": "^1.9.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"jpeg-js": "^0.4.4",
"lucide-react": "^0.563.0",
"next": "16.1.7",
"phaser": "^3.90.0",
"pngjs": "^7.0.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-markdown": "^10.1.0",
+237 -13
View File
@@ -4,6 +4,8 @@ const os = require("node:os");
const path = require("node:path");
const { randomUUID } = require("node:crypto");
const THREE = require("three");
const { PNG } = require("pngjs");
const jpeg = require("jpeg-js");
const DEFAULT_PORT = Number.parseInt(process.env.CLAW3D_STUDIO_PROVIDER_PORT || "3333", 10);
const DEFAULT_HOST = process.env.CLAW3D_STUDIO_PROVIDER_HOST || "127.0.0.1";
@@ -130,6 +132,11 @@ const rgbToHex = (red, green, blue) => `#${hex(red)}${hex(green)}${hex(blue)}`;
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
const smoothstep = (edge0, edge1, value) => {
const t = clamp((value - edge0) / Math.max(edge1 - edge0, 1e-6), 0, 1);
return t * t * (3 - 2 * t);
};
const parsePngSize = (buffer) => {
if (buffer.length < 24) return { width: 1024, height: 1024 };
const signature = buffer.subarray(0, 8);
@@ -141,8 +148,87 @@ const parsePngSize = (buffer) => {
};
};
const sampleIntensityGrid = (buffer, cells = 20) => {
const decodeRasterImage = (buffer, mimeType) => {
const normalized = (mimeType || "").trim().toLowerCase();
if (normalized === "image/png") {
try {
const png = PNG.sync.read(buffer);
return {
width: png.width,
height: png.height,
channels: 4,
data: png.data,
};
} catch {
const { width, height } = parsePngSize(buffer);
return {
width,
height,
channels: 0,
data: null,
};
}
}
if (normalized === "image/jpeg" || normalized === "image/jpg") {
try {
const decoded = jpeg.decode(buffer, { useTArray: true });
return {
width: decoded.width,
height: decoded.height,
channels: 4,
data: decoded.data,
};
} catch {
const { width, height } = parsePngSize(buffer);
return {
width,
height,
channels: 0,
data: null,
};
}
}
const { width, height } = parsePngSize(buffer);
return {
width,
height,
channels: 0,
data: null,
};
};
const sampleIntensityGrid = (params, cells = 20) => {
const { raster, buffer } = params;
const grid = [];
if (raster?.data && raster.width > 0 && raster.height > 0) {
const data = raster.data;
for (let row = 0; row < cells; row += 1) {
const values = [];
const y0 = Math.floor((row / cells) * raster.height);
const y1 = Math.max(y0 + 1, Math.floor(((row + 1) / cells) * raster.height));
for (let col = 0; col < cells; col += 1) {
const x0 = Math.floor((col / cells) * raster.width);
const x1 = Math.max(x0 + 1, Math.floor(((col + 1) / cells) * raster.width));
let total = 0;
let count = 0;
for (let y = y0; y < y1; y += 1) {
for (let x = x0; x < x1; x += 1) {
const index = (y * raster.width + x) * 4;
const red = data[index] ?? 0;
const green = data[index + 1] ?? red;
const blue = data[index + 2] ?? green;
const alpha = data[index + 3] ?? 255;
const luminance = ((red * 0.2126 + green * 0.7152 + blue * 0.0722) / 255) * (alpha / 255);
total += luminance;
count += 1;
}
}
values.push(clamp(Math.round((total / Math.max(count, 1)) * 1000) / 1000, 0, 1));
}
grid.push(values);
}
return grid;
}
const length = Math.max(buffer.length, 3);
for (let row = 0; row < cells; row += 1) {
const values = [];
@@ -160,8 +246,41 @@ const sampleIntensityGrid = (buffer, cells = 20) => {
return grid;
};
const sampleColorGrid = (buffer, cells = 20) => {
const sampleColorGrid = (params, cells = 20) => {
const { raster, buffer } = params;
const grid = [];
if (raster?.data && raster.width > 0 && raster.height > 0) {
const data = raster.data;
for (let row = 0; row < cells; row += 1) {
const values = [];
const y0 = Math.floor((row / cells) * raster.height);
const y1 = Math.max(y0 + 1, Math.floor(((row + 1) / cells) * raster.height));
for (let col = 0; col < cells; col += 1) {
const x0 = Math.floor((col / cells) * raster.width);
const x1 = Math.max(x0 + 1, Math.floor(((col + 1) / cells) * raster.width));
let redTotal = 0;
let greenTotal = 0;
let blueTotal = 0;
let count = 0;
for (let y = y0; y < y1; y += 1) {
for (let x = x0; x < x1; x += 1) {
const index = (y * raster.width + x) * 4;
redTotal += data[index] ?? 0;
greenTotal += data[index + 1] ?? data[index] ?? 0;
blueTotal += data[index + 2] ?? data[index + 1] ?? 0;
count += 1;
}
}
values.push([
Math.round(redTotal / Math.max(count, 1)),
Math.round(greenTotal / Math.max(count, 1)),
Math.round(blueTotal / Math.max(count, 1)),
]);
}
grid.push(values);
}
return grid;
}
const length = Math.max(buffer.length, 3);
for (let row = 0; row < cells; row += 1) {
const values = [];
@@ -179,8 +298,91 @@ const sampleColorGrid = (buffer, cells = 20) => {
return grid;
};
const samplePalette = (buffer) => {
const smoothGrid = (grid, iterations = 1) => {
let current = grid.map((row) => row.slice());
for (let iteration = 0; iteration < iterations; iteration += 1) {
const next = current.map((row) => row.slice());
for (let row = 0; row < current.length; row += 1) {
for (let col = 0; col < current[row].length; col += 1) {
let total = 0;
let count = 0;
for (let y = Math.max(0, row - 1); y <= Math.min(current.length - 1, row + 1); y += 1) {
for (let x = Math.max(0, col - 1); x <= Math.min(current[row].length - 1, col + 1); x += 1) {
total += current[y][x];
count += 1;
}
}
next[row][col] = total / Math.max(count, 1);
}
}
current = next;
}
return current;
};
const buildPortraitMask = (rows, cols) => {
const mask = [];
for (let row = 0; row < rows; row += 1) {
const y = rows <= 1 ? 0 : row / (rows - 1);
const rowValues = [];
for (let col = 0; col < cols; col += 1) {
const x = cols <= 1 ? 0 : col / (cols - 1);
const dx = (x - 0.5) / 0.36;
const dy = (y - 0.44) / 0.54;
const oval = 1 - clamp(dx * dx + dy * dy, 0, 1.8);
const shoulders = 1 - clamp(Math.abs(x - 0.5) / 0.6, 0, 1);
const shoulderWeight = smoothstep(0.58, 0.9, y) * shoulders * 0.55;
const headWeight = smoothstep(0.02, 0.18, y) * (1 - smoothstep(0.68, 0.95, y));
rowValues.push(clamp(oval * headWeight + shoulderWeight, 0, 1));
}
mask.push(rowValues);
}
return mask;
};
const applyPortraitRelief = (intensityGrid) => {
const rows = intensityGrid.length;
const cols = intensityGrid[0]?.length ?? 0;
const smoothed = smoothGrid(intensityGrid, 2);
const portraitMask = buildPortraitMask(rows, cols);
const refined = smoothed.map((row, rowIndex) =>
row.map((value, colIndex) => {
const mask = portraitMask[rowIndex]?.[colIndex] ?? 0;
const centered = value - 0.5;
const sculpted =
0.38 +
mask * 0.72 +
centered * 0.42 +
Math.max(0, mask - 0.45) * 0.25;
return clamp(sculpted, 0, 1);
}),
);
return smoothGrid(refined, 1);
};
const samplePalette = (params) => {
const { raster, buffer } = params;
const buckets = new Map();
if (raster?.data && raster.width > 0 && raster.height > 0) {
const data = raster.data;
const step = Math.max(1, Math.floor((raster.width * raster.height) / 1800));
for (let pixel = 0; pixel < raster.width * raster.height; pixel += step) {
const index = pixel * 4;
const red = data[index] ?? 0;
const green = data[index + 1] ?? 0;
const blue = data[index + 2] ?? 0;
const key = `${red >> 4}:${green >> 4}:${blue >> 4}`;
const current = buckets.get(key);
if (current) {
current.count += 1;
current.red += red;
current.green += green;
current.blue += blue;
} else {
buckets.set(key, { count: 1, red, green, blue });
}
}
} else {
const step = Math.max(3, Math.floor(buffer.length / 1800));
for (let index = 0; index + 2 < buffer.length; index += step) {
const red = buffer[index] ?? 0;
@@ -197,6 +399,7 @@ const samplePalette = (buffer) => {
buckets.set(key, { count: 1, red, green, blue });
}
}
}
const colors = Array.from(buckets.values())
.sort((left, right) => right.count - left.count)
.slice(0, 4)
@@ -266,8 +469,9 @@ const decodeImageUrl = async (imageUrl) => {
const createHeightfieldScene = async (params) => {
const { GLTFExporter } = await import("three/examples/jsm/exporters/GLTFExporter.js");
const { colorGrid, intensityGrid, palette, width, height } = params;
const rows = intensityGrid.length;
const cols = intensityGrid[0]?.length ?? 0;
const refinedIntensity = applyPortraitRelief(intensityGrid);
const rows = refinedIntensity.length;
const cols = refinedIntensity[0]?.length ?? 0;
const scene = new THREE.Scene();
const panelWidth = clamp((width / Math.max(height, 1)) * 5.8, 2.8, 7.2);
@@ -289,8 +493,13 @@ const createHeightfieldScene = async (params) => {
for (let row = 0; row < rows; row += 1) {
for (let col = 0; col < cols; col += 1) {
const index = row * cols + col;
const intensity = intensityGrid[row]?.[col] ?? 0.5;
positions.setZ(index, intensity * 0.9);
const intensity = refinedIntensity[row]?.[col] ?? 0.5;
const xNormalized = cols <= 1 ? 0 : col / (cols - 1);
const yNormalized = rows <= 1 ? 0 : row / (rows - 1);
const edgeFadeX = smoothstep(0, 0.12, xNormalized) * (1 - smoothstep(0.88, 1, xNormalized));
const edgeFadeY = smoothstep(0, 0.08, yNormalized) * (1 - smoothstep(0.92, 1, yNormalized));
const edgeFade = clamp(edgeFadeX * edgeFadeY, 0, 1);
positions.setZ(index, intensity * edgeFade * 1.25);
const rgb = colorGrid[row]?.[col] ?? [127, 127, 127];
colors[index * 3] = (rgb[0] ?? 127) / 255;
colors[index * 3 + 1] = (rgb[1] ?? 127) / 255;
@@ -306,8 +515,9 @@ const createHeightfieldScene = async (params) => {
new THREE.MeshStandardMaterial({
vertexColors: true,
side: THREE.DoubleSide,
roughness: 0.82,
metalness: 0.03,
roughness: 0.76,
metalness: 0.04,
flatShading: false,
}),
);
relief.position.set(0, panelHeight * 0.5, 0.02);
@@ -356,10 +566,15 @@ const createHeightfieldScene = async (params) => {
const createHeightfieldAdapter = () => ({
id: "heightfield-relief",
async generate(params) {
const size = parsePngSize(params.buffer);
const palette = samplePalette(params.buffer);
const intensityGrid = sampleIntensityGrid(params.buffer, 20);
const colorGrid = sampleColorGrid(params.buffer, 20);
const raster = decodeRasterImage(params.buffer, params.mimeType);
const size = {
width: raster.width || parsePngSize(params.buffer).width,
height: raster.height || parsePngSize(params.buffer).height,
};
const sampleParams = { raster, buffer: params.buffer };
const palette = samplePalette(sampleParams);
const intensityGrid = sampleIntensityGrid(sampleParams, 24);
const colorGrid = sampleColorGrid(sampleParams, 24);
const glb = await createHeightfieldScene({
colorGrid,
intensityGrid,
@@ -455,6 +670,7 @@ const createTaskStore = () => {
try {
const result = await adapter.generate({
buffer: params.buffer,
mimeType: params.mimeType || "image/png",
sourceImagePath,
prompt: params.prompt || "",
mode: params.mode || "image_mesh",
@@ -554,6 +770,14 @@ const createStudioAiWorkerServer = (params = {}) => {
: "",
mode:
body.model_type === "lowpoly" ? "image_avatar" : "image_mesh",
mimeType:
typeof body.image_url === "string" && body.image_url.startsWith("data:image/jpeg")
? "image/jpeg"
: typeof body.image_url === "string" && body.image_url.startsWith("data:image/png")
? "image/png"
: typeof body.image_url === "string" && body.image_url.startsWith("data:image/webp")
? "image/webp"
: "image/png",
},
baseUrl,
);