From 001d5eeb54c2694b940644e83cbf4007fc4ecf73 Mon Sep 17 00:00:00 2001 From: hypercross Date: Sat, 8 Aug 2026 12:56:46 +0800 Subject: [PATCH] Add 3D object viewers with r3f stack Add per-class 3D viewers for tiles, tokens, cards, and custom models using React Three Fiber, drei, and postprocessing. Viewers are lazy-loaded and registered through the existing viewer registry, with a shared scene wrapper for lighting, orbit controls, and subtle effects. Add a CORS-safe /asset proxy route so three.js loaders can fetch Workshop-hosted textures and models, and extend TTSObject with the CustomMesh and CustomTile/CustomToken fields the viewers read. --- apps/proxy/src/index.ts | 2 + apps/proxy/src/routes/asset.test.ts | 43 ++ apps/proxy/src/routes/asset.ts | 47 ++ apps/web/package.json | 5 + .../web/src/components/viewers/CardViewer.tsx | 30 + .../components/viewers/CustomModelViewer.tsx | 76 +++ apps/web/src/components/viewers/Scene.tsx | 50 ++ .../web/src/components/viewers/TileViewer.tsx | 27 + .../src/components/viewers/TokenViewer.tsx | 27 + apps/web/src/components/viewers/assetUrl.ts | 8 + apps/web/src/components/viewers/register.ts | 22 + apps/web/src/pages/ModPage.tsx | 16 +- apps/web/vite.config.ts | 1 + docs/architecture.md | 10 +- docs/decisions.md | 31 +- docs/implementation-plan.md | 39 +- packages/shared/src/types.ts | 25 + pnpm-lock.yaml | 592 +++++++++++++++++- 18 files changed, 1037 insertions(+), 14 deletions(-) create mode 100644 apps/proxy/src/routes/asset.test.ts create mode 100644 apps/proxy/src/routes/asset.ts create mode 100644 apps/web/src/components/viewers/CardViewer.tsx create mode 100644 apps/web/src/components/viewers/CustomModelViewer.tsx create mode 100644 apps/web/src/components/viewers/Scene.tsx create mode 100644 apps/web/src/components/viewers/TileViewer.tsx create mode 100644 apps/web/src/components/viewers/TokenViewer.tsx create mode 100644 apps/web/src/components/viewers/assetUrl.ts create mode 100644 apps/web/src/components/viewers/register.ts diff --git a/apps/proxy/src/index.ts b/apps/proxy/src/index.ts index 852b5b6..d6f6998 100644 --- a/apps/proxy/src/index.ts +++ b/apps/proxy/src/index.ts @@ -2,6 +2,7 @@ import { serve } from '@hono/node-server'; import { cors } from 'hono/cors'; import { Hono } from 'hono'; import { loadEnv, type Bindings } from './env.js'; +import asset from './routes/asset.js'; import health from './routes/health.js'; import items from './routes/items.js'; import search from './routes/search.js'; @@ -14,6 +15,7 @@ app.use('*', cors()); app.route('/health', health); app.route('/search', search); app.route('/items', items); +app.route('/asset', asset); serve( { diff --git a/apps/proxy/src/routes/asset.test.ts b/apps/proxy/src/routes/asset.test.ts new file mode 100644 index 0000000..7295f02 --- /dev/null +++ b/apps/proxy/src/routes/asset.test.ts @@ -0,0 +1,43 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import asset from './asset.js'; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe('asset route', () => { + it('rejects a missing url', async () => { + const res = await asset.request('/'); + expect(res.status).toBe(400); + expect(await res.json()).toEqual({ error: 'Missing url query param' }); + }); + + it('rejects a non-http url', async () => { + const res = await asset.request('/?url=file%3A%2F%2F%2Fetc%2Fpasswd'); + expect(res.status).toBe(400); + }); + + it('streams the fetched asset with a content-type header', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue( + new Response(new Uint8Array([1, 2, 3]), { + headers: { 'content-type': 'image/png' }, + }), + ), + ); + const res = await asset.request('/?url=https%3A%2F%2Fexample.com%2Fa.png'); + expect(res.status).toBe(200); + expect(res.headers.get('content-type')).toBe('image/png'); + expect(await res.arrayBuffer()).toEqual(new Uint8Array([1, 2, 3]).buffer); + }); + + it('returns 502 when the upstream fetch fails', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue(new Response('error', { status: 500 })), + ); + const res = await asset.request('/?url=https%3A%2F%2Fexample.com%2Fa.png'); + expect(res.status).toBe(502); + }); +}); \ No newline at end of file diff --git a/apps/proxy/src/routes/asset.ts b/apps/proxy/src/routes/asset.ts new file mode 100644 index 0000000..a48ac93 --- /dev/null +++ b/apps/proxy/src/routes/asset.ts @@ -0,0 +1,47 @@ +import { Hono } from 'hono'; + +const app = new Hono(); + +/** + * Fetch an external asset (texture, model, etc.) and stream it back to the + * client. Workshop asset hosts (steamusercontent.com, etc.) often omit CORS + * headers, which would block `TextureLoader` / `GLTFLoader` in the browser. + * Routing through the proxy makes those assets loadable. + */ +app.get('/', async (c) => { + const raw = c.req.query('url'); + if (!raw) { + return c.json({ error: 'Missing url query param' }, 400); + } + + let url: URL; + try { + url = new URL(raw); + } catch { + return c.json({ error: 'Invalid url query param' }, 400); + } + // Only allow http(s) to avoid SSRF via file://, etc. + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + return c.json({ error: 'Only http(s) urls are allowed' }, 400); + } + + let res: Response; + try { + res = await fetch(url); + } catch (err) { + return c.json({ error: `Failed to fetch asset: ${String(err)}` }, 502); + } + if (!res.ok) { + return c.json({ error: `Asset responded ${res.status}` }, 502); + } + + const contentType = res.headers.get('content-type') ?? 'application/octet-stream'; + return new Response(res.body, { + headers: { + 'Content-Type': contentType, + 'Cache-Control': 'public, max-age=86400', + }, + }); +}); + +export default app; \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index 20228cd..6da4f1f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,17 +15,22 @@ "@iconify-json/mdi": "^1.2.3", "@iconify/react": "^6.0.2", "@iconify/utils": "^3.1.4", + "@react-three/drei": "^10.7.8", + "@react-three/fiber": "^9.7.0", + "@react-three/postprocessing": "^3.0.4", "@tts/extract": "workspace:*", "@tts/shared": "workspace:*", "react": "^19.2.8", "react-dom": "^19.2.8", "react-router-dom": "^7.18.2", + "three": "^0.185.1", "zustand": "^5.0.14" }, "devDependencies": { "@tailwindcss/vite": "^4.3.3", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", + "@types/three": "^0.185.4", "@vitejs/plugin-react": "^6.0.5", "tailwindcss": "^4.3.3", "typescript": "^5.7.2", diff --git a/apps/web/src/components/viewers/CardViewer.tsx b/apps/web/src/components/viewers/CardViewer.tsx new file mode 100644 index 0000000..61a8ca7 --- /dev/null +++ b/apps/web/src/components/viewers/CardViewer.tsx @@ -0,0 +1,30 @@ +import { useTexture } from '@react-three/drei'; +import type { TTSObject } from '@tts/shared'; +import Scene from './Scene'; +import { assetUrl } from './assetUrl'; + +/** + * A playing card: a thin box with the face texture on the front and the back + * texture on the rear. Reads `CustomDeck` face/back URLs, falling back to a + * neutral color when absent. + */ +export default function CardViewer({ object }: { object: TTSObject }) { + const deck = object.CustomDeck ? Object.values(object.CustomDeck)[0] : undefined; + const faceUrl = deck?.FaceURL; + const backUrl = deck?.BackURL; + const face = faceUrl ? useTexture(assetUrl(faceUrl)) : null; + const back = backUrl ? useTexture(assetUrl(backUrl)) : null; + + return ( + + + + + + + ); +} \ No newline at end of file diff --git a/apps/web/src/components/viewers/CustomModelViewer.tsx b/apps/web/src/components/viewers/CustomModelViewer.tsx new file mode 100644 index 0000000..be35812 --- /dev/null +++ b/apps/web/src/components/viewers/CustomModelViewer.tsx @@ -0,0 +1,76 @@ +import { Suspense, useLayoutEffect } from 'react'; +import { useLoader } from '@react-three/fiber'; +import { useFBX, useGLTF, useTexture } from '@react-three/drei'; +import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'; +import type { TTSObject } from '@tts/shared'; +import type * as THREE from 'three'; +import Scene from './Scene'; +import { assetUrl } from './assetUrl'; + +/** + * A custom 3D model loaded from `CustomMesh.MeshURL`. Supports GLTF/GLB, OBJ, + * and FBX by sniffing the URL extension; falls back to GLTF for unknown + * extensions. `DiffuseURL` is applied to the model's materials when present. + */ +export default function CustomModelViewer({ object }: { object: TTSObject }) { + const meshUrl = object.CustomMesh?.MeshURL; + if (!meshUrl) { + return ( + + + + + + + ); + } + + return ( + + + + + + ); +} + +function Model({ + meshUrl, + diffuseUrl, +}: { + meshUrl: string; + diffuseUrl?: string; +}) { + const ext = meshUrl.split('?')[0]!.split('.').pop()!.toLowerCase(); + const url = assetUrl(meshUrl); + + const diffuse = diffuseUrl ? useTexture(assetUrl(diffuseUrl)) : null; + + let root: THREE.Object3D; + if (ext === 'obj') { + root = useLoader(OBJLoader, url); + } else if (ext === 'fbx') { + root = useFBX(url); + } else { + root = useGLTF(url).scene; + } + + // Apply the diffuse texture to every mesh material on the loaded model. + useLayoutEffect(() => { + if (!diffuse) return; + root.traverse((child) => { + const mesh = child as THREE.Mesh; + if (mesh.isMesh) { + const material = Array.isArray(mesh.material) + ? mesh.material[0] + : mesh.material; + if (material && 'map' in material) { + material.map = diffuse; + material.needsUpdate = true; + } + } + }); + }, [root, diffuse]); + + return ; +} \ No newline at end of file diff --git a/apps/web/src/components/viewers/Scene.tsx b/apps/web/src/components/viewers/Scene.tsx new file mode 100644 index 0000000..7be705e --- /dev/null +++ b/apps/web/src/components/viewers/Scene.tsx @@ -0,0 +1,50 @@ +import { Suspense, type ReactNode } from 'react'; +import { Canvas } from '@react-three/fiber'; +import { ContactShadows, OrbitControls } from '@react-three/drei'; +import { Bloom, EffectComposer, Vignette } from '@react-three/postprocessing'; + +/** + * Shared 3D scene wrapper for object viewers. Provides a consistent camera, + * lighting, orbit controls, a soft contact shadow, and subtle post-processing + * (bloom + vignette). Children are wrapped in a Suspense boundary so loading + * assets (textures, models) can suspend without blanking the page. + */ +export default function Scene({ children }: { children: ReactNode }) { + return ( +
+ + + + + + + {children} + + + + + + + + + +
+ ); +} \ No newline at end of file diff --git a/apps/web/src/components/viewers/TileViewer.tsx b/apps/web/src/components/viewers/TileViewer.tsx new file mode 100644 index 0000000..a236dc6 --- /dev/null +++ b/apps/web/src/components/viewers/TileViewer.tsx @@ -0,0 +1,27 @@ +import { useTexture } from '@react-three/drei'; +import type { TTSObject } from '@tts/shared'; +import Scene from './Scene'; +import { assetUrl } from './assetUrl'; + +/** + * A flat tile with a texture on its top face. Uses `CustomImage.ImageURL` + * (falling back to `ImageSecondaryURL`), with a neutral color when absent. + */ +export default function TileViewer({ object }: { object: TTSObject }) { + const url = object.CustomImage?.ImageURL ?? object.CustomImage?.ImageSecondaryURL; + const texture = url ? useTexture(assetUrl(url)) : null; + const thickness = object.CustomImage?.CustomTile?.Thickness ?? 0.1; + + return ( + + + + + + + ); +} \ No newline at end of file diff --git a/apps/web/src/components/viewers/TokenViewer.tsx b/apps/web/src/components/viewers/TokenViewer.tsx new file mode 100644 index 0000000..a1996ac --- /dev/null +++ b/apps/web/src/components/viewers/TokenViewer.tsx @@ -0,0 +1,27 @@ +import { useTexture } from '@react-three/drei'; +import type { TTSObject } from '@tts/shared'; +import Scene from './Scene'; +import { assetUrl } from './assetUrl'; + +/** + * A round token: a short cylinder with the texture on its top face. Uses + * `CustomImage.ImageURL`, with a neutral color when absent. + */ +export default function TokenViewer({ object }: { object: TTSObject }) { + const url = object.CustomImage?.ImageURL ?? object.CustomImage?.ImageSecondaryURL; + const texture = url ? useTexture(assetUrl(url)) : null; + const thickness = object.CustomImage?.CustomToken?.Thickness ?? 0.1; + + return ( + + + + + + + ); +} \ No newline at end of file diff --git a/apps/web/src/components/viewers/assetUrl.ts b/apps/web/src/components/viewers/assetUrl.ts new file mode 100644 index 0000000..b29a988 --- /dev/null +++ b/apps/web/src/components/viewers/assetUrl.ts @@ -0,0 +1,8 @@ +/** + * Route an external asset URL through the proxy so it can be loaded by + * three.js loaders (TextureLoader, GLTFLoader, etc.) despite the upstream host + * omitting CORS headers. + */ +export function assetUrl(url: string): string { + return `/asset?url=${encodeURIComponent(url)}`; +} \ No newline at end of file diff --git a/apps/web/src/components/viewers/register.ts b/apps/web/src/components/viewers/register.ts new file mode 100644 index 0000000..cc52157 --- /dev/null +++ b/apps/web/src/components/viewers/register.ts @@ -0,0 +1,22 @@ +import { lazy } from 'react'; +import { registerViewer } from '../viewers'; + +// Register 3D viewers for the object classes that carry renderable assets. +// Importing this module has the side effect of populating the viewer registry. +// The viewers are lazy-loaded so the three.js stack is code-split out of the +// main bundle and only fetched when a 3D-capable object is actually selected. +const TileViewer = lazy(() => import('./TileViewer')); +const TokenViewer = lazy(() => import('./TokenViewer')); +const CardViewer = lazy(() => import('./CardViewer')); +const CustomModelViewer = lazy(() => import('./CustomModelViewer')); + +registerViewer('Tile', TileViewer); +registerViewer('Custom_Tile', TileViewer); +registerViewer('Custom_Token', TokenViewer); +registerViewer('Card', CardViewer); +registerViewer('Deck', CardViewer); +registerViewer('DeckCustom', CardViewer); +registerViewer('Custom_Deck', CardViewer); +registerViewer('Custom_Model', CustomModelViewer); +registerViewer('Custom_Model_Bag', CustomModelViewer); +registerViewer('Custom_Model_Infinite_Bag', CustomModelViewer); \ No newline at end of file diff --git a/apps/web/src/pages/ModPage.tsx b/apps/web/src/pages/ModPage.tsx index a2e97fe..9433506 100644 --- a/apps/web/src/pages/ModPage.tsx +++ b/apps/web/src/pages/ModPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { Suspense, useEffect, useMemo, useState } from 'react'; import { Icon } from '@iconify/react'; import { useParams } from 'react-router-dom'; import { buildTree, collectRefs } from '@tts/extract'; @@ -8,6 +8,8 @@ import { modFileUrl } from '../api'; import ObjectTree from '../components/ObjectTree'; import { resolveViewer } from '../components/viewers'; import { iconsForObject } from '../components/objectIcons'; +// Register the 3D viewers (side effect) so `resolveViewer` can find them. +import '../components/viewers/register'; export default function ModPage() { const { id } = useParams<{ id: string }>(); @@ -75,7 +77,17 @@ export default function ModPage() { {selected.object.GUID}

- {Viewer && } + {Viewer && ( + + Loading viewer… + + } + > + + + )} ) : (

diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index fe68fa3..85fe1c2 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -10,6 +10,7 @@ export default defineConfig({ '/search': 'http://localhost:3000', '/items': 'http://localhost:3000', '/health': 'http://localhost:3000', + '/asset': 'http://localhost:3000', }, }, }); \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md index 9adf36c..124a686 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -45,8 +45,8 @@ apps/web ──► apps/proxy ──► packages/tts ──► packages/shared ### Edges -- **`apps/web` → `apps/proxy`** — calls `/search`, `/items/:id`, and - `/items/:id/file` over HTTP. +- **`apps/web` → `apps/proxy`** — calls `/search`, `/items/:id`, + `/items/:id/file`, and `/asset` (CORS-safe asset proxy) over HTTP. - **`apps/web` → `packages/extract`** — uses `buildTree` / `collectRefs` to analyze a loaded `TTSMod` in the browser (tree sidebar + asset refs). - **`apps/proxy` → `packages/tts`** — calls `fetchMod` / `getFileName` to serve @@ -82,6 +82,10 @@ apps/web ──► apps/proxy ──► packages/tts ──► packages/shared | `bson` | BSON deserialization of TTS save files | `packages/tts` | | `cheerio` | Workshop browse page scraping | `apps/proxy` | | `zod` | Runtime validation | `apps/proxy`, `packages/shared` | +| `three` | 3D rendering | `apps/web` | +| `@react-three/fiber` | React renderer for three.js | `apps/web` | +| `@react-three/drei` | three.js helpers (controls, textures) | `apps/web` | +| `@react-three/postprocessing` | Post-processing effects | `apps/web` | ### Runtime constraints @@ -103,7 +107,7 @@ apps/web ──► apps/proxy ──► packages/tts ──► packages/shared - The proxy runs as a single Node process via `@hono/node-server`. - `apps/web` is a static Vite build served separately; during development it - proxies `/search`, `/items`, and `/health` to the proxy. + proxies `/search`, `/items`, `/health`, and `/asset` to the proxy. - `packages/extract` is published/consumed as a plain ESM module usable from a browser bundle or Node. - No shared state between requests; each request fetches fresh. \ No newline at end of file diff --git a/docs/decisions.md b/docs/decisions.md index 6bff799..46838bc 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -128,4 +128,33 @@ without coupling the page to any one viewer. **Alternatives considered:** A single monolithic inspector component. Rejected — would grow unboundedly as per-class viewers are added; the registry keeps each -viewer isolated and swappable. \ No newline at end of file +viewer isolated and swappable. + +## D11 — 3D viewers on the r3f/drei/postprocessing stack + +**Decision:** Per-class 3D viewers for tiles, tokens, cards, and custom models +are built on `@react-three/fiber`, `@react-three/drei`, and +`@react-three/postprocessing`, registered through the existing viewer registry +and lazy-loaded so the three.js stack is code-split out of the main bundle. + +**Context:** The user wants to visualize objects in a save in 3D. The viewer +registry (D10) already provides the extension point. Custom models load from +`CustomMesh.MeshURL` (GLTF/OBJ/FBX); tiles/tokens/cards use their +`CustomImage`/`CustomDeck` textures. Unity asset bundles are out of scope. + +**Alternatives considered:** A single monolithic 3D inspector; a WebGL library +other than three.js. Rejected — the registry keeps viewers isolated, and r3f is +the de-facto React/three.js integration. + +## D12 — CORS-safe asset proxy + +**Decision:** The proxy exposes `GET /asset?url=...`, which fetches an external +asset and streams it back with a `Content-Type` header. Only `http(s)` URLs are +allowed. + +**Context:** Workshop asset hosts (steamusercontent.com, etc.) often omit CORS +headers, which would block three.js `TextureLoader` / `GLTFLoader` in the +browser. Routing asset fetches through the proxy makes them loadable. + +**Alternatives considered:** Loading assets directly in the browser. Rejected — +CORS failures on common Workshop hosts would break the viewers. \ No newline at end of file diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md index ef200d8..8d6a16f 100644 --- a/docs/implementation-plan.md +++ b/docs/implementation-plan.md @@ -58,12 +58,13 @@ tts-workshop/ │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── src/ -│ │ ├── index.ts # Hono app + @hono/node-server -│ │ ├── routes/ -│ │ │ ├── search.ts # GET /search?q=...&page=1 -│ │ │ ├── items.ts # GET /items/:id, /items/:id/file -│ │ │ └── health.ts # GET /health -│ │ └── env.ts # zod env validation +│ │ ├── index.ts # Hono app + @hono/node-server +│ │ ├── routes/ +│ │ │ ├── search.ts # GET /search?q=...&page=1 +│ │ │ ├── items.ts # GET /items/:id, /items/:id/file +│ │ │ ├── asset.ts # GET /asset?url=... (CORS-safe asset proxy) +│ │ │ └── health.ts # GET /health +│ │ └── env.ts # zod env validation │ └── web/ │ ├── package.json │ ├── tsconfig.json @@ -81,6 +82,14 @@ tts-workshop/ │ │ ├── SearchResults.tsx │ │ ├── ObjectTree.tsx # containment-tree sidebar │ │ ├── viewers.tsx # viewer registry + default viewer +│ │ ├── viewers/ # 3D viewers (r3f/drei/postprocessing) +│ │ │ ├── register.ts # registers per-class 3D viewers (lazy) +│ │ │ ├── Scene.tsx # shared Canvas: lights, controls, post +│ │ │ ├── TileViewer.tsx +│ │ │ ├── TokenViewer.tsx +│ │ │ ├── CardViewer.tsx +│ │ │ ├── CustomModelViewer.tsx +│ │ │ └── assetUrl.ts # route asset URLs through the proxy │ │ ├── objectIcons.tsx # class → icon mapping │ │ ├── objectIconsData.ts # generated icon subset (do not edit) │ │ └── objectIcons.test.ts @@ -188,6 +197,11 @@ Hono server exposing search + fetch. otherwise resolves via the Steam API. - `GET /items/:id/file` — raw save bytes, filename from `getFileName`. Also accepts `fileUrl`. +- `routes/asset.ts` + - `GET /asset?url=...` — fetch an external asset (texture, model) and stream + it back with a `Content-Type` header. Workshop hosts often omit CORS + headers, which would block three.js loaders in the browser; routing through + the proxy makes those assets loadable. Only `http(s)` URLs are allowed. - `routes/health.ts` - `GET /health` — liveness. - `env.ts` — zod validation of `STEAM_API_KEY`, `PORT`. @@ -212,6 +226,16 @@ proxy API and `packages/extract` directly for analysis. - `components/viewers.tsx` — viewer registry (`registerViewer` / `resolveViewer`) plus a `DefaultViewer` that renders an object's fields; custom per-class viewers can be registered later. +- `components/viewers/` — 3D viewers built on `@react-three/fiber`, + `@react-three/drei`, and `@react-three/postprocessing`. `register.ts` + registers lazy-loaded viewers for `Tile`/`Custom_Tile` (flat box), + `Custom_Token` (cylinder), `Card`/`Deck`/`Custom_Deck` (thin box with + face/back textures), and `Custom_Model`/`Custom_Model_Bag`/ + `Custom_Model_Infinite_Bag` (GLTF/OBJ/FBX from `CustomMesh.MeshURL`). + `Scene.tsx` is a shared canvas with lighting, orbit controls, contact + shadows, and subtle bloom/vignette. `assetUrl.ts` routes asset URLs through + the proxy for CORS-safe loading. The viewers are lazy-loaded so the three.js + stack is code-split out of the main bundle. - `components/objectIcons.tsx` — maps TTS object classes to one or more Iconify icons (`iconsForObject`); unknown classes fall back to a help icon. Icons may come from multiple sets (mdi, material-symbols, file-icons, ...); @@ -234,6 +258,7 @@ proxy API and `packages/extract` directly for analysis. | GET | `/search?q=&page=` | Scrape Workshop browse, return item list | — | | GET | `/items/:id` | Full parsed `TTSMod` (`?fileUrl=` skips key) | key* | | GET | `/items/:id/file` | Raw save bytes, filename from header | key* | +| GET | `/asset?url=` | CORS-safe proxy for external assets | — | \* `STEAM_API_KEY` is optional; `/items/*` works without it when a `fileUrl` query param is supplied. @@ -259,6 +284,8 @@ packages/extract → flatten objects / extract refs / download assets - `cheerio` — Workshop browse page scraping (backend only). - `zod` — validation. - `react`, `react-dom`, `react-router-dom`, `zustand` — frontend. +- `three`, `@react-three/fiber`, `@react-three/drei`, + `@react-three/postprocessing` — 3D object viewers. - `@iconify/react`, `@iconify-json/mdi`, `@iconify/utils` — iconify icons for object class tags (subset bundled via `scripts/generate-object-icons.mjs`). - `vite`, `@vitejs/plugin-react`, `tailwindcss`, `@tailwindcss/vite` — frontend tooling. diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index b856076..329ba2c 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -31,6 +31,31 @@ export interface TTSObject { CustomImage?: { ImageURL: string; ImageSecondaryURL: string; + ImageScalar?: number; + WidthScale?: number; + CustomTile?: { + Type: number; + Thickness: number; + Stackable: boolean; + Stretch: boolean; + }; + CustomToken?: { + Thickness: number; + MergeDistancePixels: number; + StandUp: boolean; + Stackable: boolean; + }; + }; + /** A custom 3D model, e.g. `Custom_Model`, `Custom_Model_Bag`. */ + CustomMesh?: { + MeshURL: string; + DiffuseURL: string; + NormalURL: string; + ColliderURL: string; + Convex: boolean; + MaterialIndex: number; + TypeIndex: number; + CastShadows: boolean; }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 42910ad..9386836 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,6 +54,15 @@ importers: '@iconify/utils': specifier: ^3.1.4 version: 3.1.4 + '@react-three/drei': + specifier: ^10.7.8 + version: 10.7.8(@react-three/fiber@9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1))(@types/react@19.2.18)(@types/three@0.185.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1) + '@react-three/fiber': + specifier: ^9.7.0 + version: 9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1) + '@react-three/postprocessing': + specifier: ^3.0.4 + version: 3.0.4(@react-three/fiber@9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1))(@types/three@0.185.4)(react@19.2.8)(three@0.185.1) '@tts/extract': specifier: workspace:* version: link:../../packages/extract @@ -69,9 +78,12 @@ importers: react-router-dom: specifier: ^7.18.2 version: 7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + three: + specifier: ^0.185.1 + version: 0.185.1 zustand: specifier: ^5.0.14 - version: 5.0.14(@types/react@19.2.18)(react@19.2.8) + version: 5.0.14(@types/react@19.2.18)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)) devDependencies: '@tailwindcss/vite': specifier: ^4.3.3 @@ -82,6 +94,9 @@ importers: '@types/react-dom': specifier: ^19.2.4 version: 19.2.4(@types/react@19.2.18) + '@types/three': + specifier: ^0.185.4 + version: 0.185.4 '@vitejs/plugin-react': specifier: ^6.0.5 version: 6.0.5(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11)) @@ -136,6 +151,13 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@dimforge/rapier3d-compat@0.12.0': + resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} + '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} @@ -328,9 +350,60 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@mediapipe/tasks-vision@0.10.17': + resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==} + + '@monogrid/gainmap-js@3.4.0': + resolution: {integrity: sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==} + peerDependencies: + three: '>= 0.159.0' + '@oxc-project/types@0.143.0': resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==} + '@react-three/drei@10.7.8': + resolution: {integrity: sha512-rJXyuzLm2Xq0kafHuR47ajDGbOe/pEhzIr4m8E8zwzQs0iNjloFDqBwRhrXmP/w+onLeYyN3EYPFW/cwWK/4yA==} + peerDependencies: + '@react-three/fiber': ^9.0.0 + react: ^19 + react-dom: ^19 + three: '>=0.159' + peerDependenciesMeta: + react-dom: + optional: true + + '@react-three/fiber@9.7.0': + resolution: {integrity: sha512-EWm9FwcaOZQu/ExFW5rggoCMM1NJet5YbxVxKaOE+KSncrjU0Wx7017qSyGFvupviK89nMYGCWU3BIK4dI1clw==} + 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 + + '@react-three/postprocessing@3.0.4': + resolution: {integrity: sha512-e4+F5xtudDYvhxx3y0NtWXpZbwvQ0x1zdOXWTbXMK6fFLVDd4qucN90YaaStanZGS4Bd5siQm0lGL/5ogf8iDQ==} + peerDependencies: + '@react-three/fiber': ^9.0.0 + react: ^19.0 + three: '>= 0.156.0' + '@rolldown/binding-android-arm64@1.2.3': resolution: {integrity: sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -521,26 +594,57 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tweenjs/tween.js@23.1.3': + resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/draco3d@1.4.10': + resolution: {integrity: sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} '@types/node@22.20.1': resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} + '@types/offscreencanvas@2019.7.3': + resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==} + '@types/react-dom@19.2.4': resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==} peerDependencies: '@types/react': ^19.2.0 + '@types/react-reconciler@0.28.9': + resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} + peerDependencies: + '@types/react': '*' + '@types/react@19.2.18': resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} + '@types/stats.js@0.17.4': + resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} + + '@types/three@0.185.4': + resolution: {integrity: sha512-gAsBIC07NIFrxjbf7tH2t71c38uulFfk/RFoC7FNBSjMRAQ8J1x/RBvusX0N5PJouaYFJawXQqfCQ0RKUx/1nA==} + + '@types/webxr@0.5.24': + resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} + + '@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' + '@vitejs/plugin-react@6.0.5': resolution: {integrity: sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -587,10 +691,25 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + bson@6.10.4: resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==} engines: {node: '>=16.20.1'} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + 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' + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -602,13 +721,28 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + 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'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + 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'} + draco3d@1.5.7: + resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==} + enhanced-resolve@5.24.5: resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} engines: {node: '>=10.13.0'} @@ -637,25 +771,57 @@ packages: picomatch: optional: true + fflate@0.6.11: + resolution: {integrity: sha512-3JyEFWGjFn7zHmoa9+zG1BmW7X2okcmAB+0Cnu9UFbVs/jCBnl2A8o065ZlXiw145K3eBM3uLuzrYXC0RK7eDg==} + + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + glsl-noise@0.0.0: + resolution: {integrity: sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + hls.js@1.6.17: + resolution: {integrity: sha512-NUplVGVuc1hSPwdB/9/cbRkUmLrYi75/hqiXKdA+l300pJNxDu96R7jRb2imDzWJqIUF4I5ThmAdp9GvOCXsuQ==} + hono@4.13.1: resolution: {integrity: sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==} engines: {node: '>=16.9.0'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + its-fine@2.0.0: + resolution: {integrity: sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==} + peerDependencies: + react: ^19.0.0 + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -804,9 +970,35 @@ packages: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} + maath@0.10.8: + resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==} + peerDependencies: + '@types/three': '>=0.134.0' + three: '>=0.134.0' + + maath@0.6.0: + resolution: {integrity: sha512-dSb2xQuP7vDnaYqfoKzlApeRcR2xtN8/f7WV/TMAkBC8552TwTLtOO0JTcSygkYMjNDPoo6V01jTw/aPi4JrMw==} + peerDependencies: + '@types/three': '>=0.144.0' + three: '>=0.144.0' + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + meshline@3.3.1: + resolution: {integrity: sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==} + peerDependencies: + three: '>=0.137' + + meshoptimizer@1.1.1: + resolution: {integrity: sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==} + + n8ao@1.10.3: + resolution: {integrity: sha512-JFRk4WgUAUWO2KdJTqmBT02dP9kG7rE2oQbBy64E9+YwQ3d96KB9QBanqNy/NnYUfUTYU6Z/5OS5t/yLey7+6Q==} + peerDependencies: + postprocessing: '>=6.30.0' + three: '>=0.137' + nanoid@3.3.18: resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -819,6 +1011,10 @@ packages: package-manager-detector@1.8.0: resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -833,6 +1029,17 @@ packages: resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} + postprocessing@6.39.4: + resolution: {integrity: sha512-oAS/PjAbc/xT3OzjUrGsorJ4J064XwhVD2t0OwKLP/E8QwDMUJ0oOv6ZI1SYMtLchv4g0ySEx+JcLy92+48vlA==} + peerDependencies: + three: '>= 0.168.0 < 0.186.0' + + potpack@1.0.2: + resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} + + promise-worker-transferable@1.0.4: + resolution: {integrity: sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==} + react-dom@19.2.8: resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} peerDependencies: @@ -855,10 +1062,23 @@ packages: react-dom: optional: true + 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.8: resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + rolldown@1.2.3: resolution: {integrity: sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==} engines: {node: ^20.19.0 || >=22.12.0} @@ -870,6 +1090,14 @@ packages: set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -880,9 +1108,23 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + 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==} + std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + suspend-react@0.1.3: + resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==} + peerDependencies: + react: '>=17.0' + tailwindcss@4.3.3: resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} @@ -890,6 +1132,19 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} + 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.185.1: + resolution: {integrity: sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -905,11 +1160,27 @@ packages: resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} + troika-three-text@0.52.5: + resolution: {integrity: sha512-Ry3jRhic9pzcY4JduSvRRyDmVOSqEW19gT4vtK+aCiPNVcDlmkxvGG0YbFd36RTDq1wExOupXnvNF/j1oiHHDA==} + peerDependencies: + three: '>=0.125.0' + + troika-three-utils@0.52.5: + resolution: {integrity: sha512-WsePbcX8RtfidRfsxK1eCZCjF81ZDzAKHH/evLs0hdV2wpoCb0vArGZHdzdOJrSS3k4zfdtbKDaBh8+phkrYnw==} + peerDependencies: + three: '>=0.125.0' + + troika-worker-utils@0.52.0: + resolution: {integrity: sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==} + tsx@4.23.11: resolution: {integrity: sha512-Ry2oTEUnhBdeEdWIztY8kf3/nBGnPnjMLVGL0YfdRXMORuPER5NlKmayqxtxRxwB1xBN+RivRaJfe7PM1rtiyw==} engines: {node: '>=18.0.0'} hasBin: true + tunnel-rat@0.1.2: + resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -918,6 +1189,15 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + vite@8.2.1: resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1002,6 +1282,17 @@ packages: jsdom: optional: true + 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==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -1010,6 +1301,21 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + 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.14: resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==} engines: {node: '>=12.20.0'} @@ -1035,6 +1341,10 @@ snapshots: package-manager-detector: 1.8.0 tinyexec: 1.3.0 + '@babel/runtime@7.29.7': {} + + '@dimforge/rapier3d-compat@0.12.0': {} + '@esbuild/aix-ppc64@0.28.1': optional: true @@ -1153,8 +1463,79 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mediapipe/tasks-vision@0.10.17': {} + + '@monogrid/gainmap-js@3.4.0(three@0.185.1)': + dependencies: + promise-worker-transferable: 1.0.4 + three: 0.185.1 + '@oxc-project/types@0.143.0': {} + '@react-three/drei@10.7.8(@react-three/fiber@9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1))(@types/react@19.2.18)(@types/three@0.185.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1)': + dependencies: + '@babel/runtime': 7.29.7 + '@mediapipe/tasks-vision': 0.10.17 + '@monogrid/gainmap-js': 3.4.0(three@0.185.1) + '@react-three/fiber': 9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1) + '@use-gesture/react': 10.3.1(react@19.2.8) + camera-controls: 3.1.2(three@0.185.1) + cross-env: 7.0.3 + detect-gpu: 5.0.70 + glsl-noise: 0.0.0 + hls.js: 1.6.17 + maath: 0.10.8(@types/three@0.185.4)(three@0.185.1) + meshline: 3.3.1(three@0.185.1) + react: 19.2.8 + stats-gl: 2.4.2(@types/three@0.185.4)(three@0.185.1) + stats.js: 0.17.0 + suspend-react: 0.1.3(react@19.2.8) + three: 0.185.1 + three-mesh-bvh: 0.8.3(three@0.185.1) + three-stdlib: 2.36.1(three@0.185.1) + troika-three-text: 0.52.5(three@0.185.1) + tunnel-rat: 0.1.2(@types/react@19.2.18)(react@19.2.8) + use-sync-external-store: 1.6.0(react@19.2.8) + utility-types: 3.11.0 + zustand: 5.0.14(@types/react@19.2.18)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)) + optionalDependencies: + react-dom: 19.2.8(react@19.2.8) + transitivePeerDependencies: + - '@types/react' + - '@types/three' + - immer + + '@react-three/fiber@9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1)': + dependencies: + '@babel/runtime': 7.29.7 + '@types/webxr': 0.5.24 + base64-js: 1.5.1 + buffer: 6.0.3 + its-fine: 2.0.0(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-use-measure: 2.1.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + scheduler: 0.27.0 + suspend-react: 0.1.3(react@19.2.8) + three: 0.185.1 + use-sync-external-store: 1.6.0(react@19.2.8) + zustand: 5.0.14(@types/react@19.2.18)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)) + optionalDependencies: + react-dom: 19.2.8(react@19.2.8) + transitivePeerDependencies: + - '@types/react' + - immer + + '@react-three/postprocessing@3.0.4(@react-three/fiber@9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1))(@types/three@0.185.4)(react@19.2.8)(three@0.185.1)': + dependencies: + '@react-three/fiber': 9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1) + maath: 0.6.0(@types/three@0.185.4)(three@0.185.1) + n8ao: 1.10.3(postprocessing@6.39.4(three@0.185.1))(three@0.185.1) + postprocessing: 6.39.4(three@0.185.1) + react: 19.2.8 + three: 0.185.1 + transitivePeerDependencies: + - '@types/three' + '@rolldown/binding-android-arm64@1.2.3': optional: true @@ -1269,6 +1650,8 @@ snapshots: tailwindcss: 4.3.3 vite: 8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11) + '@tweenjs/tween.js@23.1.3': {} + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -1276,20 +1659,48 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/draco3d@1.4.10': {} + '@types/estree@1.0.9': {} '@types/node@22.20.1': dependencies: undici-types: 6.21.0 + '@types/offscreencanvas@2019.7.3': {} + '@types/react-dom@19.2.4(@types/react@19.2.18)': dependencies: '@types/react': 19.2.18 + '@types/react-reconciler@0.28.9(@types/react@19.2.18)': + dependencies: + '@types/react': 19.2.18 + '@types/react@19.2.18': dependencies: csstype: 3.2.3 + '@types/stats.js@0.17.4': {} + + '@types/three@0.185.4': + dependencies: + '@dimforge/rapier3d-compat': 0.12.0 + '@tweenjs/tween.js': 23.1.3 + '@types/stats.js': 0.17.4 + '@types/webxr': 0.5.24 + fflate: 0.8.3 + meshoptimizer: 1.1.1 + + '@types/webxr@0.5.24': {} + + '@use-gesture/core@10.3.1': {} + + '@use-gesture/react@10.3.1(react@19.2.8)': + dependencies: + '@use-gesture/core': 10.3.1 + react: 19.2.8 + '@vitejs/plugin-react@6.0.5(vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11))': dependencies: '@rolldown/pluginutils': 1.0.1 @@ -1338,18 +1749,49 @@ snapshots: assertion-error@2.0.1: {} + base64-js@1.5.1: {} + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + bson@6.10.4: {} + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + camera-controls@3.1.2(three@0.185.1): + dependencies: + three: 0.185.1 + chai@6.2.2: {} convert-source-map@2.0.0: {} cookie@1.1.1: {} + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + csstype@3.2.3: {} + detect-gpu@5.0.70: + dependencies: + webgl-constants: 1.1.1 + detect-libc@2.1.2: {} + draco3d@1.5.7: {} + enhanced-resolve@5.24.5: dependencies: graceful-fs: 4.2.11 @@ -1396,17 +1838,44 @@ snapshots: optionalDependencies: picomatch: 4.0.5 + fflate@0.6.11: {} + + fflate@0.8.3: {} + fsevents@2.3.3: optional: true + glsl-noise@0.0.0: {} + graceful-fs@4.2.11: {} + hls.js@1.6.17: {} + hono@4.13.1: {} + ieee754@1.2.1: {} + + immediate@3.0.6: {} + import-meta-resolve@4.2.0: {} + is-promise@2.2.2: {} + + isexe@2.0.0: {} + + its-fine@2.0.0(@types/react@19.2.18)(react@19.2.8): + dependencies: + '@types/react-reconciler': 0.28.9(@types/react@19.2.18) + react: 19.2.8 + transitivePeerDependencies: + - '@types/react' + jiti@2.7.0: {} + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lightningcss-android-arm64@1.32.0: optional: true @@ -1505,16 +1974,39 @@ snapshots: lightningcss-win32-arm64-msvc: 1.33.0 lightningcss-win32-x64-msvc: 1.33.0 + maath@0.10.8(@types/three@0.185.4)(three@0.185.1): + dependencies: + '@types/three': 0.185.4 + three: 0.185.1 + + maath@0.6.0(@types/three@0.185.4)(three@0.185.1): + dependencies: + '@types/three': 0.185.4 + three: 0.185.1 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + meshline@3.3.1(three@0.185.1): + dependencies: + three: 0.185.1 + + meshoptimizer@1.1.1: {} + + n8ao@1.10.3(postprocessing@6.39.4(three@0.185.1))(three@0.185.1): + dependencies: + postprocessing: 6.39.4(three@0.185.1) + three: 0.185.1 + nanoid@3.3.18: {} obug@2.1.4: {} package-manager-detector@1.8.0: {} + path-key@3.1.1: {} + pathe@2.0.3: {} picocolors@1.1.1: {} @@ -1527,6 +2019,17 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postprocessing@6.39.4(three@0.185.1): + dependencies: + three: 0.185.1 + + potpack@1.0.2: {} + + promise-worker-transferable@1.0.4: + dependencies: + is-promise: 2.2.2 + lie: 3.3.0 + react-dom@19.2.8(react@19.2.8): dependencies: react: 19.2.8 @@ -1546,8 +2049,16 @@ snapshots: optionalDependencies: react-dom: 19.2.8(react@19.2.8) + react-use-measure@2.1.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + react: 19.2.8 + optionalDependencies: + react-dom: 19.2.8(react@19.2.8) + react@19.2.8: {} + require-from-string@2.0.2: {} + rolldown@1.2.3: dependencies: '@oxc-project/types': 0.143.0 @@ -1572,18 +2083,51 @@ snapshots: set-cookie-parser@2.7.2: {} + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + siginfo@2.0.0: {} source-map-js@1.2.1: {} stackback@0.0.2: {} + stats-gl@2.4.2(@types/three@0.185.4)(three@0.185.1): + dependencies: + '@types/three': 0.185.4 + three: 0.185.1 + + stats.js@0.17.0: {} + std-env@4.2.0: {} + suspend-react@0.1.3(react@19.2.8): + dependencies: + react: 19.2.8 + tailwindcss@4.3.3: {} tapable@2.3.3: {} + three-mesh-bvh@0.8.3(three@0.185.1): + dependencies: + three: 0.185.1 + + three-stdlib@2.36.1(three@0.185.1): + dependencies: + '@types/draco3d': 1.4.10 + '@types/offscreencanvas': 2019.7.3 + '@types/webxr': 0.5.24 + draco3d: 1.5.7 + fflate: 0.6.11 + potpack: 1.0.2 + three: 0.185.1 + + three@0.185.1: {} + tinybench@2.9.0: {} tinyexec@1.3.0: {} @@ -1595,16 +2139,44 @@ snapshots: tinyrainbow@3.1.1: {} + troika-three-text@0.52.5(three@0.185.1): + dependencies: + bidi-js: 1.0.3 + three: 0.185.1 + troika-three-utils: 0.52.5(three@0.185.1) + troika-worker-utils: 0.52.0 + webgl-sdf-generator: 1.1.1 + + troika-three-utils@0.52.5(three@0.185.1): + dependencies: + three: 0.185.1 + + troika-worker-utils@0.52.0: {} + tsx@4.23.11: dependencies: esbuild: 0.28.1 optionalDependencies: fsevents: 2.3.3 + tunnel-rat@0.1.2(@types/react@19.2.18)(react@19.2.8): + dependencies: + zustand: 4.5.7(@types/react@19.2.18)(react@19.2.8) + transitivePeerDependencies: + - '@types/react' + - immer + - react + typescript@5.9.3: {} undici-types@6.21.0: {} + use-sync-external-store@1.6.0(react@19.2.8): + dependencies: + react: 19.2.8 + + utility-types@3.11.0: {} + vite@8.2.1(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.11): dependencies: lightningcss: 1.33.0 @@ -1646,6 +2218,14 @@ snapshots: transitivePeerDependencies: - msw + webgl-constants@1.1.1: {} + + webgl-sdf-generator@1.1.1: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -1653,7 +2233,15 @@ snapshots: zod@3.25.76: {} - zustand@5.0.14(@types/react@19.2.18)(react@19.2.8): + zustand@4.5.7(@types/react@19.2.18)(react@19.2.8): + dependencies: + use-sync-external-store: 1.6.0(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 react: 19.2.8 + + zustand@5.0.14(@types/react@19.2.18)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)): + optionalDependencies: + '@types/react': 19.2.18 + react: 19.2.8 + use-sync-external-store: 1.6.0(react@19.2.8)