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.
This commit is contained in:
@@ -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.
|
||||
+30
-1
@@ -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.
|
||||
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.
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user