import { useState } from 'react' import useStore from '../store/useStore' const DEG = 180 / Math.PI function Section({ title, children, defaultOpen=true }) { const [open, setOpen] = useState(defaultOpen) return (
{open &&
{children}
}
) } function Vec3({ label, value, onChange, step=0.01, scale=1, decimals=3 }) { const axes=['X','Y','Z'], colors=['#ef4444','#22c55e','#3b82f6'] return (
{label}
{axes.map((ax,i)=>(
e.currentTarget.style.borderColor=colors[i]} onBlurCapture={e=>e.currentTarget.style.borderColor=`${colors[i]}33`}> {ax} { const v=[...(value||[0,0,0])]; v[i]=(parseFloat(e.target.value)||0)/scale; onChange(v) }} style={{border:'none',background:'transparent',width:'100%', padding:'5px 4px',fontSize:11,fontFamily:'var(--font-mono)',color:'var(--text0)'}} />
))}
) } const PRESET_COLORS = ['#ffffff','#ff4444','#44ff88','#4488ff','#ffaa00','#ff44aa','#44ffff','#000000'] function MaterialEditor({ model }) { const { setModelMaterial, resetModelMaterial } = useStore.getState() const mat = model.materialOverride || {} const [showPicker, setShowPicker] = useState(false) return (
{/* Color override */}
Color Override
{PRESET_COLORS.map(c=>(
setModelMaterial(model.id,{color:c})} style={{width:22,height:22,borderRadius:4,background:c,cursor:'pointer', border:`2px solid ${mat.color===c?'var(--accent)':'rgba(255,255,255,0.15)'}`, transition:'transform 0.1s'}} onMouseEnter={e=>e.currentTarget.style.transform='scale(1.15)'} onMouseLeave={e=>e.currentTarget.style.transform='scale(1)'} /> ))} {mat.color && }
{/* PBR sliders */} {[['Roughness','roughness'],['Metalness','metalness'],['Opacity','opacity']].map(([lbl,key])=>(
{lbl} {(mat[key]!==undefined ? mat[key] : (key==='opacity'?1:key==='roughness'?0.5:0)).toFixed(2)}
setModelMaterial(model.id,{[key]:parseFloat(e.target.value)})}/>
))} {/* Toggles */}
) } export default function PropertiesPanel() { const { models, selectedModelId, updateModelTransform, setModelActiveAnimation, setModelAnimSpeed, currentFrame, addKeyframe, removeKeyframe, getKeyframesForModel, keyframes, removeModel, selectModel, duplicateModel, snapEnabled, snapTranslate, snapRotate, setSnapTranslate, setSnapRotate, pushUndo, } = useStore() const model = models.find(m => m.id === selectedModelId) const snap = (val, grid) => { if (!snapEnabled || !grid) return val return val.map(v => Math.round(v / grid) * grid) } if (!model) return (
Nothing selected
Click a model in the scene
) const kfList = getKeyframesForModel?.(model.id)||[] const hasKfNow = keyframes?.[currentFrame]?.[model.id] return (
{/* Header */}
{model.name}
{/* Transform */}
{snapEnabled && (
🧲 Snap ON — Translate: {snapTranslate}u · Rotate: {snapRotate}°
)} { pushUndo(); updateModelTransform(model.id,'position',snap(v,snapEnabled?snapTranslate:null)) }} /> { pushUndo(); updateModelTransform(model.id,'rotation',v) }} /> { pushUndo(); updateModelTransform(model.id,'scale',v) }} />
{/* Material */}
{/* Animations */} {model.animations.length > 0 && (
{model.animations.map(anim=>( ))}
Speed setModelAnimSpeed(model.id,+e.target.value)} style={{flex:1}}/> {model.animationSpeed.toFixed(1)}×
)} {/* Keyframes */}
{/* Easing selector */}
Easing for next keyframe
{['linear','ease-in','ease-out','ease-in-out'].map(e=>( ))}
{hasKfNow&&}
Frame {currentFrame} {hasKfNow?'— ◆ keyframe set':'— no keyframe'}
{kfList.length>0&&(
{kfList.map(({frame,data})=>(
useStore.getState().setCurrentFrame(frame)} style={{display:'flex',justifyContent:'space-between',alignItems:'center', padding:'4px 8px',borderRadius:'var(--radius-sm)',cursor:'pointer', background:frame===currentFrame?'rgba(245,158,11,0.1)':'var(--bg2)', border:`1px solid ${frame===currentFrame?'rgba(245,158,11,0.25)':'var(--border)'}`}}> ◆ Frame {frame} {data.easing&&data.easing!=='linear'&&({data.easing})}
))}
)}
{/* Shadow / Visibility */}
{[['Cast Shadow','castShadow'],['Receive Shadow','receiveShadow']].map(([lbl,key])=>(
{lbl}
))}
) }