Files
tts-workshop/docs/full-setup-view.md
T
hypercross dcba0ac4bd feat(web): place full setup objects at their real transforms
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.
2026-08-08 18:41:42 +08:00

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 via FlexibleModelLoader)
  • @tts/extract provides flattenObjects(mod) / traverseMod to enumerate every object in a save.
  • @tts/mesh provides 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 as TTSObjectTransform.

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 → export TileObjectMesh (accepts a TTSObject)
  • TokenViewer → export TokenObjectMesh
  • CardViewer → export CardObjectMesh
  • CustomModelViewer → export CustomModelMesh (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 as ModPage).
  • flattenObjects(mod) → filter to renderable classes (those registered in components/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 *Mesh by Name.
  • 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 Transform instead of a generated grid.
  • components/viewers/transform.ts converts 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's Bounds fit 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 by textureUrl + 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 />} /> in apps/web/src/App.tsx.
  • Add a "Full setup" link/button on ModPage next 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 shared Scene Suspense 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 wrappers
  • apps/web/src/components/viewers/sharedResources.ts — new geometry/material caches
  • apps/web/src/components/viewers/transform.ts — new TTS→three.js placement conversion
  • packages/shared/src/types.ts — add TTSObjectTransform
  • apps/web/src/pages/FullSetupPage.tsx — new
  • apps/web/src/App.tsx — route
  • apps/web/src/pages/ModPage.tsx — nav link
  • Possibly a small geometryCache/materialCache helper under components/viewers/

Open decisions (defaults in bold)

  • Layout stylereal Transform placement (was a grouped grid before the transform data was wired in).
  • Geometry/material cache lifetimesession-level module cache (simplest, consistent with drei's global texture cache) vs dispose-on-unmount.