Files
hyper 8eff44712f feat(tabletop): add free-interaction layer (layer 3)
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.
2026-08-18 10:20:22 +08:00

9.5 KiB
Raw Permalink 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
  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:

  • interactions.ts — the held part + dialog stack (UI state, outside the game store), plus pure helpers (interactionsFor, dropPaths, pickPath, partFacings, nextFacing).
  • state.tssetFacing alongside movePart (the move(id, path, index?) primitive, defaulting to top of stack).
  • placement.tsxPartPlacement 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.tsxDialogLayer, 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: 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 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.