diff --git a/apps/web/src/components/tabletop/TabletopScene.tsx b/apps/web/src/components/tabletop/TabletopScene.tsx
new file mode 100644
index 0000000..5a5fac5
--- /dev/null
+++ b/apps/web/src/components/tabletop/TabletopScene.tsx
@@ -0,0 +1,49 @@
+import { Suspense, useMemo } from 'react';
+import type { SerializedPackage, Setup } from '@tts/bgm';
+import {
+ SetupLoader,
+ WorldSurfaceView,
+ HudSurfaceView,
+ resolveMountTree,
+ serializedToPackage,
+ useTabletopStore,
+} from '@tts/tabletop';
+import Scene from '../viewers/Scene';
+
+/**
+ * Render a bgm setup as an interactive 3D table.
+ *
+ * Seeds the tabletop store from the setup, resolves the surface mount tree,
+ * and renders each enabled surface — world surfaces in world space, HUD
+ * surfaces as an overlay — with their parts placed on routes. The store is
+ * seeded on every render so a setup change re-seeds it.
+ */
+export default function TabletopScene({
+ pkg,
+ setup,
+}: {
+ pkg: SerializedPackage;
+ setup: Setup;
+}) {
+ const packageData = useMemo(() => serializedToPackage(pkg), [pkg]);
+ const surfaces = useTabletopStore((s) => s.surfaces);
+
+ const tree = useMemo(
+ () => resolveMountTree(packageData.surfaces, new Set(Object.keys(surfaces))),
+ [packageData, surfaces],
+ );
+
+ return (
+
+ {Object.keys(found.setup).length} path + {Object.keys(found.setup).length === 1 ? '' : 's'} +
+
{JSON.stringify(found.setup, null, 2)}
diff --git a/docs/bgm-tabletop-plan.md b/docs/bgm-tabletop-plan.md
index b0ed9f9..07cfcc4 100644
--- a/docs/bgm-tabletop-plan.md
+++ b/docs/bgm-tabletop-plan.md
@@ -3,8 +3,9 @@
> **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; web part-inspection route renders `PartView`
-> from the library. Remaining: wiring the full tabletop scene into a web route.
+> **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
diff --git a/packages/tabletop/src/index.ts b/packages/tabletop/src/index.ts
index b389b8a..c65c0fc 100644
--- a/packages/tabletop/src/index.ts
+++ b/packages/tabletop/src/index.ts
@@ -11,6 +11,9 @@ export {
} from './part.js';
export { resolveAssetUrl, assetUrl, traceImage } from './http.js';
+// Serialized package -> Package conversion.
+export { serializedToPackage } from './package.js';
+
// State store + derived render state.
export {
useTabletopStore,
diff --git a/packages/tabletop/src/package.test.ts b/packages/tabletop/src/package.test.ts
new file mode 100644
index 0000000..3e58628
--- /dev/null
+++ b/packages/tabletop/src/package.test.ts
@@ -0,0 +1,19 @@
+import { describe, expect, it } from 'vitest';
+import { serializedToPackage } from './package.js';
+
+describe('serializedToPackage', () => {
+ it('converts plain-object maps to Map instances', () => {
+ const serialized = {
+ meta: { id: 'harbor' },
+ parts: { 'token#wood': { type: 'token', id: 'wood' } },
+ surfaces: { 'board#harbor': { type: 'board', id: 'harbor', layout: [] } },
+ setups: { 'game#main': { type: 'game', id: 'main', setup: {} } },
+ };
+ const pkg = serializedToPackage(serialized);
+ expect(pkg.meta.id).toBe('harbor');
+ expect(pkg.parts).toBeInstanceOf(Map);
+ expect(pkg.parts.get('token#wood')).toEqual({ type: 'token', id: 'wood' });
+ expect(pkg.surfaces.get('board#harbor')).toMatchObject({ type: 'board' });
+ expect(pkg.setups.get('game#main')).toMatchObject({ type: 'game' });
+ });
+});
\ No newline at end of file
diff --git a/packages/tabletop/src/package.ts b/packages/tabletop/src/package.ts
new file mode 100644
index 0000000..6889864
--- /dev/null
+++ b/packages/tabletop/src/package.ts
@@ -0,0 +1,16 @@
+/**
+ * Convert a serialized package (plain objects, as emitted by the bgm vite
+ * plugin) into a `Package` (Maps). Consumers that load a package from
+ * `virtual:bgm/*` get the serialized form; the tabletop components take the
+ * `Package` form.
+ */
+import type { Package, SerializedPackage } from '@tts/bgm';
+
+export function serializedToPackage(serialized: SerializedPackage): Package {
+ return {
+ meta: serialized.meta,
+ parts: new Map(Object.entries(serialized.parts)),
+ surfaces: new Map(Object.entries(serialized.surfaces)),
+ setups: new Map(Object.entries(serialized.setups)),
+ };
+}
\ No newline at end of file