File size: 19,254 Bytes
0fffd55
cad548d
ad2f294
 
0fffd55
ad2f294
0fffd55
 
cad548d
5b9b5c7
 
0fffd55
 
cad548d
 
0fffd55
 
 
 
 
 
 
 
 
 
 
 
cad548d
 
 
 
0fffd55
 
 
 
 
 
 
 
 
afd2779
 
0fffd55
 
 
 
afd2779
 
 
 
b690ac0
 
b7eb43c
 
0fffd55
b690ac0
 
 
0fffd55
b7eb43c
b690ac0
0fffd55
b690ac0
0fffd55
b690ac0
0fffd55
b690ac0
0fffd55
b7eb43c
 
0fffd55
b7eb43c
 
 
 
b690ac0
 
 
b7eb43c
5b9b5c7
ad2f294
cad548d
 
b690ac0
0fffd55
ad2f294
 
cad548d
 
b7eb43c
ad2f294
 
cad548d
ad2f294
 
 
 
 
 
 
cad548d
 
 
 
b690ac0
0fffd55
ad2f294
cad548d
0fffd55
 
 
 
b690ac0
0fffd55
cad548d
 
 
 
b690ac0
5b9b5c7
 
b690ac0
 
5b9b5c7
ad2f294
5b9b5c7
b690ac0
72b08e2
ad2f294
 
 
5b9b5c7
 
 
 
 
b690ac0
0fffd55
 
 
 
 
ad2f294
5b9b5c7
 
 
b690ac0
 
39353da
5b9b5c7
 
39353da
 
0fffd55
 
39353da
5b9b5c7
b690ac0
0fffd55
5b9b5c7
b690ac0
 
0fffd55
5b9b5c7
b690ac0
5b9b5c7
b690ac0
 
5b9b5c7
 
39353da
 
0fffd55
 
39353da
 
5b9b5c7
 
 
 
 
 
 
0fffd55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b690ac0
 
5b9b5c7
b690ac0
 
0fffd55
b690ac0
 
0fffd55
 
 
 
 
 
ad2f294
5b9b5c7
 
 
 
b690ac0
ad2f294
b690ac0
 
0fffd55
ad2f294
5b9b5c7
ad2f294
5b9b5c7
 
0fffd55
ad2f294
0fffd55
5b9b5c7
 
b690ac0
0fffd55
5b9b5c7
0fffd55
ad2f294
 
 
 
 
0fffd55
 
ad2f294
 
 
 
b690ac0
ad2f294
 
 
 
 
0fffd55
b690ac0
 
 
 
 
0fffd55
 
b690ac0
0fffd55
5b9b5c7
 
 
 
b6aaffe
 
 
b690ac0
0fffd55
 
 
5b9b5c7
0fffd55
b6aaffe
0fffd55
 
 
 
 
 
 
 
 
 
 
 
 
5b9b5c7
 
cad548d
ad2f294
0fffd55
 
ad2f294
 
 
 
 
 
 
 
0fffd55
 
 
 
ad2f294
b7eb43c
0fffd55
 
 
 
b690ac0
 
0fffd55
ad2f294
 
b690ac0
0fffd55
 
5b9b5c7
 
b7eb43c
 
cad548d
ad2f294
0fffd55
 
ad2f294
0fffd55
 
 
ad2f294
 
 
 
b7eb43c
cad548d
ad2f294
5b9b5c7
 
ad2f294
0fffd55
ad2f294
 
5b9b5c7
0fffd55
5b9b5c7
b690ac0
 
5b9b5c7
afd2779
b690ac0
 
5b9b5c7
b690ac0
0fffd55
b690ac0
 
 
5b9b5c7
b690ac0
ad2f294
b690ac0
5b9b5c7
b690ac0
ad2f294
5b9b5c7
cad548d
 
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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
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 <ambientLight    key={l.id} color={col} intensity={int} />
        if (l.type==='hemisphere')  return <hemisphereLight key={l.id} args={[l.skyColor||'#88aaff',l.groundColor||'#443322',int]} />
        if (l.type==='directional') return <directionalLight key={l.id} color={col} intensity={int} position={pos} castShadow={!!l.castShadow} shadow-mapSize={[sms,sms]} shadow-camera-near={0.1} shadow-camera-far={200} shadow-camera-left={-30} shadow-camera-right={30} shadow-camera-top={30} shadow-camera-bottom={-30} />
        if (l.type==='point')  return <pointLight key={l.id} color={col} intensity={int} position={pos} distance={l.distance||0} castShadow={!!l.castShadow} shadow-mapSize={[sms,sms]} />
        if (l.type==='spot')   return <spotLight  key={l.id} color={col} intensity={int} position={pos} angle={l.angle||0.4} penumbra={l.penumbra||0.2} distance={l.distance||0} castShadow={!!l.castShadow} shadow-mapSize={[sms,sms]} />
        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 (
    <>
      <ambientLight intensity={cfg.amb[0]} color={cfg.amb[1]} />
      <directionalLight position={cfg.key.p}  intensity={cfg.key.i}  color={cfg.key.c}  castShadow shadow-mapSize={[2048,2048]} shadow-camera-near={0.1} shadow-camera-far={100} shadow-camera-left={-20} shadow-camera-right={20} shadow-camera-top={20} shadow-camera-bottom={-20} />
      <directionalLight position={cfg.fill.p} intensity={cfg.fill.i} color={cfg.fill.c} />
      <directionalLight position={cfg.rim.p}  intensity={cfg.rim.i}  color={cfg.rim.c}  />
    </>
  )
}

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 <Environment preset={map[preset]||'studio'} background={!!skybox.showBg} blur={0.6} />
}

function Floor() {
  const isRender = useStore(s => s.isRenderMode||s.isExporting)
  const showGrid = useStore(s => s.showGrid)
  const showCS   = useStore(s => s.showContactShadows)
  return (
    <>
      <mesh receiveShadow rotation={[-Math.PI/2,0,0]} position={[0,-0.01,0]}>
        <planeGeometry args={[200,200]} />
        <meshStandardMaterial color="#0d0d1a" roughness={0.95} />
      </mesh>
      {!isRender && showGrid && (
        <Grid args={[40,40]} cellSize={1} cellThickness={0.4} cellColor="#1a1a3a"
          sectionSize={5} sectionThickness={1} sectionColor="#2a2a5a" fadeDistance={50} />
      )}
      {!isRender && showCS && (
        <ContactShadows opacity={0.4} scale={30} blur={2} far={5} color="#000033" />
      )}
    </>
  )
}

function Deselect() {
  const isRender = useStore(s => s.isRenderMode||s.isExporting)
  if (isRender) return null
  return (
    <mesh position={[0,-500,0]} onClick={() => {
      useStore.getState().selectModel(null)
      useStore.getState().selectCamera?.(null)
    }}>
      <planeGeometry args={[5000,5000]} />
      <meshBasicMaterial transparent opacity={0} depthWrite={false} />
    </mesh>
  )
}

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 (
          <group key={cam.id} position={pos}
            onClick={e => {
              e.stopPropagation()
              useStore.getState().setActiveCameraId?.(cam.id)
              useStore.getState().selectCamera?.(cam.id)
            }}>
            <mesh>
              <boxGeometry args={[0.25,0.18,0.32]} />
              <meshStandardMaterial color={isAct?'#4f8eff':'#666'} emissive={isAct?'#4f8eff':'#000'} emissiveIntensity={isAct?0.3:0} roughness={0.3} metalness={0.7} />
            </mesh>
            <mesh position={[0,0,-0.2]}>
              <cylinderGeometry args={[0.06,0.09,0.1,10]} />
              <meshStandardMaterial color={isAct?'#00e5ff':'#333'} emissive={isAct?'#00e5ff':'#000'} emissiveIntensity={0.3} />
            </mesh>
            {isAct && (
              <mesh position={[0,0,-0.5]} rotation={[Math.PI/2,0,0]}>
                <coneGeometry args={[0.45,0.9,4,1,true]} />
                <meshBasicMaterial color="#4f8eff" wireframe transparent opacity={0.2} />
              </mesh>
            )}
            {isSel && (
              <mesh rotation={[-Math.PI/2,0,0]}>
                <ringGeometry args={[0.32,0.42,32]} />
                <meshBasicMaterial color="#4f8eff" transparent opacity={0.85} side={THREE.DoubleSide} depthWrite={false} />
              </mesh>
            )}
          </group>
        )
      })}
    </>
  )
}

// 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 ? <group ref={groupRef} position={cam.position||[0,2,5]} /> : null
  )

  return (
    <group ref={groupRef} position={cam.position||[0,2,5]}>
      {groupRef.current && (
        <TransformControls
          key={`camtc-${selId}-${mode}`}
          object={groupRef.current}
          mode={mode}
          onChange={onChange}
          size={0.75}
        />
      )}
    </group>
  )
}

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 (
    <OrbitControls
      ref={orbitControlsRef}
      makeDefault
      enableDamping dampingFactor={0.07}
      rotateSpeed={0.8}
      zoomSpeed={1.4}
      panSpeed={0.9}
      minDistance={0.02}
      maxDistance={1000}
      minPolarAngle={0}
      maxPolarAngle={Math.PI}
      enablePan screenSpacePanning
      mouseButtons={{ LEFT:THREE.MOUSE.ROTATE, MIDDLE:THREE.MOUSE.DOLLY, RIGHT:THREE.MOUSE.PAN }}
      touches={{ ONE:THREE.TOUCH.ROTATE, TWO:THREE.TOUCH.DOLLY_PAN }}
    />
  )
}

function EditorGizmo() {
  const isRend=useStore(s=>s.isRenderMode||s.isExporting), show=useStore(s=>s.showGizmo)
  if (isRend||!show) return null
  return (
    <GizmoHelper alignment="bottom-right" margin={[72,80]}>
      <GizmoViewport axisColors={['#ff4060','#40ff80','#4080ff']} labelColor="#fff" />
    </GizmoHelper>
  )
}

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 (
    <div style={{position:'absolute',inset:0,pointerEvents:'none',border:'2px solid rgba(79,142,255,0.5)'}}>
      {[{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)=><div key={i} style={{position:'absolute',width:18,height:18,...c}}/>)}
      <div style={{position:'absolute',top:10,left:'50%',transform:'translateX(-50%)',background:'rgba(8,8,20,0.75)',border:'1px solid rgba(79,142,255,0.4)',borderRadius:4,padding:'3px 12px',fontSize:11,color:'rgba(79,142,255,0.9)',fontFamily:'var(--font-mono)',backdropFilter:'blur(4px)',whiteSpace:'nowrap'}}>
        🎥 {cam.name} · {cam.fov||50}°
      </div>
      <div style={{position:'absolute',top:'50%',left:'50%',transform:'translate(-50%,-50%)',width:16,height:16,pointerEvents:'none'}}>
        <div style={{position:'absolute',top:'50%',left:0,right:0,height:1,background:'rgba(79,142,255,0.5)'}}/>
        <div style={{position:'absolute',left:'50%',top:0,bottom:0,width:1,background:'rgba(79,142,255,0.5)'}}/>
      </div>
      <div style={{position:'absolute',bottom:10,left:'50%',transform:'translateX(-50%)',fontSize:9,color:'rgba(79,142,255,0.5)',fontFamily:'var(--font-mono)',background:'rgba(0,0,0,0.4)',padding:'2px 10px',borderRadius:3,whiteSpace:'nowrap'}}>
        WASD move · Q/E up/down · drag look · scroll speed
      </div>
    </div>
  )
}

function RenderIndicator() {
  const isRend=useStore(s=>s.isRenderMode||s.isExporting), pct=useStore(s=>s.exportProgress)
  if(!isRend)return null
  return (
    <div style={{position:'absolute',top:10,left:'50%',transform:'translateX(-50%)',background:'rgba(239,68,68,0.15)',border:'1px solid rgba(239,68,68,0.4)',borderRadius:6,padding:'5px 16px',pointerEvents:'none',display:'flex',alignItems:'center',gap:8,zIndex:100}}>
      <div style={{width:8,height:8,borderRadius:'50%',background:'var(--danger)',animation:'pulse 1s ease infinite'}}/>
      <span style={{fontSize:12,fontWeight:700,color:'var(--danger)',fontFamily:'var(--font-mono)'}}>RENDERING {pct>0?`${pct}%`:''}</span>
    </div>
  )
}

export default function Scene({ canvasRef }) {
  return (
    <div style={{position:'absolute',inset:0}}>
      <Canvas
        shadows
        dpr={[1,Math.min(typeof window!=='undefined'?window.devicePixelRatio:1,2)]}
        gl={{ antialias:true, preserveDrawingBuffer:true, outputColorSpace:THREE.SRGBColorSpace, toneMapping:THREE.ACESFilmicToneMapping, toneMappingExposure:1.2, powerPreference:'high-performance', failIfMajorPerformanceCaveat:false }}
        style={{width:'100%',height:'100%',display:'block'}}
        onCreated={({gl})=>{gl.domElement.style.touchAction='none'}}
      >
        <PerspectiveCamera makeDefault position={[5,3,5]} fov={50} near={0.01} far={2000} />
        <Suspense fallback={null}>
          <SkyboxApplier />
          <PresetEnv />
          <LightingRig />
          <SceneLights />
          <Floor />
          <Deselect />
          <ModelManager />
          <CameraMarkers />
          <CameraGizmo />
          <CamSync />
          <FlyControls />
          <Playback />
        </Suspense>
        <OrbitCam />
        <EditorGizmo />
        <PhysicsEngine />
      </Canvas>
      <CamHUD />
      <RenderIndicator />
    </div>
  )
}