diff --git a/package.json b/package.json index 9c2797819..f226eb302 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,8 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.5.0", "@sentry/nextjs": "^10.29.0", "@sentry/opentelemetry": "^10.29.0", "@slack/oauth": "^3.0.4", @@ -91,6 +93,7 @@ "@types/js-cookie": "^3.0.6", "@types/js-yaml": "^4.0.9", "@types/mdx": "^2.0.13", + "@types/three": "^0.183.1", "@vercel/functions": "^3.3.3", "@vercel/otel": "^2.1.0", "@workos-inc/node": "^8.0.0", @@ -111,6 +114,7 @@ "drizzle-orm": "catalog:", "event-source-polyfill": "^1.0.31", "eventsource-parser": "^3.0.6", + "gsap": "^3.14.2", "jotai": "^2.15.1", "jotai-minidb": "^0.0.8", "js-cookie": "^3.0.5", @@ -136,6 +140,7 @@ "stripe": "^19.1.0", "stytch": "^12.43.0", "tailwind-merge": "^3.3.1", + "three": "^0.183.2", "uuid": "11.1.0", "vaul": "^1.1.2", "zod": "catalog:" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c30682678..9aed19e68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -208,6 +208,12 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.2.8 version: 1.2.8(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@react-three/drei': + specifier: ^10.7.7 + version: 10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.2)(immer@10.1.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(three@0.183.2))(@types/react@19.2.2)(@types/three@0.183.1)(immer@10.1.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(three@0.183.2) + '@react-three/fiber': + specifier: ^9.5.0 + version: 9.5.0(@types/react@19.2.2)(immer@10.1.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(three@0.183.2) '@sentry/nextjs': specifier: ^10.29.0 version: 10.29.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(webpack@5.105.2(@swc/core@1.12.5)(esbuild@0.27.0)) @@ -247,6 +253,9 @@ importers: '@types/mdx': specifier: ^2.0.13 version: 2.0.13 + '@types/three': + specifier: ^0.183.1 + version: 0.183.1 '@vercel/functions': specifier: ^3.3.3 version: 3.3.3(@aws-sdk/credential-provider-web-identity@3.972.3) @@ -300,13 +309,16 @@ importers: version: 14.25.1 drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(pg@8.18.0) + version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.10)(pg@8.18.0) event-source-polyfill: specifier: ^1.0.31 version: 1.0.31 eventsource-parser: specifier: ^3.0.6 version: 3.0.6 + gsap: + specifier: ^3.14.2 + version: 3.14.2 jotai: specifier: ^2.15.1 version: 2.15.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0) @@ -382,6 +394,9 @@ importers: tailwind-merge: specifier: ^3.3.1 version: 3.3.1 + three: + specifier: ^0.183.2 + version: 0.183.2 uuid: specifier: 11.1.0 version: 11.1.0 @@ -535,7 +550,7 @@ importers: version: 11.8.0(typescript@5.9.3) drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(pg@8.18.0) + version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.10)(pg@8.18.0) jsonwebtoken: specifier: 'catalog:' version: 9.0.3 @@ -611,7 +626,7 @@ importers: version: 11.9.0(typescript@5.9.3) drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(pg@8.18.0) + version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.10)(pg@8.18.0) hono: specifier: 'catalog:' version: 4.12.2 @@ -672,7 +687,7 @@ importers: devDependencies: '@types/bun': specifier: latest - version: 1.3.9 + version: 1.3.10 '@types/node': specifier: ^20.0.0 version: 20.19.27 @@ -684,7 +699,7 @@ importers: devDependencies: '@types/bun': specifier: latest - version: 1.3.9 + version: 1.3.10 '@types/node': specifier: ^20.0.0 version: 20.19.27 @@ -699,7 +714,7 @@ importers: version: link:../packages/worker-utils drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(pg@8.18.0) + version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.10)(pg@8.18.0) hono: specifier: 'catalog:' version: 4.12.2 @@ -745,7 +760,7 @@ importers: version: 8.0.3 drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(pg@8.18.0) + version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.10)(pg@8.18.0) jsonwebtoken: specifier: 'catalog:' version: 9.0.3 @@ -1034,7 +1049,7 @@ importers: version: 11.9.0(typescript@5.9.3) drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(pg@8.18.0) + version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.10)(pg@8.18.0) hono: specifier: 'catalog:' version: 4.12.2 @@ -1123,7 +1138,7 @@ importers: version: 8.1.2 drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(pg@8.18.0) + version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.10)(pg@8.18.0) zod: specifier: 'catalog:' version: 4.3.6 @@ -1194,7 +1209,7 @@ importers: version: link:../packages/worker-utils drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(pg@8.18.0) + version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.10)(pg@8.18.0) hono: specifier: 'catalog:' version: 4.12.2 @@ -1234,7 +1249,7 @@ importers: version: link:../packages/db drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(pg@8.18.0) + version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.10)(pg@8.18.0) workers-tagged-logger: specifier: 'catalog:' version: 1.0.0 @@ -1265,7 +1280,7 @@ importers: version: link:../packages/db drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(pg@8.18.0) + version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.10)(pg@8.18.0) zod: specifier: 'catalog:' version: 4.3.6 @@ -1296,7 +1311,7 @@ importers: version: 0.0.22 drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(pg@8.18.0) + version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.10)(pg@8.18.0) hono: specifier: 'catalog:' version: 4.12.2 @@ -1348,7 +1363,7 @@ importers: version: link:../packages/worker-utils drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(pg@8.18.0) + version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.10)(pg@8.18.0) hono: specifier: 'catalog:' version: 4.12.2 @@ -1409,7 +1424,7 @@ importers: version: link:../packages/worker-utils drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(pg@8.18.0) + version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.10)(pg@8.18.0) hono: specifier: 'catalog:' version: 4.12.2 @@ -1465,7 +1480,7 @@ importers: dependencies: drizzle-orm: specifier: 'catalog:' - version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(pg@8.18.0) + version: 0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.10)(pg@8.18.0) pg: specifier: ^8.16.3 version: 8.18.0 @@ -2735,6 +2750,9 @@ packages: resolution: {integrity: sha512-Y6+WUMsTFWE5jb20IFP4YGa5IrGY/+a/FbOSjDF/wz9gepU2hwCYSXRHP/vPwBvwcY3SVMASt4yXxbXNXigmZQ==} engines: {node: '>=18'} + '@dimforge/rapier3d-compat@0.12.0': + resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} + '@discordjs/builders@1.13.1': resolution: {integrity: sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==} engines: {node: '>=16.11.0'} @@ -3575,6 +3593,9 @@ packages: '@types/react': '>=16' react: '>=16' + '@mediapipe/tasks-vision@0.10.17': + resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==} + '@mistralai/mistralai@1.10.0': resolution: {integrity: sha512-tdIgWs4Le8vpvPiUEWne6tK0qbVc+jMenujnvTqOjogrJUsCSQhus0tHTU1avDDh5//Rq2dFgP9mWRAdIEoBqg==} @@ -3598,6 +3619,11 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@monogrid/gainmap-js@3.4.0': + resolution: {integrity: sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==} + peerDependencies: + three: '>= 0.159.0' + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -4646,6 +4672,42 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@react-three/drei@10.7.7': + resolution: {integrity: sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==} + peerDependencies: + '@react-three/fiber': ^9.0.0 + react: ^19 + react-dom: ^19 + three: '>=0.159' + peerDependenciesMeta: + react-dom: + optional: true + + '@react-three/fiber@9.5.0': + resolution: {integrity: sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==} + peerDependencies: + expo: '>=43.0' + expo-asset: '>=8.4' + expo-file-system: '>=11.0' + expo-gl: '>=11.0' + react: '>=19 <19.3' + react-dom: '>=19 <19.3' + react-native: '>=0.78' + three: '>=0.156' + peerDependenciesMeta: + expo: + optional: true + expo-asset: + optional: true + expo-file-system: + optional: true + expo-gl: + optional: true + react-dom: + optional: true + react-native: + optional: true + '@redis/bloom@1.2.0': resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} peerDependencies: @@ -5783,6 +5845,9 @@ packages: resolution: {integrity: sha512-w071DSzP94YfN6XiWhOxnLpYT3uqtxJBDYdh6Jdjzt+Ce6DNspJsPQgpC7rbts/B8tEkq0LHoYuIF/O5Jh5rPg==} engines: {node: '>=18'} + '@tweenjs/tween.js@23.1.3': + resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -5798,6 +5863,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/bun@1.3.10': + resolution: {integrity: sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ==} + '@types/bun@1.3.9': resolution: {integrity: sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw==} @@ -5843,6 +5911,9 @@ packages: '@types/doctrine@0.0.9': resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} + '@types/draco3d@1.4.10': + resolution: {integrity: sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==} + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -5912,6 +5983,9 @@ packages: '@types/node@25.2.0': resolution: {integrity: sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==} + '@types/offscreencanvas@2019.7.3': + resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==} + '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -5929,6 +6003,11 @@ packages: peerDependencies: '@types/react': ^19.2.0 + '@types/react-reconciler@0.28.9': + resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} + peerDependencies: + '@types/react': '*' + '@types/react@19.2.2': resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} @@ -5944,12 +6023,18 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/stats.js@0.17.4': + resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} + '@types/tar-stream@3.1.4': resolution: {integrity: sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg==} '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + '@types/three@0.183.1': + resolution: {integrity: sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -5968,6 +6053,9 @@ packages: '@types/wait-on@5.3.4': resolution: {integrity: sha512-EBsPjFMrFlMbbUFf9D1Fp+PAB2TwmUn7a3YtHyD9RLuTIk1jDd8SxXVAoez2Ciy+8Jsceo2MYEYZzJ/DvorOKw==} + '@types/webxr@0.5.24': + resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} + '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -6218,6 +6306,14 @@ packages: cpu: [x64] os: [win32] + '@use-gesture/core@10.3.1': + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} + + '@use-gesture/react@10.3.1': + resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==} + peerDependencies: + react: '>= 16.8.0' + '@vercel/functions@3.3.3': resolution: {integrity: sha512-Gf+Nc/h7YjTpIhVk9UqGqKUcOIlnuSTqEKr7aApEeYjPUeqr/C4UddU6FyCyrFM0tnefKcOXVA6m7op+1JSZBg==} engines: {node: '>= 20'} @@ -6383,6 +6479,9 @@ packages: '@webassemblyjs/wast-printer@1.14.1': resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + '@webgpu/types@0.1.69': + resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==} + '@workflow/serde@4.1.0-beta.2': resolution: {integrity: sha512-8kkeoQKLDaKXefjV5dbhBj2aErfKp1Mc4pb6tj8144cF+Em5SPbyMbyLCHp+BVrFfFVCBluCtMx+jjvaFVZGww==} @@ -6719,6 +6818,9 @@ packages: resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} engines: {node: '>=12.0.0'} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} @@ -6815,6 +6917,9 @@ packages: builtin-status-codes@3.0.0: resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==} + bun-types@1.3.10: + resolution: {integrity: sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg==} + bun-types@1.3.9: resolution: {integrity: sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg==} @@ -6857,6 +6962,12 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} + camera-controls@3.1.2: + resolution: {integrity: sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==} + engines: {node: '>=22.0.0', npm: '>=10.5.1'} + peerDependencies: + three: '>=0.126.1' + caniuse-lite@1.0.30001760: resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} @@ -7155,6 +7266,11 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -7346,6 +7462,9 @@ packages: des.js@1.1.0: resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==} + detect-gpu@5.0.70: + resolution: {integrity: sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -7488,6 +7607,9 @@ packages: resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} + draco3d@1.5.7: + resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==} + drange@1.1.1: resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==} engines: {node: '>=4'} @@ -8004,6 +8126,9 @@ packages: fflate@0.4.8: resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + fflate@0.6.10: + resolution: {integrity: sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==} + fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -8251,6 +8376,9 @@ packages: globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + glsl-noise@0.0.0: + resolution: {integrity: sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==} + gonzales-pe@4.3.0: resolution: {integrity: sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==} engines: {node: '>=0.6.0'} @@ -8266,6 +8394,9 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + gsap@3.14.2: + resolution: {integrity: sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==} + gzip-size@6.0.0: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} engines: {node: '>=10'} @@ -8332,6 +8463,9 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hls.js@1.6.15: + resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==} + hmac-drbg@1.0.1: resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} @@ -8431,6 +8565,9 @@ packages: engines: {node: '>=16.x'} hasBin: true + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immer@10.1.3: resolution: {integrity: sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==} @@ -8581,6 +8718,9 @@ packages: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -8678,6 +8818,11 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} + its-fine@2.0.0: + resolution: {integrity: sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==} + peerDependencies: + react: ^19.0.0 + itty-time@1.0.6: resolution: {integrity: sha512-+P8IZaLLBtFv8hCkIjcymZOp4UJ+xW6bSlQsXGqrkmJh7vSiMFSlNne0mCYagEE0N7HDNR5jJBRxwN0oYv61Rw==} @@ -9120,6 +9265,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -9297,6 +9445,12 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + maath@0.10.8: + resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==} + peerDependencies: + '@types/three': '>=0.134.0' + three: '>=0.134.0' + madge@8.0.0: resolution: {integrity: sha512-9sSsi3TBPhmkTCIpVQF0SPiChj1L7Rq9kU2KDG1o6v2XH9cCw086MopjVCD+vuoL5v8S77DTbVopTO8OUiQpIw==} engines: {node: '>=18'} @@ -9430,6 +9584,14 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + meshline@3.3.1: + resolution: {integrity: sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==} + peerDependencies: + three: '>=0.137' + + meshoptimizer@1.0.1: + resolution: {integrity: sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==} + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -10187,6 +10349,9 @@ packages: resolution: {integrity: sha512-uNN+YUuOdbDSbDMGk/Wq57o2YBEH0Unu1kEq2PuYmqFmnu+oYsKyJBrb58VNwEuYsaXVJmk4FtbD+Tl8BT69+w==} engines: {node: '>=20'} + potpack@1.0.2: + resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} + preact-render-to-string@5.2.6: resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==} peerDependencies: @@ -10307,6 +10472,9 @@ packages: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} + promise-worker-transferable@1.0.4: + resolution: {integrity: sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -10459,6 +10627,15 @@ packages: react: '>= 16.13.1' react-dom: '>= 16.13.1' + react-use-measure@2.1.7: + resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==} + peerDependencies: + react: '>=16.13' + react-dom: '>=16.13' + peerDependenciesMeta: + react-dom: + optional: true + react@19.2.0: resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} engines: {node: '>=0.10.0'} @@ -10924,6 +11101,15 @@ packages: state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + stats-gl@2.4.2: + resolution: {integrity: sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==} + peerDependencies: + '@types/three': '*' + three: '*' + + stats.js@0.17.0: + resolution: {integrity: sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -11086,6 +11272,11 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + suspend-react@0.1.3: + resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==} + peerDependencies: + react: '>=17.0' + synckit@0.11.11: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -11144,6 +11335,19 @@ packages: third-party-capital@1.0.20: resolution: {integrity: sha512-oB7yIimd8SuGptespDAZnNkzIz+NWaJCu2RMsbs4Wmp9zSDUM8Nhi3s2OOcqYuv3mN4hitXc8DVx+LyUmbUDiA==} + three-mesh-bvh@0.8.3: + resolution: {integrity: sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==} + peerDependencies: + three: '>= 0.159.0' + + three-stdlib@2.36.1: + resolution: {integrity: sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==} + peerDependencies: + three: '>=0.128.0' + + three@0.183.2: + resolution: {integrity: sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==} + timers-browserify@2.0.12: resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==} engines: {node: '>=0.6.0'} @@ -11214,6 +11418,19 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + troika-three-text@0.52.4: + resolution: {integrity: sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==} + peerDependencies: + three: '>=0.125.0' + + troika-three-utils@0.52.4: + resolution: {integrity: sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==} + peerDependencies: + three: '>=0.125.0' + + troika-worker-utils@0.52.0: + resolution: {integrity: sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==} + trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} @@ -11312,6 +11529,9 @@ packages: tty-browserify@0.0.1: resolution: {integrity: sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==} + tunnel-rat@0.1.2: + resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==} + tw-animate-css@1.4.0: resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} @@ -11515,6 +11735,10 @@ packages: utila@0.4.0: resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} + utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + uuid@11.1.0: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true @@ -11696,6 +11920,12 @@ packages: web-vitals@4.2.4: resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + webgl-constants@1.1.1: + resolution: {integrity: sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==} + + webgl-sdf-generator@1.1.1: + resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -11961,6 +12191,39 @@ packages: zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + + zustand@5.0.11: + resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -13630,6 +13893,8 @@ snapshots: gonzales-pe: 4.3.0 node-source-walk: 7.0.1 + '@dimforge/rapier3d-compat@0.12.0': {} + '@discordjs/builders@1.13.1': dependencies: '@discordjs/formatters': 0.6.2 @@ -14525,6 +14790,8 @@ snapshots: '@types/react': 19.2.2 react: 19.2.3 + '@mediapipe/tasks-vision@0.10.17': {} + '@mistralai/mistralai@1.10.0': dependencies: zod: 3.25.76 @@ -14563,6 +14830,11 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + '@monogrid/gainmap-js@3.4.0(three@0.183.2)': + dependencies: + promise-worker-transferable: 1.0.4 + three: 0.183.2 + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.5.0 @@ -15605,6 +15877,59 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@react-three/drei@10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.2)(immer@10.1.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(three@0.183.2))(@types/react@19.2.2)(@types/three@0.183.1)(immer@10.1.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(three@0.183.2)': + dependencies: + '@babel/runtime': 7.28.4 + '@mediapipe/tasks-vision': 0.10.17 + '@monogrid/gainmap-js': 3.4.0(three@0.183.2) + '@react-three/fiber': 9.5.0(@types/react@19.2.2)(immer@10.1.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(three@0.183.2) + '@use-gesture/react': 10.3.1(react@19.2.0) + camera-controls: 3.1.2(three@0.183.2) + cross-env: 7.0.3 + detect-gpu: 5.0.70 + glsl-noise: 0.0.0 + hls.js: 1.6.15 + maath: 0.10.8(@types/three@0.183.1)(three@0.183.2) + meshline: 3.3.1(three@0.183.2) + react: 19.2.0 + stats-gl: 2.4.2(@types/three@0.183.1)(three@0.183.2) + stats.js: 0.17.0 + suspend-react: 0.1.3(react@19.2.0) + three: 0.183.2 + three-mesh-bvh: 0.8.3(three@0.183.2) + three-stdlib: 2.36.1(three@0.183.2) + troika-three-text: 0.52.4(three@0.183.2) + tunnel-rat: 0.1.2(@types/react@19.2.2)(immer@10.1.3)(react@19.2.0) + use-sync-external-store: 1.6.0(react@19.2.0) + utility-types: 3.11.0 + zustand: 5.0.11(@types/react@19.2.2)(immer@10.1.3)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)) + optionalDependencies: + react-dom: 19.2.0(react@19.2.0) + transitivePeerDependencies: + - '@types/react' + - '@types/three' + - immer + + '@react-three/fiber@9.5.0(@types/react@19.2.2)(immer@10.1.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(three@0.183.2)': + dependencies: + '@babel/runtime': 7.28.4 + '@types/webxr': 0.5.24 + base64-js: 1.5.1 + buffer: 6.0.3 + its-fine: 2.0.0(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-use-measure: 2.1.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + scheduler: 0.27.0 + suspend-react: 0.1.3(react@19.2.0) + three: 0.183.2 + use-sync-external-store: 1.6.0(react@19.2.0) + zustand: 5.0.11(@types/react@19.2.2)(immer@10.1.3)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)) + optionalDependencies: + react-dom: 19.2.0(react@19.2.0) + transitivePeerDependencies: + - '@types/react' + - immer + '@redis/bloom@1.2.0(@redis/client@1.6.1)': dependencies: '@redis/client': 1.6.1 @@ -17234,6 +17559,8 @@ snapshots: '@ts-graphviz/ast': 2.0.7 '@ts-graphviz/common': 2.1.5 + '@tweenjs/tween.js@23.1.3': {} + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -17260,6 +17587,10 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@types/bun@1.3.10': + dependencies: + bun-types: 1.3.10 + '@types/bun@1.3.9': dependencies: bun-types: 1.3.9 @@ -17304,6 +17635,8 @@ snapshots: '@types/doctrine@0.0.9': {} + '@types/draco3d@1.4.10': {} + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -17385,6 +17718,8 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/offscreencanvas@2019.7.3': {} + '@types/parse-json@4.0.2': {} '@types/pg-pool@2.0.6': @@ -17407,6 +17742,10 @@ snapshots: dependencies: '@types/react': 19.2.2 + '@types/react-reconciler@0.28.9(@types/react@19.2.2)': + dependencies: + '@types/react': 19.2.2 + '@types/react@19.2.2': dependencies: csstype: 3.1.3 @@ -17419,6 +17758,8 @@ snapshots: '@types/stack-utils@2.0.3': {} + '@types/stats.js@0.17.4': {} + '@types/tar-stream@3.1.4': dependencies: '@types/node': 22.19.1 @@ -17427,6 +17768,16 @@ snapshots: dependencies: '@types/node': 22.19.1 + '@types/three@0.183.1': + dependencies: + '@dimforge/rapier3d-compat': 0.12.0 + '@tweenjs/tween.js': 23.1.3 + '@types/stats.js': 0.17.4 + '@types/webxr': 0.5.24 + '@webgpu/types': 0.1.69 + fflate: 0.8.2 + meshoptimizer: 1.0.1 + '@types/trusted-types@2.0.7': optional: true @@ -17442,6 +17793,8 @@ snapshots: dependencies: '@types/node': 22.19.1 + '@types/webxr@0.5.24': {} + '@types/ws@8.18.1': dependencies: '@types/node': 22.19.1 @@ -17689,6 +18042,13 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@use-gesture/core@10.3.1': {} + + '@use-gesture/react@10.3.1(react@19.2.0)': + dependencies: + '@use-gesture/core': 10.3.1 + react: 19.2.0 + '@vercel/functions@3.3.3(@aws-sdk/credential-provider-web-identity@3.972.3)': dependencies: '@vercel/oidc': 3.0.5 @@ -17824,7 +18184,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.2.0)(@vitest/ui@3.2.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) '@vitest/utils@3.2.4': dependencies: @@ -17947,6 +18307,8 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 + '@webgpu/types@0.1.69': {} + '@workflow/serde@4.1.0-beta.2': {} '@workos-inc/node@8.2.0': @@ -18311,6 +18673,10 @@ snapshots: dependencies: open: 8.4.2 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + big.js@5.2.2: {} binary-extensions@2.3.0: {} @@ -18440,6 +18806,10 @@ snapshots: builtin-status-codes@3.0.0: {} + bun-types@1.3.10: + dependencies: + '@types/node': 22.19.1 + bun-types@1.3.9: dependencies: '@types/node': 22.19.1 @@ -18483,6 +18853,10 @@ snapshots: camelcase@6.3.0: {} + camera-controls@3.1.2(three@0.183.2): + dependencies: + three: 0.183.2 + caniuse-lite@1.0.30001760: {} case-sensitive-paths-webpack-plugin@2.4.0: {} @@ -18773,6 +19147,10 @@ snapshots: - supports-color - ts-node + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -18967,6 +19345,10 @@ snapshots: inherits: 2.0.4 minimalistic-assert: 1.0.1 + detect-gpu@5.0.70: + dependencies: + webgl-constants: 1.1.1 + detect-libc@2.1.2: {} detect-newline@3.1.0: {} @@ -19135,6 +19517,8 @@ snapshots: dotenv@17.2.3: {} + draco3d@1.5.7: {} + drange@1.1.1: {} drizzle-kit@0.31.9: @@ -19146,12 +19530,12 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.9)(pg@8.18.0): + drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260305.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.10)(pg@8.18.0): optionalDependencies: '@cloudflare/workers-types': 4.20260305.0 '@opentelemetry/api': 1.9.0 '@types/pg': 8.16.0 - bun-types: 1.3.9 + bun-types: 1.3.10 pg: 8.18.0 dset@3.1.4: {} @@ -19690,6 +20074,8 @@ snapshots: fflate@0.4.8: {} + fflate@0.6.10: {} + fflate@0.8.2: {} file-entry-cache@8.0.0: @@ -19950,6 +20336,8 @@ snapshots: globrex@0.1.2: {} + glsl-noise@0.0.0: {} + gonzales-pe@4.3.0: dependencies: minimist: 1.2.8 @@ -19960,6 +20348,8 @@ snapshots: graphemer@1.4.0: {} + gsap@3.14.2: {} + gzip-size@6.0.0: dependencies: duplexer: 0.1.2 @@ -20066,6 +20456,8 @@ snapshots: dependencies: hermes-estree: 0.25.1 + hls.js@1.6.15: {} + hmac-drbg@1.0.1: dependencies: hash.js: 1.1.7 @@ -20165,6 +20557,8 @@ snapshots: image-size@2.0.2: {} + immediate@3.0.6: {} + immer@10.1.3: {} import-fresh@3.3.1: @@ -20282,6 +20676,8 @@ snapshots: is-plain-object@5.0.0: {} + is-promise@2.2.2: {} + is-promise@4.0.0: {} is-reference@1.2.1: @@ -20396,6 +20792,13 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 + its-fine@2.0.0(@types/react@19.2.2)(react@19.2.0): + dependencies: + '@types/react-reconciler': 0.28.9(@types/react@19.2.2) + react: 19.2.0 + transitivePeerDependencies: + - '@types/react' + itty-time@1.0.6: {} javascript-stringify@2.1.0: {} @@ -21312,6 +21715,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lightningcss-android-arm64@1.30.2: optional: true @@ -21446,6 +21853,11 @@ snapshots: dependencies: react: 19.2.0 + maath@0.10.8(@types/three@0.183.1)(three@0.183.2): + dependencies: + '@types/three': 0.183.1 + three: 0.183.2 + madge@8.0.0(typescript@5.9.3): dependencies: chalk: 4.1.2 @@ -21694,6 +22106,12 @@ snapshots: merge2@1.4.1: {} + meshline@3.3.1(three@0.183.2): + dependencies: + three: 0.183.2 + + meshoptimizer@1.0.1: {} + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.2.0 @@ -22658,6 +23076,8 @@ snapshots: dependencies: '@posthog/core': 1.3.0 + potpack@1.0.2: {} + preact-render-to-string@5.2.6(preact@10.28.3): dependencies: preact: 10.28.3 @@ -22736,6 +23156,11 @@ snapshots: progress@2.0.3: {} + promise-worker-transferable@1.0.4: + dependencies: + is-promise: 2.2.2 + lie: 3.3.0 + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -22908,6 +23333,12 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + react-use-measure@2.1.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + react: 19.2.0 + optionalDependencies: + react-dom: 19.2.0(react@19.2.0) + react@19.2.0: {} react@19.2.3: {} @@ -23504,6 +23935,13 @@ snapshots: state-local@1.0.7: {} + stats-gl@2.4.2(@types/three@0.183.1)(three@0.183.2): + dependencies: + '@types/three': 0.183.1 + three: 0.183.2 + + stats.js@0.17.0: {} + statuses@2.0.2: {} std-env@3.10.0: {} @@ -23698,6 +24136,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + suspend-react@0.1.3(react@19.2.0): + dependencies: + react: 19.2.0 + synckit@0.11.11: dependencies: '@pkgr/core': 0.2.9 @@ -23761,6 +24203,22 @@ snapshots: third-party-capital@1.0.20: {} + three-mesh-bvh@0.8.3(three@0.183.2): + dependencies: + three: 0.183.2 + + three-stdlib@2.36.1(three@0.183.2): + dependencies: + '@types/draco3d': 1.4.10 + '@types/offscreencanvas': 2019.7.3 + '@types/webxr': 0.5.24 + draco3d: 1.5.7 + fflate: 0.6.10 + potpack: 1.0.2 + three: 0.183.2 + + three@0.183.2: {} + timers-browserify@2.0.12: dependencies: setimmediate: 1.0.5 @@ -23810,6 +24268,20 @@ snapshots: trim-lines@3.0.1: {} + troika-three-text@0.52.4(three@0.183.2): + dependencies: + bidi-js: 1.0.3 + three: 0.183.2 + troika-three-utils: 0.52.4(three@0.183.2) + troika-worker-utils: 0.52.0 + webgl-sdf-generator: 1.1.1 + + troika-three-utils@0.52.4(three@0.183.2): + dependencies: + three: 0.183.2 + + troika-worker-utils@0.52.0: {} + trough@2.2.0: {} ts-api-utils@2.1.0(typescript@5.9.3): @@ -23919,6 +24391,14 @@ snapshots: tty-browserify@0.0.1: {} + tunnel-rat@0.1.2(@types/react@19.2.2)(immer@10.1.3)(react@19.2.0): + dependencies: + zustand: 4.5.7(@types/react@19.2.2)(immer@10.1.3)(react@19.2.0) + transitivePeerDependencies: + - '@types/react' + - immer + - react + tw-animate-css@1.4.0: {} type-check@0.4.0: @@ -24129,6 +24609,8 @@ snapshots: utila@0.4.0: {} + utility-types@3.11.0: {} + uuid@11.1.0: {} uuid@8.3.2: {} @@ -24458,6 +24940,10 @@ snapshots: web-vitals@4.2.4: {} + webgl-constants@1.1.1: {} + + webgl-sdf-generator@1.1.1: {} + webidl-conversions@3.0.1: {} webpack-bundle-analyzer@4.10.1: @@ -24773,4 +25259,19 @@ snapshots: zod@4.3.6: {} + zustand@4.5.7(@types/react@19.2.2)(immer@10.1.3)(react@19.2.0): + dependencies: + use-sync-external-store: 1.6.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + immer: 10.1.3 + react: 19.2.0 + + zustand@5.0.11(@types/react@19.2.2)(immer@10.1.3)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)): + optionalDependencies: + '@types/react': 19.2.2 + immer: 10.1.3 + react: 19.2.0 + use-sync-external-store: 1.6.0(react@19.2.0) + zwitch@2.0.4: {} diff --git a/public/gastown-viz/hdr/venice_sunset_1k.hdr b/public/gastown-viz/hdr/venice_sunset_1k.hdr new file mode 100755 index 000000000..048bb13a8 Binary files /dev/null and b/public/gastown-viz/hdr/venice_sunset_1k.hdr differ diff --git a/public/gastown-viz/models/hex-terrain.glb b/public/gastown-viz/models/hex-terrain.glb new file mode 100644 index 000000000..4000958af Binary files /dev/null and b/public/gastown-viz/models/hex-terrain.glb differ diff --git a/public/gastown-viz/textures/default.png b/public/gastown-viz/textures/default.png new file mode 100644 index 000000000..14cdc2536 Binary files /dev/null and b/public/gastown-viz/textures/default.png differ diff --git a/public/gastown-viz/textures/moody.png b/public/gastown-viz/textures/moody.png new file mode 100644 index 000000000..869ed48c9 Binary files /dev/null and b/public/gastown-viz/textures/moody.png differ diff --git a/public/gastown-viz/textures/summer.png b/public/gastown-viz/textures/summer.png new file mode 100644 index 000000000..015e1758a Binary files /dev/null and b/public/gastown-viz/textures/summer.png differ diff --git a/src/app/(app)/gastown/[townId]/viz/HexVizPageClient.tsx b/src/app/(app)/gastown/[townId]/viz/HexVizPageClient.tsx new file mode 100644 index 000000000..b30a6b4f9 --- /dev/null +++ b/src/app/(app)/gastown/[townId]/viz/HexVizPageClient.tsx @@ -0,0 +1,155 @@ +'use client'; + +/** + * Client component for the 3D hex visualization page. + * + * Renders the GastownHexScene with live data from the Town DO, + * plus an overlay with connection status and a detail panel + * for selected structures. + */ + +import { useState, useCallback } from 'react'; +import dynamic from 'next/dynamic'; +import { useTownSnapshot } from '@/components/gastown/hex-viz/use-town-snapshot'; +import type { StructurePlacement, TownSnapshot } from '@/components/gastown/hex-viz/types'; +import { useDrawerStack } from '@/components/gastown/DrawerStack'; + +// Dynamic import of the 3D scene to avoid SSR issues with Three.js +const GastownHexScene = dynamic( + () => + import('@/components/gastown/hex-viz/GastownHexScene').then(mod => ({ + default: mod.GastownHexScene, + })), + { ssr: false } +); + +type HexVizPageClientProps = { + townId: string; +}; + +export function HexVizPageClient({ townId }: HexVizPageClientProps) { + const { snapshot, connected, loading } = useTownSnapshot(townId); + const [selectedStructure, setSelectedStructure] = useState(null); + const { push } = useDrawerStack(); + + const handleStructureSelect = useCallback( + (structure: StructurePlacement) => { + setSelectedStructure(structure); + + // Open the appropriate drawer based on the linked object type. + // Both agent and bead panels require a real rigId for their + // rig-scoped tRPC queries. + if (structure.linkedObjectId && structure.linkedObjectType && structure.linkedRigId) { + switch (structure.linkedObjectType) { + case 'agent': + push({ + type: 'agent', + agentId: structure.linkedObjectId, + rigId: structure.linkedRigId, + }); + break; + case 'bead': + push({ type: 'bead', beadId: structure.linkedObjectId, rigId: structure.linkedRigId }); + break; + } + } + }, + [push] + ); + + if (loading) { + return ( +
+
+
+
Loading town data...
+
+
+ ); + } + + // Empty state: show the scene with a minimal snapshot + const displaySnapshot: TownSnapshot = snapshot ?? { + townId, + rigs: [], + agents: [], + beads: [], + convoys: [], + recentEvents: [], + }; + + return ( +
+ {/* 3D Hex Scene */} + + + {/* Overlay: Connection status */} +
+
+ {connected ? 'Live' : 'Reconnecting...'} +
+ + {/* Overlay: Town info */} +
+
Town
+
{townId.slice(0, 8)}...
+
+ {displaySnapshot.rigs.length} rigs + {displaySnapshot.agents.length} agents + {displaySnapshot.beads.filter(b => b.status !== 'closed').length} open beads +
+
+ + {/* Overlay: Selected structure info */} + {selectedStructure && ( +
+
+
+ {selectedStructure.label ?? selectedStructure.kind} +
+ +
+ {selectedStructure.linkedObjectType && ( +
+ {selectedStructure.linkedObjectType}: {selectedStructure.linkedObjectId?.slice(0, 8)} +
+ )} +
+ Hex ({selectedStructure.col}, {selectedStructure.row}) +
+
+ )} + + {/* Legend */} +
+
+
+
+ Working +
+
+
+ Stalled +
+
+
+ Idle +
+
+
+ Dead +
+
+
+
+ ); +} diff --git a/src/app/(app)/gastown/[townId]/viz/page.tsx b/src/app/(app)/gastown/[townId]/viz/page.tsx new file mode 100644 index 000000000..227341aee --- /dev/null +++ b/src/app/(app)/gastown/[townId]/viz/page.tsx @@ -0,0 +1,17 @@ +import { getUserFromAuthOrRedirect } from '@/lib/user.server'; +import { notFound } from 'next/navigation'; +import { isGastownEnabled } from '@/lib/gastown/feature-flags'; +import { HexVizPageClient } from './HexVizPageClient'; + +export default async function HexVizPage({ params }: { params: Promise<{ townId: string }> }) { + const { townId } = await params; + const user = await getUserFromAuthOrRedirect( + `/users/sign_in?callbackPath=/gastown/${townId}/viz` + ); + + if (!(await isGastownEnabled(user.id))) { + return notFound(); + } + + return ; +} diff --git a/src/components/gastown/GastownTownSidebar.tsx b/src/components/gastown/GastownTownSidebar.tsx index 494f22c8c..ae638e9d9 100644 --- a/src/components/gastown/GastownTownSidebar.tsx +++ b/src/components/gastown/GastownTownSidebar.tsx @@ -26,6 +26,7 @@ import { Activity, Settings, Crown, + Globe, } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; @@ -52,6 +53,7 @@ export function GastownTownSidebar({ townId, ...sidebarProps }: GastownTownSideb const navItems = [ { title: 'Overview', icon: LayoutDashboard, url: basePath }, + { title: '3D Town', icon: Globe, url: `${basePath}/viz` }, { title: 'Beads', icon: Hexagon, url: `${basePath}/beads` }, { title: 'Agents', icon: Bot, url: `${basePath}/agents` }, { title: 'Merge Queue', icon: GitMerge, url: `${basePath}/merges` }, diff --git a/src/components/gastown/hex-viz/GastownHexScene.tsx b/src/components/gastown/hex-viz/GastownHexScene.tsx new file mode 100644 index 000000000..2f54191bd --- /dev/null +++ b/src/components/gastown/hex-viz/GastownHexScene.tsx @@ -0,0 +1,219 @@ +'use client'; + +/** + * The main 3D scene for the Gastown hex visualization. + * + * Uses @react-three/fiber (R3F) to render a hex-tiled island representing + * a Gastown town. Loads KayKit medieval hex tile models from a GLB file, + * lays out terrain and structures based on town state data, and supports + * real-time updates via WebSocket. + */ + +import { Suspense, useMemo, useCallback, useState, useEffect, useRef } from 'react'; +import { Canvas } from '@react-three/fiber'; +import { + OrbitControls, + Environment, + useGLTF, + Html, + PerspectiveCamera, + ContactShadows, +} from '@react-three/drei'; +import * as THREE from 'three'; +import type { TownSnapshot, StructurePlacement, HexWorldLayout } from './types'; +import { generateLayout } from './layout-generator'; +import { InstancedHexTiles, WaterPlane } from './HexTile3D'; +import { Structure3D } from './Structure3D'; + +// ── GLB model path ───────────────────────────────────────────────────── + +const GLB_PATH = '/gastown-viz/models/hex-terrain.glb'; + +// ── Extract geometries from GLB ──────────────────────────────────────── + +function useHexGeometries(): Map { + const gltf = useGLTF(GLB_PATH); + + return useMemo(() => { + const geometries = new Map(); + + gltf.scene.traverse((child: THREE.Object3D) => { + if (child instanceof THREE.Mesh && child.geometry) { + // The GLB mesh names are the keys we use in tile/structure definitions. + // Translate Y by +1 to match the hex-map-wfc convention (surface at y=1). + const geom = child.geometry.clone(); + geom.translate(0, 1, 0); + geometries.set(child.name, geom); + } + }); + + return geometries; + }, [gltf]); +} + +// ── Shared tile material ─────────────────────────────────────────────── + +function useTileMaterial(): THREE.Material { + return useMemo(() => { + return new THREE.MeshStandardMaterial({ + vertexColors: false, + roughness: 0.8, + metalness: 0.0, + color: new THREE.Color(0x7cad5c), // Green grass tone + flatShading: true, + }); + }, []); +} + +function useStructureMaterial(): THREE.Material { + return useMemo(() => { + return new THREE.MeshStandardMaterial({ + vertexColors: false, + roughness: 0.6, + metalness: 0.1, + color: new THREE.Color(0xd4a960), // Warm building tone + flatShading: true, + }); + }, []); +} + +// ── The 3D world content (rendered inside Canvas) ────────────────────── + +type HexWorldProps = { + layout: HexWorldLayout; + onStructureSelect?: (structure: StructurePlacement) => void; +}; + +function HexWorld({ layout, onStructureSelect }: HexWorldProps) { + const geometries = useHexGeometries(); + const tileMaterial = useTileMaterial(); + const structureMaterial = useStructureMaterial(); + + // Separate land tiles from water tiles for different materials + const landTiles = useMemo( + () => layout.tiles.filter(t => t.meshName !== 'hex_water'), + [layout.tiles] + ); + + const waterTiles = useMemo( + () => layout.tiles.filter(t => t.meshName === 'hex_water'), + [layout.tiles] + ); + + // Water tile material (blue tone) + const waterTileMaterial = useMemo( + () => + new THREE.MeshStandardMaterial({ + color: new THREE.Color(0x3b7dd8), + roughness: 0.3, + metalness: 0.1, + flatShading: true, + transparent: true, + opacity: 0.85, + }), + [] + ); + + return ( + <> + {/* Environment lighting */} + + + + + {/* Camera */} + + + + {/* Water plane (below hex tiles) */} + + + {/* Land hex tiles */} + + + {/* Water hex tiles */} + + + {/* Structures (buildings, trees, crates, etc.) */} + {layout.structures.map((structure, i) => ( + + ))} + + {/* Ground contact shadows for depth */} + + + {/* Fog for atmosphere */} + + + ); +} + +// ── Loading fallback ─────────────────────────────────────────────────── + +function LoadingIndicator() { + return ( + +
+
+
Loading hex world...
+
+ + ); +} + +// ── Public component ─────────────────────────────────────────────────── + +type GastownHexSceneProps = { + snapshot: TownSnapshot; + onStructureSelect?: (structure: StructurePlacement) => void; + className?: string; +}; + +export function GastownHexScene({ snapshot, onStructureSelect, className }: GastownHexSceneProps) { + const layout = useMemo(() => generateLayout(snapshot), [snapshot]); + + return ( +
+ + }> + + + +
+ ); +} + +// Preload the GLB so it's cached when the component mounts +useGLTF.preload(GLB_PATH); diff --git a/src/components/gastown/hex-viz/HexTile3D.tsx b/src/components/gastown/hex-viz/HexTile3D.tsx new file mode 100644 index 000000000..55a815420 --- /dev/null +++ b/src/components/gastown/hex-viz/HexTile3D.tsx @@ -0,0 +1,163 @@ +'use client'; + +/** + * A single hex tile rendered in 3D using a mesh from the loaded GLB. + */ + +import { useRef, useMemo, useEffect } from 'react'; +import { useFrame } from '@react-three/fiber'; +import * as THREE from 'three'; +import type { HexTilePlacement } from './types'; +import { hexToWorld, rotationToRadians, LEVEL_HEIGHT, TILE_SURFACE_Y } from './hex-math'; + +type HexTile3DProps = { + placement: HexTilePlacement; + geometries: Map; + material: THREE.Material; +}; + +export function HexTile3D({ placement, geometries, material }: HexTile3DProps) { + const geometry = geometries.get(placement.meshName); + if (!geometry) return null; + + const [x, , z] = hexToWorld(placement.col, placement.row); + const y = placement.elevation * LEVEL_HEIGHT; + const rotation = rotationToRadians(placement.rotation); + + return ( + + ); +} + +// ── Instanced hex tiles (for performance with many tiles) ────────────── + +type InstancedHexTilesProps = { + placements: HexTilePlacement[]; + geometries: Map; + material: THREE.Material; +}; + +/** + * Groups tiles by mesh name and renders each group as an InstancedMesh. + * Much faster than individual meshes when tile count is high. + */ +export function InstancedHexTiles({ placements, geometries, material }: InstancedHexTilesProps) { + // Group placements by mesh name + const groups = useMemo(() => { + const map = new Map(); + for (const p of placements) { + const existing = map.get(p.meshName); + if (existing) { + existing.push(p); + } else { + map.set(p.meshName, [p]); + } + } + return map; + }, [placements]); + + return ( + <> + {[...groups.entries()].map(([meshName, group]) => { + const geometry = geometries.get(meshName); + if (!geometry) return null; + return ( + + ); + })} + + ); +} + +type InstancedTileGroupProps = { + geometry: THREE.BufferGeometry; + material: THREE.Material; + placements: HexTilePlacement[]; +}; + +function InstancedTileGroup({ geometry, material, placements }: InstancedTileGroupProps) { + const meshRef = useRef(null); + + // useEffect (not useMemo) so the ref is attached before we write matrices. + // useMemo runs during render before the DOM/Three.js objects exist. + useEffect(() => { + if (!meshRef.current) return; + const mesh = meshRef.current; + const matrix = new THREE.Matrix4(); + const euler = new THREE.Euler(); + const position = new THREE.Vector3(); + const quaternion = new THREE.Quaternion(); + const scale = new THREE.Vector3(1, 1, 1); + + for (let i = 0; i < placements.length; i++) { + const p = placements[i]; + const [x, , z] = hexToWorld(p.col, p.row); + const y = p.elevation * LEVEL_HEIGHT; + const rot = rotationToRadians(p.rotation); + + position.set(x, y, z); + euler.set(0, rot, 0); + quaternion.setFromEuler(euler); + matrix.compose(position, quaternion, scale); + mesh.setMatrixAt(i, matrix); + + if (p.color) { + mesh.setColorAt(i, new THREE.Color(p.color[0], p.color[1], p.color[2])); + } + } + mesh.instanceMatrix.needsUpdate = true; + if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true; + }, [placements]); + + return ( + + ); +} + +// ── Water plane ──────────────────────────────────────────────────────── + +type WaterPlaneProps = { + size: number; +}; + +export function WaterPlane({ size }: WaterPlaneProps) { + const meshRef = useRef(null); + + useFrame(({ clock }) => { + if (meshRef.current) { + const material = meshRef.current.material as THREE.MeshStandardMaterial; + // Subtle wave-like animation via opacity oscillation + material.opacity = 0.7 + Math.sin(clock.elapsedTime * 0.5) * 0.05; + } + }); + + return ( + + + + + ); +} diff --git a/src/components/gastown/hex-viz/Structure3D.tsx b/src/components/gastown/hex-viz/Structure3D.tsx new file mode 100644 index 000000000..4e6d297f1 --- /dev/null +++ b/src/components/gastown/hex-viz/Structure3D.tsx @@ -0,0 +1,219 @@ +'use client'; + +/** + * A structure (building, tree, crate, etc.) placed on a hex tile. + * Supports glow effects, labels, and click interaction. + */ + +import { useRef, useState, useCallback } from 'react'; +import { useFrame } from '@react-three/fiber'; +import { Html } from '@react-three/drei'; +import * as THREE from 'three'; +import type { ThreeEvent } from '@react-three/fiber'; +import type { StructurePlacement } from './types'; +import { hexToWorld, rotationToRadians, LEVEL_HEIGHT, TILE_SURFACE_Y } from './hex-math'; + +type Structure3DProps = { + placement: StructurePlacement; + geometries: Map; + material: THREE.Material; + onSelect?: (placement: StructurePlacement) => void; +}; + +export function Structure3D({ placement, geometries, material, onSelect }: Structure3DProps) { + const meshRef = useRef(null); + const [hovered, setHovered] = useState(false); + const geometry = geometries.get(placement.meshName); + + const handleClick = useCallback( + (e: ThreeEvent) => { + e.stopPropagation(); + if (onSelect && placement.linkedObjectId) { + onSelect(placement); + } + }, + [onSelect, placement] + ); + + // Animated structures (working agents, fires) + useFrame(({ clock }) => { + if (!meshRef.current) return; + if (placement.animate) { + if (placement.kind === 'windmill') { + // Spin Y rotation + meshRef.current.rotation.y += 0.01; + } else if (placement.kind === 'fire') { + // Bob up and down + const baseY = placement.elevation * LEVEL_HEIGHT + TILE_SURFACE_Y; + meshRef.current.position.y = baseY + Math.sin(clock.elapsedTime * 3) * 0.05; + } else if (placement.kind === 'cottage' || placement.kind === 'town-hall') { + // Gentle glow pulse for working agents + const scale = 1.0 + Math.sin(clock.elapsedTime * 2) * 0.02; + meshRef.current.scale.set(scale, scale, scale); + } + } + }); + + if (!geometry) { + // Fallback: render a simple colored box for structures without GLB meshes + return ; + } + + const [x, , z] = hexToWorld(placement.col, placement.row); + const y = placement.elevation * LEVEL_HEIGHT + TILE_SURFACE_Y; + const rotation = rotationToRadians(placement.rotation); + + return ( + + setHovered(true)} + onPointerOut={() => setHovered(false)} + /> + {/* Glow indicator */} + {placement.glow && ( + + )} + {/* Label on hover */} + {hovered && placement.label && ( + +
+
{placement.label}
+ {placement.linkedObjectType && ( +
{placement.linkedObjectType}
+ )} +
+ + )} +
+ ); +} + +// ── Fallback for missing meshes ──────────────────────────────────────── + +function FallbackStructure({ + placement, + onSelect, +}: { + placement: StructurePlacement; + onSelect?: (placement: StructurePlacement) => void; +}) { + const [hovered, setHovered] = useState(false); + const [x, , z] = hexToWorld(placement.col, placement.row); + const y = placement.elevation * LEVEL_HEIGHT + TILE_SURFACE_Y; + + const color = placement.glow + ? new THREE.Color(placement.glow[0], placement.glow[1], placement.glow[2]) + : kindColor(placement.kind); + + const size = kindSize(placement.kind); + + const handleClick = useCallback( + (e: ThreeEvent) => { + e.stopPropagation(); + if (onSelect && placement.linkedObjectId) { + onSelect(placement); + } + }, + [onSelect, placement] + ); + + return ( + + setHovered(true)} + onPointerOut={() => setHovered(false)} + > + + + + {placement.glow && ( + + )} + {hovered && placement.label && ( + +
+
{placement.label}
+ {placement.linkedObjectType && ( +
{placement.linkedObjectType}
+ )} +
+ + )} +
+ ); +} + +function kindColor(kind: StructurePlacement['kind']): THREE.Color { + switch (kind) { + case 'town-hall': + return new THREE.Color(0xdaa520); + case 'cottage': + return new THREE.Color(0x8b4513); + case 'windmill': + return new THREE.Color(0x696969); + case 'market': + return new THREE.Color(0x228b22); + case 'crate': + return new THREE.Color(0xd2691e); + case 'bridge': + return new THREE.Color(0x808080); + case 'fire': + return new THREE.Color(0xff4500); + case 'tree': + return new THREE.Color(0x2e8b57); + case 'flag': + return new THREE.Color(0x4169e1); + } +} + +function kindSize(kind: StructurePlacement['kind']): [number, number, number] { + switch (kind) { + case 'town-hall': + return [0.8, 1.2, 0.8]; + case 'cottage': + return [0.4, 0.5, 0.4]; + case 'windmill': + return [0.5, 1.0, 0.5]; + case 'market': + return [0.6, 0.4, 0.6]; + case 'crate': + return [0.15, 0.15, 0.15]; + case 'bridge': + return [0.8, 0.2, 0.3]; + case 'fire': + return [0.2, 0.4, 0.2]; + case 'tree': + return [0.3, 0.6, 0.3]; + case 'flag': + return [0.2, 1.0, 0.2]; + } +} diff --git a/src/components/gastown/hex-viz/hex-math.ts b/src/components/gastown/hex-viz/hex-math.ts new file mode 100644 index 000000000..a47358a73 --- /dev/null +++ b/src/components/gastown/hex-viz/hex-math.ts @@ -0,0 +1,114 @@ +/** + * Hex grid math utilities. + * + * Uses pointy-top hexagons with odd-r offset coordinates. + * World-space units match the KayKit hex tile dimensions: + * HEX_WIDTH = 2 (flat-to-flat) + * HEX_HEIGHT ≈ 2.31 (point-to-point) + */ + +import type { HexCoord, CubeCoord, HexDirection } from './types'; + +export const HEX_WIDTH = 2; +export const HEX_HEIGHT = 2.31; +export const LEVEL_HEIGHT = 0.5; +export const TILE_SURFACE_Y = 1.0; + +// ── Coordinate conversions ───────────────────────────────────────────── + +export function offsetToCube(col: number, row: number): CubeCoord { + const q = col - (row - (row & 1)) / 2; + const r = row; + const s = -q - r; + return { q, r, s }; +} + +export function cubeToOffset(q: number, r: number): HexCoord { + const col = q + (r - (r & 1)) / 2; + const row = r; + return { col, row }; +} + +export function cubeDistance(a: CubeCoord, b: CubeCoord): number { + return Math.max(Math.abs(a.q - b.q), Math.abs(a.r - b.r), Math.abs(a.s - b.s)); +} + +// ── World position (for Three.js scene placement) ────────────────────── + +export function hexToWorld(col: number, row: number): [number, number, number] { + const x = col * HEX_WIDTH + (Math.abs(row) % 2) * HEX_WIDTH * 0.5; + const z = row * HEX_HEIGHT * 0.75; + return [x, 0, z]; +} + +export function hexToWorldXZ(col: number, row: number): { x: number; z: number } { + const x = col * HEX_WIDTH + (Math.abs(row) % 2) * HEX_WIDTH * 0.5; + const z = row * HEX_HEIGHT * 0.75; + return { x, z }; +} + +// ── Neighbor offsets for odd-r pointy-top ────────────────────────────── + +const NEIGHBOR_OFFSETS_EVEN: Record = { + NE: { dx: 0, dz: -1 }, + E: { dx: 1, dz: 0 }, + SE: { dx: 0, dz: 1 }, + SW: { dx: -1, dz: 1 }, + W: { dx: -1, dz: 0 }, + NW: { dx: -1, dz: -1 }, +}; + +const NEIGHBOR_OFFSETS_ODD: Record = { + NE: { dx: 1, dz: -1 }, + E: { dx: 1, dz: 0 }, + SE: { dx: 1, dz: 1 }, + SW: { dx: 0, dz: 1 }, + W: { dx: -1, dz: 0 }, + NW: { dx: 0, dz: -1 }, +}; + +export function getNeighbor(col: number, row: number, dir: HexDirection): HexCoord { + const offsets = row % 2 === 0 ? NEIGHBOR_OFFSETS_EVEN : NEIGHBOR_OFFSETS_ODD; + const { dx, dz } = offsets[dir]; + return { col: col + dx, row: row + dz }; +} + +// ── Hex ring / area enumeration ──────────────────────────────────────── + +/** All hex coords within `radius` of (centerCol, centerRow), inclusive. */ +export function hexesInRadius(centerCol: number, centerRow: number, radius: number): HexCoord[] { + const center = offsetToCube(centerCol, centerRow); + const result: HexCoord[] = []; + for (let q = center.q - radius; q <= center.q + radius; q++) { + for (let r = center.r - radius; r <= center.r + radius; r++) { + const s = -q - r; + if (cubeDistance(center, { q, r, s }) <= radius) { + result.push(cubeToOffset(q, r)); + } + } + } + return result; +} + +/** Hex coords forming a ring at exactly `radius` distance from center. */ +export function hexRing(centerCol: number, centerRow: number, radius: number): HexCoord[] { + if (radius === 0) return [{ col: centerCol, row: centerRow }]; + const center = offsetToCube(centerCol, centerRow); + const result: HexCoord[] = []; + for (let q = center.q - radius; q <= center.q + radius; q++) { + for (let r = center.r - radius; r <= center.r + radius; r++) { + const s = -q - r; + if (cubeDistance(center, { q, r, s }) === radius) { + result.push(cubeToOffset(q, r)); + } + } + } + return result; +} + +// ── Rotation ─────────────────────────────────────────────────────────── + +/** Convert a rotation index (0-5) to radians for Y-axis rotation. */ +export function rotationToRadians(rotation: number): number { + return (rotation * Math.PI) / 3; +} diff --git a/src/components/gastown/hex-viz/index.ts b/src/components/gastown/hex-viz/index.ts new file mode 100644 index 000000000..efdbc1de9 --- /dev/null +++ b/src/components/gastown/hex-viz/index.ts @@ -0,0 +1,13 @@ +export { GastownHexScene } from './GastownHexScene'; +export { useTownSnapshot } from './use-town-snapshot'; +export { generateLayout } from './layout-generator'; +export type { + TownSnapshot, + AgentSnapshot, + BeadSnapshot, + RigSnapshot, + ConvoySnapshot, + HexWorldLayout, + StructurePlacement, + District, +} from './types'; diff --git a/src/components/gastown/hex-viz/layout-generator.ts b/src/components/gastown/hex-viz/layout-generator.ts new file mode 100644 index 000000000..7d741c301 --- /dev/null +++ b/src/components/gastown/hex-viz/layout-generator.ts @@ -0,0 +1,463 @@ +/** + * Transforms a TownSnapshot into a HexWorldLayout. + * + * The layout is deterministic given the same snapshot: + * - Town center is always the Mayor (town hall) + * - Each rig gets a district placed in a ring around center + * - Agents become structures (cottages/windmills) within their rig's district + * - Beads become crates near their assigned agent + * - Water surrounds the island + */ + +import type { + TownSnapshot, + RigSnapshot, + AgentSnapshot, + BeadSnapshot, + HexWorldLayout, + HexTilePlacement, + StructurePlacement, + District, +} from './types'; +import { hexesInRadius, hexRing, hexToWorldXZ, offsetToCube, cubeDistance } from './hex-math'; + +// ── District placement ───────────────────────────────────────────────── + +/** Positions for rig districts around the center (ring of radius 5 in offset coords). */ +const DISTRICT_POSITIONS: Array<{ col: number; row: number }> = [ + { col: 5, row: 0 }, + { col: 3, row: 4 }, + { col: -2, row: 4 }, + { col: -5, row: 0 }, + { col: -2, row: -4 }, + { col: 3, row: -4 }, + // Second ring for towns with 7+ rigs + { col: 8, row: -3 }, + { col: 8, row: 3 }, + { col: 0, row: 7 }, + { col: -7, row: 4 }, + { col: -7, row: -3 }, + { col: 0, row: -7 }, +]; + +const DISTRICT_RADIUS = 2; +const TOWN_CENTER = { col: 0, row: 0 }; + +// ── Mesh names from the KayKit hex-terrain.glb ───────────────────────── + +const GRASS_MESH = 'hex_grass'; +const WATER_MESH = 'hex_water'; +const ROAD_STRAIGHT = 'hex_road_A'; +const ROAD_CURVE = 'hex_road_B'; +const ROAD_END = 'hex_road_M'; + +// ── Structure mesh names ─────────────────────────────────────────────── + +const TOWN_HALL_MESH = 'building_townhall_yellow'; +const COTTAGE_MESHES = ['building_home_A_yellow', 'building_home_B_yellow']; +const WINDMILL_MESH = 'building_windmill_yellow'; +const MARKET_MESH = 'building_market_yellow'; +const BLACKSMITH_MESH = 'building_blacksmith_yellow'; +const CHURCH_MESH = 'building_church_yellow'; +const TOWER_MESH = 'building_tower_A_yellow'; +const WELL_MESH = 'building_well_yellow'; +const TREE_MESHES = ['tree_single_A', 'tree_single_B', 'tree_C', 'tree_D', 'tree_E']; +const FLAG_MESH = 'building_tower_A_yellow'; // Reuse tower as flag/banner +const HENGE_MESH = 'henge'; + +// ── Agent status → glow color ────────────────────────────────────────── + +function agentStatusGlow(status: AgentSnapshot['status']): [number, number, number] { + switch (status) { + case 'working': + return [0.2, 1.0, 0.3]; // green + case 'stalled': + return [1.0, 0.3, 0.1]; // red + case 'idle': + return [0.5, 0.5, 0.5]; // gray + case 'dead': + return [0.2, 0.2, 0.2]; // dark gray + } +} + +// ── Priority → color tint ────────────────────────────────────────────── + +function priorityColor(priority: BeadSnapshot['priority']): [number, number, number] { + switch (priority) { + case 'critical': + return [1.0, 0.84, 0.0]; // gold + case 'high': + return [1.0, 0.2, 0.2]; // red + case 'medium': + return [0.3, 0.5, 1.0]; // blue + case 'low': + return [0.6, 0.6, 0.6]; // gray + } +} + +// ── Seeded pseudo-random (deterministic from ID) ─────────────────────── + +function hashCode(str: string): number { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const ch = str.charCodeAt(i); + hash = ((hash << 5) - hash + ch) | 0; + } + return Math.abs(hash); +} + +function seededChoice(items: T[], seed: string): T { + return items[hashCode(seed) % items.length]; +} + +function seededRotation(seed: string): number { + return hashCode(seed) % 6; +} + +// ── Main layout generator ────────────────────────────────────────────── + +export function generateLayout(snapshot: TownSnapshot): HexWorldLayout { + const allTiles: HexTilePlacement[] = []; + const allStructures: StructurePlacement[] = []; + const allDistricts: District[] = []; + const waterTiles: HexTilePlacement[] = []; + const occupiedHexes = new Set(); + + const hexKey = (col: number, row: number) => `${col},${row}`; + const markOccupied = (col: number, row: number) => occupiedHexes.add(hexKey(col, row)); + const isOccupied = (col: number, row: number) => occupiedHexes.has(hexKey(col, row)); + + // ── 1. Place the Town Center (Mayor's district) ──────────────────── + + const mayorAgent = snapshot.agents.find(a => a.role === 'mayor'); + const centerHexes = hexesInRadius(TOWN_CENTER.col, TOWN_CENTER.row, 2); + + for (const hex of centerHexes) { + allTiles.push({ + col: hex.col, + row: hex.row, + meshName: GRASS_MESH, + rotation: 0, + elevation: 0, + }); + markOccupied(hex.col, hex.row); + } + + // Town Hall at center + allStructures.push({ + col: TOWN_CENTER.col, + row: TOWN_CENTER.row, + kind: 'town-hall', + meshName: TOWN_HALL_MESH, + rotation: 0, + elevation: 0, + linkedObjectId: mayorAgent?.id, + linkedObjectType: 'agent', + glow: mayorAgent ? agentStatusGlow(mayorAgent.status) : [0.5, 0.5, 0.5], + label: 'Town Hall', + animate: mayorAgent?.status === 'working', + }); + + // Decorative structures around the town hall + const centerRing1 = hexRing(TOWN_CENTER.col, TOWN_CENTER.row, 1); + if (centerRing1.length > 0) { + allStructures.push({ + col: centerRing1[0].col, + row: centerRing1[0].row, + kind: 'tree', + meshName: WELL_MESH, + rotation: 0, + elevation: 0, + label: 'Well', + }); + } + // Add trees on remaining center ring hexes + for (let i = 1; i < centerRing1.length; i++) { + const hex = centerRing1[i]; + allStructures.push({ + col: hex.col, + row: hex.row, + kind: 'tree', + meshName: seededChoice(TREE_MESHES, `center-tree-${i}`), + rotation: seededRotation(`center-tree-rot-${i}`), + elevation: 0, + }); + } + + // ── 2. Place Rig Districts ───────────────────────────────────────── + + const rigsByIndex = [...snapshot.rigs]; + + for ( + let rigIdx = 0; + rigIdx < rigsByIndex.length && rigIdx < DISTRICT_POSITIONS.length; + rigIdx++ + ) { + const rig = rigsByIndex[rigIdx]; + const pos = DISTRICT_POSITIONS[rigIdx]; + const district = buildRigDistrict(rig, pos, rigIdx, snapshot, markOccupied); + allDistricts.push(district); + allTiles.push(...district.tiles); + allStructures.push(...district.structures); + } + + // ── 3. Place roads connecting districts to center ────────────────── + + for (const district of allDistricts) { + const roadTiles = buildRoad( + TOWN_CENTER.col, + TOWN_CENTER.row, + district.centerCol, + district.centerRow, + occupiedHexes, + markOccupied + ); + allTiles.push(...roadTiles); + } + + // ── 4. Fill remaining area with water ────────────────────────────── + + const mapRadius = snapshot.rigs.length > 0 ? 10 : 5; + const allHexes = hexesInRadius(0, 0, mapRadius); + for (const hex of allHexes) { + if (!isOccupied(hex.col, hex.row)) { + const waterTile: HexTilePlacement = { + col: hex.col, + row: hex.row, + meshName: WATER_MESH, + rotation: 0, + elevation: 0, + }; + waterTiles.push(waterTile); + allTiles.push(waterTile); + markOccupied(hex.col, hex.row); + } + } + + return { + tiles: allTiles, + structures: allStructures, + districts: allDistricts, + waterTiles, + }; +} + +// ── Build a rig district ─────────────────────────────────────────────── + +function buildRigDistrict( + rig: RigSnapshot, + center: { col: number; row: number }, + rigIdx: number, + snapshot: TownSnapshot, + markOccupied: (col: number, row: number) => void +): District { + const tiles: HexTilePlacement[] = []; + const structures: StructurePlacement[] = []; + + const hexes = hexesInRadius(center.col, center.row, DISTRICT_RADIUS); + for (const hex of hexes) { + tiles.push({ + col: hex.col, + row: hex.row, + meshName: GRASS_MESH, + rotation: 0, + elevation: 0, + }); + markOccupied(hex.col, hex.row); + } + + // Flag/banner at district center with rig name + structures.push({ + col: center.col, + row: center.row, + kind: 'flag', + meshName: FLAG_MESH, + rotation: seededRotation(rig.id), + elevation: 0, + linkedObjectId: rig.id, + linkedObjectType: 'rig', + linkedRigId: rig.id, + label: rig.name, + }); + + // Place agents as structures in the district + const rigAgents = snapshot.agents.filter(a => a.rigId === rig.id); + const ring1 = hexRing(center.col, center.row, 1); + const ring2 = hexRing(center.col, center.row, 2); + const placementSlots = [...ring1, ...ring2]; + + let slotIdx = 0; + for (const agent of rigAgents) { + if (slotIdx >= placementSlots.length) break; + const slot = placementSlots[slotIdx++]; + + const isRefinery = agent.role === 'refinery'; + structures.push({ + col: slot.col, + row: slot.row, + kind: isRefinery ? 'windmill' : 'cottage', + meshName: isRefinery ? WINDMILL_MESH : seededChoice(COTTAGE_MESHES, agent.id), + rotation: seededRotation(agent.id), + elevation: 0, + linkedObjectId: agent.id, + linkedObjectType: 'agent', + linkedRigId: rig.id, + glow: agentStatusGlow(agent.status), + label: agent.name, + animate: agent.status === 'working', + }); + + // Place crates for beads assigned to this agent + const agentBeads = snapshot.beads.filter( + b => b.assigneeAgentId === agent.id && b.status !== 'closed' && b.type === 'issue' + ); + // Crates go on the same hex as the agent (stacked visually) + for (const bead of agentBeads.slice(0, 3)) { + structures.push({ + col: slot.col, + row: slot.row, + kind: 'crate', + meshName: 'rock_single_A', // Using rock as crate placeholder + rotation: seededRotation(bead.id), + elevation: 0, + linkedObjectId: bead.id, + linkedObjectType: 'bead', + linkedRigId: rig.id, + glow: priorityColor(bead.priority), + label: bead.title.slice(0, 20), + }); + } + } + + // Place trees on remaining empty slots + for (; slotIdx < placementSlots.length; slotIdx++) { + const slot = placementSlots[slotIdx]; + structures.push({ + col: slot.col, + row: slot.row, + kind: 'tree', + meshName: seededChoice(TREE_MESHES, `${rig.id}-tree-${slotIdx}`), + rotation: seededRotation(`${rig.id}-tree-rot-${slotIdx}`), + elevation: 0, + }); + } + + // Unassigned beads as market stalls + const unassignedBeads = snapshot.beads.filter( + b => b.rigId === rig.id && !b.assigneeAgentId && b.status === 'open' && b.type === 'issue' + ); + if (unassignedBeads.length > 0 && ring1.length > 0) { + structures.push({ + col: center.col, + row: center.row, + kind: 'market', + meshName: MARKET_MESH, + rotation: 0, + elevation: 0, + label: `${unassignedBeads.length} open`, + }); + } + + // Escalation beads as fire + const escalations = snapshot.beads.filter( + b => b.rigId === rig.id && b.type === 'escalation' && b.status !== 'closed' + ); + for (const esc of escalations) { + const fireSlot = placementSlots[hashCode(esc.id) % placementSlots.length]; + structures.push({ + col: fireSlot.col, + row: fireSlot.row, + kind: 'fire', + meshName: 'rock_single_B', // Placeholder — will render as particle effect + rotation: 0, + elevation: 0, + linkedObjectId: esc.id, + linkedObjectType: 'bead', + linkedRigId: rig.id, + glow: [1.0, 0.4, 0.0], // orange + label: esc.title.slice(0, 20), + animate: true, + }); + } + + return { + rigId: rig.id, + rigName: rig.name, + centerCol: center.col, + centerRow: center.row, + radius: DISTRICT_RADIUS, + tiles, + structures, + }; +} + +// ── Road building (simple line interpolation) ────────────────────────── + +function buildRoad( + fromCol: number, + fromRow: number, + toCol: number, + toRow: number, + occupiedHexes: Set, + markOccupied: (col: number, row: number) => void +): HexTilePlacement[] { + const tiles: HexTilePlacement[] = []; + const from = offsetToCube(fromCol, fromRow); + const to = offsetToCube(toCol, toRow); + const dist = cubeDistance(from, to); + if (dist <= 1) return tiles; + + // Linear interpolation in cube space + for (let i = 1; i < dist; i++) { + const t = i / dist; + const q = Math.round(from.q + (to.q - from.q) * t); + const r = Math.round(from.r + (to.r - from.r) * t); + const s = -q - r; + + // Fix rounding: ensure q + r + s === 0 + const qDiff = Math.abs(q - (from.q + (to.q - from.q) * t)); + const rDiff = Math.abs(r - (from.r + (to.r - from.r) * t)); + const sDiff = Math.abs(s - (from.s + (to.s - from.s) * t)); + + let fq = q, + fr = r; + if (qDiff > rDiff && qDiff > sDiff) { + fq = -fr - (-fq - fr); // recalc q + } else if (rDiff > sDiff) { + fr = -fq - (-fq - fr); // recalc r + } + // s is implicit + + // Convert back to offset and check if free + const col = fq + (fr - (fr & 1)) / 2; + const row = fr; + const key = `${col},${row}`; + + if (!occupiedHexes.has(key)) { + tiles.push({ + col, + row, + meshName: ROAD_STRAIGHT, + rotation: estimateRoadRotation(fromCol, fromRow, toCol, toRow), + elevation: 0, + }); + markOccupied(col, row); + } + } + + return tiles; +} + +function estimateRoadRotation( + fromCol: number, + fromRow: number, + toCol: number, + toRow: number +): number { + const dx = toCol - fromCol; + const dz = toRow - fromRow; + const angle = Math.atan2(dz, dx); + // Map angle to nearest 60-degree step (0-5) + const step = Math.round(angle / (Math.PI / 3)); + return ((step % 6) + 6) % 6; +} diff --git a/src/components/gastown/hex-viz/types.ts b/src/components/gastown/hex-viz/types.ts new file mode 100644 index 000000000..dd78f589c --- /dev/null +++ b/src/components/gastown/hex-viz/types.ts @@ -0,0 +1,132 @@ +/** + * Types for the Gastown 3D hex visualization. + * + * Maps Gastown domain objects (rigs, agents, beads, convoys) + * to hex-grid visual representations. + */ + +// ── Hex coordinate system (pointy-top, odd-r offset) ─────────────────── + +export type HexCoord = { col: number; row: number }; +export type CubeCoord = { q: number; r: number; s: number }; + +export const HEX_DIRECTIONS = ['NE', 'E', 'SE', 'SW', 'W', 'NW'] as const; +export type HexDirection = (typeof HEX_DIRECTIONS)[number]; + +// ── Town state snapshot (from WebSocket or tRPC) ─────────────────────── + +export type TownSnapshot = { + townId: string; + rigs: RigSnapshot[]; + agents: AgentSnapshot[]; + beads: BeadSnapshot[]; + convoys: ConvoySnapshot[]; + recentEvents: EventSnapshot[]; +}; + +export type RigSnapshot = { + id: string; + name: string; + gitUrl: string; + defaultBranch: string; +}; + +export type AgentSnapshot = { + id: string; + rigId: string | null; + role: 'polecat' | 'refinery' | 'mayor'; + name: string; + identity: string; + status: 'idle' | 'working' | 'stalled' | 'dead'; + currentHookBeadId: string | null; + lastActivityAt: string | null; + statusMessage: string | null; +}; + +export type BeadSnapshot = { + id: string; + rigId: string | null; + type: 'issue' | 'message' | 'escalation' | 'merge_request' | 'convoy' | 'molecule' | 'agent'; + status: 'open' | 'in_progress' | 'closed' | 'failed'; + title: string; + priority: 'low' | 'medium' | 'high' | 'critical'; + assigneeAgentId: string | null; +}; + +export type ConvoySnapshot = { + id: string; + title: string; + status: 'active' | 'landed'; + totalBeads: number; + closedBeads: number; +}; + +export type EventSnapshot = { + id: string; + eventType: string; + message: string; + timestamp: string; +}; + +// ── Hex tile placement ───────────────────────────────────────────────── + +export type HexTilePlacement = { + col: number; + row: number; + meshName: string; + rotation: number; // 0-5 (60 degree steps) + elevation: number; // 0-4 + color?: [number, number, number]; // RGB tint +}; + +// ── Structure (decoration placed on a hex tile) ──────────────────────── + +export type StructureKind = + | 'town-hall' + | 'cottage' + | 'windmill' + | 'market' + | 'crate' + | 'bridge' + | 'fire' + | 'tree' + | 'flag'; + +export type StructurePlacement = { + col: number; + row: number; + kind: StructureKind; + meshName: string; + rotation: number; + elevation: number; + /** Optional link to a Gastown object for click interaction */ + linkedObjectId?: string; + linkedObjectType?: 'agent' | 'bead' | 'rig' | 'convoy'; + /** Rig ID for the linked object (needed for drawer queries) */ + linkedRigId?: string; + /** Visual state */ + glow?: [number, number, number]; // RGB glow color + label?: string; + animate?: boolean; // e.g., windmill spin, fire flicker +}; + +// ── District (a rig's region on the hex map) ──────────────────────────── + +export type District = { + rigId: string; + rigName: string; + centerCol: number; + centerRow: number; + radius: number; + tiles: HexTilePlacement[]; + structures: StructurePlacement[]; +}; + +// ── Full hex world layout ────────────────────────────────────────────── + +export type HexWorldLayout = { + tiles: HexTilePlacement[]; + structures: StructurePlacement[]; + districts: District[]; + waterTiles: HexTilePlacement[]; +}; diff --git a/src/components/gastown/hex-viz/use-town-snapshot.ts b/src/components/gastown/hex-viz/use-town-snapshot.ts new file mode 100644 index 000000000..c6151cefb --- /dev/null +++ b/src/components/gastown/hex-viz/use-town-snapshot.ts @@ -0,0 +1,199 @@ +'use client'; + +/** + * React hook that maintains a live TownSnapshot for the hex visualization. + * + * Fetches initial state via tRPC queries (agents + beads for ALL rigs), + * then subscribes to the Town DO status WebSocket for real-time updates. + */ + +import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; +import { useQuery, useQueries } from '@tanstack/react-query'; +import { useGastownTRPC } from '@/lib/gastown/trpc'; +import { gastownWsUrl } from '@/lib/gastown/trpc'; +import type { GastownOutputs } from '@/lib/gastown/trpc'; +import type { + TownSnapshot, + AgentSnapshot, + BeadSnapshot, + RigSnapshot, + ConvoySnapshot, +} from './types'; + +type TRPCAgent = GastownOutputs['gastown']['listAgents'][number]; +type TRPCBead = GastownOutputs['gastown']['listBeads'][number]; +type TRPCRig = GastownOutputs['gastown']['listRigs'][number]; +type TRPCConvoy = GastownOutputs['gastown']['listConvoys'][number]; + +export function useTownSnapshot(townId: string): { + snapshot: TownSnapshot | null; + connected: boolean; + loading: boolean; +} { + const trpc = useGastownTRPC(); + const [connected, setConnected] = useState(false); + const [wsAgents, setWsAgents] = useState(null); + const wsRef = useRef(null); + const reconnectTimerRef = useRef | null>(null); + const mountedRef = useRef(true); + + // ── tRPC queries for initial state ──────────────────────────────── + + const rigsQuery = useQuery(trpc.gastown.listRigs.queryOptions({ townId })); + const rigs = useMemo(() => (rigsQuery.data ?? []) as TRPCRig[], [rigsQuery.data]); + + // Query agents for ALL rigs (useQueries handles dynamic-length arrays) + const rigAgentQueries = useQueries({ + queries: rigs.map(rig => trpc.gastown.listAgents.queryOptions({ rigId: rig.id })), + }); + + // Query beads for ALL rigs + const rigBeadQueries = useQueries({ + queries: rigs.map(rig => trpc.gastown.listBeads.queryOptions({ rigId: rig.id })), + }); + + const convoysQuery = useQuery(trpc.gastown.listConvoys.queryOptions({ townId })); + + const agentsLoading = rigAgentQueries.some(q => q.isLoading); + const beadsLoading = rigBeadQueries.some(q => q.isLoading); + const loading = rigsQuery.isLoading || agentsLoading || beadsLoading || convoysQuery.isLoading; + + // ── WebSocket for live updates ──────────────────────────────────── + + const connect = useCallback(() => { + if (!mountedRef.current) return; + const url = gastownWsUrl(`/api/towns/${townId}/status/ws`); + const ws = new WebSocket(url); + wsRef.current = ws; + + ws.onopen = () => { + if (!mountedRef.current) return; + setConnected(true); + }; + + ws.onmessage = (e: MessageEvent) => { + if (!mountedRef.current || typeof e.data !== 'string') return; + try { + const parsed: unknown = JSON.parse(e.data); + if (parsed && typeof parsed === 'object') { + const data = parsed as Record; + + if ('agents' in data && Array.isArray(data.agents)) { + const agents: AgentSnapshot[] = (data.agents as Array>).map( + (a: Record) => ({ + id: String(a.id ?? ''), + rigId: a.rig_id != null ? String(a.rig_id) : null, + role: (a.role as AgentSnapshot['role']) ?? 'polecat', + name: String(a.name ?? ''), + identity: String(a.identity ?? ''), + status: (a.status as AgentSnapshot['status']) ?? 'idle', + currentHookBeadId: + a.current_hook_bead_id != null ? String(a.current_hook_bead_id) : null, + lastActivityAt: a.last_activity_at != null ? String(a.last_activity_at) : null, + statusMessage: + a.agent_status_message != null ? String(a.agent_status_message) : null, + }) + ); + setWsAgents(agents); + } + } + } catch { + // Ignore malformed messages + } + }; + + ws.onclose = () => { + if (!mountedRef.current) return; + setConnected(false); + reconnectTimerRef.current = setTimeout(connect, 3_000); + }; + + ws.onerror = () => { + // Will trigger onclose + }; + }, [townId]); + + useEffect(() => { + mountedRef.current = true; + connect(); + return () => { + mountedRef.current = false; + if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current); + wsRef.current?.close(1000, 'Component unmount'); + wsRef.current = null; + }; + }, [connect]); + + // ── Assemble the snapshot ───────────────────────────────────────── + + // Stabilize the query data arrays so useMemo deps work correctly + const rigAgentData = rigAgentQueries.map(q => q.data); + const rigBeadData = rigBeadQueries.map(q => q.data); + + const snapshot = useMemo(() => { + if (!rigsQuery.data) return null; + + const rigSnapshots: RigSnapshot[] = rigs.map((r: TRPCRig) => ({ + id: r.id, + name: r.name, + gitUrl: r.git_url, + defaultBranch: r.default_branch, + })); + + // Merge agents from all rigs + const tRPCAgents: AgentSnapshot[] = rigAgentData.flatMap((data, i) => { + const rig = rigs[i]; + if (!data || !rig) return []; + return (data as TRPCAgent[]).map((a: TRPCAgent) => ({ + id: a.id, + rigId: a.rig_id ?? null, + role: a.role as AgentSnapshot['role'], + name: a.name, + identity: a.identity, + status: a.status as AgentSnapshot['status'], + currentHookBeadId: a.current_hook_bead_id ?? null, + lastActivityAt: a.last_activity_at ?? null, + statusMessage: a.agent_status_message ?? null, + })); + }); + + // Prefer WebSocket-updated agents when available (they span all rigs) + const agents = wsAgents ?? tRPCAgents; + + // Merge beads from all rigs + const beads: BeadSnapshot[] = rigBeadData.flatMap((data, i) => { + const rig = rigs[i]; + if (!data || !rig) return []; + return (data as TRPCBead[]).map((b: TRPCBead) => ({ + id: b.bead_id, + rigId: b.rig_id ?? null, + type: b.type as BeadSnapshot['type'], + status: b.status as BeadSnapshot['status'], + title: b.title, + priority: b.priority as BeadSnapshot['priority'], + assigneeAgentId: b.assignee_agent_bead_id ?? null, + })); + }); + + const convoysList = (convoysQuery.data ?? []) as TRPCConvoy[]; + const convoys: ConvoySnapshot[] = convoysList.map((c: TRPCConvoy) => ({ + id: c.id, + title: c.title, + status: c.status as ConvoySnapshot['status'], + totalBeads: c.total_beads ?? 0, + closedBeads: c.closed_beads ?? 0, + })); + + return { + townId, + rigs: rigSnapshots, + agents, + beads, + convoys, + recentEvents: [], + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [townId, rigsQuery.data, rigs, rigAgentData, rigBeadData, convoysQuery.data, wsAgents]); + + return { snapshot, connected, loading }; +}