Replace the per-part lift with a local Y-axis tilt that fans the stack, and add zStart/zEnd to ramp the stack's height across the curve so it arches in 3D. Update the poker deck and docs accordingly.
177 lines
7.6 KiB
Markdown
177 lines
7.6 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
|
||
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)` → `{ 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`
|
||
fans 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.
|
||
|
||
## 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. |