import { useRef, useEffect, Suspense, useCallback } from 'react' import { Canvas, useFrame, useThree } from '@react-three/fiber' import { OrbitControls, Environment, Grid, GizmoHelper, GizmoViewport, ContactShadows, PerspectiveCamera, TransformControls, } from '@react-three/drei' import * as THREE from 'three' import useStore from '../store/useStore' import ModelManager from './ModelManager' import PhysicsEngine from './PhysicsEngine' function SceneLights() { const lights = useStore(s => s.sceneLights) || [] return ( <> {lights.filter(l => l?.visible).map(l => { const pos = l.position || [0,5,0] const col = l.color || '#ffffff' const int = l.intensity ?? 1 const sms = l.shadowMapSize || 1024 if (l.type==='ambient') return if (l.type==='hemisphere') return if (l.type==='directional') return if (l.type==='point') return if (l.type==='spot') return return null })} ) } function LightingRig() { const preset = useStore(s => s.lightingPreset) || 'studio' const cfgs = { studio: { amb:[0.4,'#fff8f0'], key:{p:[5,8,3], i:2,c:'#fff5e0'}, fill:{p:[-4,4,-2],i:0.6,c:'#c0d8ff'}, rim:{p:[0,6,-6], i:0.8,c:'#ffefcc'} }, outdoor: { amb:[0.6,'#b0c8ff'], key:{p:[10,20,5], i:3,c:'#fff8e1'}, fill:{p:[-8,5,-3],i:0.4,c:'#d0e8ff'}, rim:{p:[0,8,-8], i:0.3,c:'#90b8ff'} }, dramatic: { amb:[0.1,'#1a1a2e'], key:{p:[3,10,2], i:4,c:'#ff6020'}, fill:{p:[-6,2,-2],i:0.2,c:'#200840'}, rim:{p:[0,4,-8], i:1.2,c:'#8040ff'} }, neon: { amb:[0.15,'#0a0020'],key:{p:[5,6,3], i:2,c:'#00ffff'}, fill:{p:[-5,3,-3],i:1.5,c:'#ff00aa'}, rim:{p:[0,8,-6], i:1.0,c:'#aaff00'} }, } const cfg = cfgs[preset] || cfgs.studio return ( <> ) } function SkyboxApplier() { const skybox = useStore(s => s.skybox) || {} const { scene, gl } = useThree() useEffect(() => { if (!skybox.showBg || skybox.type==='color' || !skybox.value) { scene.background = new THREE.Color(skybox.bgColor || '#080810') return } let disposed=false, tex=null const apply = async () => { try { if (skybox.type==='hdr') { const { RGBELoader } = await import('three/examples/jsm/loaders/RGBELoader.js') new RGBELoader().load(skybox.value, t => { if(disposed)return t.dispose(); t.mapping=THREE.EquirectangularReflectionMapping; scene.background=scene.environment=tex=t }) } else { new THREE.TextureLoader().load(skybox.value, t => { if(disposed)return t.dispose(); t.mapping=THREE.EquirectangularReflectionMapping; t.colorSpace=THREE.SRGBColorSpace; scene.background=scene.environment=tex=t }) } } catch(e) { console.warn('[Skybox]',e) } } apply() return () => { disposed=true; tex?.dispose() } }, [skybox, scene, gl]) return null } function PresetEnv() { const skybox = useStore(s => s.skybox) || {} const preset = useStore(s => s.lightingPreset) || 'studio' if (skybox.type !== 'preset') return null const map = { studio:'studio', outdoor:'park', dramatic:'night', neon:'warehouse' } return } function Floor() { const isRender = useStore(s => s.isRenderMode||s.isExporting) const showGrid = useStore(s => s.showGrid) const showCS = useStore(s => s.showContactShadows) return ( <> {!isRender && showGrid && ( )} {!isRender && showCS && ( )} ) } function Deselect() { const isRender = useStore(s => s.isRenderMode||s.isExporting) if (isRender) return null return ( { useStore.getState().selectModel(null) useStore.getState().selectCamera?.(null) }}> ) } function Playback() { const acc = useRef(0) useFrame((_, delta) => { const s = useStore.getState() if (!s.isPlaying) return acc.current += delta if (acc.current >= 1/(s.fps||30)) { acc.current = 0 const next = s.currentFrame + 1 if (next >= s.totalFrames) { if (s.loopPlayback) s.setCurrentFrame(0) else { s.setIsPlaying(false); s.setCurrentFrame(0) } } else s.setCurrentFrame(next) } }) return null } function CameraMarkers() { const cameras = useStore(s => s.cameras) || [] const activeCamId = useStore(s => s.activeCameraId) const selectedCamId = useStore(s => s.selectedCameraId) const isRender = useStore(s => s.isRenderMode||s.isExporting) const showCams = useStore(s => s.showCameraObjects) if (isRender || !showCams) return null return ( <> {cameras.map(cam => { const pos = cam.position || [0,2,5] const isAct = cam.id === activeCamId const isSel = cam.id === selectedCamId return ( { e.stopPropagation() useStore.getState().setActiveCameraId?.(cam.id) useStore.getState().selectCamera?.(cam.id) }}> {isAct && ( )} {isSel && ( )} ) })} ) } // Camera move/rotate gizmo — proxy group drives TransformControls function CameraGizmo() { const selId = useStore(s => s.selectedCameraId) const mode = useStore(s => s.cameraTransformMode) || 'translate' const cameras = useStore(s => s.cameras) || [] const inView = useStore(s => s.inCameraView) const isRend = useStore(s => s.isRenderMode||s.isExporting) const update = useStore(s => s.updateCamera) const groupRef = useRef() const cam = cameras.find(c => c.id === selId) useEffect(() => { if (!groupRef.current || !cam) return const p = cam.position || [0,2,5] groupRef.current.position.set(p[0],p[1],p[2]) groupRef.current.rotation.set(0,0,0) }, [selId, cam?.position?.toString()]) const onChange = useCallback(() => { if (!groupRef.current || !selId) return const p = groupRef.current.position const q = groupRef.current.quaternion if (mode === 'translate') { update(selId, { position:[p.x,p.y,p.z] }) } else { const dir = new THREE.Vector3(0,0,-1).applyQuaternion(q).multiplyScalar(5).add(p) update(selId, { target:[dir.x,dir.y,dir.z] }) } }, [selId, mode, update]) if (!cam || inView || isRend || !groupRef) return ( cam && !inView && !isRend ? : null ) return ( {groupRef.current && ( )} ) } function CamSync() { const { camera } = useThree() useFrame(() => { const s = useStore.getState() if (!s.inCameraView || !s.activeCameraId) return const cam = (s.cameras||[]).find(c => c.id===s.activeCameraId) if (!cam) return const interp = interpolateCamKF(s.activeCameraId, s.currentFrame, s.keyframes) const src = interp || cam const pos = src.position || [5,3,5] const tgt = src.target || [0,0,0] const fov = src.fov || 50 camera.position.set(pos[0]||5,pos[1]||3,pos[2]||5) camera.lookAt(tgt[0]||0,tgt[1]||0,tgt[2]||0) if (Math.abs(camera.fov-fov)>0.1) { camera.fov=fov; camera.updateProjectionMatrix() } }) return null } function interpolateCamKF(camId, frame, keyframes) { if (!keyframes||!camId) return null const key = `__cam_${camId}__` const keys = Object.entries(keyframes) .filter(([,v])=>v?.[key]).map(([f,v])=>({frame:parseInt(f),data:v[key]})) .sort((a,b)=>a.frame-b.frame) if (!keys.length) return null const before=keys.filter(k=>k.frame<=frame), after=keys.filter(k=>k.frame>frame) if (!before.length) return keys[0].data if (!after.length) return keys[keys.length-1].data const k0=before[before.length-1],k1=after[0],t=(frame-k0.frame)/(k1.frame-k0.frame) const lv=(a,b,t)=>(a||[0,0,0]).map((v,i)=>v+((b||[0,0,0])[i]-v)*t) return {position:lv(k0.data.position,k1.data.position,t),target:lv(k0.data.target,k1.data.target,t),fov:(k0.data.fov||50)+((k1.data.fov||50)-(k0.data.fov||50))*t} } function FlyControls() { const inView=useStore(s=>s.inCameraView), actId=useStore(s=>s.activeCameraId) const { camera, gl } = useThree() const keys=useRef({}),drag=useRef({on:false,lx:0,ly:0}),yaw=useRef(0.5),pitch=useRef(-0.3),spd=useRef(0.08) useEffect(()=>{ if(!inView||!actId) return const kd=e=>{keys.current[e.code]=true},ku=e=>{keys.current[e.code]=false} const md=e=>{drag.current={on:true,lx:e.clientX,ly:e.clientY}},mu=()=>{drag.current.on=false} const mm=e=>{if(!drag.current.on)return;yaw.current-=(e.clientX-drag.current.lx)*0.003;pitch.current=Math.max(-1.4,Math.min(1.4,pitch.current-(e.clientY-drag.current.ly)*0.003));drag.current.lx=e.clientX;drag.current.ly=e.clientY} const mw=e=>{spd.current=Math.max(0.005,Math.min(5,spd.current*(1-e.deltaY*0.001)))} const ts=e=>{if(e.touches.length===1)drag.current={on:true,lx:e.touches[0].clientX,ly:e.touches[0].clientY}} const tm=e=>{if(e.touches.length===1&&drag.current.on){yaw.current-=(e.touches[0].clientX-drag.current.lx)*0.004;pitch.current=Math.max(-1.4,Math.min(1.4,pitch.current-(e.touches[0].clientY-drag.current.ly)*0.004));drag.current.lx=e.touches[0].clientX;drag.current.ly=e.touches[0].clientY}} const te=()=>{drag.current.on=false} window.addEventListener('keydown',kd);window.addEventListener('keyup',ku) gl.domElement.addEventListener('mousedown',md);window.addEventListener('mouseup',mu);window.addEventListener('mousemove',mm) gl.domElement.addEventListener('wheel',mw,{passive:true}) gl.domElement.addEventListener('touchstart',ts,{passive:true});gl.domElement.addEventListener('touchmove',tm,{passive:true});gl.domElement.addEventListener('touchend',te) return()=>{window.removeEventListener('keydown',kd);window.removeEventListener('keyup',ku);gl.domElement.removeEventListener('mousedown',md);window.removeEventListener('mouseup',mu);window.removeEventListener('mousemove',mm);gl.domElement.removeEventListener('wheel',mw);gl.domElement.removeEventListener('touchstart',ts);gl.domElement.removeEventListener('touchmove',tm);gl.domElement.removeEventListener('touchend',te)} },[inView,actId,gl]) useFrame(()=>{ if(!inView||!actId)return const s=spd.current,fwd=new THREE.Vector3(Math.sin(yaw.current)*Math.cos(pitch.current),Math.sin(pitch.current),Math.cos(yaw.current)*Math.cos(pitch.current)) const right=new THREE.Vector3().crossVectors(fwd,new THREE.Vector3(0,1,0)).normalize() if(keys.current.KeyW||keys.current.ArrowUp) camera.position.addScaledVector(fwd,s) if(keys.current.KeyS||keys.current.ArrowDown) camera.position.addScaledVector(fwd,-s) if(keys.current.KeyA||keys.current.ArrowLeft) camera.position.addScaledVector(right,-s) if(keys.current.KeyD||keys.current.ArrowRight) camera.position.addScaledVector(right,s) if(keys.current.KeyQ||keys.current.KeyZ)camera.position.y-=s if(keys.current.KeyE||keys.current.KeyX)camera.position.y+=s const tgt=new THREE.Vector3().copy(camera.position).addScaledVector(fwd,5) useStore.getState().updateCamera?.(actId,{position:[camera.position.x,camera.position.y,camera.position.z],target:[tgt.x,tgt.y,tgt.z]}) }) return null } // Orbit controls ref exposed globally so TC can disable it during drag export let orbitControlsRef = { current: null } function OrbitCam() { const inView = useStore(s=>s.inCameraView) const isRend = useStore(s=>s.isRenderMode||s.isExporting) if (inView||isRend) return null return ( ) } function EditorGizmo() { const isRend=useStore(s=>s.isRenderMode||s.isExporting), show=useStore(s=>s.showGizmo) if (isRend||!show) return null return ( ) } function CamHUD() { const inView=useStore(s=>s.inCameraView), actId=useStore(s=>s.activeCameraId) const cameras=useStore(s=>s.cameras)||[], isRend=useStore(s=>s.isRenderMode||s.isExporting) const cam=cameras.find(c=>c.id===actId) if(!inView||!cam||isRend) return null const bl='2px solid rgba(79,142,255,0.8)' return (
{[{top:8,left:8,borderTop:bl,borderLeft:bl},{top:8,right:8,borderTop:bl,borderRight:bl},{bottom:8,left:8,borderBottom:bl,borderLeft:bl},{bottom:8,right:8,borderBottom:bl,borderRight:bl}] .map((c,i)=>
)}
🎥 {cam.name} · {cam.fov||50}°
WASD move · Q/E up/down · drag look · scroll speed
) } function RenderIndicator() { const isRend=useStore(s=>s.isRenderMode||s.isExporting), pct=useStore(s=>s.exportProgress) if(!isRend)return null return (
RENDERING {pct>0?`${pct}%`:''}
) } export default function Scene({ canvasRef }) { return (
{gl.domElement.style.touchAction='none'}} >
) }