import { OrbitControls } from '@react-three/drei'; import { useState } from 'react'; import type { ThreeEvent } from '@react-three/fiber'; import type { Voxel, V3 } from '../../types'; type Props = { voxels: Voxel[]; mode: 'add' | 'remove'; color: string; typeId: string; onAdd: (voxel: Voxel) => void; onRemove: (position: V3) => void; }; const FLOOR_Y = 0; function adjPosition(pos: V3, nx: number, ny: number, nz: number): V3 { return [pos[0] + Math.round(nx), pos[1] + Math.round(ny), pos[2] + Math.round(nz)]; } function posEq(a: V3, b: V3) { return a[0] === b[0] && a[1] === b[1] && a[2] === b[2]; } const DRAG_THRESHOLD = 5; export function VoxelEditorScene({ voxels, mode, color, typeId, onAdd, onRemove }: Props) { const [ghost, setGhost] = useState(null); function handleVoxelClick(e: ThreeEvent, voxel: Voxel) { if (e.delta > DRAG_THRESHOLD) return; e.stopPropagation(); if (mode === 'remove') { onRemove(voxel.position); setGhost(null); } else { if (!e.face) return; const { x, y, z } = e.face.normal; const pos = adjPosition(voxel.position, x, y, z); onAdd({ typeId, position: pos, color }); } } function handleVoxelMove(e: ThreeEvent, voxel: Voxel) { e.stopPropagation(); if (mode === 'remove') { setGhost(voxel.position); } else { setGhost(null); } } function handleFloorClick(e: ThreeEvent) { if (e.delta > DRAG_THRESHOLD || mode !== 'add') return; e.stopPropagation(); const pos: V3 = [Math.floor(e.point.x), 0, Math.floor(e.point.z)]; onAdd({ typeId, position: pos, color }); } function handleFloorMove(e: ThreeEvent) { if (mode !== 'add') return; setGhost([Math.floor(e.point.x), 0, Math.floor(e.point.z)]); } const ghostOccupied = ghost && voxels.some(v => posEq(v.position, ghost)); return ( <> {/* Floor plane */} setGhost(null)} > {/* Voxels */} {voxels.map((v, i) => { const isHovered = !!ghost && mode === 'remove' && posEq(v.position, ghost); return ( handleVoxelClick(e, v)} onPointerMove={(e) => handleVoxelMove(e, v)} onPointerLeave={() => setGhost(null)} > ); })} {/* Ghost voxel preview */} {ghost && mode === 'add' && !ghostOccupied && ( null} > )} ); }