From 5808e15c45fd40b76ec799c5b77b305bb26910d9 Mon Sep 17 00:00:00 2001 From: hypercross Date: Sun, 9 Aug 2026 23:04:51 +0800 Subject: [PATCH] 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. --- packages/tabletop/src/index.ts | 2 + packages/tabletop/src/placement.tsx | 12 ++++-- .../tabletop/src/surfaces/HudSurfaceView.tsx | 17 +++++++-- .../tabletop/src/surfaces/SurfaceBounds.tsx | 34 +++++++++++++++++ .../tabletop/src/surfaces/SurfacePlane.tsx | 24 ++++++++++++ .../src/surfaces/WorldSurfaceView.tsx | 38 ++++++++++++++++--- 6 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 packages/tabletop/src/surfaces/SurfaceBounds.tsx create mode 100644 packages/tabletop/src/surfaces/SurfacePlane.tsx diff --git a/packages/tabletop/src/index.ts b/packages/tabletop/src/index.ts index c65c0fc..0a3f879 100644 --- a/packages/tabletop/src/index.ts +++ b/packages/tabletop/src/index.ts @@ -33,6 +33,8 @@ export { resolveMountTree } from './mount.js'; export type { MountNode, MountTree } from './mount.js'; export { WorldSurfaceView, SurfaceNode } from './surfaces/WorldSurfaceView.js'; export { HudSurfaceView } from './surfaces/HudSurfaceView.js'; +export { SurfaceBounds } from './surfaces/SurfaceBounds.js'; +export { SurfacePlane } from './surfaces/SurfacePlane.js'; // Part placement. export { PartPlacement } from './placement.js'; diff --git a/packages/tabletop/src/placement.tsx b/packages/tabletop/src/placement.tsx index 18e0742..e91fea0 100644 --- a/packages/tabletop/src/placement.tsx +++ b/packages/tabletop/src/placement.tsx @@ -9,6 +9,7 @@ import type { Package } from '@tts/bgm'; import { useStacking } from './stacking.js'; import type { Placement } from './state.js'; import { PartView } from './partView.js'; +import { MM_TO_WORLD } from './part.js'; export function PartPlacement({ pkg, placement }: { pkg: Package; placement: Placement }) { const { route, candidate, piece, index, stackSize } = placement; @@ -18,13 +19,18 @@ export function PartPlacement({ pkg, placement }: { pkg: Package; placement: Pla const { x, y, rotation } = useStacking(route.stacking, index, stackSize); - const anchorX = (candidate?.x ?? route.x ?? 0) + x; - const anchorY = (candidate?.y ?? route.y ?? 0) + y; + // Route anchors and stacking offsets are in mm; convert to world units so + // parts land on the (world-scaled) surface. + const anchorX = ((candidate?.x ?? route.x ?? 0) + x) * MM_TO_WORLD; + const anchorY = ((candidate?.y ?? route.y ?? 0) + y) * MM_TO_WORLD; const anchorRotation = (candidate?.rotation ?? route.rotation ?? 0) + rotation; return ( - + {/* The part mesh extrudes along +Z; lay it flat so its face points up. */} + + + ); } \ No newline at end of file diff --git a/packages/tabletop/src/surfaces/HudSurfaceView.tsx b/packages/tabletop/src/surfaces/HudSurfaceView.tsx index c0ad2fa..7f4e3d0 100644 --- a/packages/tabletop/src/surfaces/HudSurfaceView.tsx +++ b/packages/tabletop/src/surfaces/HudSurfaceView.tsx @@ -10,24 +10,35 @@ 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 { SurfaceNode } from './WorldSurfaceView.js'; -export function HudSurfaceView({ pkg, node }: { pkg: Package; node: MountNode }) { +export function HudSurfaceView({ + pkg, + node, + showSurface = false, +}: { + pkg: Package; + node: MountNode; + showSurface?: boolean; +}) { const placements = useRenderState(pkg); const own = placements.filter((p) => p.surface === node.id); return ( + {showSurface && } {own.map((p) => ( ))} {node.children.map((child) => ( - + ))} ); diff --git a/packages/tabletop/src/surfaces/SurfaceBounds.tsx b/packages/tabletop/src/surfaces/SurfaceBounds.tsx new file mode 100644 index 0000000..427f0cb --- /dev/null +++ b/packages/tabletop/src/surfaces/SurfaceBounds.tsx @@ -0,0 +1,34 @@ +/** + * `SurfaceBounds` — a debug visualization of a surface's area. + * + * Draws a rectangle outline at the surface's reference `size` (mm, converted + * to world units) centered on the mount anchor, plus an HTML label with the + * surface's name. Used by the surface views when `showSurface` is enabled. + */ +import { Html, Line } from '@react-three/drei'; +import type { Surface } from '@tts/bgm'; +import { MM_TO_WORLD } from '../part.js'; + +export function SurfaceBounds({ surface }: { surface: Surface }) { + const [w, h] = surface.size ?? [0, 0]; + const hw = (w * MM_TO_WORLD) / 2; + const hh = (h * MM_TO_WORLD) / 2; + const points: [number, number, number][] = [ + [-hw, 0, -hh], + [hw, 0, -hh], + [hw, 0, hh], + [-hw, 0, hh], + [-hw, 0, -hh], + ]; + + return ( + + {surface.size && } + +
+ {surface.type}#{surface.id} +
+ +
+ ); +} \ No newline at end of file diff --git a/packages/tabletop/src/surfaces/SurfacePlane.tsx b/packages/tabletop/src/surfaces/SurfacePlane.tsx new file mode 100644 index 0000000..64feb20 --- /dev/null +++ b/packages/tabletop/src/surfaces/SurfacePlane.tsx @@ -0,0 +1,24 @@ +/** + * `SurfacePlane` — a visible table surface. + * + * A flat rectangle at the surface's reference `size` (mm, converted to world + * units), centered on the mount anchor. Gives the parts a scale reference and + * reads as the table the game is played on. Rendered only when `showSurface` + * is enabled. + */ +import type { Surface } from '@tts/bgm'; +import { MM_TO_WORLD } from '../part.js'; + +export function SurfacePlane({ surface }: { surface: Surface }) { + if (!surface.size) return null; + const [w, h] = surface.size; + const width = w * MM_TO_WORLD; + const height = h * MM_TO_WORLD; + + return ( + + + + + ); +} \ No newline at end of file diff --git a/packages/tabletop/src/surfaces/WorldSurfaceView.tsx b/packages/tabletop/src/surfaces/WorldSurfaceView.tsx index e2b6dd0..492b1f1 100644 --- a/packages/tabletop/src/surfaces/WorldSurfaceView.tsx +++ b/packages/tabletop/src/surfaces/WorldSurfaceView.tsx @@ -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 ; +export function WorldSurfaceView({ + pkg, + node, + showSurface = false, +}: { + pkg: Package; + node: MountNode; + showSurface?: boolean; +}) { + return ; } /** 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 ( - + + {showSurface && ( + <> + + + + )} {own.map((p) => ( ))} {node.children.map((child) => ( - + ))} );