docs: add bgm-tabletop implementation plan
This commit is contained in:
@@ -0,0 +1,172 @@
|
|||||||
|
# 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:** planning — no code yet.
|
||||||
|
|
||||||
|
## 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)` → `{ 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 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.
|
||||||
Reference in New Issue
Block a user