feat(tabletop): show surface bounds and lay parts flat

Add a toggleable surface plane, bounds outline, and constant-size HTML
label. Scale placement and mount anchors to world units so parts land on
the surface, and rotate parts to face up.
This commit is contained in:
2026-08-09 23:04:51 +08:00
parent abf901418b
commit 5808e15c45
6 changed files with 116 additions and 11 deletions
@@ -10,23 +10,51 @@ import type { Package } from '@tts/bgm';
import { useRenderState, placementKey } from '../state.js';
import { PartPlacement } from '../placement.js';
import type { MountNode } from '../mount.js';
import { MM_TO_WORLD } from '../part.js';
import { SurfaceBounds } from './SurfaceBounds.js';
import { SurfacePlane } from './SurfacePlane.js';
export function WorldSurfaceView({ pkg, node }: { pkg: Package; node: MountNode }) {
return <SurfaceNode pkg={pkg} node={node} />;
export function WorldSurfaceView({
pkg,
node,
showSurface = false,
}: {
pkg: Package;
node: MountNode;
showSurface?: boolean;
}) {
return <SurfaceNode pkg={pkg} node={node} showSurface={showSurface} />;
}
/** Render a mount node at its anchor, placing parts and recursing into children. */
export function SurfaceNode({ pkg, node }: { pkg: Package; node: MountNode }) {
export function SurfaceNode({
pkg,
node,
showSurface = false,
}: {
pkg: Package;
node: MountNode;
showSurface?: boolean;
}) {
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]}>
<group
position={[node.x * MM_TO_WORLD, 0, node.y * MM_TO_WORLD]}
rotation={[0, node.rotation, 0]}
>
{showSurface && (
<>
<SurfacePlane surface={node.surface} />
<SurfaceBounds surface={node.surface} />
</>
)}
{own.map((p) => (
<PartPlacement key={placementKey(p)} pkg={pkg} placement={p} />
))}
{node.children.map((child) => (
<SurfaceNode key={child.id} pkg={pkg} node={child} />
<SurfaceNode key={child.id} pkg={pkg} node={child} showSurface={showSurface} />
))}
</group>
);