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 { 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. * * The camera is fitted to the bounds of the content on mount. `Bounds` sits * inside the Suspense boundary, so it only mounts once the (suspending) content * has loaded and its geometry is present. * * `autoRotate` and `enablePan` tune the orbit controls (a tabletop view, for * example, pans instead of rotating). `fullscreen` adds a toggle button that * expands the scene to the full screen. `maxPolarAngle` (radians) clamps how * far the camera can tilt below the horizon, e.g. to stop a tabletop view from * peeking under face-down cards. */ export default function Scene({ children, autoRotate = true, enablePan = false, fullscreen = false, overlay, shadowScale = 22, maxPolarAngle = Math.PI, }: { children: ReactNode; autoRotate?: boolean; enablePan?: boolean; 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; /** Max camera polar angle in radians; defaults to unrestricted (π). */ maxPolarAngle?: number; }) { const containerRef = useRef(null); const [isFullscreen, setIsFullscreen] = useState(false); useEffect(() => { const onChange = () => setIsFullscreen(!!document.fullscreenElement); document.addEventListener('fullscreenchange', onChange); return () => document.removeEventListener('fullscreenchange', onChange); }, []); const toggleFullscreen = () => { if (document.fullscreenElement) void document.exitFullscreen(); else void containerRef.current?.requestFullscreen(); }; return (
{fullscreen && ( )} {overlay} {children}
); } /** * A loading overlay shown while assets (textures, models, traces) are being * fetched. Reads drei's global progress store, which tracks every loader in * the scene, so it works outside the Canvas. Hidden once loading completes. */ function LoadingOverlay() { const { active, progress, item, loaded, total } = useProgress(); if (!active) return null; return (
Loading assets… {Math.round(progress)}%
{loaded}/{total} {item}
); }