Files
tts-workshop/docs/bgm-tabletop-plan.md
T
hypercross f494a6f9be feat(web): render bgm setups as a 3D table
Wire the tabletop library into the setup detail route: seed the store
from the setup, resolve the surface mount tree, and render world and HUD
surfaces. Add serializedToPackage to convert the vite-emitted package.
2026-08-09 22:46:03 +08:00

7.4 KiB
Raw Blame History

bgm-tabletop — Implementation Plan / Status

Scope: A standalone r3f component library that renders bgm board games: a state store, surface mounting, part placement with stacking, and per-part meshes. Design: bgm-tabletop.md. Status: items 18 implemented and the full tabletop scene is wired into the web app's setup detail route (/bgm/:id/setups/:type/:setup). The part-inspection route renders PartView from the library.

Goal

A library (new packages/tabletop) that takes a bgm package and renders it as an interactive 3D table: enabled surfaces mounted in world/HUD space, parts placed on their routes, stacked per the format's stacking strategy. The web app's bgm inspector routes are one consumer; the library must not depend on the web app.

Stack

react, react-router (types only), tailwind (styles only), r3f (@react-three/fiber), drei, postprocessing, zustand, three, @tts/bgm (types), @tts/mesh (geometry).

Package layout

packages/tabletop/
  package.json            # @tts/tabletop
  tsconfig.json
  vitest.config.ts
  src/
    index.ts              # public exports
    state.ts              # zustand store + derived render state
    setup.ts              # SetupLoader: seed state from a setup
    mount.ts              # resolve surface mount tree (table/hud/child)
    stacking.ts           # useStacking hook
    placement.ts          # PartPlacement
    partView.tsx          # PartView: mesh from a part definition
    surfaces/
      WorldSurfaceView.tsx
      HudSurfaceView.tsx
    *.test.ts             # colocated unit tests

Work items

1. Package scaffold

  • New packages/tabletop workspace package (pnpm-workspace.yaml already globs packages/*).
  • Deps: @tts/bgm, @tts/mesh, three, @react-three/fiber, @react-three/drei, @react-three/postprocessing, zustand. Dev: vitest, typescript, @types/three.
  • tsconfig.json mirroring packages/bgm's (strict, ESM, dist output).

2. Part meshes + export + web integration

First deliverable: PartView renders a single part's mesh from its definition, reusing @tts/mesh geometry (not the web app's viewers). This is the smallest useful slice and unblocks the web app's part inspection route immediately.

  • PartView (partView.tsx): creates a mesh from a Part definition:
    • size → world dimensions; fillet → corner radius.
    • face/faceCrop/back/backCrop → textures (drei useTexture), sprite UVs from faceCrop/backCrop (a [col,row,cols,rows] grid cell).
    • shape → traced silhouette (via the proxy /trace, like the web token viewer) or a fallback rect/rounded-rect.
    • extrudeShapeParts from @tts/mesh for the mesh.
  • Shared geometry/material caching (module-level Maps) so repeated parts reuse buffers, mirroring the web viewers' sharedResources.
  • Export PartView from index.ts.
  • Web integration: replace the web app's part inspection route (/bgm/:id/parts/:type/:part) to render PartView from the library, proving it end-to-end.

3. State store (state.ts)

Source-of-truth game state per bgm-tabletop.md §2:

interface GameState {
  surfaces: Record<string, boolean>; // enabled per surface id
  paths: Record<string, string[]>;   // path -> part list
}
  • A zustand store holding GameState.
  • Derived render state: game state + surface routes => map of piece id to { surface, route, candidate, index, stackSize }``, per enabled surface. Computed with a selector/memo so the render list is stable.
  • Assumption: each piece id is unique within a path (documented in bgm-tabletop.md); the render map is keyed by piece id.

4. Setup seeding (setup.ts)

  • SetupLoader: side-effect-only component that seeds the store from a Setup — enables its surfaces (or all when omitted) and places parts on setup paths.
  • setup value expansion: a bare type (no id) expands to all parts of that type (documented in bgm-format.md §3; the loader doesn't do this — it's a game-state init concern, so it lives here).

5. Surface mounting (mount.ts)

  • Resolve the surface mount tree from Surface.mount + Surface.children:
    • kind: table — root, world space.
    • kind: hud — HUD area (mount.area).
    • kind: child — mounted relative to a parent that lists it in children.
  • WorldSurfaceView / HudSurfaceView mount an enabled surface; a disabled surface isn't rendered. Child surfaces mount relative to their parent's anchor (x/y/rotation).

6. Part placement (placement.ts)

  • PartPlacement: stable per-part component that positions a part on a surface location from the derived render state (route anchor + candidate anchor).
  • Applies the route's stacking strategy via useStacking.

7. Stacking (stacking.ts)

  • useStacking(route.stacking, index, stackSize){ offset, rotation }.
  • Implements the format's positioning process (bgm-format.md §4): step length from curve length / max(steps, count-1), alignment (start/end/ center), and limit (0 all, n first n, -n last n).
  • Curve length from an SVG path string (small helper; no new dep).

8. Public API (index.ts)

Export SetupLoader, WorldSurfaceView, HudSurfaceView, PartPlacement, PartView, useStacking, and the store hooks. The web app consumes these; the library never imports from apps/*.

Reuse from @tts/mesh

  • extrudeShapeParts / extrudeShape — front/back/walls geometry.
  • rectShape, roundedRectShape, circleShape, polygonShape, hexShape, frameShape, scaleShape — shape generators for parts without a shape sprite.
  • shapeFromThree — author shapes with the three.js path API.
  • ExtrudedGeometry / UVBounds — raw typed arrays + UV framing.

The web viewers (TokenViewer/CardViewer) contain logic we'll mirror rather than import: trace-to-shape conversion, sprite UV math, texture flipping. These are candidates to lift into @tts/mesh or @tts/tabletop later so both consumers share them (see Open decisions).

Testing

  • state.ts — derived render state: enabled surfaces, route matching, candidate selection, stacking index/stackSize.
  • stacking.ts — positioning process: step length, alignment, limit.
  • setup.ts — seeding + bare-type expansion.
  • mount.ts — mount tree resolution (table/hud/child, children refs).
  • partView.tsx — geometry from a part def (size/fillet/crop), sprite UVs.
  • A real vite build integration test (mirroring packages/bgm/src/vite.test.ts) proving the library bundles against a fixture package.

Validation

  • pnpm --filter @tts/tabletop build / typecheck / test.
  • Root pnpm test stays green.
  • pnpm --filter @tts/web build — the part inspection route renders PartView from the library (work item 2), proving it end-to-end.

Open decisions (defaults in bold)

  • Where the trace/sprite helpers livelift into @tts/mesh (shared by web viewers + tabletop) vs duplicate in @tts/tabletop. Lifting is cleaner but touches the web viewers; decide when PartView (work item 2) needs them.
  • HUD renderingdrei Html/orthographic overlay vs a second Canvas. Default to an overlay so world + HUD share one scene.
  • Curve lengthsmall internal SVG-path length helper vs a dependency (e.g. svg-path-properties). Prefer the helper to avoid a dep.