Spaces:
Running
Running
| import React, { useRef, useState } from 'react'; | |
| import { useStudioStore, SceneObject } from '../store/useStudioStore'; | |
| import './LeftPanel.css'; | |
| const LeftPanel: React.FC = () => { | |
| const { objects, selectedId, setSelectedId, addObject, removeObject } = useStudioStore(); | |
| const fileInputRef = useRef<HTMLInputElement>(null); | |
| const [dragging, setDragging] = useState(false); | |
| const handleFiles = (files: FileList | null) => { | |
| if (!files) return; | |
| Array.from(files).forEach((file) => { | |
| if (!file.name.endsWith('.glb') && !file.name.endsWith('.gltf')) { | |
| alert('Please upload .glb or .gltf files only'); | |
| return; | |
| } | |
| const url = URL.createObjectURL(file); | |
| const newObj: SceneObject = { | |
| id: Math.random().toString(36).slice(2), | |
| name: file.name.replace(/\.(glb|gltf)$/, ''), | |
| url, | |
| file, | |
| position: [0, 0, 0], | |
| rotation: [0, 0, 0], | |
| scale: [1, 1, 1], | |
| color: '#ffffff', | |
| metalness: 0.2, | |
| roughness: 0.8, | |
| envMapIntensity: 1, | |
| animations: [], | |
| selectedAnimIndex: 0, | |
| animPlaying: false, | |
| animSpeed: 1, | |
| animLoop: true, | |
| }; | |
| addObject(newObj); | |
| }); | |
| }; | |
| const handleDrop = (e: React.DragEvent) => { | |
| e.preventDefault(); | |
| setDragging(false); | |
| handleFiles(e.dataTransfer.files); | |
| }; | |
| return ( | |
| <div className="left-panel"> | |
| <div className="panel-header"> | |
| <span className="panel-icon">โฌก</span> | |
| <span>ASSETS</span> | |
| </div> | |
| <div | |
| className={`drop-zone ${dragging ? 'drop-zone--active' : ''}`} | |
| onDrop={handleDrop} | |
| onDragOver={(e) => { e.preventDefault(); setDragging(true); }} | |
| onDragLeave={() => setDragging(false)} | |
| onClick={() => fileInputRef.current?.click()} | |
| > | |
| <div className="drop-zone-icon">โ</div> | |
| <div className="drop-zone-text">Drop .GLB / .GLTF</div> | |
| <div className="drop-zone-sub">or click to browse</div> | |
| <input | |
| ref={fileInputRef} | |
| type="file" | |
| accept=".glb,.gltf" | |
| multiple | |
| style={{ display: 'none' }} | |
| onChange={(e) => handleFiles(e.target.files)} | |
| /> | |
| </div> | |
| <div className="object-list-header">SCENE OBJECTS</div> | |
| <div className="object-list"> | |
| {objects.length === 0 && ( | |
| <div className="empty-list">No models loaded</div> | |
| )} | |
| {objects.map((obj) => ( | |
| <div | |
| key={obj.id} | |
| className={`object-item ${selectedId === obj.id ? 'object-item--selected' : ''}`} | |
| onClick={() => setSelectedId(obj.id)} | |
| > | |
| <span className="obj-icon">โ</span> | |
| <span className="obj-name">{obj.name}</span> | |
| {obj.animations.length > 0 && ( | |
| <span className="anim-badge">{obj.animations.length}A</span> | |
| )} | |
| <button | |
| className="obj-delete" | |
| onClick={(e) => { e.stopPropagation(); removeObject(obj.id); }} | |
| > | |
| โ | |
| </button> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default LeftPanel; | |