Model TTSObjectTransform in shared types and convert each object's position, rotation, and scale into three.js placement, with per-class base-size corrections and a lay-flat rotation for extruded meshes.
5.8 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.- Every object in a save carries a
Transform(position/rotation/scale in TTS world units), which the proxy passes through in the raw BSON. The shared type now models it asTTSObjectTransform.
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
- Place each object at its real position from the save's
Transforminstead of a generated grid. components/viewers/transform.tsconverts a TTS transform to three.js: TTS is left-handed (Y up, +Z toward the player), three.js is right-handed, so Z is reflected in position and rotation. Each class also gets a base-size correction (our viewer meshes are authored for inspection, in arbitrary units) and a "lay flat" rotation for the extruded tile/token/card meshes.Scene'sBounds fitauto-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/components/viewers/transform.ts— new TTS→three.js placement conversionpackages/shared/src/types.ts— addTTSObjectTransformapps/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 — real
Transformplacement (was a grouped grid before the transform data was wired in). - Geometry/material cache lifetime — session-level module cache (simplest, consistent with drei's global texture cache) vs dispose-on-unmount.