feat(tabletop): add state, setup, mounting, and stacking

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.
This commit is contained in:
2026-08-09 22:34:18 +08:00
parent ef0695ed04
commit cd08e6af04
20 changed files with 1281 additions and 11 deletions
@@ -0,0 +1,34 @@
/**
* `HudSurfaceView` — mount a surface to a HUD area.
*
* Renders a `kind: hud` mount node (and its child surfaces) anchored to a HUD
* area. The default is a drei `Html` overlay so world and HUD share one scene
* (see `bgm-tabletop.md` Open decisions). Parts are placed via `PartPlacement`.
*/
import { Html } from '@react-three/drei';
import type { Package } from '@tts/bgm';
import { useRenderState } from '../state.js';
import { PartPlacement } from '../placement.js';
import type { MountNode } from '../mount.js';
import { SurfaceNode } from './WorldSurfaceView.js';
export function HudSurfaceView({ pkg, node }: { pkg: Package; node: MountNode }) {
const placements = useRenderState(pkg);
const own = placements.filter((p) => p.surface === node.id);
return (
<Html
position={[node.x, 0, node.y]}
transform
distanceFactor={1}
style={{ pointerEvents: 'none' }}
>
{own.map((p) => (
<PartPlacement key={`${p.surface}:${p.piece}`} pkg={pkg} placement={p} />
))}
{node.children.map((child) => (
<SurfaceNode key={child.id} pkg={pkg} node={child} />
))}
</Html>
);
}
@@ -0,0 +1,33 @@
/**
* `WorldSurfaceView` — mount a surface in world space.
*
* Renders a world mount node (a `kind: table` surface and its child surfaces)
* at its anchor. Parts on the surface are placed via `PartPlacement` from the
* derived render state. A disabled surface isn't part of the mount tree, so
* it's never rendered.
*/
import type { Package } from '@tts/bgm';
import { useRenderState } from '../state.js';
import { PartPlacement } from '../placement.js';
import type { MountNode } from '../mount.js';
export function WorldSurfaceView({ pkg, node }: { pkg: Package; node: MountNode }) {
return <SurfaceNode pkg={pkg} node={node} />;
}
/** Render a mount node at its anchor, placing parts and recursing into children. */
export function SurfaceNode({ pkg, node }: { pkg: Package; node: MountNode }) {
const placements = useRenderState(pkg);
const own = placements.filter((p) => p.surface === node.id);
return (
<group position={[node.x, 0, node.y]} rotation={[0, node.rotation, 0]}>
{own.map((p) => (
<PartPlacement key={`${p.surface}:${p.piece}`} pkg={pkg} placement={p} />
))}
{node.children.map((child) => (
<SurfaceNode key={child.id} pkg={pkg} node={child} />
))}
</group>
);
}