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:
2026-08-08 12:56:46 +08:00
parent f390f170da
commit 001d5eeb54
18 changed files with 1037 additions and 14 deletions
+7 -3
View File
@@ -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
View File
@@ -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.
+33 -6
View File
@@ -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.