/** * LightsPanel.jsx — Scene lighting system * Add/remove/configure point lights, spot lights, directional lights, rect area lights. * All lights rendered in Scene.jsx via sceneLights store array. */ import { useState } from 'react' import useStore from '../store/useStore' const generateId = () => `light_${Math.random().toString(36).substr(2,7)}` const LIGHT_TYPES = [ { type:'point', icon:'💡', label:'Point Light', desc:'Radiates in all directions from a point' }, { type:'spot', icon:'🔦', label:'Spot Light', desc:'Cone-shaped beam, like a flashlight' }, { type:'directional', icon:'☀️', label:'Directional', desc:'Parallel rays like sunlight, infinite distance' }, { type:'ambient', icon:'🌫', label:'Ambient', desc:'Flat fill light, no shadows, no position' }, { type:'hemisphere', icon:'🌅', label:'Hemisphere', desc:'Sky/ground gradient, natural outdoor fill' }, ] const PRESET_COLORS = [ '#ffffff','#fffae0','#ffd27a','#ff9a3c','#ff4d4d', '#ff88ff','#88aaff','#00e5ff','#00ff88','#ffaa00', ] function ColorPicker({ value, onChange }) { return (
Color
{PRESET_COLORS.map(c=>(
onChange(c)} style={{ width:22, height:22, borderRadius:4, background:c, cursor:'pointer', border:`2px solid ${value===c?'var(--text0)':'rgba(255,255,255,0.12)'}`, transition:'transform 0.1s' }} onMouseEnter={e=>e.currentTarget.style.transform='scale(1.15)'} onMouseLeave={e=>e.currentTarget.style.transform='scale(1)'} /> ))}
{value}
) } function Vec3Input({ label, value, onChange, step=0.5 }) { const axes=['X','Y','Z'], colors=['#ef4444','#22c55e','#3b82f6'] const val = value || [0,0,0] return (
{label}
{axes.map((ax,i)=>(
{ax} { const v=[...val]; v[i]=parseFloat(e.target.value)||0; onChange(v) }} style={{ border:'none', background:'transparent', width:'100%', padding:'5px 4px', fontSize:10, fontFamily:'var(--font-mono)', color:'var(--text0)' }}/>
))}
) } function LightCard({ light }) { const { updateSceneLight, removeSceneLight } = useStore() const [open, setOpen] = useState(false) const upd = (props) => updateSceneLight(light.id, props) const hasPosition = !['ambient','hemisphere'].includes(light.type) const hasTarget = ['spot','directional'].includes(light.type) const hasAngle = light.type === 'spot' const hasDist = ['point','spot'].includes(light.type) const typeInfo = LIGHT_TYPES.find(t=>t.type===light.type) return (
{/* Header */}
{typeInfo?.icon}
upd({name:e.target.value})} onClick={e=>e.stopPropagation()} style={{ border:'none', background:'transparent', color:'var(--text0)', fontSize:12, fontWeight:700, width:'100%', padding:0, cursor:'text' }}/>
{typeInfo?.label}
{/* Intensity display */} {(light.intensity||1).toFixed(1)}× {/* Visible toggle */} {/* Shadow toggle */} {hasPosition && ( )}
{open && (
upd({color:c})} /> {/* Intensity */}
Intensity {(light.intensity||1).toFixed(2)}
upd({intensity:+e.target.value})} />
{/* Position */} {hasPosition && ( upd({position:v})} /> )} {/* Target/direction */} {hasTarget && ( upd({target:v})} /> )} {/* Hemisphere sky/ground colors */} {light.type==='hemisphere' && (
Sky Color
upd({skyColor:e.target.value})} style={{ width:'100%', height:32, borderRadius:'var(--radius-sm)', border:'1px solid var(--border)', cursor:'pointer' }}/>
Ground Color
upd({groundColor:e.target.value})} style={{ width:'100%', height:32, borderRadius:'var(--radius-sm)', border:'1px solid var(--border)', cursor:'pointer' }}/>
)} {/* Spot angle */} {hasAngle && (
Spot Angle {Math.round((light.angle||0.3)*(180/Math.PI))}°
upd({angle:+e.target.value})} />
Penumbra (soft edge) {(light.penumbra||0).toFixed(2)}
upd({penumbra:+e.target.value})} />
)} {/* Distance */} {hasDist && (
Distance (0 = infinite) {light.distance||0}
upd({distance:+e.target.value})} />
)} {/* Shadow map size */} {light.castShadow && hasPosition && (
Shadow Map Size
{[512,1024,2048,4096].map(sz=>( ))}
)}
)}
) } // ── Main LightsPanel ────────────────────────────────────────────────────────── export default function LightsPanel() { const { sceneLights, addSceneLight } = useStore() const addLight = (type) => { const defaults = { ambient: { intensity:0.4, color:'#ffffff', position:[0,10,0] }, point: { intensity:2, color:'#ffffff', position:[3,5,3], distance:20, castShadow:true, shadowMapSize:1024 }, spot: { intensity:3, color:'#ffffff', position:[5,10,0], target:[0,0,0], angle:0.4, penumbra:0.2, distance:30, castShadow:true, shadowMapSize:1024 }, directional: { intensity:2, color:'#fff5e0', position:[5,10,3], target:[0,0,0], castShadow:true, shadowMapSize:2048 }, hemisphere: { intensity:0.6, skyColor:'#88aaff', groundColor:'#443322', position:[0,10,0] }, } addSceneLight({ id: generateId(), type, name: LIGHT_TYPES.find(t=>t.type===type)?.label || type, visible: true, castShadow: false, ...defaults[type], }) } // Scene presets const applyPreset = (name) => { const { clearAndSet } = (() => { // build preset light sets const presets = { studio: [ { id:generateId(), type:'ambient', name:'Studio Ambient', color:'#fff8f0', intensity:0.4, visible:true, castShadow:false }, { id:generateId(), type:'directional', name:'Key Light', color:'#fff5e0', intensity:2, visible:true, castShadow:true, position:[5,8,3], target:[0,0,0], shadowMapSize:2048 }, { id:generateId(), type:'directional', name:'Fill Light', color:'#c0d8ff', intensity:0.6, visible:true, castShadow:false, position:[-4,4,-2], target:[0,0,0] }, { id:generateId(), type:'directional', name:'Rim Light', color:'#ffefcc', intensity:0.8, visible:true, castShadow:false, position:[0,6,-6], target:[0,0,0] }, ], day: [ { id:generateId(), type:'hemisphere', name:'Sky', skyColor:'#87ceeb', groundColor:'#556b2f', intensity:0.7, visible:true, castShadow:false }, { id:generateId(), type:'directional', name:'Sun', color:'#fff8e1', intensity:3, visible:true, castShadow:true, position:[10,20,5], target:[0,0,0], shadowMapSize:2048 }, ], night: [ { id:generateId(), type:'ambient', name:'Night Ambient', color:'#0a0a2e', intensity:0.15, visible:true, castShadow:false }, { id:generateId(), type:'point', name:'Street Lamp 1', color:'#ffa040', intensity:4, visible:true, castShadow:true, position:[5,5,0], distance:15, shadowMapSize:512 }, { id:generateId(), type:'point', name:'Street Lamp 2', color:'#ffa040', intensity:4, visible:true, castShadow:true, position:[-5,5,0], distance:15, shadowMapSize:512 }, { id:generateId(), type:'point', name:'Street Lamp 3', color:'#ffa040', intensity:4, visible:true, castShadow:true, position:[0,5,8], distance:15, shadowMapSize:512 }, ], traffic: [ { id:generateId(), type:'hemisphere', name:'Sky', skyColor:'#87ceeb', groundColor:'#333333', intensity:0.6, visible:true, castShadow:false }, { id:generateId(), type:'directional', name:'Sun', color:'#fff5e0', intensity:2, visible:true, castShadow:true, position:[8,15,3], target:[0,0,0], shadowMapSize:2048 }, { id:generateId(), type:'point', name:'Traffic Red', color:'#ff2020', intensity:3, visible:true, castShadow:false, position:[0,5,0], distance:8 }, { id:generateId(), type:'point', name:'Traffic Green', color:'#20ff20', intensity:3, visible:true, castShadow:false, position:[3,5,0], distance:8 }, { id:generateId(), type:'point', name:'Traffic Amber', color:'#ffaa00', intensity:3, visible:true, castShadow:false, position:[-3,5,0], distance:8 }, ], neon: [ { id:generateId(), type:'ambient', name:'Dark Ambient', color:'#0a0020', intensity:0.1, visible:true, castShadow:false }, { id:generateId(), type:'point', name:'Neon Cyan', color:'#00ffff', intensity:5, visible:true, castShadow:false, position:[5,3,0], distance:20 }, { id:generateId(), type:'point', name:'Neon Pink', color:'#ff00aa', intensity:5, visible:true, castShadow:false, position:[-5,3,0], distance:20 }, { id:generateId(), type:'point', name:'Neon Green', color:'#00ff88', intensity:5, visible:true, castShadow:false, position:[0,3,8], distance:20 }, ], } return { clearAndSet: presets[name] || [] } })() useStore.setState({ sceneLights: clearAndSet }) } return (
{/* Presets */}
Scene Presets
{[['🎬 Studio','studio'],['☀️ Daytime','day'],['🌙 Night','night'],['🚦 Traffic','traffic'],['🌆 Neon','neon']] .map(([lbl,id])=>( ))}
{/* Add light */}
Add Light
{LIGHT_TYPES.map(t=>( ))}
{/* Light list */} {sceneLights.length > 0 && (
{sceneLights.length} LIGHT{sceneLights.length!==1?'S':''}
{sceneLights.map(l=>)}
)}
) }