studio3d / src /components /RightPanel.tsx
Studio3D Deploy
๐Ÿš€ Initial Studio3D deployment โ€” React Three Fiber 3D Animation Studio
43024e4
import React, { useRef } from 'react';
import { useStudioStore } from '../store/useStudioStore';
import './RightPanel.css';
const SliderRow: React.FC<{
label: string;
value: number;
min: number;
max: number;
step?: number;
onChange: (v: number) => void;
}> = ({ label, value, min, max, step = 0.01, onChange }) => (
<div className="prop-row">
<label>{label}</label>
<input
type="range"
min={min}
max={max}
step={step}
value={value}
onChange={(e) => onChange(parseFloat(e.target.value))}
/>
<span className="prop-val">{value.toFixed(2)}</span>
</div>
);
const Vec3Row: React.FC<{
label: string;
value: [number, number, number];
step?: number;
onChange: (v: [number, number, number]) => void;
}> = ({ label, value, step = 0.1, onChange }) => (
<div className="vec3-row">
<label className="vec3-label">{label}</label>
<div className="vec3-inputs">
{(['X', 'Y', 'Z'] as const).map((axis, i) => (
<div key={axis} className="vec3-item">
<span className={`vec3-axis axis-${axis.toLowerCase()}`}>{axis}</span>
<input
type="number"
step={step}
value={value[i].toFixed(2)}
onChange={(e) => {
const v: [number, number, number] = [...value] as any;
v[i] = parseFloat(e.target.value) || 0;
onChange(v);
}}
/>
</div>
))}
</div>
</div>
);
const RightPanel: React.FC = () => {
const {
mode, objects, selectedId, updateObject,
ambientIntensity, directionalIntensity, bgColor, showGrid, showAxes,
bloomIntensity, postProcessing,
skyboxType, skyboxUrl,
setAmbientIntensity, setDirectionalIntensity, setBgColor,
setShowGrid, setShowAxes, setBloomIntensity, setPostProcessing,
setSkybox,
} = useStudioStore();
const skyFileRef = useRef<HTMLInputElement>(null);
const texFileRef = useRef<HTMLInputElement>(null);
const selectedObj = objects.find(o => o.id === selectedId);
const update = (patch: any) => {
if (selectedId) updateObject(selectedId, patch);
};
return (
<div className="right-panel">
{/* Object Properties */}
{selectedObj && (
<div className="prop-section">
<div className="section-title">
<span className="section-icon">โ—ˆ</span>
{selectedObj.name}
</div>
<Vec3Row
label="Position"
value={selectedObj.position}
onChange={(v) => update({ position: v })}
/>
<Vec3Row
label="Rotation"
value={selectedObj.rotation}
step={1}
onChange={(v) => update({ rotation: v })}
/>
<Vec3Row
label="Scale"
value={selectedObj.scale}
step={0.05}
onChange={(v) => update({ scale: v })}
/>
<div className="prop-divider" />
<div className="prop-row">
<label>Color</label>
<input
type="color"
value={selectedObj.color}
onChange={(e) => update({ color: e.target.value })}
className="color-input"
/>
<span className="prop-val">{selectedObj.color}</span>
</div>
<SliderRow label="Metalness" value={selectedObj.metalness} min={0} max={1} onChange={(v) => update({ metalness: v })} />
<SliderRow label="Roughness" value={selectedObj.roughness} min={0} max={1} onChange={(v) => update({ roughness: v })} />
<SliderRow label="Reflection" value={selectedObj.envMapIntensity} min={0} max={3} onChange={(v) => update({ envMapIntensity: v })} />
<div className="prop-divider" />
<div className="texture-section">
<label className="section-sublabel">TEXTURE MAP</label>
<button
className="btn-small"
onClick={() => texFileRef.current?.click()}
>
{selectedObj.textureUrl ? 'โœ“ Texture Loaded' : '+ Upload Texture'}
</button>
{selectedObj.textureUrl && (
<button className="btn-small btn-danger" onClick={() => update({ textureUrl: undefined })}>
Remove
</button>
)}
<input
ref={texFileRef}
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={(e) => {
const file = e.target.files?.[0];
if (file) update({ textureUrl: URL.createObjectURL(file) });
}}
/>
</div>
</div>
)}
{/* Scene / Rendering */}
<div className="prop-section">
<div className="section-title">
<span className="section-icon">โ—Ž</span>
LIGHTING & SCENE
</div>
<SliderRow label="Ambient" value={ambientIntensity} min={0} max={3} onChange={setAmbientIntensity} />
<SliderRow label="Directional" value={directionalIntensity} min={0} max={5} onChange={setDirectionalIntensity} />
<div className="prop-row">
<label>BG Color</label>
<input type="color" value={bgColor} onChange={(e) => setBgColor(e.target.value)} className="color-input" />
</div>
<div className="toggle-row">
<label>Grid</label>
<button className={`toggle-btn ${showGrid ? 'on' : ''}`} onClick={() => setShowGrid(!showGrid)}>
{showGrid ? 'ON' : 'OFF'}
</button>
</div>
<div className="toggle-row">
<label>Axes</label>
<button className={`toggle-btn ${showAxes ? 'on' : ''}`} onClick={() => setShowAxes(!showAxes)}>
{showAxes ? 'ON' : 'OFF'}
</button>
</div>
<div className="toggle-row">
<label>Bloom</label>
<button className={`toggle-btn ${postProcessing ? 'on' : ''}`} onClick={() => setPostProcessing(!postProcessing)}>
{postProcessing ? 'ON' : 'OFF'}
</button>
</div>
{postProcessing && (
<SliderRow label="Bloom Int." value={bloomIntensity} min={0} max={3} onChange={setBloomIntensity} />
)}
</div>
{/* Skybox */}
<div className="prop-section">
<div className="section-title">
<span className="section-icon">โฌก</span>
SKYBOX
</div>
<div className="skybox-btns">
{(['none', 'gradient', 'uploaded'] as const).map((t) => (
<button
key={t}
className={`sky-btn ${skyboxType === t ? 'active' : ''}`}
onClick={() => setSkybox(t, t !== 'uploaded' ? undefined : skyboxUrl || undefined)}
>
{t.toUpperCase()}
</button>
))}
</div>
{skyboxType === 'uploaded' && (
<button className="btn-small" onClick={() => skyFileRef.current?.click()}>
{skyboxUrl ? 'โœ“ HDR/JPG Loaded' : '+ Upload HDR/JPG'}
</button>
)}
<input
ref={skyFileRef}
type="file"
accept=".hdr,.jpg,.jpeg,.png"
style={{ display: 'none' }}
onChange={(e) => {
const file = e.target.files?.[0];
if (file) setSkybox('uploaded', URL.createObjectURL(file));
}}
/>
</div>
{/* Animation panel */}
{selectedObj && selectedObj.animations.length > 0 && mode === 'animate' && (
<div className="prop-section">
<div className="section-title">
<span className="section-icon">โ–ท</span>
ANIMATION
</div>
<div className="anim-list">
{selectedObj.animations.map((anim, i) => (
<div
key={i}
className={`anim-item ${selectedObj.selectedAnimIndex === i ? 'active' : ''}`}
onClick={() => update({ selectedAnimIndex: i })}
>
<span className="anim-name">{anim.name}</span>
<span className="anim-dur">{anim.duration.toFixed(1)}s</span>
</div>
))}
</div>
<div className="anim-controls">
<button
className={`play-btn ${selectedObj.animPlaying ? 'playing' : ''}`}
onClick={() => update({ animPlaying: !selectedObj.animPlaying })}
>
{selectedObj.animPlaying ? 'โธ Pause' : 'โ–ถ Play'}
</button>
</div>
<SliderRow label="Speed" value={selectedObj.animSpeed} min={0.1} max={3} step={0.1}
onChange={(v) => update({ animSpeed: v })} />
<div className="toggle-row">
<label>Loop</label>
<button
className={`toggle-btn ${selectedObj.animLoop ? 'on' : ''}`}
onClick={() => update({ animLoop: !selectedObj.animLoop })}
>
{selectedObj.animLoop ? 'ON' : 'OFF'}
</button>
</div>
</div>
)}
</div>
);
};
export default RightPanel;