Spaces:
Sleeping
Sleeping
File size: 3,999 Bytes
d325ede | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | /**
* ModelViewer3D - Interactive 3D model viewer using React Three Fiber.
*/
import { Suspense, useEffect } from 'react';
import { Canvas, useFrame, useThree } from '@react-three/fiber';
import { useGLTF, OrbitControls, Html, Loader, Environment, ContactShadows } from '@react-three/drei';
import * as THREE from 'three';
import { motion } from 'framer-motion';
import type { LightPreset } from '../types';
interface ModelViewer3DProps {
url: string;
wireframe: boolean;
lightPreset: LightPreset;
autoRotate: boolean;
}
function AutoScaledModel({ url, wireframe }: { url: string; wireframe: boolean }) {
const { scene } = useGLTF(url);
useEffect(() => {
const box = new THREE.Box3().setFromObject(scene);
const center = new THREE.Vector3();
box.getCenter(center);
scene.position.sub(center);
const size = new THREE.Vector3();
box.getSize(size);
const maxDim = Math.max(size.x, size.y, size.z);
const scale = maxDim > 0 ? 2 / maxDim : 1;
scene.scale.setScalar(scale);
}, [scene]);
useEffect(() => {
scene.traverse((child) => {
if ((child as THREE.Mesh).isMesh) {
const mesh = child as THREE.Mesh;
if (Array.isArray(mesh.material)) {
mesh.material.forEach((m) => { (m as THREE.Material & { wireframe: boolean }).wireframe = wireframe; });
} else {
(mesh.material as THREE.Material & { wireframe: boolean }).wireframe = wireframe;
}
}
});
}, [wireframe, scene]);
return <primitive object={scene} />;
}
function SceneLighting({ preset }: { preset: LightPreset }) {
switch (preset) {
case 'studio':
return (
<>
<ambientLight intensity={0.4} />
<directionalLight position={[5, 5, 5]} intensity={1} castShadow />
<directionalLight position={[-5, 5, -5]} intensity={0.5} />
<pointLight position={[0, -5, 0]} intensity={0.3} color="#6366f1" />
</>
);
case 'outdoor':
return (
<>
<ambientLight intensity={0.6} color="#b1d4e0" />
<directionalLight position={[10, 20, 5]} intensity={1.5} color="#fff5e0" castShadow />
<hemisphereLight args={['#87ceeb', '#8B7355', 0.4]} />
</>
);
case 'dramatic':
return (
<>
<ambientLight intensity={0.1} />
<spotLight position={[5, 10, 5]} angle={0.3} penumbra={0.5} intensity={2} castShadow />
<pointLight position={[-5, 0, -5]} intensity={0.5} color="#6366f1" />
</>
);
default:
return null;
}
}
function Turntable({ autoRotate }: { autoRotate: boolean }) {
const { scene } = useThree();
useFrame((_, delta) => {
if (autoRotate) {
scene.rotation.y += delta * 0.5;
}
});
return null;
}
export default function ModelViewer3D({
url,
wireframe,
lightPreset,
autoRotate,
}: ModelViewer3DProps) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="relative w-full h-full min-h-[400px]"
>
<Canvas
camera={{ position: [0, 0, 5], fov: 50 }}
gl={{ antialias: true, preserveDrawingBuffer: true }}
dpr={[1, 2]}
shadows
>
<Suspense fallback={<Html center><Loader /></Html>}>
<Environment preset="studio" />
<SceneLighting preset={lightPreset} />
<AutoScaledModel url={url} wireframe={wireframe} />
<ContactShadows
position={[0, -1.2, 0]}
opacity={0.45}
scale={10}
blur={2.5}
far={8}
resolution={1024}
/>
<Turntable autoRotate={autoRotate} />
</Suspense>
<OrbitControls
enablePan={true}
enableZoom={true}
enableRotate={true}
minDistance={0.5}
maxDistance={10}
touches={{ ONE: THREE.TOUCH.ROTATE, TWO: THREE.TOUCH.DOLLY_PAN }}
/>
</Canvas>
</motion.div>
);
}
|