Add a /mod/:id/setup page that lays out every renderable object in a single shared scene. Export object-facing mesh wrappers from the viewers and share geometry and materials across objects via module-level caches.
5.2 KiB
Full Setup View — Plan
Scope: A new page that renders every loadable object in a save into a single shared 3D scene, sharing materials and geometry where possible. This complements the existing per-object inspection on the mod page.
Motivation
The mod page (apps/web/src/pages/ModPage.tsx) inspects one object at a time
inside a shared Scene. A "full setup" view renders the whole save at once —
every tile, token, card, and custom model laid out on a virtual table — so a
user can see the entire scene at a glance.
Current architecture (relevant pieces)
- Each viewer owns its own
<Scene>wrapper (components/viewers/Scene.tsx), which provides the camera, lights, orbit controls, contact shadows, and post-processing. The mesh content lives inside each viewer:TileViewer→<TileMesh>(extruded tile, textured top face)TokenViewer→<TokenMesh>(alpha-traced extruded token)CardViewer→<CardMesh>(rounded-rect card, sprite UVs)CustomModelViewer→<Model>(GLTF/OBJ/FBX viaFlexibleModelLoader)
@tts/extractprovidesflattenObjects(mod)/traverseModto enumerate every object in a save.@tts/meshprovides the shape + extrusion helpers used by the viewers.TTSObjectcarries no position data, so exact table placement is not recoverable — the full setup lays objects out itself.
Plan
1. Refactor viewers to expose their mesh content (no Scene)
Each viewer currently wraps its mesh in <Scene>. To compose everything into
one scene, extract and export the inner mesh components, keeping the existing
viewers as thin wrappers:
TileViewer→ exportTileObjectMesh(accepts aTTSObject)TokenViewer→ exportTokenObjectMeshCardViewer→ exportCardObjectMeshCustomModelViewer→ exportCustomModelMesh(the<Model>+ fallback box)
Each viewer now renders <Scene><XxxObjectMesh object={…} /></Scene>, so the
object-facing wrapper is the single source of truth for both the per-object
view and the full-setup view.
2. New page FullSetupPage.tsx at /mod/:id/setup
- Reuse
useModStore(same load path asModPage). flattenObjects(mod)→ filter to renderable classes (those registered incomponents/viewers/register.ts:Tile,Custom_Tile,Custom_Token,Card,CardCustom,Deck,DeckCustom,Custom_Deck,Custom_Model*).- Render one
<Scene>containing all renderable objects as<group position={…}>entries, dispatching to the right*MeshbyName. - Show a summary header (total objects, rendered count, skipped count) and
loading/error states matching
ModPage.
3. Layout
- Arrange objects in a grid on the "table" (fixed spacing, wrapping by row), grouping by type so cards sit together, tiles together, etc.
Scene'sBounds fitalready auto-fits the camera to the full layout, so no camera work is needed.- Non-renderable objects (bags, dice, boards without assets) are skipped and counted, not dropped silently.
4. Share materials & geometry
- Geometry cache (module-level
Map<string, BufferGeometry>): key by a canonical string —card:{w}:{h}:{t},tile:{type}:{aspect}:{t},token:{url}:{t},model:{meshUrl}. All cards of the same size, or tiles of the same type/size, reuse one geometry instead of rebuilding per object. Token geometry is keyed by the source image URL (the trace is cached per URL, so the silhouette is deterministic). - Material cache (module-level
Map<string, Material>): key bytextureUrl + color + roughness. drei already caches textures by URL globally, so sharing the material on top avoids per-object material allocation for tiles/tokens with the same image. - Cards are the exception: each card clones its texture for sprite UVs, so its face material cannot be shared — but its geometry still can (same card size).
- Dispose shared resources on page unmount, or accept a module-level cache for the session (see Open decisions).
5. Routing & navigation
- Add
<Route path="/mod/:id/setup" element={<FullSetupPage />} />inapps/web/src/App.tsx. - Add a "Full setup" link/button on
ModPagenext to the download button.
6. Edge cases
- Objects with no asset (no
ImageURL/MeshURL) render as neutral-colored placeholders (matching the current viewers' fallback behavior). - Token tracing suspends per URL (already cached in
TokenViewer); the sharedSceneSuspense boundary handles it. - Large saves: the grid + shared geometry keeps it performant, but cap or warn on very large object counts if needed.
Files touched
apps/web/src/components/viewers/{Tile,Token,Card,CustomModel}Viewer.tsx— export object-facing mesh wrappersapps/web/src/components/viewers/sharedResources.ts— new geometry/material cachesapps/web/src/pages/FullSetupPage.tsx— newapps/web/src/App.tsx— routeapps/web/src/pages/ModPage.tsx— nav link- Possibly a small
geometryCache/materialCachehelper undercomponents/viewers/
Open decisions (defaults in bold)
- Layout style — grouped grid vs a fan/stack for cards.
- Geometry/material cache lifetime — session-level module cache (simplest, consistent with drei's global texture cache) vs dispose-on-unmount.