Implement the tabletop library's game state store, setup seeding with bare-type expansion, surface mount tree resolution, part placement, and the stacking positioning process with a dependency-free SVG path helper. Wire the public API and add unit plus vite integration tests.
7.4 KiB
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 1–8 implemented; web part-inspection route rendersPartViewfrom the library. Remaining: wiring the full tabletop scene into a web route.
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/tabletopworkspace package (pnpm-workspace.yamlalready globspackages/*). - Deps:
@tts/bgm,@tts/mesh,three,@react-three/fiber,@react-three/drei,@react-three/postprocessing,zustand. Dev:vitest,typescript,@types/three. tsconfig.jsonmirroringpackages/bgm's (strict, ESM,distoutput).
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 aPartdefinition:size→ world dimensions;fillet→ corner radius.face/faceCrop/back/backCrop→ textures (dreiuseTexture), sprite UVs fromfaceCrop/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.extrudeShapePartsfrom@tts/meshfor the mesh.
- Shared geometry/material caching (module-level
Maps) so repeated parts reuse buffers, mirroring the web viewers'sharedResources. - Export
PartViewfromindex.ts. - Web integration: replace the web app's part inspection route
(
/bgm/:id/parts/:type/:part) to renderPartViewfrom 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 aSetup— enables itssurfaces(or all when omitted) and places parts onsetuppaths.setupvalue expansion: a baretype(no id) expands to all parts of that type (documented inbgm-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 inchildren.
WorldSurfaceView/HudSurfaceViewmount 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), andlimit(0all,nfirst n,-nlast 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 ashapesprite.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 buildintegration test (mirroringpackages/bgm/src/vite.test.ts) proving the library bundles against a fixture package.
Validation
pnpm --filter @tts/tabletop build/typecheck/test.- Root
pnpm teststays green. pnpm --filter @tts/web build— the part inspection route rendersPartViewfrom the library (work item 2), proving it end-to-end.
Open decisions (defaults in bold)
- Where the trace/sprite helpers live — lift into
@tts/mesh(shared by web viewers + tabletop) vs duplicate in@tts/tabletop. Lifting is cleaner but touches the web viewers; decide whenPartView(work item 2) needs them. - HUD rendering — drei
Html/orthographic overlay vs a secondCanvas. Default to an overlay so world + HUD share one scene. - Curve length — small internal SVG-path length helper vs a dependency
(e.g.
svg-path-properties). Prefer the helper to avoid a dep.