Add 3D object viewers with r3f stack

Add per-class 3D viewers for tiles, tokens, cards, and custom models
using React Three Fiber, drei, and postprocessing. Viewers are
lazy-loaded and registered through the existing viewer registry, with a
shared scene wrapper for lighting, orbit controls, and subtle effects.

Add a CORS-safe /asset proxy route so three.js loaders can fetch
Workshop-hosted textures and models, and extend TTSObject with the
CustomMesh and CustomTile/CustomToken fields the viewers read.
This commit is contained in:
2026-08-08 12:56:46 +08:00
parent f390f170da
commit 001d5eeb54
18 changed files with 1037 additions and 14 deletions
+50
View File
@@ -0,0 +1,50 @@
import { Suspense, type ReactNode } from 'react';
import { Canvas } from '@react-three/fiber';
import { ContactShadows, OrbitControls } from '@react-three/drei';
import { Bloom, EffectComposer, Vignette } from '@react-three/postprocessing';
/**
* Shared 3D scene wrapper for object viewers. Provides a consistent camera,
* lighting, orbit controls, a soft contact shadow, and subtle post-processing
* (bloom + vignette). Children are wrapped in a Suspense boundary so loading
* assets (textures, models) can suspend without blanking the page.
*/
export default function Scene({ children }: { children: ReactNode }) {
return (
<div className="h-80 w-full overflow-hidden rounded-lg bg-linear-to-b from-zinc-900 to-zinc-950">
<Canvas
camera={{ position: [2.2, 1.8, 2.6], fov: 40 }}
dpr={[1, 2]}
gl={{ antialias: true }}
>
<ambientLight intensity={0.5} />
<directionalLight position={[4, 6, 3]} intensity={1.4} />
<directionalLight position={[-4, 2, -3]} intensity={0.4} color="#b3c7ff" />
<pointLight position={[0, 3, 0]} intensity={0.3} />
<Suspense fallback={null}>{children}</Suspense>
<ContactShadows
position={[0, -0.5, 0]}
opacity={0.55}
scale={8}
blur={2.4}
far={3}
resolution={256}
/>
<OrbitControls
enablePan={false}
minDistance={1}
maxDistance={8}
autoRotate
autoRotateSpeed={1.2}
/>
<EffectComposer>
<Bloom intensity={0.25} luminanceThreshold={0.85} mipmapBlur />
<Vignette eskil={false} offset={0.25} darkness={0.6} />
</EffectComposer>
</Canvas>
</div>
);
}