From 5a3a9c1fcc1d39b14f8a71536012051aae0ad19b Mon Sep 17 00:00:00 2001 From: hypercross Date: Mon, 10 Aug 2026 00:57:32 +0800 Subject: [PATCH] feat(web): lighten tabletop scene and tune contact shadows Replace the dark surface plane with a grey background and a spotlight, size the contact shadow to the table, and scale its blur and fade with the shadow plane. Disable depth test on the stacking curve overlay. --- .../src/components/tabletop/TabletopScene.tsx | 16 +++++++++++++ apps/web/src/components/viewers/Scene.tsx | 23 ++++++++++-------- packages/tabletop/src/index.ts | 2 +- .../tabletop/src/surfaces/StackingCurve.tsx | 4 ++-- .../tabletop/src/surfaces/SurfacePlane.tsx | 24 ------------------- .../src/surfaces/WorldSurfaceView.tsx | 2 -- 6 files changed, 32 insertions(+), 39 deletions(-) delete mode 100644 packages/tabletop/src/surfaces/SurfacePlane.tsx diff --git a/apps/web/src/components/tabletop/TabletopScene.tsx b/apps/web/src/components/tabletop/TabletopScene.tsx index 734716f..b89c534 100644 --- a/apps/web/src/components/tabletop/TabletopScene.tsx +++ b/apps/web/src/components/tabletop/TabletopScene.tsx @@ -8,6 +8,7 @@ import { resolveMountTree, serializedToPackage, useTabletopStore, + MM_TO_WORLD, } from '@tts/tabletop'; import Scene from '../viewers/Scene'; @@ -35,11 +36,26 @@ export default function TabletopScene({ [packageData, surfaces], ); + // Size the contact shadow to cover 2x the largest enabled surface, so the + // shadow plane always extends past the table. Surface sizes are in mm; + // `MM_TO_WORLD` converts to world units. + const shadowScale = useMemo(() => { + let maxMm = 0; + for (const [id, enabled] of Object.entries(surfaces)) { + if (!enabled) continue; + const surface = packageData.surfaces.get(id); + if (!surface?.size) continue; + maxMm = Math.max(maxMm, ...surface.size); + } + return maxMm > 0 ? maxMm * 2 * MM_TO_WORLD : 22; + }, [packageData, surfaces]); + return ( setShowSurface((v) => !v)} diff --git a/apps/web/src/components/viewers/Scene.tsx b/apps/web/src/components/viewers/Scene.tsx index fc826d7..c2d0dd1 100644 --- a/apps/web/src/components/viewers/Scene.tsx +++ b/apps/web/src/components/viewers/Scene.tsx @@ -2,7 +2,7 @@ import { Suspense, useEffect, useRef, useState, type ReactNode } from 'react'; import { Icon } from '@iconify/react'; import { Canvas } from '@react-three/fiber'; import { Bounds, ContactShadows, OrbitControls, useProgress } from '@react-three/drei'; -import { Bloom, EffectComposer, Vignette } from '@react-three/postprocessing'; +import { EffectComposer, Vignette } from '@react-three/postprocessing'; /** * Shared 3D scene wrapper for object viewers. Provides a consistent camera, @@ -24,6 +24,7 @@ export default function Scene({ enablePan = false, fullscreen = false, overlay, + shadowScale = 22, }: { children: ReactNode; autoRotate?: boolean; @@ -31,6 +32,8 @@ export default function Scene({ fullscreen?: boolean; /** HTML rendered inside the scene container (e.g. controls), shown in fullscreen. */ overlay?: ReactNode; + /** Contact shadow plane size in world units; defaults to a generous 22. */ + shadowScale?: number; }) { const containerRef = useRef(null); const [isFullscreen, setIsFullscreen] = useState(false); @@ -49,7 +52,7 @@ export default function Scene({ return (
{fullscreen && ( @@ -66,7 +69,7 @@ export default function Scene({ @@ -78,12 +81,13 @@ export default function Scene({ - diff --git a/packages/tabletop/src/index.ts b/packages/tabletop/src/index.ts index 0a3f879..5b8669a 100644 --- a/packages/tabletop/src/index.ts +++ b/packages/tabletop/src/index.ts @@ -8,6 +8,7 @@ export { fallbackShape, traceToShape, traceToUvBounds, + MM_TO_WORLD, } from './part.js'; export { resolveAssetUrl, assetUrl, traceImage } from './http.js'; @@ -34,7 +35,6 @@ 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/surfaces/StackingCurve.tsx b/packages/tabletop/src/surfaces/StackingCurve.tsx index 3db7f4a..d02c832 100644 --- a/packages/tabletop/src/surfaces/StackingCurve.tsx +++ b/packages/tabletop/src/surfaces/StackingCurve.tsx @@ -29,11 +29,11 @@ export function StackingCurve({ route }: { route: Route }) { return ( - + {/* Marker at the curve's start, showing its direction. */} - + ); diff --git a/packages/tabletop/src/surfaces/SurfacePlane.tsx b/packages/tabletop/src/surfaces/SurfacePlane.tsx deleted file mode 100644 index 64feb20..0000000 --- a/packages/tabletop/src/surfaces/SurfacePlane.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/** - * `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 bca13fe..3b7af7d 100644 --- a/packages/tabletop/src/surfaces/WorldSurfaceView.tsx +++ b/packages/tabletop/src/surfaces/WorldSurfaceView.tsx @@ -12,7 +12,6 @@ import { PartPlacement } from '../placement.js'; import type { MountNode } from '../mount.js'; import { MM_TO_WORLD, DEG_TO_RAD } from '../part.js'; import { SurfaceBounds } from './SurfaceBounds.js'; -import { SurfacePlane } from './SurfacePlane.js'; import { StackingCurve } from './StackingCurve.js'; export function WorldSurfaceView({ @@ -47,7 +46,6 @@ export function SurfaceNode({ > {showSurface && ( <> - {node.surface.layout.map((route, i) => route.stacking ? : null,