Add the interaction half of the sandbox: a held part and dialog stack as UI state outside the game store, with pure helpers (interactionsFor, dropPaths, pickPath, partFacings, nextFacing). PartPlacement is now interactive (pick up, drag to move, click to cycle facing), and a drop on a path with a stack-dialog interaction opens the deck dialog instead of moving directly. DialogLayer renders the stack-inspector dialog whose insert button issues a move. Add the dialog role, Setup.interactions, and Package.dialogs to @tts/bgm, and declare the deck insert dialog in games/poker.
215 lines
9.5 KiB
Markdown
215 lines
9.5 KiB
Markdown
# bgm-tabletop — Implementation Plan / Status
|
||
|
||
> **Scope:** A standalone r3f component library that renders
|
||
> [bgm](../bgm/format.md) board games: a state store, surface mounting, part
|
||
> placement with stacking, and per-part meshes. Design:
|
||
> [`../bgm/tabletop.md`](../bgm/tabletop.md).
|
||
> **Status:** items 1–8 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 `Map`s) 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:
|
||
|
||
```ts
|
||
interface GameState {
|
||
surfaces: Record<string, boolean>; // enabled per surface id
|
||
parts: Record<string, PartState>; // part id -> placement state
|
||
}
|
||
|
||
interface PartState {
|
||
path: string; // the path key this part is on
|
||
index: number; // the part's position in its path's stack
|
||
facing: 'face' | 'back' | 'standing'; // how the part is oriented on the board
|
||
}
|
||
```
|
||
|
||
- A zustand store holding `GameState`.
|
||
- **Derived render state**: `game state + surface routes => map of piece id to
|
||
`{ surface, route, candidate, index, stackSize, face }``, per enabled surface.
|
||
Computed with a selector/memo so the render list is stable. A path's ordered
|
||
children (for stacking) are derived from the parts map by sorting on `index`.
|
||
- **Assumption**: each piece id is unique on the board (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 applies its
|
||
ordered `setup` placements (each moves its `parts` to a `path`).
|
||
- `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)` → `{ x, y, rotation, z, tilt }`.
|
||
- 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).
|
||
- `z` ramps linearly from `zStart` to `zEnd` across the curve's span; `tilt`
|
||
rotates each shown part about its local Y (long) axis.
|
||
- 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, z ramp,
|
||
tilt.
|
||
- `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.
|
||
|
||
## Free interaction (layer 3) ✅
|
||
|
||
Sandbox interaction is built per [`../bgm/interactions.md`](../bgm/interactions.md):
|
||
|
||
- `interactions.ts` — the held part + dialog stack (UI state, outside the game
|
||
store), plus pure helpers (`interactionsFor`, `dropPaths`, `pickPath`,
|
||
`partFacings`, `nextFacing`).
|
||
- `state.ts` — `setFacing` alongside `movePart` (the `move(id, path, index?)`
|
||
primitive, defaulting to top of stack).
|
||
- `placement.tsx` — `PartPlacement` is now interactive: pick up a part (held,
|
||
lifted above the board), drag to a path anchor to `move`, click to cycle
|
||
facing. A drop on a path with a stack-dialog interaction opens the deck
|
||
dialog instead of moving directly.
|
||
- `dialog.tsx` — `DialogLayer`, the stack-inspector dialog: an alternate view
|
||
of a stack with an insertion cursor; its insert button issues a `move`.
|
||
- `@tts/bgm` — the `dialog` role, `Setup.interactions`, and `Package.dialogs`
|
||
(schema + collection + serialization).
|
||
- Demo: `games/poker` declares an `interactions:` + `role: dialog` (stack
|
||
insert) on the deck.
|
||
|
||
The rule seam (layer 4) is a no-op filter: sandbox applies intents directly.
|
||
|
||
## Commands (not yet implemented)
|
||
|
||
Scripted interaction is designed in [`../bgm/commands.md`](../bgm/commands.md):
|
||
async commands with `ok`/`cancel`/`error` results, per-invocation run
|
||
contexts, fire-and-forget vs self-managed waiting, and tap interaction with
|
||
part-local trigger points. Implementation order: types + run-context manager,
|
||
tap detection, then the first commands (`wait: tap`, `focus`).
|
||
|
||
## 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 when `PartView` (work item 2)
|
||
needs them.
|
||
- **HUD rendering** — **drei `Html`/orthographic overlay** vs a second
|
||
`Canvas`. 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. |