/**
* 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)'}
/>
))}
)
}
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}
{/* Intensity display */}
{(light.intensity||1).toFixed(1)}×
{/* Visible toggle */}
{/* Shadow toggle */}
{hasPosition && (
)}
{open && (
upd({color:c})} />
{/* Intensity */}
{/* Position */}
{hasPosition && (
upd({position:v})} />
)}
{/* Target/direction */}
{hasTarget && (
upd({target:v})} />
)}
{/* Hemisphere sky/ground colors */}
{light.type==='hemisphere' && (
)}
{/* Spot angle */}
{hasAngle && (
)}
{/* Distance */}
{hasDist && (
)}
{/* 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=>
)}
)}
)
}